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的加载和绑定过程,谈不上深度解析,欢迎指正。后续会继续深入,具体到一些关键细节的实现,以及开发过程中常涉及的需求修改点。

猜你喜欢

转载自blog.csdn.net/qinhai1989/article/details/82496717