文档章节

Android 桌面加载图标过程分析

蔡小鹏
 蔡小鹏
发布于 2017/07/24 16:54
字数 2574
阅读 1819
收藏 62

桌面应用图标流程

前言

本人工作上碰到这么一个需求,开发一款滤镜引擎,将桌面上所有的图标进行统一的滤镜化,这就需要了解一下整个桌面去取图标的过程,了解了整个过程,找到真正拿图标的地方,在真正取图标的地方将图片进行替换,或者滤镜化,之前分析情况,现在整理下,与大家分享。本文所用的代码,是基于Android 5.1

桌面组件介绍

一级页面

  • 一级菜单

    • WorkSpace:他是一个ViewGroup,要想在桌面上显示东西,就得往这个ViewGroup里添加自己的View

    • BubbleTextView:他是一个TextView,上方是图标,下方是名称,在桌面上的图标都是由这个类表示

    • FolderIcon:他也是一个ViewGroup,用来表示桌面上的文件夹图标,里面添加了缩略处理过的bitmap,他的背景图片就是文件夹的形状

    • HotSeat: 他是个FrameLayout,是桌面下方的固定快捷区,包含了几个常用的图标,中间的AllApp按钮是固定位置,也是一个TextView

抽屉桌面

  • 抽屉页面 组件

    • PagedView:他是一个viewgroup,代表进入抽屉页后的界面,应用图标需要添加到这个viewgoup里面才能显示,一个或几个PagedView 承载了手机上所有的应用图标
    • PagedViewIcon:他是一个TextView,和BubblTextView一样,只是在抽屉容器里换了个名字

桌面加载图标流程

  • 先来看一张流程图

    桌面加载流程图

  • 桌面Activity 也就是Launcher.java 类,该类里面维护了一个 LauncherModel,该对象会在onCreate 方法中去调用startLoader() 方法,

  • 下面看一下startLoader() 方法的源码,

      public void startLoader(boolean isLaunching, int synchronousBindPage) {
          synchronized (mLock) {
              if (DEBUG_LOADERS) {
                  Log.d(TAG, "startLoader isLaunching=" + isLaunching);
              }
    
              // Clear any deferred bind-runnables from the synchronized load process
              // We must do this before any loading/binding is scheduled below.
              mDeferredBindRunnables.clear();
    
              // Don't bother to start the thread if we know it's not going to do anything
              if (mCallbacks != null && mCallbacks.get() != null) {
                  // If there is already one running, tell it to stop.
                  // also, don't downgrade isLaunching if we're already running
                  isLaunching = isLaunching || stopLoaderLocked();
      			// 这搞了一个异步任务去加载
                  mLoaderTask = new LoaderTask(mApp.getContext(), isLaunching);
                  if (synchronousBindPage > -1 && mAllAppsLoaded && mWorkspaceLoaded) {
                      mLoaderTask.runBindSynchronousPage(synchronousBindPage);
                  } else {
                      sWorkerThread.setPriority(Thread.NORM_PRIORITY);
                      sWorker.post(mLoaderTask);
                  }
              }
          }
     	 }
    

    我们看到,这里面有个关键的类,loaderTask,见名只义,可以猜到这里面会起一个线程,去加载一些资源。看看里面去加载什么

  • LoaderTask.java

    可以看到LoaderTask实现了Runnable接口,直接去看该类的run() 方法

      public void run() {
          boolean isUpgrade = false;
    
          synchronized (mLock) {
              mIsLoaderTaskRunning = true;
          }
          // Optimize for end-user experience: if the Launcher is up and // running with the
          // All Apps interface in the foreground, load All Apps first. Otherwise, load the
          // workspace first (default).
          keep_running: {
              // Elevate priority when Home launches for the first time to avoid
              // starving at boot time. Staring at a blank home is not cool.
              synchronized (mLock) {
                  if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to " +
                          (mIsLaunching ? "DEFAULT" : "BACKGROUND"));
                  android.os.Process.setThreadPriority(mIsLaunching
                          ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND);
              }
              if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
    
      		//加载一级菜单的方法
      		isUpgrade = loadAndBindWorkspace();
    
              if (mStopped) {
                  break keep_running;
              }
    
              // Whew! Hard work done.  Slow us down, and wait until the UI thread has
              // settled down.
              synchronized (mLock) {
                  if (mIsLaunching) {
                      if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to BACKGROUND");
                      android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                  }
              }
              waitForIdle();
    
              // second step
              if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
              //加载二级菜单里面的方法
      		loadAndBindAllApps();
    
              // Restore the default thread priority after we are done loading items
              synchronized (mLock) {
                  android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
              }
          }
    
          // Update the saved icons if necessary
          if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons");
          synchronized (sBgLock) {
              for (Object key : sBgDbIconCache.keySet()) {
                  updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key));
              }
              sBgDbIconCache.clear();
          }
    
          if (AppsCustomizePagedView.DISABLE_ALL_APPS) {
              // Ensure that all the applications that are in the system are
              // represented on the home screen.
              if (!UPGRADE_USE_MORE_APPS_FOLDER || !isUpgrade) {
                  verifyApplications();
              }
          }
    
          // Clear out this reference, otherwise we end up holding it until all of the
          // callback runnables are done.
          mContext = null;
    
          synchronized (mLock) {
              // If we are still the last one to be scheduled, remove ourselves.
              if (mLoaderTask == this) {
                  mLoaderTask = null;
              }
              mIsLoaderTaskRunning = false;
          }
      }
    

    可以看到在该类中主要有两个方法,

    • loadAndBindWorkSpace(), WorkSpace是一级菜单里面的容器类,该方法是加载一及菜单的方法
    • loadAndBindAllapp() ,这是抽屉内二级菜单的加载方法

    下面着重分析下这两个方法的加载流程

####loadAndBindWorkSpace()

  • 这里加载主要分为两个流程一个是 loadWorkSpace 另一个是 bindWorkSpace,可以看下源代码

      private boolean loadAndBindWorkspace() {
          mIsLoadingAndBindingWorkspace = true;
    
          // Load the workspace
          if (DEBUG_LOADERS) {
              Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
          }
    
          boolean isUpgradePath = false;
          if (!mWorkspaceLoaded) {
              isUpgradePath = loadWorkspace();
              synchronized (LoaderTask.this) {
                  if (mStopped) {
                      return isUpgradePath;
                  }
                  mWorkspaceLoaded = true;
              }
          }
    
          // Bind the workspace
          bindWorkspace(-1, isUpgradePath);
          return isUpgradePath;
      }
    

    可以看到并没有直接去加载,而是先判断了一些条件,然后去加载

    • loadWorkSpace() 方法比较长大概分为三步,

      • 初始化后面要用到的对象实例

          final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
        
          final Context context = mContext;
          final ContentResolver contentResolver = context.getContentResolver();
          final PackageManager manager = context.getPackageManager();
          final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
          final boolean isSafeMode = manager.isSafeMode();
        
          LauncherAppState app = LauncherAppState.getInstance();
          DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
          int countX = (int) grid.numColumns;
          int countY = (int) grid.numRows;
        
      • 加载默认配置,并保存数据库中

          synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) {
              String spKey = LauncherAppState.getSharedPreferencesKey();
              SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
        
              if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
                  int workspaceResId = origWorkspaceResId;
        
                  // Use default workspace resource if none provided
          		//如果workspaceResId=0,就会加载默认的配置(default_workspace_xxx.xml),并保存到数据库中
                  if (workspaceResId == 0) {
                      workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace);
                  }
        
                  // Populate favorites table with initial favorites
                  SharedPreferences.Editor editor = sp.edit();
                  editor.remove(EMPTY_DATABASE_CREATED);
                  if (origWorkspaceResId != 0) {
                      editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId);
                  }
        
                  mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId);
                  mOpenHelper.setFlagJustLoadedOldDb();
                  editor.commit();
              }
          }
        
      • 读取数据库,获取需要加载的应用快捷方式

        此段代码较多,就是去读取数据库的一些操作,具体过程是根据一些不同的type 存到不同的list中。

    • bindWorkSpace()

      应用信息读取完之后,刚才的几个变量中就存储了该信息,然后将其绑定到workspace中去,这个过程也是很复杂的

      • 创建局部变量,将全局变量的信息复制过来,单独进行操作

          ArrayList<iteminfo> workspaceItems = new ArrayList<iteminfo>();
          ArrayList<launcherappwidgetinfo> appWidgets = new ArrayList<launcherappwidgetinfo>();
          HashMap<long, folderinfo=""> folders = new HashMap<long, folderinfo="">();
          HashMap<long, iteminfo=""> itemsIdMap = new HashMap<long, iteminfo="">();
          ArrayList<long> orderedScreenIds = new ArrayList<long>();
          synchronized (sBgLock) {
              workspaceItems.addAll(sBgWorkspaceItems);
              appWidgets.addAll(sBgAppWidgets);
              folders.putAll(sBgFolders);
              itemsIdMap.putAll(sBgItemsIdMap);
              orderedScreenIds.addAll(sBgWorkspaceScreens);
          }
        
          final boolean isLoadingSynchronously = synchronizeBindPage != PagedView.INVALID_RESTORE_PAGE;
          int currScreen = isLoadingSynchronously ? synchronizeBindPage : oldCallbacks.getCurrentWorkspaceScreen();
          if (currScreen >= orderedScreenIds.size()) {
              // There may be no workspace screens (just hotseat items and an empty page).
              currScreen = PagedView.INVALID_RESTORE_PAGE;
          }
          final int currentScreen = currScreen;// 当前screen
          final long currentScreenId = currentScreen < 0 ? INVALID_SCREEN_ID : orderedScreenIds.get(currentScreen);// 当前screen id
        
          // Load all the items that are on the current page first (and in the process, unbind
          // all the existing workspace items before we call startBinding() below.
          unbindWorkspaceItemsOnMainThread();// 先解除绑定
        
      • 根据item中的screenID将items分成当前screen和其他screen,并进行排序

          // Separate the items that are on the current screen, and all the other remaining items
          ArrayList<iteminfo> currentWorkspaceItems = new ArrayList<iteminfo>();// 存放当前workspace上的items
          ArrayList<iteminfo> otherWorkspaceItems = new ArrayList<iteminfo>();// 存放除当前workspace之外的items
          ArrayList<launcherappwidgetinfo> currentAppWidgets = new ArrayList<launcherappwidgetinfo>();// 存放当前workspace上的appwidgets
          ArrayList<launcherappwidgetinfo> otherAppWidgets = new ArrayList<launcherappwidgetinfo>();// 存放除当前workspace之外的appwidgets
          HashMap<long, folderinfo=""> currentFolders = new HashMap<long, folderinfo="">();// 存放当前workspace上的folder
          HashMap<long, folderinfo=""> otherFolders = new HashMap<long, folderinfo="">();// 存放除当前workspace之外的folder
        
          filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems, otherWorkspaceItems);// 过滤items,区分当前screen和其他screen上的items
          filterCurrentAppWidgets(currentScreenId, appWidgets, currentAppWidgets, otherAppWidgets);// 同上
          filterCurrentFolders(currentScreenId, itemsIdMap, folders, currentFolders, otherFolders);// 同上
          sortWorkspaceItemsSpatially(currentWorkspaceItems);// 对workspace上的items进行排序,按照从上到下和从左到右的顺序
          sortWorkspaceItemsSpatially(otherWorkspaceItems);// 同上
        
      • runnable执行块,告诉workspace要开始绑定items了,startBinding方法在Launcher中实现,做一些清除工作

          // Tell the workspace that we're about to start binding items
          r = new Runnable() {
              public void run() {
                  Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                  if (callbacks != null) {
                      callbacks.startBinding();
                  }
              }
          };
          runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
        

        可以看一下实现类launcher中startBinding()方法

          public void startBinding() {
              // If we're starting binding all over again, clear any bind calls we'd postponed in
              // the past (see waitUntilResume) -- we don't need them since we're starting binding
              // from scratch again
              mBindOnResumeCallbacks.clear();
        
              // Clear the workspace because it's going to be rebound
              mWorkspace.clearDropTargets();
              mWorkspace.removeAllWorkspaceScreens();
        
              mWidgetsToAdvance.clear();
              if (mHotseat != null) {
                  mHotseat.resetLayout();
              }
          }
        

        可以看到主要做的是清除和重置工作

      • 绑定workspace screen

           bindWorkspaceScreens(oldCallbacks, orderedScreenIds);
           具体实现在launcher中
        
      • Workspace绑定完成之后,就是将items、widgets和folders放到上面去

loadAndBindAllapp()

  • 可以看到加载过程也是分为两步:如果所有app已经加载过了,就只需要绑定就行了,否则的话,加载所有app,第一次启动肯定是加载所有的,我们按照这种情况来分析

    • 1.获取需要显示到Launcher中的app列表,创建app图标

        private void loadAndBindAllApps() {
            if (DEBUG_LOADERS) {
                Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
            }
            if (!mAllAppsLoaded) {
                loadAllApps();
                synchronized (LoaderTask.this) {
                    if (mStopped) {
                        return;
                    }
                    mAllAppsLoaded = true;
                }
            } else {
                onlyBindAllApps();
            }
        }
      
    • loadAllApps()

        final long loadTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
      
        final Callbacks oldCallbacks = mCallbacks.get();
        if (oldCallbacks == null) {
            // This launcher has exited and nobody bothered to tell us.  Just bail.
            Log.w(TAG, "LoaderTask running with no launcher (loadAllApps)");
            return;
        }
      
        final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
      
        final List<userhandlecompat> profiles = mUserManager.getUserProfiles();
      
        // Clear the list of apps
        mBgAllAppsList.clear();// 清除所有app列表
        SharedPreferences prefs = mContext.getSharedPreferences(
                LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
        for (UserHandleCompat user : profiles) {
            // Query for the set of apps
            final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
            List<launcheractivityinfocompat> apps = mLauncherApps.getActivityList(null, user);// 获取需要显示在Launcher上的activity列表
            if (DEBUG_LOADERS) {
                Log.d(TAG, "getActivityList took "
                        + (SystemClock.uptimeMillis()-qiaTime) + "ms for user " + user);
                Log.d(TAG, "getActivityList got " + apps.size() + " apps for user " + user);
            }
            // Fail if we don't have any apps
            // TODO: Fix this. Only fail for the current user.
            if (apps == null || apps.isEmpty()) {// 没有需要显示的,直接返回
                return;
            }
            // Sort the applications by name
            final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
            Collections.sort(apps, new LauncherModel.ShortcutNameComparator(mLabelCache));// 排序
            if (DEBUG_LOADERS) {
                Log.d(TAG, "sort took " + (SystemClock.uptimeMillis()-sortTime) + "ms");
            }
      
            // Create the ApplicationInfos
            for (int i = 0; i < apps.size(); i++) {
                LauncherActivityInfoCompat app = apps.get(i);
                // This builds the icon bitmaps.
                mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, mLabelCache));// 创建应用图标对象,并添加到所有APP列表中
      
            }
      
            if (ADD_MANAGED_PROFILE_SHORTCUTS && !user.equals(UserHandleCompat.myUserHandle())) {
                // Add shortcuts for packages which were installed while launcher was dead.
                String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX + mUserManager.getSerialNumberForUser(user);
                Set<string> packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET);
                HashSet<string> newPackageSet = new HashSet<string>();
      
                for (LauncherActivityInfoCompat info : apps) {
                    String packageName = info.getComponentName().getPackageName();
                    if (!packagesAdded.contains(packageName)
                            && !newPackageSet.contains(packageName)) {
                        InstallShortcutReceiver.queueInstallShortcut(info, mContext);
                    }
                    newPackageSet.add(packageName);
                }
      
                prefs.edit().putStringSet(shortcutsSetKey, newPackageSet).commit();
            }
        }
        // Huh? Shouldn't this be inside the Runnable below?
        final ArrayList added = mBgAllAppsList.added;// 获取自上次更新(notify()广播)后新增加的应用清单,如果是开机初次启动Launcher,那么added就是mBgAllAppsList
        mBgAllAppsList.added = new ArrayList();// 将AllAppsList的added清空,不影响后续新增的app
      
        // Post callback on main thread
        mHandler.post(new Runnable() {
            public void run() {
                final long bindTime = SystemClock.uptimeMillis();
                final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                if (callbacks != null) {
                    callbacks.bindAllApplications(added);
                    if (DEBUG_LOADERS) {
                        Log.d(TAG, "bound " + added.size() + " apps in "
                                + (SystemClock.uptimeMillis() - bindTime) + "ms");
                    }
                } else {
                    Log.i(TAG, "not binding apps: no Launcher activity");
                }
            }
        });
      
        if (DEBUG_LOADERS) {
            Log.d(TAG, "Icons processed in "
                    + (SystemClock.uptimeMillis() - loadTime) + "ms");
        }
      
    • 绑定app--bindAllApplications

       	public void bindAllApplications(final ArrayList apps) {
       	    if (LauncherAppState.isDisableAllApps()) {// 判断是否禁用所有app,就是所有应用都显示在一级目录
       	        if (mIntentsOnWorkspaceFromUpgradePath != null) {
       	            if (LauncherModel.UPGRADE_USE_MORE_APPS_FOLDER) {
       	                getHotseat().addAllAppsFolder(mIconCache, apps,
       	                        mIntentsOnWorkspaceFromUpgradePath, Launcher.this, mWorkspace);
       	            }
       	            mIntentsOnWorkspaceFromUpgradePath = null;
       	        }
       	        if (mAppsCustomizeContent != null) {
       	            mAppsCustomizeContent.onPackagesUpdated(
       	                    LauncherModel.getSortedWidgetsAndShortcuts(this));
       	        }
       	    } else {
       	        if (mAppsCustomizeContent != null) {
       	            mAppsCustomizeContent.setApps(apps);
       	            mAppsCustomizeContent.onPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(this));
       	        }
       	    }
       	    if (mLauncherCallbacks != null) {
       	        mLauncherCallbacks.bindAllApplications(apps);
       	    }
          	}
      
    可以看到无论是一级桌面拿图标,还是抽屉页面拿图标,都是去走,IconCache的getIcon()方法,
    

IconCache

  • getIcon()

      public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo,
                        HashMap<Object, CharSequence> labelCache) {
          synchronized (mCache) {
              if (resolveInfo == null || component == null) {
                  return null;
              }
    
              CacheEntry entry = cacheLocked(component, resolveInfo, labelCache);
              return entry.icon;
          }
      }
    

    这里判断一下条件,会去走cacheLocked() 方法

  • cacheLocked()

      private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info,
                                 HashMap<Object, CharSequence> labelCache) {
          CacheEntry entry = mCache.get(componentName);
          if (entry == null) {
              entry = new CacheEntry();
    
              mCache.put(componentName, entry);
    
              ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info);
              if (labelCache != null && labelCache.containsKey(key)) {
                  entry.title = labelCache.get(key).toString();
              } else {
                  entry.title = info.loadLabel(mPackageManager).toString();
                  if (labelCache != null) {
                      labelCache.put(key, entry.title);
                  }
              }
              if (entry.title == null) {
                  entry.title = info.activityInfo.name;
              }
    
              entry.icon = Utilities.createIconBitmap(
                      getFullResIcon(info), mContext);         
    
          }
          return entry;
      }
    

    这个方法里面。就是最终去拿图标的方法,里面去拿一些必要信息,去给entry赋值

© 著作权归作者所有

共有 人打赏支持
蔡小鹏
粉丝 18
博文 25
码字总数 52558
作品 0
海淀
Android工程师
加载中

评论(8)

OSC_ZJqUVa
OSC_ZJqUVa
��
Mr_Qi
Mr_Qi
这个代码好眼熟,以前也是搞Launcher的~
Dewdrop
Dewdrop

引用来自“pkxutao”的评论

这是直接修改系统代码? 请问做framework开发怎样测试呢? 不可能每次修改都刷机一次吧?

引用来自“蔡小鹏”的评论

桌面是上层App呀,你可以单独搞出来,做测试,不一定每次都make,

引用来自“pkxutao”的评论

喔,就怕关联太多,一个个类抠出来越抠越多
Launcher是独立的,AndroidStudio直接就可以跑起来。https://android.googlesource.com/platform/packages/apps/Launcher3
贪婪的白木晓
贪婪的白木晓
。;
宇润
宇润
现在还有应用抽屉这种东西?安卓2.1时代的。。。
pkxutao
pkxutao

引用来自“pkxutao”的评论

这是直接修改系统代码? 请问做framework开发怎样测试呢? 不可能每次修改都刷机一次吧?

引用来自“蔡小鹏”的评论

桌面是上层App呀,你可以单独搞出来,做测试,不一定每次都make,
喔,就怕关联太多,一个个类抠出来越抠越多
蔡小鹏
蔡小鹏

引用来自“pkxutao”的评论

这是直接修改系统代码? 请问做framework开发怎样测试呢? 不可能每次修改都刷机一次吧?
桌面是上层App呀,你可以单独搞出来,做测试,不一定每次都make,
pkxutao
pkxutao
这是直接修改系统代码? 请问做framework开发怎样测试呢? 不可能每次修改都刷机一次吧?
设置phonegap启动界面图片和图标

启动界面: 将加载界面图片(比如:bg.jpg)复制到 res/drawable下 在src下的Activity的onCreate方法下。在loadUrl之前加上 super.setIntegerProperty("splashscreen", R.drawable.bg); R.drawa...

kisshua
2013/10/22
0
0
AndroidManifest.xml文件一些便签

<application android:icon="@drawable/app" //桌面图标,应用程序的图标,即启动程序所点击的图标; android:label="@string/app_name" > //应用程序管理的里面显示的名字 <activity androi...

vane_
2012/05/25
0
0
使android桌面图标变大

在平板上android系统默认的桌面图标太小了,需要将其放大。之前在launcher中做了简单的图片放大,带 来了图标模糊的问题,重新研究源码寻求解决办法。 (1)解决思路是先找到应用程序的图标等...

我爱咸蛋黄
2013/11/20
0
2
Android Widget 开发实例:桌面便签程序的实现详解和源码

桌面便签软件是Android上常用软件的一种,比如比较早的Sticky Note,就曾非常流行,而实际上使用android平台对widget开发的支持,桌面便签类软件是非常易于开发的。 本文通过逐步实现一个简单...

simpower
2014/08/31
0
0
Android深入四大组件(六)Android8.0 根Activity启动过程(前篇)

相关文章 Android深入四大组件系列 Android系统启动系列 Android应用程序进程系列 Android深入解析AMS系列 前言 在几个月前我写了Android深入四大组件(一)应用程序启动过程(前篇)和Andro...

刘望舒
2017/11/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

阿里云API网关使用教程

API 网关(API Gateway)提供高性能、高可用的 API 托管服务,帮助用户对外开放其部署在 ECS、容器服务等阿里云产品上的应用,提供完整的 API 发布、管理、维护生命周期管理。用户只需进行简...

mcy0425
32分钟前
4
0
解决远程登陆误按ctrl+s锁屏假死恢复

使用putty时,偶尔发生屏幕假死,不能输入等情况。 后来发现,只要数据ctrl+s,就会假死;输入ctrl+q就可以恢复过来。 很多刚从windows转移到linux上来工作的朋友,在用vi/vim编辑文件时,常常...

HJCui
35分钟前
0
0
@Transactional

事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编程式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于...

asdf08442a
39分钟前
2
0
widows下强制解除8080端口占用问题

使用win+R打开命令窗口 输入以下命令查看哪个任务占用了8080端口 netstat -ano |findstr "8080" 然后通过任务id强制关闭占用该端口的进程 tskill 10044 // 自己的试情况而定,这个ID是LISTE...

_Artisan
49分钟前
2
0
productFlavors简单实用

最近项目中,不同环境需要配置的参数越来越多,为了减少修改代码次数。研究了一下productFlavors的使用方式,总结如下 1. as3.0以上版本使用productFlavors时需要指定一个flavorDimensions,...

火云
51分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部