美文网首页Android进阶之路Android开发Android开发
Android 9.0 Launcher3源码分析(二)——La

Android 9.0 Launcher3源码分析(二)——La

作者: 当心你的背后 | 来源:发表于2019-05-10 22:06 被阅读14次

    转载请注明原地址:https://www.jianshu.com/p/725bdb3d08aa

    上一篇文章中分析了系统是如何把桌面应用拉起的。(见Android 9.0 Launcher3源码分析(一)——系统启动Launcher流程

    现在接上文,分析一下Launcher应用的启动流程。
    首先把Launcher的onCreate贴出来。

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            if (DEBUG_STRICT_MODE) {
                StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                        .detectDiskReads()
                        .detectDiskWrites()
                        .detectNetwork()   // or .detectAll() for all detectable problems
                        .penaltyLog()
                        .build());
                StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                        .detectLeakedSqlLiteObjects()
                        .detectLeakedClosableObjects()
                        .penaltyLog()
                        .penaltyDeath()
                        .build());
            }
            TraceHelper.beginSection("Launcher-onCreate");
    
            super.onCreate(savedInstanceState);
            TraceHelper.partitionSection("Launcher-onCreate", "super call");
    
            LauncherAppState app = LauncherAppState.getInstance(this);
            mOldConfig = new Configuration(getResources().getConfiguration());
            mModel = app.setLauncher(this);
            initDeviceProfile(app.getInvariantDeviceProfile());
    
            mSharedPrefs = Utilities.getPrefs(this);
            mIconCache = app.getIconCache();
            mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
    
            mDragController = new DragController(this);
            mAllAppsController = new AllAppsTransitionController(this);
            mStateManager = new LauncherStateManager(this);
            UiFactory.onCreate(this);
    
            mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
    
            mAppWidgetHost = new LauncherAppWidgetHost(this);
            mAppWidgetHost.startListening();
    
            mLauncherView = LayoutInflater.from(this).inflate(R.layout.launcher, null);
    
            setupViews();
            mPopupDataProvider = new PopupDataProvider(this);
    
            mRotationHelper = new RotationHelper(this);
            mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
    
            boolean internalStateHandled = InternalStateHandler.handleCreate(this, getIntent());
            if (internalStateHandled) {
                if (savedInstanceState != null) {
                    // InternalStateHandler has already set the appropriate state.
                    // We dont need to do anything.
                    savedInstanceState.remove(RUNTIME_STATE);
                }
            }
            restoreState(savedInstanceState);
    
            // We only load the page synchronously if the user rotates (or triggers a
            // configuration change) while launcher is in the foreground
            int currentScreen = PagedView.INVALID_RESTORE_PAGE;
            if (savedInstanceState != null) {
                currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
            }
    
            if (!mModel.startLoader(currentScreen)) {
                if (!internalStateHandled) {
                    // If we are not binding synchronously, show a fade in animation when
                    // the first page bind completes.
                    mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
                }
            } else {
                // Pages bound synchronously.
                mWorkspace.setCurrentPage(currentScreen);
    
                setWorkspaceLoading(true);
            }
    
            // For handling default keys
            setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
    
            setContentView(mLauncherView);
            getRootView().dispatchInsets();
    
            // Listen for broadcasts
            registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
    
            getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
                    Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
    
            if (mLauncherCallbacks != null) {
                mLauncherCallbacks.onCreate(savedInstanceState);
            }
            mRotationHelper.initialize();
    
            TraceHelper.endSection("Launcher-onCreate");
        }
    
    

    我们从头开始看,在super.onCreate过后,首先调用了LauncherAppState.getInstance(this)来初始化一个单例对象。LauncherAppState里面保存了一些比较常用的对象,方便其他地方通过单例来获取,比如IconCache(图标缓存)、LauncherModel(负责数据加载和处理各种回调)等。getInstance函数如下,注意这里初始化使用的application的Context,因为单例作为static对象,生命周期是与application生命周期一样长的,如果这里使用了Activity的Context,会导致activity退出后,该Context依然被单例持有而无法回收,于是出现内存泄露。

    public static LauncherAppState getInstance(final Context context) {
            if (INSTANCE == null) {
                if (Looper.myLooper() == Looper.getMainLooper()) {
                    INSTANCE = new LauncherAppState(context.getApplicationContext());
                } else {
                    try {
                        return new MainThreadExecutor().submit(new Callable<LauncherAppState>() {
                            @Override
                            public LauncherAppState call() throws Exception {
                                return LauncherAppState.getInstance(context);
                            }
                        }).get();
                    } catch (InterruptedException|ExecutionException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return INSTANCE;
        }
    
    

    继续看LauncherAppState的初始化过程。这里面其实就是各个对象的实例创建过程,并且注册了一些系统事件的监听。

        private LauncherAppState(Context context) {
            if (getLocalProvider(context) == null) {
                throw new RuntimeException(
                        "Initializing LauncherAppState in the absence of LauncherProvider");
            }
            Log.v(Launcher.TAG, "LauncherAppState initiated");
            Preconditions.assertUIThread();
            mContext = context;
    
            mInvariantDeviceProfile = new InvariantDeviceProfile(mContext);
            mIconCache = new IconCache(mContext, mInvariantDeviceProfile);
            mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
            mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
    
            LauncherAppsCompat.getInstance(mContext).addOnAppsChangedCallback(mModel);
    
            // Register intent receivers
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_LOCALE_CHANGED);
            // For handling managed profiles
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
    
            if (FeatureFlags.IS_DOGFOOD_BUILD) {
                filter.addAction(ACTION_FORCE_ROLOAD);
            }
    
            mContext.registerReceiver(mModel, filter);
            UserManagerCompat.getInstance(mContext).enableAndResetCache();
            new ConfigMonitor(mContext).register();
    
            if (!mContext.getResources().getBoolean(R.bool.notification_badging_enabled)) {
                mNotificationBadgingObserver = null;
            } else {
                // Register an observer to rebind the notification listener when badging is re-enabled.
                mNotificationBadgingObserver = new SettingsObserver.Secure(
                        mContext.getContentResolver()) {
                    @Override
                    public void onSettingChanged(boolean isNotificationBadgingEnabled) {
                        if (isNotificationBadgingEnabled) {
                            NotificationListener.requestRebind(new ComponentName(
                                    mContext, NotificationListener.class));
                        }
                    }
                };
                mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
            }
        }
    
    

    回到刚才的Launcher创建流程,LauncherAppState初始化完成后,有这样一句mModel = app.setLauncher(this),这里调用了mModel.initialize(launcher),这里将传过来的Callbacks对象(也就是Launcher,Launcher实现了Callbacks接口)保存为了弱引用。同样是基于避免内存泄露的考虑。还记得上文提到的LauncherAppState,LauncherModel是其内部的一个成员变量,生命周期也是比Launcher这个Activity要长的。

    public void initialize(Callbacks callbacks) {
            synchronized (mLock) {
                Preconditions.assertUIThread();
                mCallbacks = new WeakReference<>(callbacks);
            }
        }
    
    

    继续Launcher的create,之后是initDeviceProfile(app.getInvariantDeviceProfile()),DeviceProfile是与Launcher布局相关的一个重要类,这里面保存了所有布局相关数据比如图标大小、页面宽高、各种padding等等。然后创建一些其他对象后,终于inflate了R.layout.launcher。继续往下就执行到一个关键函数mModel.startLoader(currentScreen),前面执行的都是诸如对象创建、View的inflate等逻辑,并没有涉及到数据相关的内容,此函数就是开启Launcher数据加载的一个调用。

    之后又是一些初始化的逻辑。所以我们前面啰嗦一大堆,其实onCreate干的事情简单说来就是初始化对象、加载布局、注册一些事件监听、以及开启数据加载。

    接着看数据加载与绑定流程。数据加载的调用实际是这样的startLoader()→startLoaderForResults()。从如下代码中可知,数据加载时在一个工作线程去做的,这是很正常的一个选择,避免阻塞主线程。

    public void startLoaderForResults(LoaderResults results) {
            synchronized (mLock) {
                stopLoader();
                mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
                runOnWorkerThread(mLoaderTask);
            }
        }
    
    private static void runOnWorkerThread(Runnable r) {
            if (sWorkerThread.getThreadId() == Process.myTid()) {
                r.run();
            } else {
                // If we are not on the worker thread, then post to the worker handler
                sWorker.post(r);
            }
        }
    
    

    在工作线程跑的是一个LoaderTask类,实现了Runnable接口。我们直接来看其run函数的定义。

        public void run() {
            synchronized (this) {
                // Skip fast if we are already stopped.
                if (mStopped) {
                    return;
                }
            }
    
            TraceHelper.beginSection(TAG);
            try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
                TraceHelper.partitionSection(TAG, "step 1.1: loading workspace");
                loadWorkspace();
    
                verifyNotStopped();
                TraceHelper.partitionSection(TAG, "step 1.2: bind workspace workspace");
                mResults.bindWorkspace();
    
                // 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();
    
                TraceHelper.partitionSection(TAG, "step 2.2: Binding all apps");
                verifyNotStopped();
                mResults.bindAllApps();
    
                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");
            }
            TraceHelper.endSection(TAG);
        }
    
    

    非常清晰明了,一步一步通过注释和Log都标出来了。Launcher里面数据比较多,包括所有应用的图标和应用数据,所有应用的Widget数据,桌面已添加的用户数据等,随着Android大版本演进,还有DeepShortcuts等新的数据类型。如果按照常规的加载做法,等加载数据完成后再显示到View,耗时就太长了。为了优化体验,Launcher于是采用了分批加载、分批绑定的做法。这是大家在应用开发时可以借鉴的一种优化方案。整体的加载绑定流程如下。

    我们以其中的加载与绑定桌面内容为例来进行说明,后面的三步在弄明白第一步如何做之后也就是业务逻辑上的差异,不再赘述。

    加载桌面内容,调用函数为loadWorkspace()。这个函数很长,这里就不贴代码了。简述一下其流程。

    首先我们要知道一个BgDataModel类,这个类用于把所有数据对应的实例管理起来。如下面代码,可以看到有workspaceItems(所有应用图标数据对应的ItemInfo),appWidgets(所有AppWidgets数据对应的LauncherAppWidgetInfo)等等。

        public final LongArrayMap<ItemInfo> itemsIdMap = new LongArrayMap<>();
    
        public final ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
    
        public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
    
        public final LongArrayMap<FolderInfo> folders = new LongArrayMap<>();
    
        public final ArrayList<Long> workspaceScreens = new ArrayList<>();
    
        public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
    
        public boolean hasShortcutHostPermission;
    
        public final MultiHashMap<ComponentKey, String> deepShortcutMap = new MultiHashMap<>();
    
        public final WidgetsModel widgetsModel = new WidgetsModel();
    
    

    然后正式来看loadWorkspace

    • 通过LauncherSettings.Favorites.CONTENT_URI查询Favorites表的所有内容,拿到cursor。
    • 遍历cursor,进行数据的整理。每一行数据都有一个对应的itemType,标志着这一行的数据对应的是一个应用、还是一个Widget或文件夹等。不同的类型会进行不同的处理。
    • 对于图标类型(itemType是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先经过一系列判断,判断其是否还可用(比如应用在Launcher未启动时被卸载导致不可用),不可用的话就标记为可删除,继续循环。如果可用的话,就根据当前cursor的内容,生成一个ShortcutInfo对象,保存到BgDataModel。
    • 对于文件夹类型(itemType是ITEM_TYPE_FOLDER),直接生成一个对应的FolderInfo对象,保存到BgDataModel。
    • 对于AppWidget(itemType是ITEM_TYPE_APPWIDGETITEM_TYPE_CUSTOM_APPWIDGET),也需要经过是否可用的判断,但是可用条件与图标类型是有差异的。如果可用,生成一个LauncherAppWidgetInfo对象,保存到BgDataModel。
    • 经过上述流程,现在所有数据库里读出的内容已经分类完毕,并且保存到了内存(BgDataModel)中。然后开始处理之前标记为可删除的内容。显示从数据库中删除对应的行,然后还要判断此次删除操作是否带来了其他需要删除的内容。比如某个文件夹或者某一页只有一个图标,这个图标因为某些原因被删掉了,那么此文件夹或页面也需要被删掉。

    至此数据加载完毕,开始要进行绑定了,也就是mResults.bindWorkspace()

    此函数在LoaderResults类中。函数在执行数据绑定之前,会执行这样一段代码。

            // Separate the items that are on the current screen, and all the other remaining items
            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
            ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
    
            filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
                    otherWorkspaceItems);
            filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
                    otherAppWidgets);
            sortWorkspaceItemsSpatially(currentWorkspaceItems);
            sortWorkspaceItemsSpatially(otherWorkspaceItems);
    
    

    这段代码做的事情是,把Launcher启动后默认显示出来那一页所拥有的数据筛选到currentWorkspaceItems与currentAppWidgets,其他页的数据筛选到otherWorkspaceItems与otherAppWidgets。然后对每个list,按照从上到下,从左到右的顺序进行排序。然后可以开始绑定了。下面代码的Callbacks就是Launcher activity实例,首先通知Launcher要开始绑定了(callbacks.startBinding()),然后先把空页面添加到View tree中(callbacks.bindScreens(orderedScreenIds)),之后先绑定默认页的所有元素(下段代码的最后一句)。当然这些所有的操作都是通过mUiExecutor放到主线程执行的。

            // 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);
    
    

    最后一句的内容如下,也是通过callbacks调用在Launcher中的bindItems函数。

    private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
                final ArrayList<LauncherAppWidgetInfo> appWidgets,
                final Executor executor) {
    
            // Bind the workspace items
            int N = workspaceItems.size();
            for (int i = 0; i < N; i += ITEMS_CHUNK) {
                final int start = i;
                final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
                final Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        Callbacks callbacks = mCallbacks.get();
                        if (callbacks != null) {
                            callbacks.bindItems(workspaceItems.subList(start, start+chunkSize), false);
                        }
                    }
                };
                executor.execute(r);
            }
    
            // Bind the widgets, one at a time
            N = appWidgets.size();
            for (int i = 0; i < N; i++) {
                final ItemInfo widget = appWidgets.get(i);
                final Runnable r = new Runnable() {
                    public void run() {
                        Callbacks callbacks = mCallbacks.get();
                        if (callbacks != null) {
                            callbacks.bindItems(Collections.singletonList(widget), false);
                        }
                    }
                };
                executor.execute(r);
            }
        }
    
    

    bindItems函数我们看一下其中的关键代码。根据不同的itemType来生产不同的View,然后通过addInScreenFromBind函数将View add到相应的ViewGroup去。

        @Override
        public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
            ...
                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");
                }
    
                ...
                workspace.addInScreenFromBind(view, item);
                ...
        }
    
    

    默认页的元素绑定完了,然后继续绑定其他页的元素。这里我们从注释也可以看出,Launcher为了让默认页尽快显示,自定义了一个ViewOnDrawExecutor,这里面会让绑定其他页的操作在绑定完第一页的元素并且第一次onDraw执行完之后才执行。读者有兴趣的话可以去看看这个Executor的实现。

            // 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);
    
    

    经过其他页的绑定之后,桌面数据的加载与绑定也就到此为止。接下来就是之前提到的另外三步后续加载与绑定内容了,不再赘述。但是在本文结束前,还想说一个值得一提的地方。

    桌面数据的加载与绑定完之后,我们看这里执行了一个waitForIdle的函数,然后才是继续执行第二步。这个函数是做什么的呢?

                // 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();
    
    

    我们看下它的实现。

        protected synchronized void waitForIdle() {
            // Wait until the either we're stopped or the other threads are done.
            // This way we don't start loading all apps until the workspace has settled
            // down.
            LooperIdleLock idleLock = mResults.newIdleLock(this);
            // Just in case mFlushingWorkerThread changes but we aren't woken up,
            // wait no longer than 1sec at a time
            while (!mStopped && idleLock.awaitLocked(1000));
        }
    
        public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {
    
        private final Object mLock;
    
        private boolean mIsLocked;
    
        public LooperIdleLock(Object lock, Looper looper) {
            mLock = lock;
            mIsLocked = true;
            if (Utilities.ATLEAST_MARSHMALLOW) {
                looper.getQueue().addIdleHandler(this);
            } else {
                // Looper.myQueue() only gives the current queue. Move the execution to the UI thread
                // so that the IdleHandler is attached to the correct message queue.
                new LooperExecutor(looper).execute(this);
            }
        }
    
        @Override
        public void run() {
            Looper.myQueue().addIdleHandler(this);
        }
    
        @Override
        public boolean queueIdle() {
            synchronized (mLock) {
                mIsLocked = false;
                mLock.notify();
            }
            return false;
        }
    
        public boolean awaitLocked(long ms) {
            if (mIsLocked) {
                try {
                    // Just in case mFlushingWorkerThread changes but we aren't woken up,
                    // wait no longer than 1sec at a time
                    mLock.wait(ms);
                } catch (InterruptedException ex) {
                    // Ignore
                }
            }
            return mIsLocked;
        }
    }
    

    这里面涉及到一个应用启动优化的技术。我们知道应用的启动优化可以有延迟加载、懒加载、异步加载等手段。而用一个名为IdleHandler的类,就可以比较方便的实现延迟加载。这个后面有空的话再来细说吧,本文就先到这里。

    下一篇将分析Launcher布局相关内容。

    相关文章

      网友评论

        本文标题:Android 9.0 Launcher3源码分析(二)——La

        本文链接:https://www.haomeiwen.com/subject/yeizoqtx.html