前文回顾
在前面的系列文章中,我们沿着 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 核心触发源码
privatevoidstartOtherServices() {// 省略大量系统服务初始化逻辑...// 所有核心服务启动完成后,调用AMS的systemReady方法mActivityManagerService.systemReady(() -> {/* 开机收尾回调逻辑 */}, BOOT_TIMINGS_TRACE_LOG);}
当所有系统服务初始化完毕后,会调用 AMS.systemReady(),标志系统开机流程进入收尾阶段,Launcher 桌面的启动逻辑就封装在该方法中。
1.2 AMS.systemReady 启动桌面核心逻辑
publicvoidsystemReady(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 为多屏幕桌面启动统一入口,支持主屏、副屏、车载扩展屏等多设备场景。
@Overridepublic boolean startHomeOnAllDisplays(int userId, String reason) {synchronized (mGlobalLock) {// 全局锁保证桌面启动线程安全return mRootWindowContainer.startHomeOnAllDisplays(userId, reason);}}
2.2 核心容器:RootWindowContainer 层级架构
RootWindowContainer 是 Android 窗口容器树的唯一根节点,所有屏幕、窗口、任务栈、应用页面都挂载在其下,继承关系:RootWindowContainer → WindowContainer → ConfigurationContainer。
完整窗口容器树层级(自上而下):
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 遍历所有屏幕启动桌面
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 单屏幕任务区域桌面启动逻辑
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。
boolean startHomeOnTaskDisplayArea(int userId, // 当前系统用户IDString 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桌面IntenthomeIntent = mService.getHomeIntent();// 核心:通过PMS查询系统默认的Launcher桌面ActivityaInfo = 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:屏幕IDfinal String myReason = reason + ":" + userId + ":" + UserHandle.getUserId(aInfo.applicationInfo.uid) + ":" + taskDisplayArea.getDisplayId();// 进入标准Activity启动流水线,拉起LaunchermService.getActivityStartController().startHomeActivity(homeIntent, aInfo, myReason,taskDisplayArea);return true;}
3.1 标准桌面 Intent 构建规则
系统通过固定 Action、Category 唯一匹配桌面应用,这也是普通应用无法成为默认桌面的核心原因。
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。
void startHomeActivity(Intent intent, // 组装完成的HOME IntentActivityInfo 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 初始化
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() -> 后台线程加载数据
// addCallbacksAndLoad 核心逻辑funaddCallbacksAndLoad(callbacks: Callbacks): Boolean {addCallbacks(callbacks) // 注册UI回调return startLoader(callbacks) // 启动数据加载器}funstartLoader(newCallbacks: Callbacks): Boolean {return if (mModelLoaded) {// 数据已加载,直接同步绑定UIbindDirectly(newCallbacks)true} else {// 未加载,创建LoaderTask,投递到后台线程执行MODEL_EXECUTOR.post(mLoaderTask)false}}
5.3 LoaderTask 后台全量数据加载流程
文件路径:src/com/android/launcher3/model/LoaderTask.java
后台线程异步加载桌面所有数据源,避免阻塞 UI,核心四大步骤:
@Overridepublic void run() {// Step1:加载桌面布局、快捷方式、小组件配置(本地数据库)loadWorkspace(...);mLauncherBinder.bindWorkspace(...);// Step2:加载系统所有已安装应用(核心步骤)loadAllApps();mLauncherBinder.bindAllApps();// Step3:加载应用深度快捷方式loadDeepShortcuts();mLauncherBinder.bindDeepShortcuts();// Step4:加载桌面Widget小组件数据loadWidgets();mLauncherBinder.bindWidgets();}
5.4 loadAllApps 应用列表查询核心逻辑
遍历系统所有用户空间,查询所有可在桌面展示的应用,封装为应用数据模型。
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
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 解析符合桌面规则的应用。
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 意图解析应用
private List<LauncherActivityInfoInternal> queryIntentLauncherActivities(Intent intent, int callingUid, UserHandle user) {// 调用PMS核心方法,解析所有匹配Intent的Activityfinal 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 的应用,才会被桌面识别、展示图标。
七、跨进程调用全链路图示
┌─────────────────────────────────────────────────────────────┐│ 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
publicvoidbindAllApps() {// 拷贝排序后的稳定数据快照,避免后台线程数据篡改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 差分刷新,避免全局重绘,提升桌面流畅度。
public void setApps(@NullableAppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {mApps = apps == null ? EMPTY_ARRAY : apps; // 存储最新应用数据mModelFlags = flags;notifyUpdate(); // 通知UI刷新mPackageUserKeytoUidMap = map;}// 通知所有监听者更新UIprivate 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)
private funcreateShortcut(info: WorkspaceItemInfo, parent: ViewGroup): View {val favorite = LayoutInflater.from(parent.context).inflate(R.layout.app_icon, parent, false) as BubbleTextViewfavorite.applyFromWorkspaceItem(info)favorite.setOnClickListener(clickListener) // ← ItemClickHandler.INSTANCE 全局单例return favorite}
9.1.2 全部应用抽屉图标监听器注册(BaseAllAppsAdapter.java)
icon.setOnClickListener(mOnIconClickListener); // 同样指向 ItemClickHandler.INSTANCE9.1.3 全局监听器绑定原理
为什么两处点击监听最终都是 ItemClickHandler.INSTANCE?
Launcher继承 BaseDraggingActivityLauncher 重写 getItemOnClickListener()方法页面上下文 mActivityContext实际为 Launcher 实例,方法固定返回全局单例ItemClickHandler.INSTANCE
设计优势:全局图标事件统一入口,便于统一拦截、埋点、动画、异常处理,架构简洁易扩展。
9.2 统一事件分发入口:ItemClickHandler.onClick ()
所有图标点击事件都会进入该静态统一方法,通过 View.getTag() 取出控件绑定的数据对象,根据实体类型分发不同启动逻辑。 文件: ItemClickHandler.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),单独做异常状态拦截,适配禁用应用、安装中占位图标等特殊场景。
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 适配、特殊场景、启动动画,再调用安全启动接口。
private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {// ① 获取预构建好的启动 IntentIntent intent = item.getIntent();// ② 特殊处理:正在安装中的应用,替换商店跳转Intentif (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
// AppInfo.javapublic 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 字段。
public Intent intent; // 序列化存入数据库,启动直接读取9.6 分层安全启动:多层 startActivitySafely 职责拆分
采用分层架构,三层 startActivitySafely 层层包装,各司其职、解耦业务逻辑。
9.6.1 第一层 QuickstepLauncher:桌面 UI 动画适配
负责 Hotseat 预测行 UI 暂停与恢复,避免启动动画时界面闪烁错乱。
@Overridepublic 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 场景延迟启动,添加图标按压视觉反馈。
@Overridepublic 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。
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 三种启动路径对比表
9.8 图标点击事件完整全链路流程图
┌──────────────────────────────────────────────────────────────────┐│ 用户手指触摸 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 传递图标坐标,系统实现图标放大过渡动画,提升交互体验。
十、全文整体流程总览
┌───────────── 系统服务端流程(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 携带图标坐标,实现桌面图标到应用窗口的流畅过渡动画。
夜雨聆风