1. 程式人生 > >Launcher3 Workspace載入流程淺析(基於Aosp P版本)

Launcher3 Workspace載入流程淺析(基於Aosp P版本)

launcher3 Workspace載入流程淺析

前言

Aosp launcher3原始碼隨著Android大版本的迭代,也在不斷的更新,大體功能變化不大,但卻在不斷的重構和優化程式碼,程式碼的封裝和擴充套件性變得越來越好,作為launcher開發者,也要緊跟步伐去學習,把好的實現及時運用到實際開發中去。本文將基於最新的Android P版本的launcher3,分析下這個流程。

幾個重要的類

這幾個類可以算是launcher主要的框架類,熟悉了它們,對launcher就基本有個大概瞭解了:
- Launcher:launcher主介面,是一個Activity,開機後由系統自動啟動。
- LauncherAppState:這是一個單例類,其中初始化了基本所有涉及的重要物件,包括LauncherModel
- LauncherModel:這是workspace資料載入最核心的一個類,負責workspace資料的載入和更新,並維護了所有快取,還負責監聽package的各種變化。
- LoaderResults:該類是Launcher3 P版本新增的,把之前繫結view的操作都封裝了起來。
- LauncherProvider:封裝了workspace相關的資料庫操作,外部通過該provider呼叫操作
- BgDataModel:該類是Launcher3 O版本新增的,對memory快取作了封裝,快取了所有app、shortcut、folder、widget、screen等資料
- Workspace:這就是桌面上網格顯示應用圖示的View,可以左右滑動,顯示多頁。

原始碼淺析

1. Launcher.java onCreate方法開始

    private LauncherModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ......
        LauncherAppState app = LauncherAppState.getInstance(this);

        mModel = app.setLauncher(this);

        mModel.startLoader(currentScreen);//開始載入資料
...... }

onCreate中呼叫LauncherAppState.getInstance會建立LauncherAppState單例,在LauncherAppState建構函式中,做了很多初始化操作,其中一個就是例項化LauncherModel,並且會持有它的引用。然後使用mModel物件呼叫startLoader開始載入workspace和all apps資料,本文主要討論workspace的資料載入。

LauncherAppState建構函式如下:

    private final LauncherModel mModel;

    private
LauncherAppState(Context context) { ...... mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext)); }

LauncherModel只在此處例項化了一次,它實際上也是一個單例項的存在。

2. LauncherModel中startLoader方法被吊起

    public boolean startLoader(int synchronousBindPage) {
        ......
        LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
                mBgAllAppsList, synchronousBindPage, mCallbacks);
        startLoaderForResults(loaderResults);
    }

    public void startLoaderForResults(LoaderResults results) {
        synchronized (mLock) {
            stopLoader();
            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
            runOnWorkerThread(mLoaderTask);//在子執行緒載入資料
        }
    }

LoaderTask實現了Runnable介面,封裝了載入資料的操作,作為一個任務被加入執行緒佇列。

3. LoaderTask執行run方法

run方法實現如下:

    public void run() {
        ......

        try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
            TraceHelper.partitionSection(TAG, "step 1.1: loading workspace");
            loadWorkspace();//載入workspace

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 1.2: bind workspace workspace");
            mResults.bindWorkspace();//繫結workspace

            // Notify the installer packages of packages with active installs on the first screen.
            TraceHelper.partitionSection(TAG, "step 1.3: send first screen broadcast");
            sendFirstScreenActiveInstallsBroadcast();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 1 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // second step
            TraceHelper.partitionSection(TAG, "step 2.1: loading all apps");
            loadAllApps();//載入all apps

            TraceHelper.partitionSection(TAG, "step 2.2: Binding all apps");
            verifyNotStopped();
            mResults.bindAllApps();//繫結app apps資料到介面

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 2.3: Update icon cache");
            updateIconCache();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 2 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // third step
            TraceHelper.partitionSection(TAG, "step 3.1: loading deep shortcuts");
            loadDeepShortcuts();

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 3.2: bind deep shortcuts");
            mResults.bindDeepShortcuts();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 3 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // fourth step
            TraceHelper.partitionSection(TAG, "step 4.1: loading widgets");
            mBgDataModel.widgetsModel.update(mApp, null);

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 4.2: Binding widgets");
            mResults.bindWidgets();

            transaction.commit();
        } catch (CancellationException e) {
            // Loader stopped, ignore
            TraceHelper.partitionSection(TAG, "Cancelled");
        }
        ......
    }

run方法中依次loadWorkspace、bindWorkspace、loadAllApps、bindAllApps等直至所有資料載入完畢。

4. loadWorkspace

這裡主要分析下Workspace的載入過程:

    private void loadWorkspace() {
        ......
        Log.d(TAG, "loadWorkspace: loading default favorites");
        LauncherSettings.Settings.call(contentResolver,
                LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);

        synchronized (mBgDataModel) {
            mBgDataModel.clear();
            ......
            // 這邊LoaderCursor對Cursor操作做了封裝
            final LoaderCursor c = new LoaderCursor(contentResolver.query(
                    LauncherSettings.Favorites.CONTENT_URI, null, null, null, null), mApp);
        }
    }

loadWorkspace方法主要做了兩件事情,一件是載入預設配置xml檔案中的items,並把預設資料存入資料庫,另一件是載入資料庫中的資料到快取,當然還會做各種有效性校驗,這裡不做具體分析。

此處載入預設配置到資料庫是呼叫LauncherProvider中call方法實現的,程式碼如下:

    @Override
    public Bundle call(String method, final String arg, final Bundle extras) {
        ......
        switch (method) {
            ......

            case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
                loadDefaultFavoritesIfNecessary();
                return null;
            }

        }
    }

LauncherProvide的call方法,又呼叫了loadDefaultFavoritesIfNecessary去載入預設配置,程式碼如下:

    /**
     * Loads the default workspace based on the following priority scheme:
     *   1) From the app restrictions
     *   2) From a package provided by play store
     *   3) From a partner configuration APK, already in the system image
     *   4) The default configuration for the particular device
     */
    synchronized private void loadDefaultFavoritesIfNecessary() {
        SharedPreferences sp = Utilities.getPrefs(getContext());

        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
            Log.d(TAG, "loading default workspace");

            AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
            if (loader == null) {
                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
            }
            if (loader == null) {
                final Partner partner = Partner.get(getContext().getPackageManager());
                if (partner != null && partner.hasDefaultLayout()) {
                    final Resources partnerRes = partner.getResources();
                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                            "xml", partner.getPackageName());
                    if (workspaceResId != 0) {
                        loader = new DefaultLayoutParser(getContext(), widgetHost,
                                mOpenHelper, partnerRes, workspaceResId);
                    }
                }
            }

            final boolean usingExternallyProvidedLayout = loader != null;
            if (loader == null) {
                loader = getDefaultLayoutParser(widgetHost);
            }

            // There might be some partially restored DB items, due to buggy restore logic in
            // previous versions of launcher.
            mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
            // Populate favorites table with initial favorites
            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
                    && usingExternallyProvidedLayout) {
                // Unable to load external layout. Cleanup and load the internal layout.
                mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
                        getDefaultLayoutParser(widgetHost));
            }
            clearFlagEmptyDbCreated();
        }
    }

如上面方法註釋,此處會按照優先順序順序,檢查4種配置是否存在。預設配置xml檔案,如果是定製ROM,可以配置在系統裡面,以apk的形式,或者固定路徑的形式提供配置;如果是第三方Launcher,需要放在在apk裡面。

5. LoaderResults封裝繫結

android P上新增了LoaderResults類,封裝了資料繫結到介面的操作,如下面的bindWorkspace,使程式碼結構更加合理和清晰。

    public void bindWorkspace() {
        Runnable r;
        ......

        // Tell the workspace that we're about to start binding items
        r = new Runnable() {
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.clearPendingBinds();
                    callbacks.startBinding();
                }
            }
        };
        mUiExecutor.execute(r);

        // Bind workspace screens
        mUiExecutor.execute(new Runnable() {
            @Override
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.bindScreens(orderedScreenIds);
                }
            }
        });

        Executor mainExecutor = mUiExecutor;
        // Load items on the current page.
        bindWorkspaceItems(currentWorkspaceItems, currentAppWidgets, mainExecutor);

        // In case of validFirstPage, only bind the first screen, and defer binding the
        // remaining screens after first onDraw (and an optional the fade animation whichever
        // happens later).
        // This ensures that the first screen is immediately visible (eg. during rotation)
        // In case of !validFirstPage, bind all pages one after other.
        final Executor deferredExecutor =
                validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;

        mainExecutor.execute(new Runnable() {
            @Override
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.finishFirstPageBind(
                            validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null);
                }
            }
        });

        bindWorkspaceItems(otherWorkspaceItems, otherAppWidgets, deferredExecutor);

        // Tell the workspace that we're done binding items
        r = new Runnable() {
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.finishBindingItems();
                }
            }
        };
        deferredExecutor.execute(r);

    }

6. LauncherModel.Callbacks介面

Launcher實現了LauncherModel.Callbacks介面,用於回撥介面顯示更新,程式碼如下:

    public interface Callbacks {

        ......
        //這邊只列出了這邊分析涉及的介面,便於檢視
        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);//繫結items
        public void bindScreens(ArrayList<Long> orderedScreenIds);//繫結screen
        public void bindAllApplications(ArrayList<AppInfo> apps);//繫結所有應用

    }

Launcher中的實現,繫結螢幕的程式碼如下:

    @Override
    public void bindScreens(ArrayList<Long> orderedScreenIds) {
        // Make sure the first screen is always at the start.
        if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
                orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
            orderedScreenIds.remove(Workspace.FIRST_SCREEN_ID);
            orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
            LauncherModel.updateWorkspaceScreenOrder(this, orderedScreenIds);
        } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
            // If there are no screens, we need to have an empty screen
            mWorkspace.addExtraEmptyScreen();
        }
        bindAddScreens(orderedScreenIds);

        // After we have added all the screens, if the wallpaper was locked to the default state,
        // then notify to indicate that it can be released and a proper wallpaper offset can be
        // computed before the next layout
        mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
    }

bindAddScreens會根據實際計算所得的螢幕個數,建立螢幕的View,launcher中每一屏對應的View是一個CellLayout。

繫結Item的程式碼如下:

    @Override
    public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
        ......
        int end = items.size();
        for (int i = 0; i < end; i++) {
            final ItemInfo item = items.get(i);

            // Short circuit if we are loading dock items for a configuration which has no dock
            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                    mHotseat == null) {
                continue;
            }

            final View view;
            switch (item.itemType) {
                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
                    ShortcutInfo info = (ShortcutInfo) item;
                    view = createShortcut(info);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
                    view = FolderIcon.fromXml(R.layout.folder_icon, this,
                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
                            (FolderInfo) item);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
                    view = inflateAppWidget((LauncherAppWidgetInfo) item);
                    if (view == null) {
                        continue;
                    }
                    break;
                }
                default:
                    throw new RuntimeException("Invalid Item Type");
            }

             /*
             * Remove colliding items.
             */
            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
                if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
                    View v = cl.getChildAt(item.cellX, item.cellY);
                    Object tag = v.getTag();
                    String desc = "Collision while binding workspace item: " + item
                            + ". Collides with " + tag;
                    if (FeatureFlags.IS_DOGFOOD_BUILD) {
                        throw (new RuntimeException(desc));
                    } else {
                        Log.d(TAG, desc);
                        getModelWriter().deleteItemFromDatabase(item);
                        continue;
                    }
                }
            }
            workspace.addInScreenFromBind(view, item);
        }

        .......
        workspace.requestLayout();
    }

bindItems會迴圈建立對於型別的View,重新整理顯示到桌面上,這邊建立的View就是我們最終看到的應用圖示、資料夾、小部件。

總結

以上即本次分析的所有內容,主要討論了androd P版本launcher3 workspace的載入和繫結過程,談不上深度解析,歡迎指正。後續會繼續深入,具體到一些關鍵細節的實現,以及開發過程中常涉及的需求修改點。