美文网首页移动开发狂热者(299402133)LauncherAndroid技术进阶
墨香带你学Launcher之(二)-数据加载流程

墨香带你学Launcher之(二)-数据加载流程

作者: 翰墨飘香 | 来源:发表于2016-08-24 14:53 被阅读2714次

    上一篇墨香带你学Launcher之-概述,我已经介绍了Launcher的布局以及相关的界面跳转,今天我们继续学习,按照计划,我们开始学习Launcher启动之数据加载,主要是图标、Widget和文件夹的加载.

    1.基础知识


    在介绍加载之前我先介绍一点需要用的相关知识:

    • Launcher:继承Activity,是桌面的主界面,因此可知,桌面其实就是一个activity,只是和平常的应用不同,他用来显示图标、Widget和文件夹等;

    • LauncherModel:继承BroadcastReceiver,由此可知他是一个广播接收器,用来接收广播,另外,LauncherModel还主要加载数据;

    • LauncherProvider:继承ContentProvider,主要是处理数据库操作;

    • LauncherAppState:单例模式的全局管理类,主要是初始化一些对象,注册广播等.

    • Compat:兼容包,带有这个后缀的都是做兼容处理的类.

    2.默认图标配置


    我们在买回新的手机或者第一次安装新的Launcher后,会发现手机的第一页已经有了一些应用的图标和时钟或者天气插件,那么这个是怎么实现的呢?其实,手机在出厂的时候或者Launcher发到市场的时候已经默认排布了一些应用,在第一启动时就会加载并且判断手机中是否有这些图标,如果有则显示到固定位置,这个位置其实是已经写好的.下面我们看看这个位置到底在哪里写好的.

    下面是Launcher的资源文件,我们看这个比我们平时的多一个xml文件夹,里面有很多xml文件,那么这些是做什么用的,我来解释一下,有三个文件,分别为default_workspace_4x4.xml,default_workspace_5x5.xml和default_workspace_5x6.xml,这三个文件就是我们默认的布局文件,后面的跟着的4x4、5x5和5x6表示桌面图标的列数和行数,也就是4行4列,5行5列,5行6列,这个怎么用我们后面再说.

    launcher01.png

    我们先看一下default_workspace_4x4.xml这个文件中的代码:

    launcher02.png

    第20行是一个include的文件,在xml文件夹中的名字dw_phone_hotseat文件,我们后面在看,接着看上图的下面的代码,下面是三个resolve文件,里面包含一些信息,screen表示第几屏,x表示横向的位置,y表示纵向的位置,那么这个位置怎定的呢,我来画一个4x4的图你就明白了:

    launcher03.png

    先看上半部分,就是我们说的4x4部分,没一格表示一格图标,在我们绘制图标的时候已经分好了格,每格的大小,只要知道知道他的位置即可绘制图标到相应的位置,那么代码中的x,y就是这个图标的位置.上面resolve中还有两个favorite,在第一个中最后面有个"APP_",这个我们一看就知道是应用的属性,其实这就表示我们配置了那个app在这个位置,我们再看一下上面介绍的hotseat那个xml文件:

    launcher04.png

    这个图我只截图了一部分,想看全部的可以下载我github上的源码查看,其实只是重复,我介绍一个就知道了,上一章我介绍过hotseat这个概念,其实就是我们手机最下面的那个四个或者五个最常用的app图标,这个就是在这里面配置的,我以第一个为例来介绍这个Hotseat配置,我们先看第21行,这个比我们前面介绍的多个属性就是这个container,之前的是没有的,这个就表示容器,-101就是hotseat,也就是这个图标放置到Hotseat中,Hotseat只有一行,所以只有x在变,而y不变.

    到此基本的桌面默认图标显示配置就介绍完了,如果你需要默认显示哪个只需要配置这个文件即可.

    3.Launcher启动过程


    下面我们开始介绍Launcher的启动过程.分析Launcher的启动过程要从源码开始分析.在源码中是通过startHomeActivityLocked这个方法调用的启动Launcher,我们先看一下哪里开始调用的这个函数,

    launcher05.png

    从上面的调用图可知有三个地方调用了启动Launcher的方法,这三个方法中首次启动应该是中间的那个systemReady方法,系统准备过程中调用启动Launcher,我们看一下systemReady方法是哪里调用的来验证一下:

    launcher06.png

    从上代码静态分析图来看最开始是在System.main方法开始的,正好这个方法就是启动系统的一个入口,也就是在这个过程中启动了Launcher,找到调用的地方后,我们来看一下startHomeActivityLocked是怎么启动Launcher的,首先看一下源码:

    launcher07.png

    我们看上面的3473行,获取Intent,再看3451行,如果不为空,则启动HomeActivity,我们看一下这个Intent是什么的Intent:

    launcher08.png

    上面的3424行,有个Intent.CATEGORY_HOME,我们在Intent中找到这个属性的代码:

    launcher09.png

    这个就是我们上一章讲的设置app为launcher的属性值.

    通过上面这些分析可以看到系统是怎么启动launcher的.下面我们看是介绍Launcher内部是如何启动的.

    4.Launcher初始化


    我们知道App的启动是从Application开始的,但是我们最新的Launcher3中,谷歌工程师把这个类移除,再次之前的版本都是有这个类的,我在这提一下就是因为开发以前launcher的时候遇到一个问题,就是在Application和ContentProvider同时存在时,ContentProvider的onCreate方法要比Application的onCreate方法先启动,下面我们通过源码分析来验证这个问题.

    启动Application是从ActivityManagerService中的attachApplication方法开始的,代码:

    
     public final void attachApplication(IApplicationThread thread) {
            synchronized (this) {
                int callingPid = Binder.getCallingPid();
                final long origId = Binder.clearCallingIdentity();
                attachApplicationLocked(thread, callingPid);
                Binder.restoreCallingIdentity(origId);
            }
        }
    
    

    接着调用attachApplicationLocked方法,代码如下:

    
       private final boolean attachApplicationLocked(IApplicationThread thread, int pid) {
            app.makeActive(thread, mProcessStats);
            app.curAdj = app.setAdj = -100;
            app.curSchedGroup = app.setSchedGroup = Process.THREAD_GROUP_DEFAULT;
            app.forcingToForeground = null;
            updateProcessForegroundLocked(app, false, false);
            app.hasShownUi = false;
            app.debugging = false;
            app.cached = false;
            app.killedByAm = false;
    
            mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
    
            boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
            List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null;
    
            if (!normalMode) {
                Slog.i(TAG, "Launching preboot mode app: " + app);
            }
    
            if (DEBUG_ALL) Slog.v(
                TAG, "New app record " + app
                + " thread=" + thread.asBinder() + " pid=" + pid);
            try {
               ...
    
                ProfilerInfo profilerInfo = profileFile == null ? null
                        : new ProfilerInfo(profileFile, profileFd, samplingInterval, profileAutoStop);
                thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
                        profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
                        app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
                        isRestrictedBackupMode || !normalMode, app.persistent,
                        new Configuration(mConfiguration), app.compat,
                        getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked());
                updateLruProcessLocked(app, false, null);
                app.lastRequestedGc = app.lastLowMemory = SystemClock.uptimeMillis();
            } catch (Exception e) {
    
                app.resetPackageList(mProcessStats);
                app.unlinkDeathRecipient();
                startProcessLocked(app, "bind fail", processName);
                return false;
            }
    
            ...
    
            return true;
        }
    
    

    上面代码中主要有一个thread.bindApplication方法来绑定application,接着看bindApplication代码:

     public final void bindApplication(String processName, ApplicationInfo appInfo,
                    List<ProviderInfo> providers, ComponentName instrumentationName,
                    ProfilerInfo profilerInfo, Bundle instrumentationArgs,
                    IInstrumentationWatcher instrumentationWatcher,
                    IUiAutomationConnection instrumentationUiConnection, int debugMode,
                    boolean enableOpenGlTrace, boolean isRestrictedBackupMode, boolean persistent,
                    Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services,
                    Bundle coreSettings) {
    
                ...
    
                AppBindData data = new AppBindData();
                data.processName = processName;
                data.appInfo = appInfo;
                data.providers = providers;
                data.instrumentationName = instrumentationName;
                data.instrumentationArgs = instrumentationArgs;
                data.instrumentationWatcher = instrumentationWatcher;
                data.instrumentationUiAutomationConnection = instrumentationUiConnection;
                data.debugMode = debugMode;
                data.enableOpenGlTrace = enableOpenGlTrace;
                data.restrictedBackupMode = isRestrictedBackupMode;
                data.persistent = persistent;
                data.config = config;
                data.compatInfo = compatInfo;
                data.initProfilerInfo = profilerInfo;
                sendMessage(H.BIND_APPLICATION, data);
            }
    

    准备data数据,然后发送消息到Handler,Handler中处理消息的代码如下:

    
    public void handleMessage(Message msg) {
                if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
                switch (msg.what) {
    
    
                    case BIND_APPLICATION:
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                        AppBindData data = (AppBindData)msg.obj;
                        handleBindApplication(data);
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                        break;
    
                }
                if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
            }
    
    

    根据消息类型BIND_APPLICATION来判断调用handleBindApplication方法,

     private void handleBindApplication(AppBindData data) {
     
            ...
            
            try {
                // If the app is being launched for full backup or restore, bring it up in
                // a restricted environment with the base application class.
                Application app = data.info.makeApplication(data.restrictedBackupMode, null);
                mInitialApplication = app;
    
                // don't bring up providers in restricted mode; they may depend on the
                // app's custom Application class
                if (!data.restrictedBackupMode) {
                    List<ProviderInfo> providers = data.providers;
                    if (providers != null) {
                    
                        //安装ContentProviders
                        installContentProviders(app, providers);
                        
                        // For process that contains content providers, we want to
                        // ensure that the JIT is enabled "at some point".
                        mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
                    }
                }
    
                ...
    
                try {
                    //启动Application的onCreate方法
                    mInstrumentation.callApplicationOnCreate(app);
                } catch (Exception e) {
                    if (!mInstrumentation.onException(app, e)) {
                        throw new RuntimeException(
                            "Unable to create application " + app.getClass().getName()
                            + ": " + e.toString(), e);
                    }
                }
            } finally {
                StrictMode.setThreadPolicy(savedPolicy);
            }
        }
    
    

    在上面函数中调用installContentProviders方法来安装ContentProvider,代码如下:

     private void installContentProviders(
                Context context, List<ProviderInfo> providers) {
            final ArrayList<IActivityManager.ContentProviderHolder> results =
                new ArrayList<IActivityManager.ContentProviderHolder>();
    
            for (ProviderInfo cpi : providers) {
                if (DEBUG_PROVIDER) {
                    StringBuilder buf = new StringBuilder(128);
                    buf.append("Pub ");
                    buf.append(cpi.authority);
                    buf.append(": ");
                    buf.append(cpi.name);
                    Log.i(TAG, buf.toString());
                }
                IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi,
               
                ...
                
            }
    
          ...
          
        }
    
    

    调用installProvider返回一个IActivityManager.ContentProviderHolder对象,我们看这个方法里面做了哪些处理,

    private IActivityManager.ContentProviderHolder installProvider(Context context,
                IActivityManager.ContentProviderHolder holder, ProviderInfo info,
                boolean noisy, boolean noReleaseNeeded, boolean stable) {
            ContentProvider localProvider = null;
            IContentProvider provider;         if (holder == null || holder.provider == null) {
                if (DEBUG_PROVIDER || noisy) {
                    Slog.d(TAG, "Loading provider " + info.authority + ": "
                            + info.name);
                }
                Context c = null;
                ApplicationInfo ai = info.applicationInfo;
                if (context.getPackageName().equals(ai.packageName)) {
                    c = context;
                } else if (mInitialApplication != null &&
                        mInitialApplication.getPackageName().equals(ai.packageName)) {
                    c = mInitialApplication;
                } else {
                    try {
                        c = context.createPackageContext(ai.packageName,
                                Context.CONTEXT_INCLUDE_CODE);
                    } catch (PackageManager.NameNotFoundException e) {
                        // Ignore
                    }
                }
                if (c == null) {
                    Slog.w(TAG, "Unable to get context for package " +
                          ai.packageName +
                          " while loading content provider " +
                          info.name);
                    return null;
                }
                try {
                    final java.lang.ClassLoader cl = c.getClassLoader();
                    localProvider = (ContentProvider)cl.
                        loadClass(info.name).newInstance();
                    //获取ContentProvider
                    provider = localProvider.getIContentProvider();
                    if (provider == null) {
                        Slog.e(TAG, "Failed to instantiate class " +
                              info.name + " from sourceDir " +
                              info.applicationInfo.sourceDir);
                        return null;
                    }
                    if (DEBUG_PROVIDER) Slog.v(
                        TAG, "Instantiating local provider " + info.name);
                        
                    // XXX Need to create the correct context for this provider.
                    localProvider.attachInfo(c, info);
                    
                } catch (java.lang.Exception e) {
                    if (!mInstrumentation.onException(null, e)) {
                        throw new RuntimeException(
                                "Unable to get provider " + info.name
                                + ": " + e.toString(), e);
                    }
                    return null;
                }
            } else {
                provider = holder.provider;
                if (DEBUG_PROVIDER) Slog.v(TAG, "Installing external provider " + info.authority + ": "
                        + info.name);
            }    
            
            ...
    
            return retHolder;
        }
    
    

    上面代码中有个关键方法:localProvider.attachInfo(c, info),这个方法就是添加Provider的,代码如下:

    private void attachInfo(Context context, ProviderInfo info, boolean testing) {
    
            /*
             * Only allow it to be set once, so after the content service gives
             * this to us clients can't change it.
             */
            if (mContext == null) {
                mContext = context;
                if (context != null) {
                    mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService(
                            Context.APP_OPS_SERVICE);
                }
                mMyUid = Process.myUid();
                if (info != null) {
                    setReadPermission(info.readPermission);
                    setWritePermission(info.writePermission);
                    setPathPermissions(info.pathPermissions);
                    mExported = info.exported;
                }
                ContentProvider.this.onCreate();
            }
        }
    
    

    我们看到在最后调用了ContentProvider.this.onCreate()这个方法,然后会返回到handleBindApplication方法中执行mInstrumentation.callApplicationOnCreate(app)方法,代码如下:

    
     public void callApplicationOnCreate(Application app) {
          app.onCreate();
     }
    
    

    因此我们看到ContentProvider的onCreate方法比Application的onCreate方法调用早。这里只是简单介绍详细过程去看源码。

    我现在讲解的是基于最新的Launcher3代码,因此我们这个Launcher中没有Application,所以程序启动最开始的是ContentProvider的onCreate方法,代码如下:

    public boolean onCreate() {
            final Context context = getContext();
            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
            mOpenHelper = new DatabaseHelper(context);
            StrictMode.setThreadPolicy(oldPolicy);
            LauncherAppState.setLauncherProvider(this);
            return true;
    }
    
    

    代码中处理的事情不多,主要是启动严苛模式和创建数据库,关于严苛模式的具体信息看官方文档或者博客,都有很详细的讲解,然后将ContentProvider放置到整个Launcher的管理类LauncherAppState中,以方便获取。

    接下来就是启动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());
            }
    
            if (mLauncherCallbacks != null) {
                mLauncherCallbacks.preOnCreate();
            }
    
            super.onCreate(savedInstanceState);
    
            LauncherAppState.setApplicationContext(getApplicationContext());
            LauncherAppState app = LauncherAppState.getInstance();
    
            // Load configuration-specific DeviceProfile
            mDeviceProfile = getResources().getConfiguration().orientation
                    == Configuration.ORIENTATION_LANDSCAPE ?
                    app.getInvariantDeviceProfile().landscapeProfile
                    : app.getInvariantDeviceProfile().portraitProfile;
    
            mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(),
                    Context.MODE_PRIVATE);
            mIsSafeModeEnabled = getPackageManager().isSafeMode();
            mModel = app.setLauncher(this);
            mIconCache = app.getIconCache();
    
            mDragController = new DragController(this);
            mInflater = getLayoutInflater();
            mStateTransitionAnimation = new LauncherStateTransitionAnimation(this);
    
            mStats = new Stats(this);
    
            mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
    
            mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
            mAppWidgetHost.startListening();
    
            // If we are getting an onCreate, we can actually preempt onResume and unset mPaused here,
            // this also ensures that any synchronous binding below doesn't re-trigger another
            // LauncherModel load.
            mPaused = false;
    
            if (PROFILE_STARTUP) {
                android.os.Debug.startMethodTracing(
                        Environment.getExternalStorageDirectory() + "/launcher");
            }
    
            setContentView(R.layout.launcher);
    
            registerHomeKey();
            setupViews();
    
            //动态设置各布局的参数
            mDeviceProfile.layout(this);
    
            mSavedState = savedInstanceState;
            restoreState(mSavedState);
    
            if (PROFILE_STARTUP) {
                android.os.Debug.stopMethodTracing();
            }
    
            if (!mRestoring) {
                if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) {
                    // If the user leaves launcher, then we should just load items asynchronously when
                    // they return.
                    mModel.startLoader(PagedView.INVALID_RESTORE_PAGE);
                } else {
                    // We only load the page synchronously if the user rotates (or triggers a
                    // configuration change) while launcher is in the foreground
                    mModel.startLoader(mWorkspace.getRestorePage());
                }
            }
    
            // For handling default keys
            mDefaultKeySsb = new SpannableStringBuilder();
            Selection.setSelection(mDefaultKeySsb, 0);
    
            IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
            registerReceiver(mCloseSystemDialogsReceiver, filter);
    
            mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext());
            // In case we are on a device with locked rotation, we should look at preferences to check
            // if the user has specifically allowed rotation.
            if (!mRotationEnabled) {
                mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext(), false);
            }
    
            // On large interfaces, or on devices that a user has specifically enabled screen rotation,
            // we want the screen to auto-rotate based on the current orientation
            setOrientation();
    
            if (mLauncherCallbacks != null) {
                mLauncherCallbacks.onCreate(savedInstanceState);
                if (mLauncherCallbacks.hasLauncherOverlay()) {
                    ViewStub stub = (ViewStub) findViewById(R.id.launcher_overlay_stub);
                    mLauncherOverlayContainer = (InsettableFrameLayout) stub.inflate();
                    mLauncherOverlay = mLauncherCallbacks.setLauncherOverlayView(
                            mLauncherOverlayContainer, mLauncherOverlayCallbacks);
                    mWorkspace.setLauncherOverlay(mLauncherOverlay);
                }
            }
    
            if (shouldShowIntroScreen()) {
                showIntroScreen();
            } else {
                showFirstRunActivity();
                showFirstRunClings();
            }
        }
    
    

    代码比较多我们看一下执行过程图:

    launcher11.png

    首先是启动严苛模式,准备回调接口,初始化LauncherAppState:

        private LauncherAppState() {
            if (sContext == null) {
                throw new IllegalStateException("LauncherAppState inited before app context set");
            }
    
            if (sContext.getResources().getBoolean(R.bool.debug_memory_enabled)) {
                MemoryTracker.startTrackingMe(sContext, "L");
            }
            
            //初始化固定的设备配置
            mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
            
            //初始化图标管理工具
            mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
            
            //初始化Widget加载混存工具
            mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);
    
            mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
            mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class));
            
            //初始化广播
            mModel = new LauncherModel(this, mIconCache, mAppFilter);
    
            LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);
    
            // Register intent receivers
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_LOCALE_CHANGED);
            filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
            // For handling managed profiles
            filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED);
            filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED);
    
            //注册广播
            sContext.registerReceiver(mModel, filter);
            UserManagerCompat.getInstance(sContext).enableAndResetCache();
        }
    
    

    然后初始化手机固件信息对象DeviceProfile,初始化拖拽管理器DragController,然后初始化小部件管理器,加载布局,初始化桌面各个控件,并且设置各个控件的位置:

    public void layout(Launcher launcher) {
            FrameLayout.LayoutParams lp;
            boolean hasVerticalBarLayout = isVerticalBarLayout();
            final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources());
    
            // Layout the search bar space
            View searchBar = launcher.getSearchDropTargetBar();
            lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams();
            if (hasVerticalBarLayout) {
                // Vertical search bar space -- The search bar is fixed in the layout to be on the left
                //                              of the screen regardless of RTL
                lp.gravity = Gravity.LEFT;
                lp.width = searchBarSpaceHeightPx;
    
                LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
                targets.setOrientation(LinearLayout.VERTICAL);
                FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams();
                targetsLp.gravity = Gravity.TOP;
                targetsLp.height = LayoutParams.WRAP_CONTENT;
    
            } else {
                // Horizontal search bar space
                lp.gravity = Gravity.TOP;
                lp.height = searchBarSpaceHeightPx;
    
                LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
                targets.getLayoutParams().width = searchBarSpaceWidthPx;
            }
            searchBar.setLayoutParams(lp);
    
            //其他省略
            ...
    
        }
    
    

    这里就是动态设置桌面各个控件的位置及宽高等属性。当所有信息初始化完成后,就开始调用mModel.startLoader方法来加载应用数据。下面我们详细来讲数据加载流程。

    5.Launcher数据加载


    数据加载主要是从LauncherModel中的startLoader方法开始,先看一下这个方法做的事情:

    launcher12.png

    这里的事情不多,主要是调用LoaderTask这个任务,LoaderTask实现了Runnable这个接口,因此首先执行润run方法,我么看一下这个run方法里面做了哪些事情,

     public void run() {
                
                ...
                
                keep_running:
                {
                    
                    loadAndBindWorkspace();
    
                    if (mStopped) {
                        break keep_running;
                    }
    
                    waitForIdle();
    
                    ...
                    
                    loadAndBindAllApps();
                }
    
              ...
              
            }
    
    

    在这个方法中主要是三件事,我们用时序图表一下:

    launcher13.png

    首先是执行loadAndBindWorkspace方法:

     private void loadAndBindWorkspace() {
     
                ...
                
                //判断workspace是否已经加载
                if (!mWorkspaceLoaded) {
                    loadWorkspace();
                    synchronized (LoaderTask.this) {
                        if (mStopped) {
                            return;
                        }
                        mWorkspaceLoaded = true;
                    }
                }
    
                // Bind the workspace
                bindWorkspace(-1);
            }
    
    

    这里面主要是执行loadWorkspace和bindWorkspace,也就是加载workspace的应用并且进行绑定。先看loadWorkspace方法,代码很多,我们只贴关键部分:

    private void loadWorkspace() {
                //初始化一些值
                
                ...
                
    
                if ((mFlags & LOADER_FLAG_MIGRATE_SHORTCUTS) != 0) {
                    // append the user's Launcher2 shortcuts
                    Launcher.addDumpLog(TAG, "loadWorkspace: migrating from launcher2", true);
                    LauncherAppState.getLauncherProvider().migrateLauncher2Shortcuts();
                } else {
                    // Make sure the default workspace is loaded
                    Launcher.addDumpLog(TAG, "loadWorkspace: loading default favorites", false);
                    LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary();
                }
    
                synchronized (sBgLock) {
                    //初始化一些值
                    ...
    
                    try {
                       //从数据库查询解析出来的所有应用信息
                       ...
    
                        while (!mStopped && c.moveToNext()) {
                            try {
                                int itemType = c.getInt(itemTypeIndex);
                                boolean restored = 0 != c.getInt(restoredIndex);
                                boolean allowMissingTarget = false;
                                container = c.getInt(containerIndex);
    
                                switch (itemType) {
                                    case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                                    
                                        ...
                                        
                                        try {
                                            intent = Intent.parseUri(intentDescription, 0);
                                            ComponentName cn = intent.getComponent();
                                            if (cn != null && cn.getPackageName() != null) {
                                                //检测数据库(从xml文件解析出来存入数据库的)中取出来的app包是否存在
                                                boolean validPkg = launcherApps.isPackageEnabledForProfile(
                                                        cn.getPackageName(), user);
                                                //检测数据库(从xml文件解析出来存入数据库的)中取出来的app组件是否存在
                                                boolean validComponent = validPkg &&
                                                        launcherApps.isActivityEnabledForProfile(cn, user);
    
                                                if (validComponent) {
                                                
                                                    ...
                                                    
                                                } else if (validPkg) {
                                                   
                                                   ...
                                                   
                                                } else if (restored) {
                                                    
                                                    ...
                                                    
                                                } else if (launcherApps.isAppEnabled(
                                                        manager, cn.getPackageName(),
                                                        PackageManager.GET_UNINSTALLED_PACKAGES)) {
                                                   
                                                   ...
                                                   
                                                } else if (!isSdCardReady) {
                                                    
                                                    ...
    
                                                } else {
                                                    
                                                    ...
                                                    
                                                }
                                            } else if (cn == null) {
                                                // For shortcuts with no component, keep them as they are
                                                restoredRows.add(id);
                                                restored = false;
                                            }
                                        } catch (URISyntaxException e) {
                                            Launcher.addDumpLog(TAG,
                                                    "Invalid uri: " + intentDescription, true);
                                            itemsToRemove.add(id);
                                            continue;
                                        }
    
                                        ...
    
                                        if (info != null) {
                                            info.id = id;
                                            info.intent = intent;
                                            info.container = container;
                                            info.screenId = c.getInt(screenIndex);
                                            info.cellX = c.getInt(cellXIndex);
                                            info.cellY = c.getInt(cellYIndex);
                                            info.rank = c.getInt(rankIndex);
                                            info.spanX = 1;
                                            info.spanY = 1;
                                            info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber);
                                            ...
    
                                            switch (container) {
                                                case LauncherSettings.Favorites.CONTAINER_DESKTOP:
                                                case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
                                                    sBgWorkspaceItems.add(info);
                                                    break;
                                                default:
                                                    // Item is in a user folder
                                                    FolderInfo folderInfo =
                                                            findOrMakeFolder(sBgFolders, container);
                                                    folderInfo.add(info);
                                                    break;
                                            }
                                            sBgItemsIdMap.put(info.id, info);
                                        } else {
                                            throw new RuntimeException("Unexpected null ShortcutInfo");
                                        }
                                        break;
    
                                    case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                                    
                                        ...
                                    
                                        break;
    
                                    case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                                    case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                                    
                                        ...
                                                                        break;
                                }
                            } catch (Exception e) {
                                Launcher.addDumpLog(TAG, "Desktop items loading interrupted", e, true);
                            }
                        }
                    } finally {
                        ...
                    }
    
                    ...
    
                    // 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;
                            }
                        }
                    }
    
                    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);
                    }
    
                    if (!isSdCardReady && !sPendingPackages.isEmpty()) {
                        context.registerReceiver(new AppsAvailabilityCheck(),
                                new IntentFilter(StartupReceiver.SYSTEM_READY),
                                null, sWorker);
                    }
    
                    // Remove any empty screens
                    ...
    
                    // If there are any empty screens remove them, and update.
                    if (unusedScreens.size() != 0) {
                        sBgWorkspaceScreens.removeAll(unusedScreens);
                        updateWorkspaceScreenOrder(context, sBgWorkspaceScreens);
                    }
    
                   ...
                }
            }
    
    

    首先是调用loadDefaultFavoritesIfNecessary这个方法,来解析我们上面讲的配置默认的桌面图标的xml文件,流程就是:初始化AutoInstallsLayout,然后调用LauncherProvider中的loadFavorites方法,在这个方法中调用AutoInstallsLayout中的loadLayout方法来解析配置的xml文件,在AutoInstallsLayout中通过对小部件,图标,文件夹等分类进行分辨解析,解析过程中如果有include标签,则对相应的xml文件进行解析,解析过程相对简单,不在做详细讲解,解析过程中将解析的各种信息存储到数据库中,以方便后面使用,当xml文件解析完成后,开始读取解析xml配置文件存储到数据库的数据,读取出来后,根据相应的类型(图标,小部件,文件夹等)进行判断,判断系统中这个应用是否存在,是否可用,如果可用则生成相应对象并存储到想定的map中,如果不存在则删除数据库中的数据,这样整个判断完成后数据库中的数据就只剩下系统中存在的配置应用过了。

    加载完配置应用图标后,开始执行bindWorkspace方法绑定应用图标到桌面,代码略过,我们看一下UML图:

    launcher14.png

    通过上面的时序图,我们看到,首先执行过滤工作,比如这个图标是在workspace中还是在Hotseat中,不同的位置放置不同的分类,然后进行排序处理,然后执行bindWorkspaceScreens方法来绑定手机有几个屏幕,接着调用bindWorkspaceItems方法绑定当前屏幕的图标、文件夹和小插件信息,最后调用绑定其他屏幕的应用图标、文件夹和小插件,关于绑定我们下一章再讲。

    接着执行LoadTask中的waitForIdle方法,改方法主要是等待加载数据结束。

    最后执行loadAndBindAllApps方法来加载第二层的多有图标信息,看代码:

     private void loadAndBindAllApps() {
                if (DEBUG_LOADERS) {
                    Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
                }
                if (!mAllAppsLoaded) {
                    loadAllApps();
                    synchronized (LoaderTask.this) {
                        if (mStopped) {
                            return;
                        }
                    }
                    updateIconCache();
                    synchronized (LoaderTask.this) {
                        if (mStopped) {
                            return;
                        }
                        mAllAppsLoaded = true;
                    }
                } else {
                    onlyBindAllApps();
                }
            }
    
    

    主要是如果已经加载了所有应用这只是执行绑定应用,如果没有加载则执行加载操作。下面看加载操作:

    private void loadAllApps() {
    
                ...
    
                final List<UserHandleCompat> profiles = mUserManager.getUserProfiles();
    
                // Clear the list of apps
                mBgAllAppsList.clear();
                for (UserHandleCompat user : profiles) {
                
                    ...
                
                   final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);
                    
                    // Fail if we don't have any apps
                    // TODO: Fix this. Only fail for the current user.
                    if (apps == null || apps.isEmpty()) {
                        return;
                    }
    
                    // 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));
                    }
                    
                ...
                
                // Huh? Shouldn't this be inside the Runnable below?
                final ArrayList<AppInfo> added = mBgAllAppsList.added;
                mBgAllAppsList.added = new ArrayList<AppInfo>();
    
                // 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");
                        }
                    }
                });
                ...
    
                loadAndBindWidgetsAndShortcuts(tryGetCallbacks(oldCallbacks), true /* refresh */);
                ...
            }
    
    

    上面代码中通过mLauncherApps.getActivityList方法获取所有应用启动界面的一个对象列表,然后根据LauncherActivityInfoCompat来初始化对应的app对象,这样就可以获取手机中所有的应用列表。获取完成后就执行绑定操作,最后调用loadAndBindWidgetsAndShortcuts方法加载绑定小部件和快捷方式到小部件界面。

    public void loadAndBindWidgetsAndShortcuts(final Callbacks callbacks, final boolean refresh) {
    
            runOnWorkerThread(new Runnable() {
                @Override
                public void run() {
                    updateWidgetsModel(refresh);
                    final WidgetsModel model = mBgWidgetsModel.clone();
    
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            Callbacks cb = getCallback();
                            if (callbacks == cb && cb != null) {
                                callbacks.bindAllPackages(model);
                            }
                        }
                    });
                    // update the Widget entries inside DB on the worker thread.
                    LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews(
                            model.getRawList());
                }
            });
        }
    
    

    在这个方法中首先调用updateWidgetsModel方法,代码如下:

     void updateWidgetsModel(boolean refresh) {
            PackageManager packageManager = mApp.getContext().getPackageManager();
            final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
            widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh));
            Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
            widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0));
            mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts);
        }
    
    

    上面代码中通过调用getWidgetProviders来获取所有小部件,通过shortcutsIntent来获取所有的跨界方式,最后通过mBgWidgetsModel.setWidgetsAndShortcuts方法把小部件和快捷方式放到WidgetsModel对象中,在后期加载中可以从这个里面获取小部件和快捷方式。

    到这整个launcher的数据加载基本就完成了,还有很多细节没有讲,xml解析等,这个谷歌工程师设计都是非常好的,有兴趣的可以看看源码。

    参考


    Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    Android应用程序组件Content Provider的启动过程源代码分析


    本文的源码是基于Android 6.0系统;

    Launcher源码:Launcher3_mx

    首发地址:墨香博客

    公众账号:Code-MX

    本文原创,转载请注明出处。

    相关文章

      网友评论

      • 鸩羽千夜92:LZ,系统源码中的Launcher3能放到AndroidStudio 中 编译跑起来吗
        翰墨飘香:@鸩羽千夜92 可以
      • Lollipop小良:大佬,萌新看哭了,能不能有简化版或者说解说版啊,你这样大段的贴代码,完全吃不消啊。
        翰墨飘香: @小生是良民 这个只是官方讲解没法简化,简化了更看不懂了,我看看画个流程图什么的吧,帮你分解一下
      • 小李子吃苹果:亲你的流程图和时序图使用啥画的?
        翰墨飘香:@小李子吃苹果 代码阅读软件Understand
        小李子吃苹果:@翰墨飘香 用的啥软件呀
        翰墨飘香: @小李子吃苹果 自动生成的
      • yunhen: :joy: 新手看不懂啊
        翰墨飘香:@yunhen 要照着源码跟一下才行

      本文标题:墨香带你学Launcher之(二)-数据加载流程

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