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<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賦值
最後給大家分享一份非常系統和全面的Android進階技術大綱及進階資料,及面試題集
想學習更多Android知識,請加入Android技術開發交流 7520 16839
進群與大牛們一起討論,還可獲取Android高階架構資料、原始碼、筆記、視訊
包括 高階UI、Gradle、RxJava、小程式、Hybrid、移動架構、React Native、效能優化等全面的Android高階實踐技術講解效能優化架構思維導圖,和BATJ面試題及答案!
群裡免費分享給有需要的朋友,希望能夠幫助一些在這個行業發展迷茫的,或者想系統深入提升以及困於瓶頸的朋友,在網上部落格論壇等地方少花些時間找資料,把有限的時間,真正花在學習上,所以我在這免費分享一些架構資料及給大家。希望在這些資料中都有你需要的內容。
Android高階技術大綱,以及系統進階視訊,及面試題和答案

面試題及答案

Android高階技術大綱

Android 進階視訊資料