Android-Framework(十四)-Launcher的启动过程

文章内容

Launcher 启动过程的四个阶段

一、AMS.systemReady

桌面启动请求由 AMS 在 systemReady 里发出( frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 第 11810 行附近):

java
// systemReady 走到这里时,PMS、WMS、Zygote 都已可用// 标记系统正在完成启动,后面 activityIdle 会据此决定是否收尾mBooting = true;// 以当前用户身份启动 home,理由字符串会进入事件日志startHomeActivityLocked(mCurrentUserId, "systemReady");

startHomeActivityLocked 做的事分三件:

java
// 以 mTopAction 构造意图,mTopAction 默认是 ACTION_MAINIntent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);// mTopComponent 通常为空,非空时用于工厂测试指定组件intent.setComponent(mTopComponent);// 非低级工厂测试模式才把 CATEGORY_HOME 加进去if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {    // CATEGORY_HOME 是系统识别"桌面"的唯一标记    intent.addCategory(Intent.CATEGORY_HOME);// 结束工厂测试模式判断}// 返回给上层使用return intent;
  1. 拼home intent、让 PMS 解析出具体组件
  2. 交给 ActivityStackSupervisor 去启动
  3. 拿到 intent 之后,AMS 并不自己去找组件,而是把解析工作交回 PMS:
java
// 复用 getHomeIntent 的结果,避免再拼一次Intent intent = getHomeIntent();// 让 PMS 按 intent 的 action 与 category 解析出 ActivityInfoActivityInfo aInfo =    // 入参依次是意图、包管理查询标志与目标用户    resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);// 解析成功才继续,解析失败说明系统里没有声明 CATEGORY_HOME 的应用if (aInfo != null) {    // 用解析结果补上包名和类名,变成显式组件    intent.setComponent(new ComponentName(            // 用解析得到的包名与类名构造显式组件            aInfo.applicationInfo.packageName, aInfo.name));    // 复制一份 ActivityInfo,避免后续修改污染 PMS 内部对象    aInfo = new ActivityInfo(aInfo);    // 把 ApplicationInfo 换成对应用户的那一份    aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);    // 查这个组件所属进程是否已经在跑    ProcessRecord app = getProcessRecordLocked(aInfo.processName,            // 入参依次是进程名、uid,第三个 true 表示允许创建进程记录            aInfo.applicationInfo.uid, true);    // 只有在没有被 instrumentation 接管时才允许启动    if (app == null || app.instrumentationClass == null) {        // 桌面必须落在自己的任务栈里,NEW_TASK 是必需的        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);        // 交给 ActivityStackSupervisor,后面的流程和普通 startActivity 一致        mStackSupervisor.startHomeActivity(intent, aInfo, reason);    // 结束进程是否可启动的判断    }// 结束解析成功分支}

这里有两个容易忽略的细节:

  1. resolveActivityInfo 内部在组件为空时走的是 AppGlobals.getPackageManager().resolveIntent(...),也就是一次跨进程调用,解析结果是 PMS 里的 ActivityInfo,不是 Activity 对象。
  2. startHomeActivityLocked 之后真的启动 Activity 的仍然是 startActivityMayWait。

startHomeActivityLocked 这一行不是 systemReady 的结尾,方法在它之后还有约 45 行:两次用户启动广播、resumeTopActivitiesLocked() 和 sendUserSwitchBroadcastsLocked()。启动桌面的请求发在用户状态广播之前,所以桌面进程可能比 ACTION_USER_STARTED 更早开始创建。

从 systemReady 到 startHomeActivity 这条线横跨 system_server 内部两个进程内对象和一次 PMS 查询,调用顺序如下:

home intent 的解析与 Launcher 启动时序

二、PMS 如何回答"谁是桌面"

AMS 问的是"谁声明了 ACTION_MAIN 加 CATEGORY_HOME"。回答这个问题的数据在桌面的清单文件里,Launcher3 的 AndroidManifest.xml:

xml
<!-- 桌面主界面,系统启动后由 AMS 拉起 --><activity    <!-- 桌面组件全名 -->    android:name="com.android.launcher3.Launcher"    <!-- 全局只保留一个实例,避免返回键在多个桌面间跳 -->    android:launchMode="singleTask"    <!-- 从最近任务点进来时清掉旧任务 -->    android:clearTaskOnLaunch="true"    <!-- 不依赖保存的状态即可重建 -->    android:stateNotNeeded="true"    <!-- 不随传感器旋转,桌面由代码控制方向 -->    android:screenOrientation="nosensor"    <!-- 变成前台后立刻自绘,不等动画结束 -->    android:resumeWhilePausing="true"    <!-- 空 taskAffinity,保证桌面独立成一个任务 -->    android:taskAffinity=""    <!-- 允许被系统启动 -->    android:enabled="true">    <!-- 声明桌面入口过滤器 -->    <intent-filter>        <!-- 声明为入口动作 -->        <action android:name="android.intent.action.MAIN" />        <!-- 声明为 home,AMS 的 CATEGORY_HOME 靠这条命中 -->        <category android:name="android.intent.category.HOME" />        <!-- 让隐式意图能匹配到它 -->        <category android:name="android.intent.category.DEFAULT" />        <!-- monkey 压测时可以反复回到桌面 -->        <category android:name="android.intent.category.MONKEY"/>    <!-- 结束桌面入口过滤器 -->    </intent-filter><!-- 结束桌面 Activity 声明 --></activity>

这四条 category 里,真正决定"能不能当桌面"的只有 CATEGORY_HOME。讲者在素材里用删配置的方式说明过这一点:把 HOME 去掉再跑,AMS 查不到桌面,界面上就出不来东西。

这些 intent filter 是 PMS 在扫描包的时候写进内存索引的。PackageManagerService.java 里解析完一个包的 activity 列表后逐个注册:

java
// 遍历这个包里解析出来的所有 activityfor (i=0; i<N; i++) {    // 取出单个 activity 的解析结果    PackageParser.Activity a = pkg.activities.get(i);    // 补全进程名,未显式声明时继承 application 的进程名    a.info.processName = fixProcessName(pkg.applicationInfo.processName,            // 入参依次是应用进程名、activity 声明的进程名与应用 uid            a.info.processName, pkg.applicationInfo.uid);    // 注册到 ActivityIntentResolver,索引的就是这一步建的    mActivities.addActivity(a, "activity");

ActivityIntentResolver.addActivity 里对每个 intent filter 调 addFilter(intent),写入的键是组件名,值是 filter。到这里,索引已经建好了,但查询走的不是这条路径,而是 queryIntent。

queryIntentActivities 的入口在同一个文件里。素材里讲者反复强调 setPackage 传的是空值,对应的就是下面这段的 comp == null 与 pkgName == null 两个分支:

java
// 组件不为空时只查这一个组件,属于显式查询ComponentName comp = intent.getComponent();// 组件为空,说明是隐式查询,需要走过滤器匹配if (comp == null) {    // 意图带 selector 时以 selector 为准    if (intent.getSelector() != null) {        // 换成 selector 对应的意图        intent = intent.getSelector();        // 取出 selector 里的组件        comp = intent.getComponent();    // 结束 selector 判断    }// 结束组件为空分支}

隐式查询最终落到 ActivityIntentResolver.queryIntent,再进 IntentResolver.queryIntent 做过滤器匹配。Launcher3 在 6.0 上查询桌面的图标时同样走这条路径,只是 action 和 category 换成了 ACTION_MAIN 加 CATEGORY_LAUNCHER,并且 setPackage 仍然为 null。

同一个索引既用于"谁是桌面",也用于"桌面要显示哪些图标",区别只在 category:

intent filter 的注册与查询

三、Launcher 进程创建后的第一件事:onCreate 与 startLoader

进程由 AMS 创建完之后,Launcher 的 onCreate 被执行。真正和"桌面能不能用"相关的是后半段:

java
// 加载桌面主布局setContentView(R.layout.launcher);// 找到各个子视图并挂上监听器setupViews();// 按当前设备规格摆放图标栅格mDeviceProfile.layout(this);// 在数据加载完成前锁住所有应用视图,防止点进空列表lockAllApps();// 记下传入的保存状态,供 restoreState 使用mSavedState = savedInstanceState;// 恢复上次的页面与状态restoreState(mSavedState);

lockAllApps 紧接在 setupViews 后面出现,原因是此时应用列表还是空的。填充动作被推迟到 startLoader:

java
// mRestoring 为 true 表示正在恢复状态,此时不要并行加载if (!mRestoring) {    // 默认配置下不同步绑定当前页    if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) {        // 异步加载全部数据        mModel.startLoader(PagedView.INVALID_RESTORE_PAGE);    // 数据未就绪时改走异步加载    } else {        // 只在配置变化回来时同步绑定恢复页        mModel.startLoader(mWorkspace.getRestorePage());    // 结束默认配置分支    }// 结束恢复状态判断}

LauncherModel.startLoader 要做的事是把一个 LoaderTask 投到工作线程,而不是在当前线程上直接查 PMS:

java
// 加载期间先拦住安装快捷方式的广播,避免列表被并发修改InstallShortcutReceiver.enableInstallQueue();// 加载与绑定是并发动作,加锁保护相关状态synchronized (mLock) {    // 上一轮遗留的延迟绑定任务必须先清掉    synchronized (mDeferredBindRunnables) {        // 清空延迟绑定队列        mDeferredBindRunnables.clear();    // 结束延迟绑定队列清理    }    // 没有回调说明 Launcher 已经退出,直接不启动    if (mCallbacks != null && mCallbacks.get() != null) {        // 已有任务在跑时先让它停下        stopLoaderLocked();        // 建一个加载任务,后续在工作线程上执行        mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags);        // 恢复页合法且数据已就绪时走同步绑定        if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE                // 续上一行:恢复页合法与两份数据就绪这三个条件要同时满足                && mAllAppsLoaded && mWorkspaceLoaded && !mIsLoaderTaskRunning) {            // 同步绑定指定页,常用于旋转后立即出图            mLoaderTask.runBindSynchronousPage(synchronousBindPage);        // 数据未全部就绪时改为投递到工作线程        } else {            // 常规路径:把任务投递到工作线程            sWorkerThread.setPriority(Thread.NORM_PRIORITY);            // 投递任务,工作线程随后执行 LoaderTask.run            sWorker.post(mLoaderTask);        // 结束同步绑定分支        }    // 结束回调存在判断    }// 结束工作线程锁}

LoaderTask.run 的顺序是先 loadAndBindWorkspace 再 loadAndBindAllApps,中间用 waitForIdle 等桌面这一轮绘制结束。讲者在素材里提到"先恢复桌面再加载应用"时说的就是这层先后关系:工作区的数据量比应用列表小,而且用户第一眼看到的是工作区,先出它能让开机看起来更快。

四、应用列表从哪来:LoaderTask.loadAllApps

应用列表的来源是 loadAllApps,它是整篇文章里最关键的一段:

java
// 取当前设备上的用户与托管资料,资料都会有自己的应用列表final List<UserHandleCompat> profiles = mUserManager.getUserProfiles();// 清空后台线程维护的应用列表,避免重复条目mBgAllAppsList.clear();// 逐个用户查询for (UserHandleCompat user : profiles) {    // 查询动作跨进程到 PMS,耗时需要打点    final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;    // packageName 传 null 表示不限包名;由兼容层决定用哪套 API    final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);    // 一个应用都查不到时直接返回,说明系统状态不正常    if (apps == null || apps.isEmpty()) {        // 一个可启动入口都查不到时直接返回        return;    // 结束空结果判断    }    // 逐个包装成 Launcher 自己的 AppInfo    for (int i = 0; i < apps.size(); i++) {        // 取出一个可启动的 activity 信息        LauncherActivityInfoCompat app = apps.get(i);        // 构造过程中会取图标位图并生成启动意图        mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache));    // 结束单个用户的包装循环    }// 结束用户循环}

mLauncherApps 是 LauncherAppsCompat 的实例,它按系统版本分派到不同实现。6.0 走的是 LauncherAppsCompatVL,更早的版本走 LauncherAppsCompatV16。分派逻辑在 LauncherAppsCompat.getInstance() 里,判据是 Utilities.isLmpOrAbove()。VL 分支本身没有实现查询逻辑,它把请求转给 LauncherApps 这个系统服务,最终的匹配仍然落在 PMS 的 queryIntentActivities 上。素材里讲者翻的是 V16,因为它的实现把整个查询过程写得最清楚:

java
// 查询某用户下可启动的 activity 列表public List<LauncherActivityInfoCompat> getActivityList(String packageName,        // 续上一行:第二个参数是目标用户        UserHandleCompat user) {    // 主入口意图,data 传 null    final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);    // 加上桌面图标分类,只有声明了 LAUNCHER 的 activity 会被返回    mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);    // 传 null 时不限包名,等于查全系统    mainIntent.setPackage(packageName);    // 跨进程调用 PMS 完成过滤    List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0);    // 预分配结果列表    List<LauncherActivityInfoCompat> list =            // 按查询结果数量预分配列表容量            new ArrayList<LauncherActivityInfoCompat>(infos.size());    // 逐条包装,包装类只持有数据和 Context,不持有 Activity    for (ResolveInfo info : infos) {        // 每个匹配结果对应一个可启动的入口        list.add(new LauncherActivityInfoCompatV16(mContext, info));    // 结束结果包装循环    }    // 返回给 LauncherModel    return list;// 结束 getActivityList}

LauncherActivityInfoCompatV16 只是把 ResolveInfo 包一层,读的都是 ActivityInfo 和 PMS 的公开接口。它提供四样东西给上层:getComponentName() 决定点击后要启哪一项,getLabel() 是图标下面的文字,getIcon(density) 按密度取图标,getApplicationInfo() 供后续判断是不是系统应用。

包装成 AppInfo 时,图标取好、启动意图也顺手生成了:

java
// 组件名来自 PMS 解析结果,就是点击时要启动的那一项this.componentName = info.getComponentName();// 未归属任何文件夹this.container = ItemInfo.NO_ID;// 记录是下载应用还是系统应用,影响长按菜单flags = initFlags(info);// 首次安装时间,用于排序firstInstallTime = info.getFirstInstallTime();// 第三个入参 true 表示先取低分辨率图标,真正的位图随后补上iconCache.getTitleAndIcon(this, info, true);// 生成点击时使用的启动意图intent = makeLaunchIntent(context, info, user);// 记录该条目属于哪个用户this.user = user;

makeLaunchIntent 是第六节的主角。第六节会用到的那份启动意图就是在这一步生成的:图标在列表填充阶段就拿到了完整的启动意图,点击时不需要再问 PMS。

五、从 AppInfo 到屏幕上的图标

工作线程拿到全部 AppInfo 之后,通过 bindAllApplications 交回主线程:

java
// 交给主线程绑定,列表要改视图必须在主线程final ArrayList<AppInfo> added = mBgAllAppsList.added;// 清空 added,避免同一批数据被绑定两次mBgAllAppsList.added = new ArrayList<AppInfo>();// post 到主线程mHandler.post(new Runnable() {    // 匿名 Runnable 的 run 方法,实际绑定在主线程执行    public void run() {        // 取回调,Launcher 退出后可能已经为空        final Callbacks callbacks = tryGetCallbacks(oldCallbacks);        // 回调存在才真正刷新界面        if (callbacks != null) {            // 回调进 Launcher.bindAllApplications            callbacks.bindAllApplications(added);        // 结束回调存在分支        }    // 结束 run 方法    }// 结束 Runnable 匿名类});

Launcher.bindAllApplications 把列表交给所有应用视图:

java
// 界面还没回来时先存起来,等 onResume 再刷if (waitUntilResume(mBindAllApplicationsRunnable, true)) {    // 暂存这批数据    mTmpAppsList = apps;    // 提前返回,避免在不可见时做无用绘制    return;// 结束状态恢复中的早退分支}// mAppsView 就是全部应用界面,空判断防止布局未创建if (mAppsView != null) {    // 交给 AllAppsContainerView,它的内部结构不是简单的一层列表    mAppsView.setApps(apps);// 结束 mAppsView 空判断}

AllAppsContainerView 的 setApps 只转一道手:

java
// 把应用列表交给内部的 AlphabeticalAppsListpublic void setApps(List<AppInfo> apps) {    // mApps 负责排序、分区和生成适配器条目    mApps.setApps(apps);// 结束 setApps}

真正把一长串 AppInfo 变成"第几行第几列放谁"的是 AlphabeticalAppsList。它按名称排序、按首字母或分区规则切成 section,再把 section 头、行、图标展开成 AdapterItem 列表,同时生成给右侧快速滚动条用的 FastScrollSectionInfo。AllAppsGridAdapter 只是这个列表的呈现层:

java
// 按条目类型创建视图,网格里绝大多数是图标public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {    // 按条目类型分发,网格里绝大多数是图标    switch (viewType) {        // 图标条目,布局是单个图标加标题        case ICON_VIEW_TYPE: {            // 从布局里加载出 BubbleTextView            BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(                    // 续上一行:从布局里加载出图标条目                    R.layout.all_apps_icon, parent, false);            // 触摸、点击、长按三个监听器都由 Launcher 提供            icon.setOnTouchListener(mTouchListener);            // 点击监听器最终会调到 Launcher.onClickAppShortcut            icon.setOnClickListener(mIconClickListener);            // 长按监听器用于卸载与快捷方式菜单            icon.setOnLongClickListener(mIconLongClickListener);            // 设置长按超时,保持和系统一致            icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())                    // 续上一行:取系统的长按超时时间                    .getLongPressTimeout());            // 让图标可以被键盘和遥控器聚焦            icon.setFocusable(true);            // 包装成 ViewHolder 返回            return new ViewHolder(icon);        // 结束图标条目分支        }

绑定数据时从适配器条目里取出 AppInfo,再交给 BubbleTextView:

java
// 图标条目走这里,从适配器条目取回 AppInfocase ICON_VIEW_TYPE: {    // 位置索引到 AdapterItem,再取到应用信息    AppInfo info = mApps.getAdapterItems().get(position).appInfo;    // 视图在创建时已经固定是 BubbleTextView    BubbleTextView icon = (BubbleTextView) holder.mContent;    // 一次性写入图标、标题与内容描述    icon.applyFromApplicationInfo(info);    // 本条绑定结束    break;// 结束图标条目分支}

applyFromApplicationInfo 里的 setTag(info) 直接决定了后面的点击流程:

java
// 图标位图换成 drawable 并设置尺寸setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize);// 写入标题文字setText(info.title);// 内容描述用于无障碍朗读if (info.contentDescription != null) {    // 仅在存在时写入,避免覆盖默认值    setContentDescription(info.contentDescription);// 结束内容描述判断}// 把 AppInfo 挂到视图的 tag 上,点击时靠它取回数据super.setTag(info);

setTag(info) 这一行解释了后面点击处理里为什么会出现 tag instanceof AppInfo 的判断:图标视图本身不存任何应用数据,所有信息都挂在这一个 tag 上。

素材第二段 0:25 前后,讲者从 AllAppsContainerView 的构造过程里翻出一处写法并给出了自己的判断:这个视图从 Context 强转出 Launcher 再直接持有,等于把自定义 view 和一个具体的 Activity 绑死。他的结论是这种写法在系统代码里能跑,因为这个 view 只服务一个 Activity,但换到别的界面就复用了,因此不建议在业务代码里照抄。这个判断可以独立看待:它不影响功能正确性,影响的是复用范围。

到这里,应用列表的数据流已经完整:PMS 提供查询结果,兼容层包装,AppInfo 承载数据与启动意图,AlphabeticalAppsList 负责排版,AllAppsGridAdapter 与 BubbleTextView 负责呈现。

Launcher 从 onCreate 到图标上屏的调用链

六、点一下图标发生了什么:makeLaunchIntent 与 startActivitySafely

点击链路的第一跳是 AllAppsGridAdapter 在创建条目时设的 mIconClickListener,它由 Launcher 提供,最终进入 Launcher.onClickAppShortcut,再转到 startAppShortcutOrInfoActivity:

java
// 从视图的 tag 上取回数据,tag 可能是 ShortcutInfo 也可能是 AppInfoObject tag = v.getTag();// 分支一:桌面快捷方式if (tag instanceof ShortcutInfo) {    // 强转出快捷方式数据    shortcut = (ShortcutInfo) tag;    // 快捷方式自带意图    intent = shortcut.intent;    // 取出视图在屏幕上的位置    int[] pos = new int[2];    // 位置会作为启动动画的起点    v.getLocationOnScreen(pos);    // 把位置写进意图的 sourceBounds,供转场动画使用    intent.setSourceBounds(new Rect(pos[0], pos[1],            // 续上一行:右下角坐标            pos[0] + v.getWidth(), pos[1] + v.getHeight()));// 分支二:全部应用列表里的图标} else if (tag instanceof AppInfo) {    // 没有快捷方式数据,运行时需要置空    shortcut = null;    // 直接使用第四步里已经建好的启动意图    intent = ((AppInfo) tag).intent;// 其余类型一律视为调用方传错视图} else {    // 两种都不是说明调用方传错了视图    throw new IllegalArgumentException("Input must be a Shortcut or AppInfo");// 结束类型判断链}

AppInfo 走的是 ((AppInfo) tag).intent,也就是 makeLaunchIntent 在列表填充阶段生成的那个意图:

java
// 取当前用户的序列号,多用户下用于指明启动到哪个用户long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user);// 入口动作return new Intent(Intent.ACTION_MAIN)    // 目标必须声明 CATEGORY_LAUNCHER,否则系统找不到组件    .addCategory(Intent.CATEGORY_LAUNCHER)    // 显式指定组件,包名加类名都来自 PMS 的解析结果    .setComponent(info.getComponentName())    // NEW_TASK 必需,RESET_TASK_IF_NEEDED 让已存在的任务被带到前台并复位    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)    // 附带用户序列号,供 Launcher 在启动前判断是否跨用户    .putExtra(EXTRA_PROFILE, serialNumber);

FLAG_ACTIVITY_NEW_TASK 在这里不是可选项。Launcher 的 task 和待启动应用的 task 是两个任务,不给这个标志,系统会尝试把目标 Activity 放进 Launcher 自己的任务栈里,而目标 Activity 的 taskAffinity 又和 Launcher 不同,结果是行为不可预期。FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 解决的是另一个场景:这个应用已经在后台有任务时,应该把原有任务带到前台并复位,而不是新建一个。

意图拿到手之后交给 startActivitySafely,内部走 Launcher.startActivity:

java
// 这里再补一次 NEW_TASK,防止调用方传入的意图漏掉intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 捕获安全异常:目标没有导出或没声明入口时会抛try {    // 只有带视图且在 4.4 以下才退化成缩放动画    boolean useLaunchAnimation = (v != null) &&            // 续上一行:调用方没有显式要求跳过启动动画            !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION);    // 意图里带着用户序列号时,说明可能不是当前用户    if (intent.hasExtra(AppInfo.EXTRA_PROFILE)) {        // 还原出目标用户        long serialNumber = intent.getLongExtra(AppInfo.EXTRA_PROFILE, -1);        // 按序列号取用户句柄        user = userManager.getUserForSerialNumber(serialNumber);    // 结束用户序列号判断    }    // 启动动画的参数包    Bundle optsBundle = null;    // 6.0 上优先使用以图标为圆心的 clip reveal 动画    if (useLaunchAnimation) {        // 反射调用 makeClipRevealAnimation,因为 6.0 之前的 API 里没有它        opts = (ActivityOptions) sClipRevealMethod.invoke(null, v,                // 续上一行:以图标为中心发起揭示动画                left, top, width, height);        // 反射失败时退化为普通启动        optsBundle = opts != null ? opts.toBundle() : null;    // 结束启动动画构造    }    // 同用户时走标准路径    if (user == null || user.equals(UserHandleCompat.myUserHandle())) {        // 这一步才真正发出跨进程请求,由 AMS 创建或复用目标进程        startActivity(intent, optsBundle);    // 跨用户时交回兼容层处理    } else {        // 跨用户时交回兼容层处理        launcherApps.startActivityForProfile(intent.getComponent(), user,                // 续上一行:带上源位置与动画参数                intent.getSourceBounds(), optsBundle);    // 结束同用户判断    }    // 启动成功    return true;// 目标组件未导出或没有 MAIN 入口时进入这里} catch (SecurityException e) {    // 提示用户应用不可用    Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();

素材第二段 0:43 处讲者提到一个没验证过的想法:把 startActivity 换成 startService 看看会怎样。这一条源素材没有给出结论,这里也不做推断,仅记录为未验证项。

从图标点击到进程创建,两端间隔着三次跨进程调用,链条如下:

点击图标启动应用的 Intent 组成与去向

七、开机动画进程:init.rc 里被 disabled 的 service

开机动画是一个独立的原生进程,声明在 system/core/rootdir/init.rc:

plain text
# 开机动画进程,可执行文件在 /system/bin 下service bootanim /system/bin/bootanimation    # 归入 core 类,随核心服务一起解析    class core    # 以 graphics 用户身份运行    user graphics    # 附加 graphics 与 audio 两个用户组    group graphics audio    # 只登记不启动,需要时由属性控制接口拉起    disabled    # 退出后不自动重启,避免开机动画反复出现    oneshot

关键的是 disabled。它表示 init 解析到这一段时只登记服务,不在 class core 启动时把它拉起来。oneshot 表示进程退出后不自动重启。素材第三段里讲者用"解析到但是不会启动它"来描述这一行,对应的就是 init 的这套语义。

那谁负责启动它?答案是 SurfaceFlinger,在 frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp 的 init() 末尾:

cpp
// 设置初始显示状态,例如默认不熄屏initializeDisplays(); // 启动开机动画startBootAnim();

startBootAnim 只有两行,但它是整个"起"的逻辑所在:

cpp
// 开机动画的启动入口,实现只有两行void SurfaceFlinger::startBootAnim() {    // 先把退出开关清零,防止上一次启动留下的值让动画立刻退出    property_set("service.bootanim.exit", "0");    // 通过属性控制接口请求 init 启动 bootanim 服务    property_set("ctl.start", "bootanim");// 结束 startBootAnim}

ctl.start 这个属性名不是给应用读的,是 init 的属性服务专门监听的控制通道。init 收到之后走 system/core/init/init.cpp 里的控制消息分发:

cpp
// 属性服务把 ctl.<命令> 拆出来,start 对应 msg_startvoid handle_control_message(const char *msg, const char *arg)// 控制消息分发从这里开始{    // 请求启动服务    if (!strcmp(msg,"start")) {        // arg 是服务名,这里是 bootanim        msg_start(arg);    // 请求停止服务    } else if (!strcmp(msg,"stop")) {        // 停止指定服务        msg_stop(arg);    // 请求重启服务    } else if (!strcmp(msg,"restart")) {        // 重启指定服务        msg_restart(arg);

msg_start 按名字找到服务并启动:

cpp
// 按名字查服务,名字里带冒号时冒号后面是参数if (!strchr(name, ':'))    // 无参数,直接按名字查    svc = service_find_by_name(name);// 名字里带冒号时说明后面跟了参数else {    // 复制一份再切分,避免修改原字符串    tmp = strdup(name);    // 切出服务名    if (tmp) {        // 找到冒号位置        args = strchr(tmp, ':');        // 截断成服务名        *args = '\0';        // 冒号后一位是参数的开始        args++;        // 用服务名查表        svc = service_find_by_name(tmp);    // 结束带参数分支    }// 结束名字判断} // 查到才启动if (svc) {    // 进入 service_start,内部会 fork 后 execve 出进程    service_start(svc, args);// 服务不存在时走错误分支} else {    // 查不到说明属性名写错了    ERROR("no such service '%s'\n", name);// 结束服务查找结果判断}

service_start 内部按 disabled 标志把服务状态改成运行中,然后 fork 出子进程,在子进程里 setgid、setuid、setexeccon 之后 execve 到 /system/bin/bootanimation。素材第三段把这一段跟踪得比较细,包括 publish_socket、add_environment、SVC_DISABLED 到 SVC_RUNNING 的状态切换,这些都属于 init 服务管理本身的细节,与本文主线只在这一处相关。

SurfaceFlinger 为什么必须先把动画进程拉起来再往下走,讲者在素材第三段给了一个直接的原因:开机动画需要 OpenGL 环境和一块可以提交给合成器的缓冲区,而这两样东西只能向 SurfaceFlinger 申请。

bootanim 从 init.rc 到进程 exec 的启动链

八、开机动画画在哪:readyToRun 与 EGL

bootanimation 的 main 在正常路径上只做三件事:设置显示优先级、启动 Binder 线程池、创建 BootAnimation 对象。这三件事整体被一个属性判断包住:

cpp
// 设置进程优先级为显示级,保证动画不被普通后台任务抢占setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY); // 读调试开关,决定是否跳过整段开机动画char value[PROPERTY_VALUE_MAX];// 属性不存在时按 0 处理,也就是照常播放property_get("debug.sf.nobootanimation", value, "0");// 转成整数,非 0 表示关闭动画int noBootAnimation = atoi(value);// 只在关闭时打一行日志,不影响判断ALOGI_IF(noBootAnimation,  "boot animation disabled");// 开关为 0 时才走原来的三步if (!noBootAnimation) {     // 初始化 ProcessState,同时打开 binder 驱动    sp<ProcessState> proc(ProcessState::self());    // 启动 binder 线程池,后续与 SurfaceFlinger 的调用都走 binder    ProcessState::self()->startThreadPool();     // 构造 BootAnimation,Thread 基类的引用计数归零前不会退出    sp<BootAnimation> boot = new BootAnimation();     // 主线程加入 binder 线程池,进程在此常驻    IPCThreadState::self()->joinThreadPool(); // 结束开关判断}// 开关非 0 时直接返回,BootAnimation 对象不会被创建return 0;

debug.sf.nobootanimation 这个开关的作用范围需要说清楚:它非零时进程在创建 BootAnimation 之前就返回了,readyToRun 和 threadLoop 根本不会执行。第十一节里 adb shell setprop debug.sf.nobootanimation 1 能缩短开机时间,靠的就是整段初始化被跳过,而不是动画播放得更快。

BootAnimation 继承自 Thread,sp 智能指针第一次赋值时会触发 onFirstRef,这里才真正进入动画循环:

cpp
// 智能指针首次引用时回调,此时可以安全使用 thisvoid BootAnimation::onFirstRef() {    // 监听 SurfaceFlinger 的死亡,SF 挂掉时动画进程也要收尾    status_t err = mSession->linkToComposerDeath(this);    // 只需要打印,失败不阻断启动    ALOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err));    // 注册成功才让线程跑起来    if (err == NO_ERROR) {        // Thread::run 会先执行 readyToRun,再循环 threadLoop        run("BootAnimation", PRIORITY_DISPLAY);    // 结束注册成功分支    }// 结束 onFirstRef}

readyToRun 是准备阶段,做的事情按顺序如下:

cpp
// 取主显示器的句柄sp<IBinder> dtoken(SurfaceComposerClient::getBuiltInDisplay(        // 续上一行:主显示器的显示类型        ISurfaceComposer::eDisplayIdMain));// 读取分辨率等信息,后面建 Surface 和 EGL 都要用DisplayInfo dinfo;// 取不到显示信息就直接失败退出status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &dinfo);// 失败时返回错误,Thread 不会进入 threadLoopif (status)    // 取不到显示信息时直接失败退出    return -1; // 向 SurfaceFlinger 申请一块全屏的绘制目标sp<SurfaceControl> control = session()->createSurface(String8("BootAnimation"),        // 续上一行:全屏尺寸与 RGB565 像素格式        dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565); // 打开全局事务,图层属性修改要成批提交SurfaceComposerClient::openGlobalTransaction();// 把图层放在很高的层级上,保证盖在其它窗口之上control->setLayer(0x40000000);// 提交事务SurfaceComposerClient::closeGlobalTransaction(); // 取出可写入的 Surfacesp<Surface> s = control->getSurface();

随后初始化 EGL 环境,把这块 Surface 设成 GL 的绘制目标:

cpp
// 取默认显示设备EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);// 初始化 EGLeglInitialize(display, 0, 0);// 选一个匹配的帧缓冲配置eglChooseConfig(display, attribs, &config, 1, &numConfigs);// 用上面申请的 Surface 创建窗口表面surface = eglCreateWindowSurface(display, config, s.get(), NULL);// 创建 GL 上下文context = eglCreateContext(display, config, NULL, NULL);// 把上下文设为当前,之后的 GL 调用都作用在这块表面上if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)    // 上下文无法激活时直接失败退出    return NO_INIT;

最后按固定顺序找动画资源包:

cpp
// 读系统属性判断是否处于加密流程property_get("vold.decrypt", decrypt, "");// 加密或即将重启到框架时使用加密专用的动画bool encryptedAnimation = atoi(decrypt) != 0 || !strcmp("trigger_restart_min_framework", decrypt);// 优先加密动画,其次厂商目录,最后系统目录if ((encryptedAnimation &&        // 续上一行:加密流程下优先使用加密动画包        (access(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE, R_OK) == 0) &&        // 续上一行:打开加密动画包        ((zipFile = ZipFileRO::open(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE)) != NULL)) ||         // 其次尝试厂商目录下的动画包        ((access(OEM_BOOTANIMATION_FILE, R_OK) == 0) &&        // 续上一行:打开厂商动画包        ((zipFile = ZipFileRO::open(OEM_BOOTANIMATION_FILE)) != NULL)) ||         // 最后尝试系统默认的动画包        ((access(SYSTEM_BOOTANIMATION_FILE, R_OK) == 0) &&        // 续上一行:打开系统动画包        ((zipFile = ZipFileRO::open(SYSTEM_BOOTANIMATION_FILE)) != NULL))) {    // 找到的包记下来,threadLoop 里根据它决定走 movie 还是内置动画    mZip = zipFile;// 结束资源包查找}

三个路径在同一个文件顶部定义,都是明文常量:

cpp
// 厂商可覆盖的开机动画包#define OEM_BOOTANIMATION_FILE "/oem/media/bootanimation.zip"// 系统默认的开机动画包,也是替换动画时最常改的那个文件#define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip"// 加密流程下展示的动画包#define SYSTEM_ENCRYPTED_BOOTANIMATION_FILE "/system/media/bootanimation-encrypted.zip"// 退出开关的属性名,SurfaceFlinger 和 bootanimation 共用#define EXIT_PROP_NAME "service.bootanim.exit"

素材第二段末尾提出的那个面试问题(能不能替换开机启动画面),答案在第三段给出,就落在这三个常量上:换掉 /system/media/bootanimation.zip 就能换画面,前提是包里 desc.txt 的格式和分辨率写法正确。

动画包是纯 ZIP,不是视频文件。movie() 先读 desc.txt:

cpp
// 读描述文件,读不到就直接退出if (!readFile("desc.txt", desString)) {    // 读不到描述文件时直接返回    return false;// 结束描述文件判断}// 取字符串首地址,逐行解析char const* s = desString.string();

描述文件的第一行是宽高和帧率,后面每行描述一个 part:

cpp
// 一行三个整数时按 宽 高 帧率 解析if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) {    // 记录动画画布宽度    animation.width = width;    // 记录动画画布高度    animation.height = height;    // 记录帧率,决定每帧的间隔    animation.fps = fps;// 结束宽高帧率解析分支}// 否则按 part 描述解析:类型 次数 暂停 目录 背景色else if (sscanf(l, " %c %d %d %s #%6s", &pathType, &count, &pause, path, color) >= 4) {    // 类型为 c 时表示这一 part 要完整播完才结束    part.playUntilComplete = pathType == 'c';    // 播放次数,0 表示无限循环    part.count = count;    // 每轮之间的暂停帧数    part.pause = pause;    // 帧图片所在的目录名    part.path = path;    // 背景色,缺省黑色    if (!parseColor(color, part.backgroundColor)) {        // 颜色非法时退化成黑色        part.backgroundColor[0] = 0.0f;

每一帧图片在播放前被解码成纹理,initTexture 里用的是 SkImageDecoder:

cpp
// 从包里取出一张图片资源Asset* asset = assets.open(name, Asset::ACCESS_BUFFER);// 取不到直接返回if (asset == NULL)    // 取不到图片资源时直接返回    return NO_INIT;// 解码成位图,解码模式为取出像素SkBitmap bitmap;// 把资源内容解码成位图SkImageDecoder::DecodeMemory(asset->getBuffer(false), asset->getLength(),        // 续上一行:解码模式为取出像素        &bitmap, kUnknown_SkColorType, SkImageDecoder::kDecodePixels_Mode);// 生成一张 GL 纹理并绑定glGenTextures(1, &texture->name);// 绑定到二维纹理目标glBindTexture(GL_TEXTURE_2D, texture->name);

到这一步,动画的每一帧都以纹理的形式存在于 bootanimation 进程里,但屏幕上的像素最终由 SurfaceFlinger 合成。两边的关系是固定的:bootanimation 只负责往自己申请的 Surface 上画,画完用 eglSwapBuffers 提交,SurfaceFlinger 拿着这条 layer 和系统里其它 layer 一起合成,再通过 HWComposer 写到屏幕上。

启动动画之所以必须画在 SurfaceFlinger 的图层上,而不是自己开个窗口,原因是屏幕内容的唯一出口就是合成器。讲者在素材第三段用一张示意图说明过这层关系:多个 layer 合成后写入屏幕,任何应用自己单独绘制的画面如果不提交到 SurfaceFlinger,都不可能出现在屏幕上。这也解释了后面高频提到的 16.67 毫秒:android() 分支里的合成节奏就写在这个常量上:

cpp
// 以 16667 微秒为一帧,约合 60 帧每秒float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;

开机动画的绘制与合成关系

九、开机动画如何退出:属性开关与 checkExit

Thread::run 在 readyToRun 之后循环调用 threadLoop。BootAnimation::threadLoop 只做分流:

cpp
// 没有动画包时走内置的 android 标志动画if (mZip == NULL) {    // 内置动画:一个会反光的 android 字样    r = android();// 没有动画包时改用内置动画} else {    // 有动画包时按 desc.txt 播放    r = movie();// 结束播放分支}

两个分支的循环体里都调用了同一个退出检查:

cpp
// 本轮绘制完成后检查是否需要退出checkExit();

checkExit 的实现是全文最短、也是"关闭开机画面"链路的终点:

cpp
// 退出检查,由 SurfaceFlinger 请求触发void BootAnimation::checkExit() {    // 允许 SurfaceFlinger 请求动画优雅退出    char value[PROPERTY_VALUE_MAX];    // 读退出开关,默认值为 0    property_get(EXIT_PROP_NAME, value, "0");    // 转成整数    int exitnow = atoi(value);    // 非 0 表示要退出    if (exitnow) {        // 请求退出,循环条件里的 exitPending 随之变真        requestExit();        // 有音频播放器时一并请求退出        if (mAudioPlayer != NULL) {            // 音频也要停,否则会有残留播放            mAudioPlayer->requestExit();        // 结束音频退出判断        }    // 结束退出判断    }// 结束 checkExit}

退出开关就是第七节里被 SurfaceFlinger 先置 0 的那个 service.bootanim.exit。整个动画的出入口都被这一个属性控制:SurfaceFlinger 启动动画前置 0,关闭动画时置 1。写属性的那一端在下一节。

threadLoop 返回之后,Thread 的收尾代码把 EGL 与 Surface 全部释放:

cpp
// 解开当前的 GL 上下文eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);// 销毁上下文eglDestroyContext(mDisplay, mContext);// 销毁窗口表面eglDestroySurface(mDisplay, mSurface);// 释放 SurfaceFlinger 侧的 Surface 与 SurfaceControlmFlingerSurface.clear();// 释放图层控制对象mFlingerSurfaceControl.clear();// 结束 EGLeglTerminate(mDisplay);// 退出 binder 线程池,进程随后结束IPCThreadState::self()->stopProcess();

十、关闭开机画面的链路:一个小 idle 回调的连锁反应

关闭开机画面的触发点不在 SurfaceFlinger,也不在 bootanimation,而在 Launcher 的 onResume 走完之后。这一段是本文四个步骤里跨进程次数最多的。

起点在 ActivityThread 的 handleResumeActivity:

java
// 空闲回调只在主线程消息队列没有消息要处理时才执行Looper.myQueue().addIdleHandler(new Idler());

Idler 是 MessageQueue.IdleHandler 的实现,主线程把这一帧的绘制和布局都处理完之后才会轮到它:

java
// 主线程空闲时回调,返回 false 表示只执行一次public final boolean queueIdle() {    // 取出待上报的 Activity 记录    ActivityClientRecord a = mNewActivities;    // 有待上报的记录时逐个通知 AMS    if (a != null) {        // 清空记录,避免重复上报        mNewActivities = null;        // 取 AMS 的代理        IActivityManager am = ActivityManagerNative.getDefault();        // 链表式遍历        do {            // 未 finish 的 Activity 才上报            if (a.activity != null && !a.activity.mFinished) {                // 跨进程通知 AMS 这个 Activity 已经空闲                am.activityIdle(a.token, a.createdConfig, stopProfiling);                // 上报后清掉配置,避免下次重复比较                a.createdConfig = null;            // 结束上报循环体            }            // 指向下一个            prev = a;            // 前进            a = a.nextIdle;            // 断开已处理节点的链接            prev.nextIdle = null;        // 链表遍历的结束条件        } while (a != null);    // 结束遍历    }    // 返回 false 表示这个 handler 不重复注册    return false;// 结束 queueIdle}

activityIdle 进入 AMS 的 activityIdleInternalLocked,后者在返回前调用了 ActivityStackSupervisor.checkFinishBootingLocked():

java
// 判断本次空闲是否意味着启动流程可以收尾private boolean checkFinishBootingLocked() {    // 记下 mBooting 的当前值,它由第一节的 systemReady 置位    final boolean booting = mService.mBooting;    // 是否需要开启屏幕    boolean enableScreen = false;    // 收尾只做一次,先把标志清掉    mService.mBooting = false;    // mBooted 为 false 说明这是第一次走到这里    if (!mService.mBooted) {        // 置位,防止重复触发        mService.mBooted = true;        // 这一轮需要开屏        enableScreen = true;    // 结束首次开屏判断    }    // 任一条件成立就通知 AMS    if (booting || enableScreen) {        // 两个布尔值分别决定是否收尾、是否开屏        mService.postFinishBooting(booting, enableScreen);    // 结束收尾通知判断    }    // 返回本次是否消费了 mBooting    return booting;// 结束 checkFinishBootingLocked}

postFinishBooting 把这两个布尔值塞进消息,交给 AMS 主线程处理:

java
// 参数用 arg1 和 arg2 传递,避免额外分配对象mHandler.sendMessage(mHandler.obtainMessage(FINISH_BOOTING_MSG,        // 续上一行:两个布尔值分别作为 arg1 与 arg2        finishBooting ? 1 : 0, enableScreen ? 1 : 0));

消息处理分支里,arg2 非 0 就调用 enableScreenAfterBoot():

java
// 启动完成消息的处理分支case FINISH_BOOTING_MSG: {    // arg1 非 0 表示要做启动收尾,例如通知已安装的包    if (msg.arg1 != 0) {        // 处理启动完成后的广播与安装器收尾        finishBooting();    // 结束收尾分支    }    // arg2 非 0 表示要开启屏幕,这是关闭开机画面的关键分支    if (msg.arg2 != 0) {        // 进入开屏流程        enableScreenAfterBoot();    // 结束开屏分支    }    // 处理结束    break;// 结束消息分支}

AMS.enableScreenAfterBoot 本身只做转交:

java
// AMS 侧的开屏入口,本身只做转交void enableScreenAfterBoot() {    // 记一条启动进度事件,方便 bootchart 之类的工具对齐时间轴    EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_ENABLE_SCREEN,            // 续上一行:用开机以来的毫秒数作为时间戳            SystemClock.uptimeMillis());    // 真正的工作交给窗口管理器    mWindowManager.enableScreenAfterBoot();     // 事件分发开关要和系统状态保持一致    synchronized (this) {        // 更新输入事件分发状态        updateEventDispatchingLocked();    // 结束事件分发更新    }// 结束 enableScreenAfterBoot}

AMS.enableScreenAfterBoot 里对 mWindowManager 的调用是一次普通的方法调用,不经过 Binder。AMS 和 WMS 都运行在 system_server 这一个进程里,mWindowManager 持有的是 WMS 对象本身而不是它的 Binder 代理。素材第四段开头用提问的方式强调过这一点:AMS 和 WMS 虽然在代码里各自叫一个 service,但它们不是 init.rc 里声明的那种独立进程,因此这里既不必要也不可能发生跨进程通信。真正需要走 Binder 的是紧接着 WMS.performEnableScreen 里对 SurfaceFlinger 的那次 transact,因为 SurfaceFlinger 跑在独立的 surfaceflinger 进程里。

WindowManagerService.enableScreenAfterBoot 是这段链路上第一次出现"只做一次"的守卫:

java
// 窗口管理器的开屏入口,带只执行一次的守卫public void enableScreenAfterBoot() {    // mWindowMap 保护窗口状态,整个判断在同一把锁里完成    synchronized(mWindowMap) {        // 已经开过屏就直接返回,保证只执行一次        if (mSystemBooted) {            // 已经开过屏就直接返回            return;        // 结束重复判断        }        // 置位,后续再进来会在上面那一行返回        mSystemBooted = true;        // 不再显示启动消息窗口        hideBootMessagesLocked();        // 30 秒内没有走完流程就放弃等待,直接开屏        mH.sendEmptyMessageDelayed(H.BOOT_TIMEOUT, 30*1000);    // 结束同步块    }     // 通知窗口策略系统已启动    mPolicy.systemBooted();     // 真正决定能不能开屏的是这里    performEnableScreen();// 结束 enableScreenAfterBoot}

素材第四段 0:03 处讲者提到的"30 秒超时",就是 sendEmptyMessageDelayed(H.BOOT_TIMEOUT, 30*1000)。它的作用是兜底:如果桌面一直起不来,30 秒后系统也会把屏幕打开,避免黑屏卡死。

performEnableScreen 是本文里唯一一处由 Java 层直接向 SurfaceFlinger 发 Binder 调用的地方:

java
// 已经开屏就不再处理if (mDisplayEnabled) {    // 已经开屏就不再处理    return;// 结束重复判断}// 既没启动完成也不显示启动消息时,没有开屏的理由if (!mSystemBooted && !mShowingBootMessages) {    // 既没启动完成也不显示启动消息时没有开屏理由    return;// 结束条件判断} // 所有可见窗口都画完之前不开屏,避免先露出空白if (!mForceDisplayEnabled && checkWaitingForWindowsLocked()) {    // 可见窗口没画完就继续等    return;// 结束等待判断} // 只通知 SurfaceFlinger 一次if (!mBootAnimationStopped) {    // 取 SurfaceFlinger 的 Binder 代理    try {        // 服务名就是字符串 "SurfaceFlinger"        IBinder surfaceFlinger = ServiceManager.getService("SurfaceFlinger");        // 拿不到代理说明合成器已经异常        if (surfaceFlinger != null) {            // 组装参数,只需要写入接口标识            Parcel data = Parcel.obtain();            // 接口名用于对端校验,不匹配会直接拒绝            data.writeInterfaceToken("android.ui.ISurfaceComposer");            // 发一次单向调用,事务码是 FIRST_CALL_TRANSACTION            surfaceFlinger.transact(IBinder.FIRST_CALL_TRANSACTION,                    // 续上一行:事务码、数据与单向调用标志                    data, null, 0);            // 回收 Parcel            data.recycle();        // 结束 Binder 调用        }    // 跨进程调用需要处理远端异常    } catch (RemoteException ex) {        // 合成器已经退出,只记录日志        Slog.e(TAG, "Boot completed: SurfaceFlinger is dead!");    // 结束远端异常处理    }    // 标记已通知,避免重复发事务    mBootAnimationStopped = true;// 结束首次通知判断}

mBootAnimationStopped = true 只表示"已经通知过 SurfaceFlinger",并不代表屏幕立刻打开。这一段之后跟着 checkBootAnimationCompleteLocked(),它会查 bootanim 这个 init 服务是否还在运行:

java
// bootanim 服务名,与 init.rc 里 service 声明的名字一致private static final String BOOT_ANIMATION_SERVICE = "bootanim";// 轮询间隔,单位毫秒private static final int BOOT_ANIMATION_POLL_INTERVAL = 200; // 判断开机动画对应的 init 服务是否已经退出private boolean checkBootAnimationCompleteLocked() {    // 服务还在运行,说明动画进程没有真正结束    if (SystemService.isRunning(BOOT_ANIMATION_SERVICE)) {        // 先清掉上一次排队的检查消息,避免消息堆积        mH.removeMessages(H.CHECK_IF_BOOT_ANIMATION_FINISHED);        // 200 毫秒后再检查一次        mH.sendEmptyMessageDelayed(H.CHECK_IF_BOOT_ANIMATION_FINISHED,                // 续上一行:200 毫秒后重试                BOOT_ANIMATION_POLL_INTERVAL);        // 本次判断为未完成,performEnableScreen 会直接返回        return false;    // 结束服务仍在运行分支    }    // 服务已不在运行列表中,动画进程已经退出    return true;// 结束 checkBootAnimationCompleteLocked}

这一段的含义是:通知 SurfaceFlinger 和打开屏幕是两件事,中间隔着一次对动画进程是否退出的轮询。只要 bootanim 还在运行,performEnableScreen 就每 200 毫秒重跑一次,mDisplayEnabled 不会被置为真。让 bootanim 服务退出的正是第九节那两条线:checkExit() 读到 service.bootanim.exit=1,调用 requestExit(),threadLoop 返回,进程结束。

事务码 IBinder.FIRST_CALL_TRANSACTION 之所以能用,是因为 SurfaceFlinger 的接口把 BOOT_FINISHED 定义在枚举第一位。定义在 frameworks/native/include/gui/ISurfaceComposer.h:

cpp
// 关闭开机动画的命令,取值为 Binder 的第一个事务码BOOT_FINISHED = IBinder::FIRST_CALL_TRANSACTION,// 后续命令依次递增,代理端和实现端靠这个顺序对齐CREATE_CONNECTION,// 创建缓冲区分配器CREATE_GRAPHIC_BUFFER_ALLOC,// 以下依次是显示设备与事务相关命令CREATE_DISPLAY_EVENT_CONNECTION,// 创建显示设备CREATE_DISPLAY,// 销毁显示设备DESTROY_DISPLAY,// 取内置显示设备GET_BUILT_IN_DISPLAY,// 提交事务状态SET_TRANSACTION_STATE,// 后面还有若干显示配置与截图相关的命令

对端在 frameworks/native/libs/gui/ISurfaceComposer.cpp 里按同样的顺序分发,BOOT_FINISHED 分支只做一件事:

cpp
// 收到关闭开机动画的请求case BOOT_FINISHED: {    // 校验接口名,不匹配会直接返回失败    CHECK_INTERFACE(ISurfaceComposer, data, reply);    // 转到 SurfaceFlinger 的实现    bootFinished();    // 命令处理成功    return NO_ERROR;// 结束 BOOT_FINISHED 分支}

SurfaceFlinger::bootFinished 是整个"关闭开机画面"的落点:

cpp
// 关闭开机画面的落点void SurfaceFlinger::bootFinished()// 函数体开始{    // 取当前时间,用于计算启动总耗时    const nsecs_t now = systemTime();    // 与启动时刻相减得到耗时    const nsecs_t duration = now - mBootTime;    // 打印一行"Boot is finished",这是分析启动耗时的常用锚点    ALOGI("Boot is finished (%ld ms)", long(ns2ms(duration)) );    // 置位,后续可用于判断启动是否已完成    mBootFinished = true;     // 监听 window 服务的死亡,窗口管理器重启时能重新拉起开机动画    const String16 name("window");    // 取 window 服务的 Binder 代理    sp<IBinder> window(defaultServiceManager()->getService(name));    // 拿到代理才注册死亡通知    if (window != 0) {        // 注册回调,对应上面的 binderDied        window->linkToDeath(static_cast<IBinder::DeathRecipient*>(this));    // 结束死亡通知注册    }     // 停止开机动画    // 早期实现是直接杀掉进程,现在改成请它自行退出,让它决定停在哪一帧    property_set("service.bootanim.exit", "1");// 结束 bootFinished}

置 1 之后,bootanimation 进程里正在循环的 threadLoop 会在下一次 checkExit() 时读到非 0,调用 requestExit() 退出循环,然后执行第九节末尾那段 EGL 资源清理代码。从桌面 resume 到动画消失,依次经过 Launcher 进程、system_server 和 SurfaceFlinger。

补充一点:binderDied 里 SF 会重新调用 startBootAnim(),也就是把退出开关置回 0 并再起一次动画。这是为窗口管理器异常重启准备的路径,正常开机不会走到。

关闭开机画面的跨进程时序

十一、小结

  1. AMS.systemReady 里用 ACTION_MAIN + CATEGORY_HOME 从 PMS 查出桌面组件,启动 Launcher 进程,这是"启动入口分析"。
  2. Launcher 的 LoaderTask.loadAllApps 用 ACTION_MAIN + CATEGORY_LAUNCHER 从 PMS 查出全部可启动入口,包装成 AppInfo,获取所有 app 应用信息。
  3. bindAllApplications 把列表交给 AllAppsContainerView,AlphabeticalAppsList 负责排序与分区,AllAppsGridAdapter 和 BubbleTextView 负责呈现。点击图标时用的是列表填充阶段就建好的意图,这是"填充桌面数据处理点击"。
  4. Launcher resume 后的空闲回调触发 activityIdle,经 checkFinishBootingLocked、postFinishBooting、enableScreenAfterBoot 一路到 WMS.performEnableScreen,再经一次 Binder 调用让 SurfaceFlinger::bootFinished 把 service.bootanim.exit 置 1,这是"关闭启动开机画面"。

开机动画的启动由 SurfaceFlinger 在 init() 末尾发 ctl.start 完成,退出开关也是在 SurfaceFlinger 里写的。整个生命周期里 bootanimation 只读属性、不写属性。

验证入口:

bash
# 查看开机动画的退出开关,启动阶段为 0,桌面出来后会变成 1adb shell getprop service.bootanim.exit# 查看动画进程是否还在运行adb shell ps | grep bootanimation# 直接关掉开机动画,用来缩短调试时的开机时间adb shell setprop debug.sf.nobootanimation 1# 观察 bootFinished 打印的启动耗时adb logcat -s SurfaceFlinger | grep "Boot is finished"# 确认桌面组件的解析结果,等价于 PMS 对 ACTION_MAIN + CATEGORY_HOME 的回答adb shell cmd package resolve-activity -a android.intent.action.MAIN -c android.intent.category.HOME

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(十四)-Launcher的启动过程 · Yinchen 的博客