前文回顾
在前面的系列文章中,我们沿着 Android 系统启动链路,完整梳理了 Framework 从无到有的初始化全过程。
首先,init 进程作为 Android 用户空间的首个进程,负责解析 init.rc 配置、挂载系统文件、拉起各类 Native 服务,为系统运行搭建基础环境。
随后 Zygote 进程 启动,预加载 Framework 核心类、系统资源与通用依赖库,通过 fork 机制孵化出 system_server 进程与所有应用进程,是 Android 高效进程管理的核心基石。
紧接着 SystemServer 完成 Framework 顶层初始化,依次启动 AMS、PMS、WMS 等数百个核心系统服务。上一篇文章我们重点解析了 SystemServiceManager,搞懂了系统服务的统一创建、管理逻辑,以及 BootPhase 开机阶段的核心设计思想。
至此,整个 Android 开机启动链路还剩下最后两块核心拼图:
- 开机系统如何自动拉起 Launcher 桌面;
- 用户点击桌面图标后,应用完整启动交互流程。
我们日常开机后自动展示的 Launcher 桌面,本质只是一个普通 APK,既不是 Native 服务,也不是 Framework 内置的 SystemService。衍生出几个核心疑问:
- Launcher 究竟是谁触发启动的?
- AMS 如何精准匹配、找到系统默认的 Launcher 桌面应用?
- 为什么开机只会自动启动桌面,而非其他普通应用?
- Launcher 凭什么成为系统默认的 Home 桌面应用?
- 点击桌面图标后,从触摸到打开 App 完整链路是什么?
本文延续 Framework 启动源码链路,从 SystemServer 启动收尾阶段开始,逐层拆解 Launcher3 完整启动流程,覆盖系统拉起 Launcher、Activity 初始化、桌面数据加载、UI 渲染、图标点击交互全流程,彻底打通 Android 开机进桌面、点击图标启动应用的底层原理。
一、Launcher 启动入口:SystemServer 收尾触发
Launcher 并非独立自启动,而是由 system_server 进程 在系统服务初始化完成后主动拉起。核心入口在 SystemServer 的 startOtherServices 方法中。
1.1 SystemServer 核心触发源码
java
private void startOtherServices() {
// 省略大量系统服务初始化逻辑...
// 所有核心服务启动完成后,调用AMS的systemReady方法
mActivityManagerService.systemReady(() -> {/* 开机收尾回调逻辑 */}, BOOT_TIMINGS_TRACE_LOG);
}
当所有系统服务初始化完毕后,会调用 AMS.systemReady(),标志系统开机流程进入收尾阶段,Launcher 桌面的启动逻辑就封装在该方法中。
1.2 AMS.systemReady 启动桌面核心逻辑
java
public void systemReady(final Runnable goingCallback, TimingsTraceLog traceLog) {
// 省略系统就绪校验、服务初始化收尾逻辑...
// 核心:触发所有屏幕的Home桌面启动
mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
}
关键说明:
mAtmInternal类型为ActivityTaskManagerInternal,是 ATMS(ActivityTaskManagerService)的内部本地服务实现类- 设计核心:解耦 AMS 与 ATMS,避免两大核心服务循环依赖、强耦合
两大服务职责拆分(核心设计思想):
- AMS:管控进程生命周期、应用权限、广播、四大组件服务、系统任务调度
- ATMS:专注 Activity 栈管理、Task 任务栈、窗口层级、屏幕显示、桌面启动、页面跳转
ActivityTaskManagerInternal 是 ATMS 暴露给 AMS、SystemServer 等系统模块的唯一调用入口,保证模块间低耦合。
二、ATMS 逐层分发:多屏幕桌面启动逻辑
2.1 全局桌面启动入口
ATMS 中 startHomeOnAllDisplays 为多屏幕桌面启动统一入口,支持主屏、副屏、车载扩展屏等多设备场景。
java
@Override
public boolean startHomeOnAllDisplays(int userId, String reason) {
synchronized (mGlobalLock) {
// 全局锁保证桌面启动线程安全
return mRootWindowContainer.startHomeOnAllDisplays(userId, reason);
}
}
2.2 核心容器:RootWindowContainer 层级架构
RootWindowContainer 是 Android 窗口容器树的唯一根节点 ,所有屏幕、窗口、任务栈、应用页面都挂载在其下,继承关系:RootWindowContainer → WindowContainer → ConfigurationContainer。
完整窗口容器树层级(自上而下):
bash
RootWindowContainer(全局窗口根节点)
├─ DisplayContent #0(设备主屏)
│ ├─ DisplayArea.Root
│ │ ├─ TaskDisplayArea(APP任务窗口区域)
│ │ │ ├─ Task(独立任务栈,Launcher为独立RootHomeTask)
│ │ │ │ └─ ActivityRecord(Launcher Activity实例)
│ │ │ │ └─ WindowState(应用DecorView顶层窗口)
│ │ │ └─ Task(普通应用任务栈)
│ │ ├─ ImeContainer(输入法窗口容器)
│ │ └─ DisplayArea.Tokens(状态栏、弹窗、壁纸等系统窗口)
│ └─ NonAppWindowContainers(系统装饰窗口)
├─ DisplayContent #1(副屏/车载后座屏/虚拟屏)
└─ DisplayContent #2(扩展屏幕)
文件路径:frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
2.3 遍历所有屏幕启动桌面
java
boolean startHomeOnAllDisplays(int userId, String reason) {
boolean homeStarted = false;
// 倒序遍历所有屏幕设备
for (int i = getChildCount() - 1; i >= 0; i--) {
final int displayId = getChildAt(i).mDisplayId;
// 逐个屏幕触发桌面启动
homeStarted |= startHomeOnDisplay(userId, reason, displayId);
}
return homeStarted;
}
// 单屏幕桌面启动重载方法
boolean startHomeOnDisplay(int userId, String reason, int displayId) {
return startHomeOnDisplay(userId, reason, displayId, false /* 不允许自动化测试启动 */,
false /* 非Home键触发,开机流程固定false */);
}
五参方法参数详细释义(开机流程标准入参):
- userId:当前系统用户 ID,设备开机默认用户为 0
- reason :触发标识,开机为
systemReady,手动点 Home 键为homeKey - displayId:目标启动桌面的屏幕 ID
- allowInstrumenting:是否允许自动化测试拉起桌面,正常开机为 false
- fromHomeKey:是否为用户点击 Home 键触发,开机流程为 false
2.4 单屏幕任务区域桌面启动逻辑
java
boolean startHomeOnDisplay(int userId, String reason, int displayId, boolean allowInstrumenting,
boolean fromHomeKey) {
// 兼容无效屏幕ID,兜底聚焦屏幕/默认主屏
if (displayId == INVALID_DISPLAY) {
final Task rootTask = getTopDisplayFocusedRootTask();
displayId = rootTask != null ? rootTask.getDisplayId() : DEFAULT_DISPLAY;
}
// 根据屏幕ID获取对应屏幕实例
final DisplayContent display = getDisplayContent(displayId);
// 遍历当前屏幕所有任务区域,尝试启动桌面
return display.reduceOnAllTaskDisplayAreas((taskDisplayArea, result) ->
result | startHomeOnTaskDisplayArea(userId, reason, taskDisplayArea,
allowInstrumenting, fromHomeKey),
false /* 初始值 */);
}
三、核心流程:匹配默认 Launcher 桌面应用
startHomeOnTaskDisplayArea 是启动 Launcher 的核心方法,负责构建桌面专属 Intent、通过 PMS 匹配系统默认桌面应用、校验启动权限,最终拉起 Launcher。
java
boolean startHomeOnTaskDisplayArea(
int userId, // 当前系统用户ID
String reason, // 启动触发来源
TaskDisplayArea taskDisplayArea, // 当前任务显示区域
boolean allowInstrumenting, // 是否允许自动化测试启动
boolean fromHomeKey // 是否为Home键触发) {
// 兜底无效任务区域
if (taskDisplayArea == null) {
final Task rootTask = getTopDisplayFocusedRootTask();
taskDisplayArea = rootTask != null ? rootTask.getDisplayArea()
: getDefaultTaskDisplayArea();
}
Intent homeIntent = null;
ActivityInfo aInfo = null;
// 主屏/主桌面启动逻辑
if (taskDisplayArea == getDefaultTaskDisplayArea()
|| mWmService.shouldPlacePrimaryHomeOnDisplay(
taskDisplayArea.getDisplayId(), userId)) {
// 获取标准HOME桌面Intent
homeIntent = mService.getHomeIntent();
// 核心:通过PMS查询系统默认的Launcher桌面Activity
aInfo = resolveHomeActivity(userId, homeIntent);
}
// 副屏/车载二级桌面启动逻辑
else if (shouldPlaceSecondaryHomeOnDisplayArea(taskDisplayArea)) {
Pair<ActivityInfo, Intent> info = resolveSecondaryHomeActivity(userId, taskDisplayArea);
aInfo = info.first;
homeIntent = info.second;
}
// 未查询到桌面应用,直接返回
if (aInfo == null || homeIntent == null) {
return false;
}
// 校验当前区域是否允许启动桌面
if (!canStartHomeOnDisplayArea(aInfo, taskDisplayArea, allowInstrumenting)) {
return false;
}
// 主题动画延时配置,延迟启动桌面
if (mService.mAmInternal.shouldDelayHomeLaunch(userId)) {
Slog.d(TAG, "ThemeHomeDelay: Home launch was deferred with user " + userId);
return false;
}
// 绑定真实Launcher组件信息
homeIntent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
// 新建独立任务栈启动桌面
homeIntent.setFlags(homeIntent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
// Home键专属逻辑,开机流程不执行
if (fromHomeKey) {
homeIntent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, true);
if (mWindowManager.getRecentsAnimationController() != null) {
mWindowManager.getRecentsAnimationController().cancelAnimationForHomeStart();
}
}
// 携带启动原因参数,用于日志调试
homeIntent.putExtra(WindowManagerPolicy.EXTRA_START_REASON, reason);
// 组装完整调试标识:触发原因:用户ID:应用UID:屏幕ID
final String myReason = reason + ":" + userId + ":" + UserHandle.getUserId(
aInfo.applicationInfo.uid) + ":" + taskDisplayArea.getDisplayId();
// 进入标准Activity启动流水线,拉起Launcher
mService.getActivityStartController().startHomeActivity(homeIntent, aInfo, myReason,
taskDisplayArea);
return true;
}
3.1 标准桌面 Intent 构建规则
系统通过固定 Action、Category 唯一匹配桌面应用,这也是普通应用无法成为默认桌面的核心原因。
java
Intent getHomeIntent() {
Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
intent.setComponent(mTopComponent);
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
// 工厂测试模式除外,普通开机添加HOME分类
if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
intent.addCategory(Intent.CATEGORY_HOME);
}
return intent;
}
核心匹配规则 :桌面应用必须匹配 Intent.ACTION_MAIN + Intent.CATEGORY_HOME
四、ActivityStartController 拉起 Launcher 桌面
文件目录:/frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java
通过系统启动控制器,将桌面 Activity 加入独立 RootHomeTask 任务栈,全屏启动 Launcher。
java
void startHomeActivity(
Intent intent, // 组装完成的HOME Intent
ActivityInfo aInfo, // Launcher桌面Activity信息
String reason, // 调试日志标识
TaskDisplayArea taskDisplayArea // 目标任务区域) {
// 构建Activity启动配置
final ActivityOptions options = ActivityOptions.makeBasic();
// 全屏模式启动桌面
options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
// 标记为HOME类型Activity,区别于普通应用
if (!ActivityRecord.isResolverActivity(aInfo.name)) {
options.setLaunchActivityType(ACTIVITY_TYPE_HOME);
}
final int displayId = taskDisplayArea.getDisplayId();
options.setLaunchDisplayId(displayId);
options.setLaunchTaskDisplayArea(taskDisplayArea.mRemoteToken
.toWindowContainerToken());
// 延迟Resume,避免递归启动、重复刷新
mSupervisor.beginDeferResume();
final Task rootHomeTask;
try {
// 创建/获取桌面专属根任务栈
rootHomeTask = taskDisplayArea.getOrCreateRootHomeTask(ON_TOP);
} finally {
mSupervisor.endDeferResume();
}
// 执行标准Activity启动流水线
mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
.setOutActivity(tmpOutRecord)
.setCallingUid(0)
.setActivityInfo(aInfo)
.setActivityOptions(options.toBundle())
.execute();
mLastHomeActivityStartRecord = tmpOutRecord[0];
// 调度顶层Activity Resume,完成桌面显示
if (rootHomeTask.mInResumeTopActivity) {
mSupervisor.scheduleResumeTopActivities();
}
}
五、Launcher3 客户端初始化与数据加载流程
系统服务端完成 Launcher 进程拉起、Activity 创建后,进入 Launcher3 客户端初始化逻辑,完成布局加载、应用数据刷新、桌面 UI 渲染。
5.1 Launcher 主页面 onCreate 初始化
java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化Launcher全局状态
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.getModel(); // 桌面核心数据源模型
mRotationHelper = new RotationHelper(this); // 屏幕旋转监听
InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); // 屏幕尺寸适配配置
initDeviceProfile(idp);
idp.addOnChangeListener(this);
mSharedPrefs = LauncherPrefs.getPrefs(this); // 桌面本地配置
mIconCache = app.getIconCache(); // 应用图标缓存
mAccessibilityDelegate = createAccessibilityDelegate();
initDragController(); // 图标拖拽、文件夹拖动控制器
mAllAppsController = new AllAppsTransitionController(this); // 全部应用列表动画控制器
mStateManager = new StateManager<>(this, NORMAL); // 桌面状态管理(普通/全部应用/弹窗)
setupViews(); // 初始化桌面布局:Workspace、全部应用列表、Dock栏
updateDisallowBack();
// 桌面小组件初始化
mAppWidgetManager = new WidgetManagerHelper(this);
mAppWidgetHolder = createAppWidgetHolder();
mAppWidgetHolder.startListening();
mAppWidgetHolder.addProviderChangeListener(() -> refreshAndBindWidgetsForPackageUser(null));
// 核心:注册回调并加载所有应用数据
if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
// 同步绑定失败时,延迟绘制,等待数据加载完成
mOnInitialBindListener = Boolean.FALSE::booleanValue;
}
}
setContentView(getRootView()); // 加载根布局
ComposeInitializer.initCompose(this);
// 触控板滚动配置监听
settingsCache.register(TOUCHPAD_NATURAL_SCROLLING, mNaturalScrollingChangedListener);
mIsNaturalScrollingEnabled = settingsCache.getValue(TOUCHPAD_NATURAL_SCROLLING);
// 屏幕亮灭状态监听
ScreenOnTracker.INSTANCE.get(this).addListener(mScreenOnListener);
getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
mRotationHelper.initialize(); // 初始化屏幕旋转逻辑
}
5.2 核心:后台线程加载全量应用数据
文件路径:src/com/android/launcher3/LauncherModel.kt 初始化链路:Launcher.onCreate() -> mModel.addCallbacksAndLoad() -> 后台线程加载数据
Kotlin
// addCallbacksAndLoad 核心逻辑
fun addCallbacksAndLoad(callbacks: Callbacks): Boolean {
addCallbacks(callbacks) // 注册UI回调
return startLoader(callbacks) // 启动数据加载器
}
fun startLoader(newCallbacks: Callbacks): Boolean {
return if (mModelLoaded) {
// 数据已加载,直接同步绑定UI
bindDirectly(newCallbacks)
true
} else {
// 未加载,创建LoaderTask,投递到后台线程执行
MODEL_EXECUTOR.post(mLoaderTask)
false
}
}
5.3 LoaderTask 后台全量数据加载流程
文件路径:src/com/android/launcher3/model/LoaderTask.java
后台线程异步加载桌面所有数据源,避免阻塞 UI,核心四大步骤:
java
@Override
public void run() {
// Step1:加载桌面布局、快捷方式、小组件配置(本地数据库)
loadWorkspace(...);
mLauncherBinder.bindWorkspace(...);
// Step2:加载系统所有已安装应用(核心步骤)
loadAllApps();
mLauncherBinder.bindAllApps();
// Step3:加载应用深度快捷方式
loadDeepShortcuts();
mLauncherBinder.bindDeepShortcuts();
// Step4:加载桌面Widget小组件数据
loadWidgets();
mLauncherBinder.bindWidgets();
}
5.4 loadAllApps 应用列表查询核心逻辑
遍历系统所有用户空间,查询所有可在桌面展示的应用,封装为应用数据模型。
java
private List<LauncherActivityInfo> loadAllApps() {
// 获取所有用户空间(主用户、工作空间、私有空间)
List<UserHandle> profiles = mUserCache.getUserProfiles();
mBgAllAppsList.clear();
for (UserHandle user : profiles) {
// 调用系统服务,查询当前用户下所有可启动应用
// packageName传null:查询全局所有应用
List<LauncherActivityInfo> apps = mLauncherApps.getActivityList(null, user);
// 封装为桌面应用数据模型
for (LauncherActivityInfo app : apps) {
AppInfo appInfo = new AppInfo(app, ...);
mBgAllAppsList.add(appInfo, app, false);
}
}
// 批量缓存应用图标、名称,优化加载速度
mIconCache.getTitlesAndIconsInBulk(iconRequestInfos);
}
六、Binder 跨进程通信:Launcher 与系统服务数据交互
Launcher 作为第三方应用,无法直接访问 PMS 核心数据,通过 LauncherApps 系统 API + Binder IPC 跨进程调用 system_server 服务获取应用列表。
6.1 客户端:LauncherApps 调用入口
文件:frameworks/base/core/java/android/content/pm/LauncherApps.java
java
public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
logErrorForInvalidProfileAccess(user);
try {
// Binder跨进程调用系统服务
return convertToActivityList(
mService.getLauncherActivities(mContext.getPackageName(), packageName, user),
user);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
mService 为 AIDL 代理对象,通过 ServiceManager 获取系统常驻的 launcherapps 服务。
6.2 服务端:LauncherAppsService 核心实现
文件:frameworks/base/services/core/java/com/android/server/pm/LauncherAppsService.java
系统服务端接收 Launcher 跨进程请求,通过 PMS 解析符合桌面规则的应用。
java
public ParceledListSlice<LauncherActivityInfoInternal> getLauncherActivities(
String callingPackage, String packageName, UserHandle user) {
// 核心Intent规则:匹配所有桌面可展示应用
ParceledListSlice<LauncherActivityInfoInternal> launcherActivities =
queryActivitiesForUser(callingPackage,
new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(packageName),
user);
// 处理归档应用、隐藏应用合成图标
return result;
}
6.3 最终底层:PMS 意图解析应用
java
private List<LauncherActivityInfoInternal> queryIntentLauncherActivities(
Intent intent, int callingUid, UserHandle user) {
// 调用PMS核心方法,解析所有匹配Intent的Activity
final List<ResolveInfo> apps = mPackageManagerInternal.queryIntentActivities(
intent,
intent.resolveTypeIfNeeded(mContext.getContentResolver()),
PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
callingUid,
user.getIdentifier());
// 封装数据返回
}
核心规则总结 :只有 AndroidManifest 中声明 ACTION_MAIN + CATEGORY_LAUNCHER 的应用,才会被桌面识别、展示图标。
七、跨进程调用全链路图示
bash
┌─────────────────────────────────────────────────────────────┐
│ Launcher3 应用进程(客户端) │
│ │
│ LoaderTask.loadAllApps() │
│ └─ mLauncherApps.getActivityList(null, user) │
│ └─ mService.getLauncherActivities(...) │
│ │ ← ILauncherApps AIDL代理发起请求 │
└────────────┼────────────────────────────────────────────────┘
│ Binder IPC 跨进程通信
┌────────────▼────────────────────────────────────────────────┐
│ system_server 系统进程(服务端) │
│ │
│ LauncherAppsService.getLauncherActivities(...) │
│ └─ queryActivitiesForUser(...) │
│ └─ queryIntentLauncherActivities(...) │
│ └─ PMS.queryIntentActivities( │
│ Intent(ACTION_MAIN + CATEGORY_LAUNCHER) │
│ ) │
│ 最终:解析所有声明桌面入口的应用,返回给Launcher3 │
└─────────────────────────────────────────────────────────────┘
八、UI 数据绑定与桌面最终渲染
8.1 后台数据快照拷贝与主线程切换
文件路径:src/com/android/launcher3/model/BaseLauncherBinder.java
java
public void bindAllApps() {
// 拷贝排序后的稳定数据快照,避免后台线程数据篡改
AppInfo[] apps = mBgAllAppsList.copyData();
int flags = mBgAllAppsList.getFlags();
// 构建包名-UID映射表
Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect(
Collectors.toMap(
appInfo -> new PackageUserKey(appInfo.componentName.getPackageName(),
appInfo.user),
appInfo -> appInfo.uid,
(a, b) -> a));
// 切换主线程执行UI绑定
executeCallbacksTask(c -> c.bindAllApplications(apps, flags, packageUserKeytoUidMap),
mUiExecutor);
}
8.2 数据更新与 RecyclerView 高效刷新
通过 AllAppsStore 统一管理数据源,采用 DiffUtil 差分刷新,避免全局重绘,提升桌面流畅度。
java
public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
mApps = apps == null ? EMPTY_ARRAY : apps; // 存储最新应用数据
mModelFlags = flags;
notifyUpdate(); // 通知UI刷新
mPackageUserKeytoUidMap = map;
}
// 通知所有监听者更新UI
private void notifyUpdate() {
if (mDeferUpdatesFlags != 0) {
mUpdatePending = true;
return;
}
for (OnUpdateListener listener : mUpdateListeners) {
listener.onAppsUpdated();
}
}
8.3 最终 UI 渲染流程
- AlphabeticalAppsList:过滤、排序应用列表,生成有序数据源
- DiffUtil:计算新旧数据差异,精准刷新 RecyclerView
- BaseAllAppsAdapter:创建 BubbleTextView(图标 + 文字控件),绑定应用信息
- ActivityAllAppsContainerView:多用户场景 Tab 栏管理、列表切换
九、Launcher3 图标点击事件完整执行流程
前文我们梳理完 Launcher 开机启动、数据加载、UI 渲染全流程,桌面展示图标后,核心交互就是应用图标点击启动事件。本节详解 Launcher3 图标点击监听器注册、事件分发、Intent 构建、应用启动的完整源码逻辑,吃透桌面点击交互的分层设计。
9.1 点击监听器统一注册机制
Launcher 桌面所有应用图标(桌面常驻图标、全部应用抽屉图标)的控件均为 BubbleTextView,全局共用同一个点击处理器,单例复用,规避重复创建监听器造成的内存浪费。
9.1.1 桌面图标监听器注册(ItemInflater.kt)
Kotlin
private fun createShortcut(info: WorkspaceItemInfo, parent: ViewGroup): View {
val favorite = LayoutInflater.from(parent.context)
.inflate(R.layout.app_icon, parent, false) as BubbleTextView
favorite.applyFromWorkspaceItem(info)
favorite.setOnClickListener(clickListener) // ← ItemClickHandler.INSTANCE 全局单例
return favorite
}
9.1.2 全部应用抽屉图标监听器注册(BaseAllAppsAdapter.java)
java
icon.setOnClickListener(mOnIconClickListener); // 同样指向 ItemClickHandler.INSTANCE
9.1.3 全局监听器绑定原理
为什么两处点击监听最终都是 ItemClickHandler.INSTANCE?
Launcher继承BaseDraggingActivity- Launcher 重写
getItemOnClickListener()方法 - 页面上下文
mActivityContext实际为 Launcher 实例,方法固定返回全局单例ItemClickHandler.INSTANCE
设计优势:全局图标事件统一入口,便于统一拦截、埋点、动画、异常处理,架构简洁易扩展。
9.2 统一事件分发入口:ItemClickHandler.onClick ()
所有图标点击事件都会进入该静态统一方法,通过 View.getTag() 取出控件绑定的数据对象,根据实体类型分发不同启动逻辑。 文件: ItemClickHandler.java
java
public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
private static void onClick(View v) {
if (v.getWindowToken() == null) return; // view已脱离窗口,忽略无效点击
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!launcher.getWorkspace().isFinishedSwitchingState()) return; // 桌面切换动画中,拦截点击
Object tag = v.getTag(); // 每个BubbleTextView的tag存储对应业务数据模型
if (tag instanceof WorkspaceItemInfo) {
onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher); // 桌面常驻图标
} else if (tag instanceof FolderInfo) {
onClickFolderIcon(v); // 文件夹图标
} else if (tag instanceof AppInfo) {
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher); // 应用抽屉图标
}
}
关键设计:每个 BubbleTextView 的 tag 字段提前绑定数据对象(AppInfo / WorkspaceItemInfo),点击无需二次查询,直接类型判断分发,性能高效。
9.3 桌面图标专项处理逻辑
针对桌面常驻图标(WorkspaceItemInfo),单独做异常状态拦截,适配禁用应用、安装中占位图标等特殊场景。
java
public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
// 检查应用是否被禁用、暂停、设备管理器限制
if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
return; // 弹出提示Toast,不执行启动
}
// 处理应用安装中占位图标(承诺图标)
if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
String packageName = shortcut.getIntent().getComponent().getPackageName();
if (!TextUtils.isEmpty(packageName)) {
onClickPendingAppItem(v, launcher, packageName, ...); // 跳转应用商店
return;
}
}
// 正常应用统一进入启动汇合方法
startAppShortcutOrInfoActivity(v, shortcut, launcher);
}
9.4 应用启动汇合核心方法:startAppShortcutOrInfoActivity ()
桌面图标、全部应用抽屉图标最终都会进入此方法,统一处理 Intent 适配、特殊场景、启动动画,再调用安全启动接口。
java
private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
// ① 获取预构建好的启动 Intent
Intent intent = item.getIntent();
// ② 特殊处理:正在安装中的应用,替换商店跳转Intent
if (item instanceof ItemInfoWithIcon itemInfoWithIcon) {
if ((itemInfoWithIcon.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
intent = ApiWrapper.INSTANCE.get(launcher)
.getAppMarketActivityIntent(packageName, Process.myUserHandle());
}
}
// ③ 特殊处理:支持Web UI的快捷方式,清除包名允许浏览器选择器
if (item instanceof WorkspaceItemInfo si) {
if (si.hasStatusFlag(FLAG_SUPPORTS_WEB_UI) && ACTION_VIEW.equals(intent.getAction())) {
intent = new Intent(intent);
intent.setPackage(null);
}
}
// ④ 初始化图标放大过渡动画
if (v != null && launcher.supportsAdaptiveIconAnimation(v)) {
FloatingIconView.fetchIcon(launcher, v, item, true);
}
// ⑤ 调用分层安全启动入口
launcher.startActivitySafely(v, intent, item);
}
9.5 Intent 预构建逻辑
Launcher 所有应用启动 Intent 全部预构建、持久化存储,点击时直接复用,无需实时拼接,提升点击响应速度。
9.5.1 全部应用图标 AppInfo
java
// AppInfo.java
public static Intent makeLaunchIntent(ComponentName cn) {
return new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(cn) // 绑定应用唯一组件名
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
9.5.2 桌面常驻图标 WorkspaceItemInfo
桌面图标信息持久化存储在 launcher.db 的 favorites 表中,开机加载时直接从数据库反序列化还原 intent 字段。
java
public Intent intent; // 序列化存入数据库,启动直接读取
9.6 分层安全启动:多层 startActivitySafely 职责拆分
采用分层架构,三层 startActivitySafely 层层包装,各司其职、解耦业务逻辑。
9.6.1 第一层 QuickstepLauncher:桌面 UI 动画适配
负责 Hotseat 预测行 UI 暂停与恢复,避免启动动画时界面闪烁错乱。
java
@Override
public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) {
// 暂停Hotseat预测行UI刷新
mHotseatPredictionController.setPauseUIUpdate(true);
PredictionRowView predictionRowView = ...;
predictionRowView.setPredictionUiUpdatePaused(true);
// 调用父类启动逻辑
RunnableList result = super.startActivitySafely(v, intent, item);
// 应用启动动画结束后恢复UI更新
if (result != null) {
result.add(() -> {
mHotseatPredictionController.setPauseUIUpdate(false);
predictionRowView.setPredictionUiUpdatePaused(false);
});
}
return result;
}
9.6.2 第二层 Launcher:页面生命周期保护
处理 Launcher 页面未 Resume 场景延迟启动,添加图标按压视觉反馈。
java
@Override
public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) {
// Launcher未恢复前台,延迟至Resume后再启动应用
if (!hasBeenResumed()) {
addEventCallback(EVENT_RESUMED, () -> {
startActivitySafely(v, intent, item);
});
return result;
}
// 调用底层启动逻辑
RunnableList result = super.startActivitySafely(v, intent, item);
// 图标保持按下状态,动画结束后取消
if (result != null && v instanceof BubbleTextView btv) {
btv.setStayPressed(true);
result.add(() -> btv.setStayPressed(false));
}
return result;
}
9.6.3 第三层 ActivityContext:底层系统启动入口
最底层核心,安全校验、动画参数封装、区分三种启动路径,最终调用 Framework 启动 Activity。
java
default RunnableList startActivitySafely(View v, Intent intent, @Nullable ItemInfo item) {
Context context = (Context) this;
// 安全模式校验:禁止启动第三方应用
if (isAppBlockedForSafeMode() && !appInfo.isSystem()) {
Toast.makeText(context, R.string.safemode_shortcut_error, ...).show();
return null;
}
// 构建启动动画参数,携带图标坐标、尺寸
ActivityOptionsWrapper options = v != null
? getActivityLaunchOptions(v, item)
: makeDefaultActivityOptions(...);
UserHandle user = item.user;
Bundle optsBundle = options.toBundle();
// 新建独立任务栈
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (v != null) {
intent.setSourceBounds(Utilities.getViewBounds(v)); // 动画起始坐标
}
try {
boolean isShortcut = (item instanceof WorkspaceItemInfo)
&& item.itemType == ITEM_TYPE_DEEP_SHORTCUT;
// 三条启动分支
if (isShortcut) {
// 路径A:长按深度快捷方式
String id = ((WorkspaceItemInfo) item).getDeepShortcutId();
context.getSystemService(LauncherApps.class)
.startShortcut(packageName, id, sourceBounds, optsBundle, user);
} else if (user == null || user.equals(Process.myUserHandle())) {
// 路径B:同用户普通App(最主流)
context.startActivity(intent, optsBundle);
} else {
// 路径C:工作Profile/跨用户应用
context.getSystemService(LauncherApps.class)
.startMainActivity(intent.getComponent(), user, sourceBounds, optsBundle);
}
// 埋点记录应用启动日志
logAppLaunch(getStatsLogManager(), item, instanceId);
return options.onEndCallback;
} catch (ActivityNotFoundException | SecurityException e) {
Toast.makeText(context, R.string.activity_not_found, ...).show();
}
return null;
}
9.7 三种启动路径对比表
| 场景 | 调用方式 | 说明 |
|---|---|---|
| 普通 App(同用户) | context.startActivity(intent, optsBundle) | 最常用,直接调用 Activity 启动 API,性能最优 |
| 工作 profile / 跨用户 App | LauncherApps.startMainActivity(component, user, ...) | 多用户隔离,通过系统服务代理,权限管控 |
| 深度快捷方式(长按菜单) | LauncherApps.startShortcut(pkg, id, ...) | 专属快捷方式启动逻辑,支持快捷参数透传 |
9.8 图标点击事件完整全链路流程图
bash
┌──────────────────────────────────────────────────────────────────┐
│ 用户手指触摸 BubbleTextView │
│ (tag = AppInfo / WorkspaceItemInfo) │
└───────────────┬──────────────────────────────────────────────────┘
│ View.OnClickListener.onClick()
▼
┌──────────────────────────────────────────────────────────────────┐
│ ItemClickHandler.onClick(view) │
│ ├── 检查窗口有效性,过滤已销毁View │
│ ├── 检查Workspace动画状态,动画中拦截点击 │
│ └── 根据tag实体类型分发桌面图标/抽屉图标/文件夹逻辑 │
└───────────────┬──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ startAppShortcutOrInfoActivity(v, item, launcher) │
│ ├── 取出预构建 Intent(ACTION_MAIN + CATEGORY_LAUNCHER + ComponentName) │
│ ├── 处理安装中应用、Web快捷方式特殊场景 │
│ ├── 初始化图标放大过渡动画 FloatingIconView │
│ └── 调用 launcher.startActivitySafely(v, intent, item) │
└───────────────┬──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ QuickstepLauncher.startActivitySafely() │
│ └── 暂停Hotseat预测行UI更新 → 调用父类方法 │
└───────────────┬──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Launcher.startActivitySafely() │
│ ├── 页面未Resume则延迟至前台后启动 │
│ ├── 设置图标持续按压视觉反馈 │
│ └── 调用底层ActivityContext启动接口 │
└───────────────┬──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ ActivityContext.startActivitySafely() ← 系统最终启动入口 │
│ ├── 安全模式第三方应用拦截校验 │
│ ├── 构造ActivityOptions动画参数,携带图标坐标 │
│ ├── intent添加FLAG_ACTIVITY_NEW_TASK标识 │
│ ├── 分支1:深度快捷方式 → LauncherApps.startShortcut() │
│ ├── 分支2:同用户普通应用 → context.startActivity() ★核心路径 │
│ └── 分支3:跨用户工作区应用 → LauncherApps.startMainActivity() │
│ 最后:logAppLaunch 记录启动埋点日志 │
└───────────────┬──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Android Framework (AMS) 系统层 │
│ AMS接收启动请求 → 创建/复用应用进程 → 启动目标Activity → 渲染应用窗口 │
└──────────────────────────────────────────────────────────────────┘
9.9 点击交互核心设计总结
- 全局统一单例监听器 :所有图标共用
ItemClickHandler.INSTANCE,通过 Tag 区分数据模型,统一管控点击逻辑,易埋点、易拦截、易扩展; - Intent 预加载机制:应用启动 Intent 在桌面初始化 / 数据库加载阶段提前构建存储,点击直接复用,无实时拼接耗时;
- 分层职责架构:UI 动画层、生命周期层、系统底层启动层三层拆分,代码解耦,各司其职;
- 多用户场景兼容:区分普通应用、工作 Profile 应用、深度快捷方式三套启动逻辑,适配原生多用户隔离机制;
- 流畅动画联动:通过 ActivityOptions 传递图标坐标,系统实现图标放大过渡动画,提升交互体验。
十、全文整体流程总览
bash
┌───────────── 系统服务端流程(system_server) ─────────────┐
│ SystemServer.startOtherServices() │
│ → AMS.systemReady() │
│ → ATMS.startHomeOnAllDisplays() │
│ → RootWindowContainer 多屏幕遍历 │
│ → 构建 HOME 专属 Intent(ACTION_MAIN+CATEGORY_HOME) │
│ → PMS 匹配系统默认 Launcher 应用 │
│ → ActivityStartController 拉起 Launcher Activity │
└───────────────────────────┬───────────────────────────────┘
│ 启动Launcher应用进程
┌───────────── Launcher3客户端开机初始化流程 ────────────────┐
│ Launcher.onCreate() 初始化全局组件、布局、缓存 │
│ → 后台LoaderTask线程加载桌面布局、所有应用、小组件数据 │
│ → Binder IPC调用LauncherAppsService,从PMS查询应用列表 │
│ → 主线程差分刷新RecyclerView,渲染桌面图标UI │
└───────────────────────────┬───────────────────────────────┘
│ 用户点击图标触发交互
┌───────────── Launcher图标点击交互流程 ─────────────────────┐
│ BubbleTextView点击 → ItemClickHandler统一分发 │
│ → 预取Intent、处理各类特殊场景、初始化过渡动画 │
│ → 分层startActivitySafely安全启动应用 │
│ → 区分普通应用/跨用户应用/深度快捷方式三种启动路径 │
│ → 交由AMS创建进程、启动目标应用Activity │
└───────────────────────────────────────────────────────────┘
十一、全文核心知识点总结
1. 系统开机拉起 Launcher 相关
- Launcher 由 system_server 在 systemReady 阶段主动拉起,并非自启动;
- AMS 与 ATMS 通过 ActivityTaskManagerInternal 本地接口解耦,避免循环依赖;
- 系统识别桌面的唯一标识:
ACTION_MAIN + CATEGORY_HOME; - RootWindowContainer 是全局窗口树根节点,原生支持多主屏、副屏、车载扩展屏,对应源码路径
frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java。
2. Launcher 桌面数据加载相关
- 应用列表通过 LauncherApps AIDL 跨进程调用 system_server 的 PMS 查询;
- 仅 AndroidManifest 声明
ACTION_MAIN + CATEGORY_LAUNCHER的应用才会显示桌面图标; - 数据后台子线程加载,主线程 DiffUtil 差分刷新 UI,避免开机卡顿。
3. 图标点击交互相关
- 全局单例 ItemClickHandler 统一处理所有图标点击,通过 View.tag 区分业务数据;
- Intent 预构建持久化存储,点击直接复用,降低耗时;
- 三层 startActivitySafely 分层设计,分别处理 UI 动画、页面生命周期、底层应用启动;
- 原生支持多用户隔离,普通应用、工作区应用、深度快捷方式拥有独立启动链路;
- ActivityOptions 携带图标坐标,实现桌面图标到应用窗口的流畅过渡动画。