Android Framework源码解析(六):Launcher3 启动流程详解——AMS 如何拉起系统桌面?

前文回顾

在前面的系列文章中,我们沿着 Android 系统启动链路,完整梳理了 Framework 从无到有的初始化全过程。

首先,init 进程作为 Android 用户空间的首个进程,负责解析 init.rc 配置、挂载系统文件、拉起各类 Native 服务,为系统运行搭建基础环境。

随后 Zygote 进程 启动,预加载 Framework 核心类、系统资源与通用依赖库,通过 fork 机制孵化出 system_server 进程与所有应用进程,是 Android 高效进程管理的核心基石。

紧接着 SystemServer 完成 Framework 顶层初始化,依次启动 AMS、PMS、WMS 等数百个核心系统服务。上一篇文章我们重点解析了 SystemServiceManager,搞懂了系统服务的统一创建、管理逻辑,以及 BootPhase 开机阶段的核心设计思想。

至此,整个 Android 开机启动链路还剩下最后两块核心拼图:

  1. 开机系统如何自动拉起 Launcher 桌面;
  2. 用户点击桌面图标后,应用完整启动交互流程。

我们日常开机后自动展示的 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 渲染流程

  1. AlphabeticalAppsList:过滤、排序应用列表,生成有序数据源
  2. DiffUtil:计算新旧数据差异,精准刷新 RecyclerView
  3. BaseAllAppsAdapter:创建 BubbleTextView(图标 + 文字控件),绑定应用信息
  4. 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

  1. Launcher 继承 BaseDraggingActivity
  2. Launcher 重写 getItemOnClickListener() 方法
  3. 页面上下文 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 点击交互核心设计总结

  1. 全局统一单例监听器 :所有图标共用 ItemClickHandler.INSTANCE,通过 Tag 区分数据模型,统一管控点击逻辑,易埋点、易拦截、易扩展;
  2. Intent 预加载机制:应用启动 Intent 在桌面初始化 / 数据库加载阶段提前构建存储,点击直接复用,无实时拼接耗时;
  3. 分层职责架构:UI 动画层、生命周期层、系统底层启动层三层拆分,代码解耦,各司其职;
  4. 多用户场景兼容:区分普通应用、工作 Profile 应用、深度快捷方式三套启动逻辑,适配原生多用户隔离机制;
  5. 流畅动画联动:通过 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 相关

  1. Launcher 由 system_server 在 systemReady 阶段主动拉起,并非自启动;
  2. AMS 与 ATMS 通过 ActivityTaskManagerInternal 本地接口解耦,避免循环依赖;
  3. 系统识别桌面的唯一标识:ACTION_MAIN + CATEGORY_HOME
  4. RootWindowContainer 是全局窗口树根节点,原生支持多主屏、副屏、车载扩展屏,对应源码路径 frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java

2. Launcher 桌面数据加载相关

  1. 应用列表通过 LauncherApps AIDL 跨进程调用 system_server 的 PMS 查询;
  2. 仅 AndroidManifest 声明ACTION_MAIN + CATEGORY_LAUNCHER的应用才会显示桌面图标;
  3. 数据后台子线程加载,主线程 DiffUtil 差分刷新 UI,避免开机卡顿。

3. 图标点击交互相关

  1. 全局单例 ItemClickHandler 统一处理所有图标点击,通过 View.tag 区分业务数据;
  2. Intent 预构建持久化存储,点击直接复用,降低耗时;
  3. 三层 startActivitySafely 分层设计,分别处理 UI 动画、页面生命周期、底层应用启动;
  4. 原生支持多用户隔离,普通应用、工作区应用、深度快捷方式拥有独立启动链路;
  5. ActivityOptions 携带图标坐标,实现桌面图标到应用窗口的流畅过渡动画。
相关推荐
XiaoLeisj1 小时前
HandlerThread 与 IntentService 使用及源码解析
android·handler
我命由我1234511 小时前
Android Studio - Android Studio 自定义预览尺寸
android·java·ide·java-ee·android studio·android-studio·android runtime
取个名字太难了~13 小时前
越用越便宜,越用越强大:影像 SDK 的边际成本递减与网络效应实践
android·数码相机·美颜·相机连接·demu
AFinalStone17 小时前
Android 7系统网络(四)Native层(下)—netd Controller详解
android·网络
爱笑鱼17 小时前
Handler(一):post 之后,Runnable 到底在哪个线程执行?
android
木木子2218 小时前
[特殊字符] 音乐播放器——状态驱动的多媒体控制
android·开发语言·华为·php·harmonyos
雨白18 小时前
C 语言基础:结构体、联合体与枚举
android
阿巴斯甜19 小时前
Android 代码混淆
android
apihz21 小时前
台风实时与历史详情查询免费 API 接口完整教程
android·开发语言·tcp/ip·dubbo·台风·天气预报