1. 程式人生 > >android M Launcher之LauncherModel (二)

android M Launcher之LauncherModel (二)

上一篇我們通過LauncherModel的建立 ,例項化,以及與LauncherModel之間的溝通方式。初步瞭解了LauncherModel一些功能及用法,如果對LauncherModel一系列初始化動作還不瞭解的可以看

android M Launcher之LauncherModel (一)

好了 接下來我們繼續分析,大家都知道 LauncherModel是Launcher的資料中心,但是資料中心的資料是怎麼加載出來的呢,這裡就要說到LoaderTask了,它是LauncherModel的核心任務。

1、LoaderTask的定義、屬性及構造

要了解一個類的功能和作用,我們通常從該類的定義及其定義的成員變數開始,就好比了解一個人最先看到的肯定是他的外貌了。

    private class LoaderTask implements Runnable {
        private Context mContext;
        boolean mIsLoadingAndBindingWorkspace;
        private boolean mStopped;
        boolean mLoadAndBindStepFinished;
    }

由定義可以看出LoaderTask是一個實現Runnable 介面的類,我們都知道Runnable 是一個任務,它可以被拋到執行緒中執行,也可以被拋到程序的處理器中執行,通過實現Runnable介面來實現多執行緒以及執行緒間的通訊是比較常用的做法。

LoaderTask的執行需要Launcher應用程式提供必要的支援,而外界也需要知道Loaderask的執行狀態,因此在LoaderTask中,以成員變數的方式儲存了LoaderTask執行所必需的支援以及自身相關的狀態。

  • mIsLoadingAndBindingWorkspace :它是LoaderTask執行動作的指示性變數,當正在載入和繫結桌面資料時它為true,當動作執行完成後它為false。

  • mStopped :標示整個載入任務是否已經被強制停止,LoaderTask對外提供了一個停止載入的介面,如果外部通過這個介面來停止一個載入任務,那麼它將為true,預設是false

  • mLoadAndBindStepFinished : 通過它可以知道整個載入任務所涉及的動作已經執行完畢

2、LoaderTask的run介面實現
LoaderTask是一個Run那邊了介面的實現,每一個Runnable介面的實現都需要實現run方法,LoaderTask也不例外,當LoaderTask作為一個任務在某一個執行緒中執行時,實際就是這個執行緒呼叫了它的run方法

 public void run() {
            synchronized (mLock) {
                if (mStopped) {
                    return;
                }
                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:
            {
                if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
                loadAndBindWorkspace();

                if (mStopped) {
                    break keep_running;
                }

                waitForIdle();

                // second step
                if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
                loadAndBindAllApps();
            }

            // 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;
                mHasLoaderCompletedOnce = true;
            }
        }

這裡主要做了兩件事
1、loading workspace
2、loading all apps
其他的就是一些成員變數的賦值。

首先loading workspace 我們畫流程圖來看:
這裡寫圖片描述

private void loadAndBindWorkspace() {
            mIsLoadingAndBindingWorkspace = true;

            // Load the workspace
            if (DEBUG_LOADERS) {
                Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
            }

            if (!mWorkspaceLoaded) {
                loadWorkspace();
                synchronized (LoaderTask.this) {
                    if (mStopped) {
                        return;
                    }
                    mWorkspaceLoaded = true;
                }
            }

            // Bind the workspace
            bindWorkspace(-1);
        }

結合上面程式碼 可以發現 它又分為兩步
1、生成桌面資料loadWorkspace;
2、繫結桌面配置資料bindWorkspace。

loadWorkspace

Launcher桌面的資料主要包括來自Launcher資料庫的各種表,loadWorkspace方法將負責從這些資料庫表中讀取資料並轉譯為Launcher桌面項的資料結構。
這個程式碼比較多同樣先上流程圖。
這裡寫圖片描述

  • 獲取系統服務一節裝置屬性並調整桌面頁順序

在載入資料桌面資料前,loadWorkspace 方法必需獲取到必要的支援,因為loadWorkspace方法主要處理應用程式包的相關資訊,所以首先要獲取包管理服務,其次
需要確定快捷方式或者桌面小部件的位置,因此它還需要獲取螢幕的寬和高程式碼如下:

  final Context context = mContext;
            final ContentResolver contentResolver = context.getContentResolver();
            //獲取包管理服務,並查詢當前是否處於安全模式
            final PackageManager manager = context.getPackageManager();
            final boolean isSafeMode = manager.isSafeMode();
            //獲取Launcher定製的應用程式管理介面
            final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
            final boolean isSdCardReady = context.registerReceiver(null,
                    new IntentFilter(StartupReceiver.SYSTEM_READY)) != null;
            //獲取桌面有多少行,多少列
            LauncherAppState app = LauncherAppState.getInstance();
            InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
            int countX = profile.numColumns;
            int countY = profile.numRows;

            if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) {
                long migrationStartTime = System.currentTimeMillis();
                Log.v(TAG, "Starting workspace migration after restore");
                try {
                    //根據實際情況調整桌面頁的順序
                    MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext);
                    // Clear the flags before starting the task, so that we do not run the task
                    // again, in case there was an uncaught error.
                    MigrateFromRestoreTask.clearFlags(mContext);
                    task.execute();
                } catch (Exception e) {
                    Log.e(TAG, "Error during grid migration", e);

                    // Clear workspace.
                    mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE;
                }
                Log.v(TAG, "Workspace migration completed in "
                        + (System.currentTimeMillis() - migrationStartTime));
            }
  • 根據輸入的標誌對資料進行必要的預處理
    在不同的場景下,LoadWorkspace可能要對需要處理的資料來源進行必要的處理,這取決於mFlags 標誌,程式碼如下:
 //如果mFlags中包含LOADER_FLAG_CLEAR_WORKSPACE,則清理原有的資料
            if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) {
                Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true);
                LauncherAppState.getLauncherProvider().deleteDatabase();
            }

            // Make sure the default workspace is loaded
            //確保預設資料得以載入
            Launcher.addDumpLog(TAG, "loadWorkspace: loading default favorites", false);
            LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary();
  • 清理歷史資料並準備必要資料
    在每次載入桌面資料時,需要對這些資料區域進行清理 這個平時我們往listview中新增資料前要清理一樣的道理, 它主要在clearSBgDataStructures 中完成
         /**
         * Clears all the sBg data structures
         */
        private void clearSBgDataStructures() {
            synchronized (sBgLock) {
                sBgWorkspaceItems.clear();//清理桌面項列表
                sBgAppWidgets.clear();//清理桌面快捷方式列表
                sBgFolders.clear();//清理資料夾資料列表
                sBgItemsIdMap.clear();//清理桌面項欄位
                sBgWorkspaceScreens.clear();//清理桌面頁記錄列表
            }
        }

完成清理後loadWorkspace方法需要知道當前裝置中安裝了多少應用程式,除了這些外,在載入桌面桌面資料的過程中會產出一些過程資料,他們保持在其他區域程式碼如下:

final HashMap<String, Integer> installingPkgs = PackageInstallerCompat
                        .getInstance(mContext).updateAndGetActiveSessionCache();
                sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext));

                final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
                final ArrayList<Long> restoredRows = new ArrayList<Long>();

有了上面這些基礎後 ,接下來就開始載入資料了。

  • 查詢Favorites表資料庫並準備桌面佔用情況標誌對映
    final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
                if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
                final Cursor c = contentResolver.query(contentUri, null, null, null, null);

                // +1 for the hotseat (it can be larger than the workspace)
                // Load workspace in reverse order to ensure that latest items are loaded first (and
                // before any earlier duplicates)
                final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>();

  • 從Cursor中獲取桌面項型別itemType等資料項
  int itemType = c.getInt(itemTypeIndex);//桌面項型別
                            boolean restored = 0 != c.getInt(restoredIndex);//是否已經恢復
                            boolean allowMissingTarget = false;
  • 應用程式入口及其他入口快捷方式的處理

在這裡Launcher需要處理桌面上快捷方式的記錄,將其轉換成Launcher可以處理的物件。如下圖
這裡寫圖片描述

  • 處理資料夾記錄處理

    桌面檔案只需要處理好資料夾的標題,位置以及所處的容器即可。

  • 桌面小部件記錄處理

    這裡或處理桌面快捷方式類似,可以參考處理桌面快捷方式的流程來看處理桌面小部件。

  • 從資料庫中清理需要刪除的資料項
    通過以上處理步驟,可能產生依稀需要刪除的記錄如下:

 if (itemsToRemove.size() > 0) {
                    // Remove dead items
                    contentResolver.delete(LauncherSettings.Favorites.CONTENT_URI,
                            Utilities.createDbSelectionQuery(
                                    LauncherSettings.Favorites._ID, itemsToRemove), null);
                    if (DEBUG_LOADERS) {
                        Log.d(TAG, "Removed = " + Utilities.createDbSelectionQuery(
                                LauncherSettings.Favorites._ID, itemsToRemove));
                    }

                    // Remove any empty folder
                    for (long folderId : LauncherAppState.getLauncherProvider()
                            .deleteEmptyFolders()) {
                        sBgWorkspaceItems.remove(sBgFolders.get(folderId));
                        sBgFolders.remove(folderId);
                        sBgItemsIdMap.remove(folderId);
                    }
                }

                // Sort all the folder items and make sure the first 3 items are high resolution.
                for (FolderInfo folder : sBgFolders) {
                    Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
                    int pos = 0;
                    for (ShortcutInfo info : folder.contents) {
                        if (info.usingLowResIcon) {
                            info.updateIcon(mIconCache, false);
                        }
                        pos++;
                        if (pos >= FolderIcon.NUM_ITEMS_IN_PREVIEW) {
                            break;
                        }
                    }
                }
  • 置恢復狀態為以恢復
    有些記錄可能來自備份與恢復的過程,如果當前處理的記錄屬於這種,並且已經得到完整的恢復那麼這些記錄ID都會被快取在restoredRows中,在處理往需要刪除的記錄後,就要將restoredRows中指定記錄恢復狀態置為0.
  if (restoredRows.size() > 0) {
                    // Update restored items that no longer require special handling
                    ContentValues values = new ContentValues();
                    values.put(LauncherSettings.Favorites.RESTORED, 0);
                    contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, values,
                            Utilities.createDbSelectionQuery(
                                    LauncherSettings.Favorites._ID, restoredRows), null);
                }
  • 處理安裝在SDCARD上的應用程式
  if (!isSdCardReady && !sPendingPackages.isEmpty()) {
                    context.registerReceiver(new AppsAvailabilityCheck(),
                            new IntentFilter(StartupReceiver.SYSTEM_READY),
                            null, sWorker);
                }

當sPendingPackages中有了記錄並且SDCARD沒有掛載好的時候,Launcher註冊相關的廣播接收器等待SDCARD的掛載完成。

好了 LoadWorkspace分析就完成了,真心不容易呀。

再接再厲 我們看bindWorkspace

上面我們已經準備好了需要載入的資料,那麼接下來需要做的就是將這些資料傳送到Launcher,讓它將這些資料轉化為一個個可以顯示的view,這個過程由bindWorkspace來完成。

private void bindWorkspace(int synchronizeBindPage)
  • synchronizeBindPage 如果它的值小於0說明需要對所有桌面頁進行重新整理,如果大於等於0,則對指定頁進行重新整理。

在畫圖看下bindWorkspace的流程。發現Xmind用的越來越6了 O(∩_∩)O哈哈~
這裡寫圖片描述

在載入桌面資料的過程中,已經把需要載入的資料分門別類的放在不同的資料緩衝區。由於LauncherModel的LoaderTask可以因為LauncherModel執行不同的任務而被多次例項化,這將會引起緩衝區資料共享問題,為了解決這個問題每次繫結資料的時候都要臨時將資料緩衝區的資料備份,程式碼如下:

 // Save a copy of all the bg-thread collections
            //桌面資料項列表
            ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>();
            //桌面小部件資料項列表
            ArrayList<LauncherAppWidgetInfo> appWidgets =
                    new ArrayList<LauncherAppWidgetInfo>();
            //經過排序的桌面頁索引列表
            ArrayList<Long> orderedScreenIds = new ArrayList<Long>();

            final LongArrayMap<FolderInfo> folders;
            final LongArrayMap<ItemInfo> itemsIdMap;
            //緩衝區資料複製
            synchronized (sBgLock) {
                workspaceItems.addAll(sBgWorkspaceItems);
                appWidgets.addAll(sBgAppWidgets);
                orderedScreenIds.addAll(sBgWorkspaceScreens);

                folders = sBgFolders.clone();
                itemsIdMap = sBgItemsIdMap.clone();
            }
            //獲取Launcher當前所處的頁面索引
            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;
            final long currentScreenId = currentScreen < 0
                    ? INVALID_SCREEN_ID : orderedScreenIds.get(currentScreen);

分離當前頁面與其他頁面的資料並通知Launcher載入開始

// Separate the items that are on the current screen, and all the other remaining items
            //分類資料區定義
            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>();
            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>();
            ArrayList<LauncherAppWidgetInfo> currentAppWidgets =
                    new ArrayList<LauncherAppWidgetInfo>();
            ArrayList<LauncherAppWidgetInfo> otherAppWidgets =
                    new ArrayList<LauncherAppWidgetInfo>();
            LongArrayMap<FolderInfo> currentFolders = new LongArrayMap<>();
            LongArrayMap<FolderInfo> otherFolders = new LongArrayMap<>();
            //分類桌面項資料
            filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
                    otherWorkspaceItems);
            //分類小部件資料
            filterCurrentAppWidgets(currentScreenId, appWidgets, currentAppWidgets,
                    otherAppWidgets);
            filterCurrentFolders(currentScreenId, itemsIdMap, folders, currentFolders,
                    otherFolders);
            //對資料進行排序
            sortWorkspaceItemsSpatially(currentWorkspaceItems);
            sortWorkspaceItemsSpatially(otherWorkspaceItems);

            // Tell the workspace that we're about to start binding items
            //在主執行緒上執行通知Launcher繫結開始任務
            r = new Runnable() {
                public void run() {
                    Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                    if (callbacks != null) {
                        callbacks.startBinding();
                    }
                }
            };
            runOnMainThread(r);

LoaderTask在繫結資料的過程中會產生不同的過程狀態資訊,這些資訊會通過回撥介面通知LauncherModel對資料處理的狀態資訊,比如繫結資料之前,會通過 startBinding介面通知Launcher準備開始繫結資料,在繫結資料結束時通過Launcher實現的finishBindingItems通知Launcher資料繫結完成等。這些回撥方法的實現絕大多數需要處理介面上的view因此他們都需要在UI執行緒中。

通過結束繫結為例來說明下這個過程:

 // Tell the workspace that we're done binding items
            //實現包含了結束繫結通知的任務
            r = new Runnable() {
                public void run() {
                    Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                    if (callbacks != null) {
                        callbacks.finishBindingItems();
                    }
                    //設定載入以及繫結任務結束標誌
                    mIsLoadingAndBindingWorkspace = false;

                    // Run all the bind complete runnables after workspace is bound.
                    if (!mBindCompleteRunnables.isEmpty()) {
                        synchronized (mBindCompleteRunnables) {
                            for (final Runnable r : mBindCompleteRunnables) {
                                runOnWorkerThread(r);
                            }
                            mBindCompleteRunnables.clear();
                        }
                    }

                    // If we're profiling, ensure this is the last thing in the queue.
                    if (DEBUG_LOADERS) {
                        Log.d(TAG, "bound workspace in "
                                + (SystemClock.uptimeMillis() - t) + "ms");
                    }

                }
            };
            if (isLoadingSynchronously) {
                synchronized (mDeferredBindRunnables) {
                    mDeferredBindRunnables.add(r);
                }
            } else {
                //在主執行緒中執行該任務
                runOnMainThread(r);
            }

在完成了上面一系列操作後,最新的資料就完成了從資料庫記錄到桌面上可見的圖示的轉換。

終於到loading all apps了
它的實現方法為loadAndBindAllApps()
這個方法沒有引數也沒有返回值 它只是一個主幹通過呼叫其他兩個方法來完成應用程式猜的的資料載入及繫結。
這裡寫圖片描述

載入所有應用程式選單的過程和載入桌面的過程一樣,都是由載入資料和繫結資料。
當第一次對應用程式選單資料進行處理的時候,需要將這兩個過程整合執行,如果之前已經載入了應用程式載入任務,只需要執行繫結動作。

和載入桌面一樣 我們首先看下loadAllApps()

這裡寫圖片描述

  • 獲取賬戶並清理快取

android系統提供了多賬戶的概念,不同的賬戶下可以使用的應用程式是不同的,因此Launcher需要注意這個細節,在不同賬戶下處理不同的應用程式列表資訊,所有在載入應用程式列表的時候需要獲取當前裝置上的所有賬戶。

final List<UserHandleCompat> profiles = mUserManager.getUserProfiles();

  // Clear the list of apps
  mBgAllAppsList.clear();
  • 獲取應用程式入口資訊

Android5.0後提供了LauncherApps的服務,所以只需要通過LauncherApps就可以獲取到應用程式的入口資訊了

 final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);
  • 將應用程式資訊加入快取區

到這裡,LauncherModel就查詢到了需要處理的資料,為了提高效率LauncherModel隊這些資料提供了臨時儲存的快取區 如下程式碼:

 // 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));
                }
  • 按賬戶儲存查詢到的應用列表

通過ManagedProfileHeuristic工具將查詢到的資料分類儲存到共享檔案中如下程式碼:

//建立與該使用者相關聯的篩選器例項
                final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user);
                if (heuristic != null) {
                    //建立按賬戶分類應用程式的任務
                    final Runnable r = new Runnable() {

                        @Override
                        public void run() {
                            heuristic.processUserApps(apps);
                        }
                    };
                    //在UI執行緒中執行這個任務
                    runOnMainThread(new Runnable() {

                        @Override
                        public void run() {
                            // Check isLoadingWorkspace on the UI thread, as it is updated on
                            // the UI thread.
                            if (mIsLoadingAndBindingWorkspace) {
                                synchronized (mBindCompleteRunnables) {
                                    mBindCompleteRunnables.add(r);
                                }
                            } else {
                                runOnWorkerThread(r);
                            }
                        }
                    });
  • 繫結應用程式選單資料

將需要載入到應用程式選單中的資料完成分類後,緊接著就需要將資料傳送到Launcher中處理 如下:

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");
                    }
                }
            });

剩下的onlyBindAllApps()方法做的事情 和上面的繫結應用程式選單資料是一樣的。

OK 至此我們已經把載入繫結桌面和應用程式的流程都走完了。好累