Android 桌面加载图标过程分析

桌面应用图标流程

前言

本人工作上碰到这么一个需求,开发一款滤镜引擎,将桌面上所有的图标进行统一的滤镜化,这就需要了解一下整个桌面去取图标的过程,了解了整个过程,找到真正拿图标的地方,在真正取图标的地方将图片进行替换,或者滤镜化,之前分析情况,现在整理下,与大家分享。本文所用的代码,是基于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 workspaceItems = new ArrayList();
          ArrayList appWidgets = new ArrayList();
          HashMap folders = new HashMap();
          HashMap itemsIdMap = new HashMap();
          ArrayList orderedScreenIds = new ArrayList();
          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 currentWorkspaceItems = new ArrayList();// 存放当前workspace上的items
          ArrayList otherWorkspaceItems = new ArrayList();// 存放除当前workspace之外的items
          ArrayList currentAppWidgets = new ArrayList();// 存放当前workspace上的appwidgets
          ArrayList otherAppWidgets = new ArrayList();// 存放除当前workspace之外的appwidgets
          HashMap currentFolders = new HashMap();// 存放当前workspace上的folder
          HashMap otherFolders = new HashMap();// 存放除当前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 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 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 packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET);
                HashSet newPackageSet = new HashSet();
      
                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 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 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赋值

最后给大家分享一份非常系统和全面的Android进阶技术大纲及进阶资料,及面试题集

想学习更多Android知识,请加入Android技术开发交流 7520 16839

进群与大牛们一起讨论,还可获取Android高级架构资料、源码、笔记、视频

包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案!

群里免费分享给有需要的朋友,希望能够帮助一些在这个行业发展迷茫的,或者想系统深入提升以及困于瓶颈的朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享一些架构资料及给大家。希望在这些资料中都有你需要的内容。

Android高级技术大纲,以及系统进阶视频,及面试题和答案


面试题及答案

Android高级技术大纲

Android 进阶视频资料

你可能感兴趣的:(Android 桌面加载图标过程分析)