Launcher3_启动与绑定链路分析
Launcher3 开机启动与组件加载链路分析
源码路径:
aosp/packages/apps/Launcher3(Quickstep 变体,设备默认 HOME)
摘要
本文综合分析 Launcher3 作为 Android HOME 应用的开机启动链路与核心组件加载流程,重点覆盖三段关键路径:
- 整体启动时序 — 从 AMS 解析 HOME 到 QuickstepLauncher 进入 NORMAL 状态
- LoaderTask 四阶段绑定 — MODEL_EXECUTOR 后台加载 + MAIN_EXECUTOR 分发绑定的线程模型
- UnifiedWorkspaceBinder 落位机制 — ItemInfo 如何经 Workspace/Hotseat 分到 CellLayout
第一章 整体启动时序
设备默认 HOME 是 Quickstep 变体(Launcher3QuickStep),由 devicecommon.mk 装入,HOME Activity 为 com.android.launcher3.uioverrides.QuickstepLauncher,清单中声明 singleTask + category.HOME + stateNotNeeded。
各节点详解
① HOME 解析
QuickstepLauncher 由 quickstep/AndroidManifest-launcher.xml 声明:action.MAIN + category.HOME + category.LAUNCHER_APP. singleTask 意味着进程首次拉起时 Activity 走完整 onCreate 链路,后续 onNewIntent 复用实例。
② ContentProvider 早于 Application
AMS fork 进程后,先扫清单中的 <provider> 并调 ContentProvider.onCreate(),再调 Application.onCreate()。Launcher3 的数据库 Provider 清单位于 AndroidManifest-common.xml,authority com.android.launcher3.settings。Dagger 组件采用惰性构建(见 LauncherApplication.getAppComponent() 注释 b/36917845#comment4),避免 Provider 早启时因 Application 未初始化拿不到 Context。
③ LauncherApplication
入口极薄:
public void onCreate() {
super.onCreate();
MainProcessInitializer.initialize(this);
}
MainProcessInitializer 默认只做两件事(OEM 可通过 R.string.main_process_initializer_class 资源替换):设置 FileLog 目录、开启 Bitmap 创建追踪。Dagger AppComponent 在第一次 getAppComponent() 时懒构建。
④ QuickstepLauncher.onCreate
基类 Launcher.onCreate 的关键步骤:
- 判定冷启动类型:
sIsNewProcess+LockedUserState.isUserUnlockedAtLauncherStartup()→ COLD / COLD_DEVICE_REBOOTING / WARM - 取 Model 与配置:
LauncherAppState.getInstance(this).getModel()、initDeviceProfile(idp)、mIconCache、StateManager<>(this, NORMAL) setupViews():inflateRootView(R.layout.launcher),依次建DragLayer / Workspace / Hotseat / OverviewPanel / AppsView / Scrim / DropTargetBar,Quickstep 覆写再建OverviewActionsView / SplitSelectStateController / TISBindHelper / DepthController / HotseatPredictionController等ACTIVITY_TRACKER.handleCreate(this)— 外部回调(SystemUI、LauncherInitListener)拿到"Launcher 已就绪"的同步钩子- 发起加载:
mModel.addCallbacksAndLoad(this)
若 addCallbacksAndLoad 返回 false(异步绑定),会挂 OnPreDrawListener 卡住首帧绘制,让系统继续显示"设备正在加载"提示,直到初始绑定完成。
⑤⑥ LoaderTask + onInitialBindComplete
详见第二章。收尾时 Launcher.bindComplete 关掉异步 trace、记录启动延迟;Quickstep 覆写 finishBindingItems 里懒加载 WellbeingModel。
设备定制点
Launcher.onResume 中两处 SKG/EDLA 自定义:
- 向
com.skg.SystemUI发com.skg.check.temp.props - 切换用户后的锁屏检查(
LOCKED_USER_HANDLER)
Launcher 没有 BOOT_COMPLETED 接收器 —— 作为 HOME 被 AMS 主动拉起,而非监听开机广播。
第二章 LoaderTask 四阶段绑定
LauncherModel.addCallbacksAndLoad 进入 LoaderTask(MODEL_EXECUTOR 后台线程),分四阶段 load* → bind*,每阶段之间 waitForIdle() 让 UI 主线程先消化。
线程模型
load* / sanitize:在MODEL_EXECUTOR后台线程(IO 密集,耗时不阻塞 UI)bind*:经BaseLauncherBinder.executeCallbacksTask派发到MAIN_EXECUTOR(UI 线程)waitForIdle():LooperIdleLock等 UI 主线程队列空闲后再继续下一阶段 —— 避免 AllApps 与 Workspace 争抢主线程导致掉帧
并发保护
LauncherModel 用 lastLoadId + lastBindId + mModelLoaded 做代次保护:
// BaseLauncherBinder.bindWorkspace
synchronized (mBgDataModel) {
itemsIdMap = mBgDataModel.itemsIdMap.clone();
if (incrementBindId) {
mBgDataModel.lastBindId++;
mBgDataModel.lastLoadId = mModel.getLastLoadId();
}
mMyBindingId = mBgDataModel.lastBindId;
}
任何一次重载都会让 lastBindId 自增,从而让上一代 binder 的所有后续回调在 executeCallbacksTask 里因 mMyBindingId != lastBindId 自动丢弃 —— 这是"连续重载时不会出现半新半旧界面"的根本机制。
四阶段详解
① Workspace
loadWorkspace 内部先做 DB 迁移与空库默认布局(ModelDbController.loadDefaultFavoritesIfNecessary,按优先级装载:应用限制 → Play 提供 → 预置 partner APK → 设备默认 res/xml/default_workspace_*.xml),再用 WorkspaceItemProcessor 遍历 LoaderCursor;随后 sanitize* 清理失效 folders/apppairs/widgets,最后 bindWorkspace(true, isBindSync)。
绑定由 UnifiedWorkspaceBinder 完成(详见第三章),同时 lastBindId / lastLoadId 自增防错绑。绑定完成后 workspaceLoadComplete() 并发送首发屏安装广播。
② AllApps
loadAllApps → bindAllApps(拷 AllAppsList,executeCallbacksTask 派发 c.bindAllApplications)→ IconCacheUpdateHandler.updateIcons(allActivityList / shortcuts) 落盘图标缓存。
③ DeepShortcuts
loadDeepShortcuts → bindDeepShortcuts(拷 deepShortcutMap 给 c.bindDeepShortcutMap)→ 再写缓存。
④ Widgets
widgetsModel.update(null) → bindWidgets(WidgetsListBaseEntriesBuilder.build → c.bindAllWidgets)→ 写 widget 标签缓存。Smartspace 视 flag 走 bindSmartspaceWidget(检查首屏 (0,0) 位置是否被占用)。
DB 写入的跨进程路径
外部进程(如 SetupWizard、OTA)调用 LauncherProvider.insert/update 时,LauncherProvider.executeControllerTask 会把写入任务提交到 MODEL_EXECUTOR 执行,写成功后在 MAIN_EXECUTOR 上 forceReload —— 保证 DB 写入不会与 UI 绑定并发。
第三章 UnifiedWorkspaceBinder 内部落位机制
UnifiedWorkspaceBinder 是"一次绑定"的载体,只持 4 个不可变字段:mCallbacks / mItemIdMap / mExtraItems / mOrderedScreenIds。它本身不碰 View,所有落位都通过回调进 UI 侧。
完整链路
① 分流:按"可见页 + 容器"分桶
IntSet currentScreenIds = mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
Predicate<ItemInfo> currentScreenCheck = currentScreenContentFilter(currentScreenIds);
mItemIdMap.forEach(item -> {
if (currentScreenCheck.test(item)) {
(WIDGET_FILTER.test(item) ? currentAppWidgets : currentWorkspaceItems).add(item);
} else if (item.container == CONTAINER_DESKTOP) {
(WIDGET_FILTER.test(item) ? otherAppWidgets : otherWorkspaceItems).add(item);
}
});
currentScreenContentFilter 的定义:
return item -> item.container == CONTAINER_HOTSEAT
|| (item.container == CONTAINER_DESKTOP && currentScreenIds.contains(item.screenId));
Hotseat 项无条件命中(与可见页无关),永远进 currentWorkspaceItems 第一批同步绑定;other 桶只接受 CONTAINER_DESKTOP 的非可见页 item。
可见页的判定逻辑在 ModelCallbacks.getPagesToBindSynchronously:优先用 pagesToBindSynchronously,否则在 workspace 非 loading 时取 currentPageScreenIds,再退到 synchronouslyBoundPages。双屏设备额外把 pairId 加进去("左右两屏一起同步绑")。
排序决定插入顺序
sortWorkspaceItemsSpatially:
| 同容器 | 排序键 |
|---|---|
| Desktop | screenId * cellCount + cellY * cols + cellX(空间顺序,逐屏从上到下从左到右) |
| Hotseat | screenId 当 rank(注释:"we currently use the screen id as the rank") |
| 跨容器 | Integer.compare(container) —— CONTAINER_HOTSEAT(-101) < CONTAINER_DESKTOP(-100),Hotseat 排在 Desktop 前 |
② 清场 + 建屏
executeCallbacksTask(c -> { c.clearPendingBinds(); c.startBinding(); ... }, mUiExecutor);
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
startBinding(ModelCallbacks)是"插入前必须清空"的由来:关掉除 rebind-safe 外的浮层 → dragController.cancelDrag() → workspace.clearDropTargets() → removeAllWorkspaceScreens() → clearWidgetViews() → hotseat.resetLayout(deviceProfile.isVerticalBarLayout)。最后一句解释了 b/335141365 日志 —— Hotseat 方向在每次 rebind 时被重置,所以单元格坐标必须在 resetLayout 后按新方向重算。
bindScreens 暂停翻页(setPauseScroll(true)),再 bindAddScreens 逐屏 insertNewWorkspaceScreenBeforeEmptyScreen(screenId);双屏下补齐 pair 屏。
③ 逐块建 View
两条通道:
| 通道 | 建 View 线程 | 回调 | 分批 |
|---|---|---|---|
inflateAsyncAndBind |
后台(直接 inflater.inflateItem) |
c.bindInflatedItems(List<Pair<ItemInfo,View>>) |
一次性 |
bindItemsInChunks |
UI 线程(Launcher.bindItems 内部 inflate) |
c.bindItems(subList, false) |
6 个/批(widget 1 个/批) |
可见页(mUiExecutor == MAIN)同步派发;非可见页(otherWorkspaceItems/otherAppWidgets)走 pendingExecutor,异步模式下塞进 pendingTasks 等首帧执行。
④ 落位(Desktop vs Hotseat 的分歧点)
UI 侧入口:Launcher.bindInflatedItems(bindItems 只是先 inflate 再转调它)。
Desktop 冲突检测
if (item.container == CONTAINER_DESKTOP) {
CellLayout cl = mWorkspace.getScreenWithId(presenterPos.screenId);
if (cl != null && cl.isOccupied(presenterPos.cellX, presenterPos.cellY)) {
// Studio 下抛异常;生产环境删库继续
getModelWriter().deleteItemFromDatabase(item, desc);
continue;
}
}
核心方法:addInScreenFromBind
default void addInScreenFromBind(View child, ItemInfo info) {
CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info);
int x = presenterPos.cellX;
int y = presenterPos.cellY;
if (info.container == CONTAINER_HOTSEAT
|| info.container == CONTAINER_HOTSEAT_PREDICTION) {
int screenId = presenterPos.screenId;
x = getHotseat().getCellXFromOrder(screenId);
y = getHotseat().getCellYFromOrder(screenId);
}
addInScreen(child, info.container, presenterPos.screenId, x, y, info.spanX, info.spanY);
}
这是整条链最关键的一行:Hotseat 项的 screenId 被当作顺序号(rank),不是页面号 —— Hotseat 是固定容器,数据库里没有真正的"屏"存位置,只能借 screenId 字段保存排列次序。
rank 换算:
public int getCellXFromOrder(int rank) { return mHasVerticalHotseat ? 0 : rank; }
public int getCellYFromOrder(int rank) { return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0; }
横向时 (x=rank, y=0);竖向时 (x=0, y=countY-rank-1),从底部往上排(rank 0 贴底)。
CellPosMapper 网格坐标归一化
默认 CellPosMapper.DEFAULT 直通;双屏设备用 TwoPanelCellPosMapper:奇数 screenId 的项把 cellX + columnCount 折算到左侧屏(screenId - 1),让"左右两屏"在模型上是一套连续网格。
选择落点容器 + 终点 addViewToCellLayout
final CellLayout layout;
if (container == CONTAINER_HOTSEAT || container == CONTAINER_HOTSEAT_PREDICTION) {
layout = getHotseat();
if (child instanceof FolderIcon) ((FolderIcon) child).setTextVisible(false);
} else {
if (child instanceof FolderIcon) ((FolderIcon) child).setTextVisible(true);
layout = getScreenWithId(screenId);
}
// 构造/复用 CellLayoutLayoutParams
if (spanX < 0 && spanY < 0) lp.isLockedToGrid = false;
int childId = info.getViewId();
boolean markCellsAsOccupied = !(child instanceof Folder);
layout.addViewToCellLayout(child, -1, childId, lp, markCellsAsOccupied);
addViewToCellLayout 的关键动作:
BubbleTextView.setTextVisibility(mContainerType != HOTSEAT)—— Hotseat 图标文字在这里被关掉(不是上层)spanX < 0或spanY < 0时撑满 CellLayout(自由摆放 item)child.setId(childId)—— View id 与数据库 item id 一一对应,后续findViewById/ 动画复用都靠它markCellsAsOccupiedForView(child)—— Folder 不占格
越界(cellX/cellY 超出 mCountX/mCountY)返回 false,即 addInScreen 里那句 "Failed to add to item at (...) to CellLayout"。
额外容器(Hotseat 预测等)
mExtraItems 逐个回调到 QuickstepLauncher.bindExtraContainerItems,按 containerId 分流:
CONTAINER_PREDICTION→ AllApps 预测行(PredictionRowView.setPredictedApps)CONTAINER_HOTSEAT_PREDICTION→HotseatPredictionController(内部复用addInScreenFromBind落位)CONTAINER_WIDGETS_PREDICTION→ Widget 推荐列表
第四章 关键设计决策
代次保护(lastBindId)
executeCallbacksTask 在每次派发前检查 mMyBindingId == mBgDataModel.lastBindId。任何一次重载都会让 lastBindId 自增,上一代 binder 的所有后续回调自动失效。这是 Launcher 能"连续快速重载不出错"的根本机制。
Hotseat rank 复用
Hotseat 是固定容器(只有一个 CellLayout),数据库里没有"屏"字段存位置。Launcher 把 Hotseat item 的 screenId 当顺序号用,再由 getCellX/YFromOrder 在插入时按当前方向(横/竖)换算成格点。这是 celllayout/CellPosMapper 与 Hotseat 协作的关键设计。
CellPosMapper 网格坐标归一化
TwoPanelCellPosMapper 把双屏的"两屏两网格"映射成"一屏一网格",让模型层只用一套坐标体系就能同时覆盖单屏与双屏布局。这是"左右两屏一起同步绑"的技术基础。
可见页优先绑定
currentScreenContentFilter + pendingExecutor 把 item 分成两桶:可见页(含全部 Hotseat)同步绑定保证首屏可用性;非可见页延后到首帧执行避免抢主线程。
冲突检测策略
Desktop item 在插入前检查 CellLayout 是否被占用。Studio 下抛异常暴露问题;生产环境直接删库继续(deleteItemFromDatabase),保证 Launcher 能启动。
附录:关键类与源码路径索引
| 类名 | 路径 | 作用 |
|---|---|---|
LauncherApplication |
src/com/android/launcher3/LauncherApplication.java |
Application 入口 + Dagger 惰性构建 |
MainProcessInitializer |
src/com/android/launcher3/MainProcessInitializer.java |
进程初始化钩子(可 OEM 替换) |
Launcher |
src/com/android/launcher3/Launcher.java |
HOME Activity 基类 |
QuickstepLauncher |
quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java |
Quickstep HOME Activity(设备默认) |
LauncherModel |
src/com/android/launcher3/LauncherModel.kt |
单例 Model,协调绑定时序 |
LoaderTask |
src/com/android/launcher3/model/LoaderTask.java |
四阶段后台加载任务 |
BaseLauncherBinder |
src/com/android/launcher3/model/BaseLauncherBinder.java |
绑定入口 + UnifiedWorkspaceBinder 容器 |
UnifiedWorkspaceBinder |
BaseLauncherBinder.java 内部类 |
一次绑定的载体(分桶/排序/落位) |
ModelCallbacks |
src/com/android/launcher3/ModelCallbacks.kt |
UI 侧绑定回调实现 |
BgDataModel |
src/com/android/launcher3/model/BgDataModel.java |
后台数据快照(itemsIdMap / extraItems) |
LauncherProvider |
src/com/android/launcher3/LauncherProvider.java |
数据库 + ContentProvider |
ModelDbController |
src/com/android/launcher3/model/ModelDbController.java |
DB 读写 + 默认布局 |
ModelInitializer |
src/com/android/launcher3/model/ModelInitializer.kt |
运行期事件总线(IDP/主题/安装等监听) |
CellPosMapper |
src/com/android/launcher3/celllayout/CellPosMapper.java |
模型→Presenter 网格坐标映射 |
Hotseat |
src/com/android/launcher3/Hotseat.java |
Hotseat 容器 + rank→cell 换算 |
Workspace |
src/com/android/launcher3/Workspace.java |
Workspace 容器 + 屏管理 |
CellLayout |
src/com/android/launcher3/CellLayout.java |
单个页面/Hotseat 的网格布局 |
WorkspaceLayoutManager |
src/com/android/launcher3/WorkspaceLayoutManager.java |
addInScreen 默认实现(Desktop vs Hotseat 分歧点) |
InvariantDeviceProfile |
src/com/android/launcher3/InvariantDeviceProfile.kt |
设备网格配置(numColumns / numRows) |
Dagger AppComponent |
src/com/android/launcher3/dagger/LauncherBaseAppComponent.java |
单例组件图根 |
QuickstepBaseAppComponent |
quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java |
Quickstep 扩展组件图 |
本文来自博客园,作者:阿丟啊,转载请注明原文链接:https://www.cnblogs.com/qiyuexiaxun/p/23328376

浙公网安备 33010602011771号