Android 默认提供桌面,也就是我们常说的 Launcher 。简单来说,就是系统上电后执行完初始化进入的默认桌面,对应的包名在 AOSP 是 com.android.launcher3:
bash
tspi_3m:/ $ pm path com.android.launcher3
package:/system_ext/priv-app/Launcher3QuickStep/Launcher3QuickStep.apk
在 Settings 中可以看到这个应用的存在:
设置 → 应用 → 默认应用 → 主屏幕应用

实际上,Launcher 通常会被定制,主要有两种场景:
- 基于
com.android.launcher3的源码进行二次开发定制 - 开发新的桌面应用程序
第一种方式较为复杂,且开发成本较高,因为 Launcher 本身的源码逻辑十分繁琐;第二种方式则更为常见,通常根据具体业务需求而定。本文将重点介绍第二种方式。
创建新的桌面应用
我们创建一个新的应用程序,包名:com.tlrobot.home。
xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<!-- 普通应用入口:可从应用抽屉/桌面图标启动 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 可选桌面(Launcher):显示在"默认桌面"选择器中 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
核心配置变更如下:

完成上述配置后即可生效,但每次启动都会弹出两个 Launcher 供选择。为了优化此体验,我们可以前往 设置 → 应用 → 默认应用 → 主屏幕应用:

这里多了一个我们开发的应用程序。将其设置为 tlrobothome 后,即可生效。此时无法再退回原系统的 Launcher。进一步验证重启设备后是否依然生效:
实测发现
adb reboot后依然生效,设备进入了自定义桌面应用。但在进入前仍会短暂展示原有的桌面或壁纸。本文推测这可能是fallbackhome导致,其原因已定位,后续可通过将该应用内置为系统应用来解决。
闪屏的解决方案
为什么会出现闪屏现象?
Android 14 里 packages/apps/Settings/src/com/android/settings/FallbackHome.java 的行为是:
- 设备 DEVICE_PROVISIONED 之后,
onCreate会调用loadWallpaperColors(),此时窗口背景为壁纸(第 93-104 行)。 - 开机时
ActivityManagerService.startHomeActivityLocked()一开始很可能解析不到目标 Launcher(它是adb install到 data 分区的,user 0 解锁前 data 分区中的应用不可用),于是系统先启动FallbackHome兜底显示,界面就是"一张纯壁纸"。 maybeFinish()(168-183 行)等在UserManager.isUserUnlocked()为真、解析到真正的 HOME 且不是自身包时,才finish()。此时才真正冷启动目标 Launcher。
所以,"先显示壁纸/旧桌面再进自家桌面"这一现象,= FallbackHome 的壁纸窗口 (1) + Launcher 冷启动的时间差 (3)。
关键约束 :
android:persistent="true"、directBootAware、预加载这些"根治"手段,只对系统内置应用生效。当前是 adb 安装的 debug 应用,仅修改 manifest 是无效的。因此要根治闪屏,第一步需将 app 做成设备镜像中的系统应用。
做成 persistent 系统应用
persistent 应用在开机时会被 AMS 非常早地 启动(startPersistentAppsLocked),进程常驻。到 AMS 要求"start home"时 Launcher 进程已经活着,几乎没有冷启动空窗 → 闪屏基本消失。这正是 Launcher3 / Quickstep 等真实桌面的做法。
做法:
- 给 Manifest 加
<application android:persistent="true"> - app 变成系统应用,编进镜像:在
TaishanPi-3-Android14/packages/apps/tlrobothome放一个Android.bp并加进PRODUCT_PACKAGES,或者把 APK 打进/system_ext/priv-app目录。
内置系统应用
下面我们将这个普通的 AS 应用迁移和改造到系统源码中,目标是:使自制的桌面应用程序既支持 .bp 构建,也能在 AS 中直接打开运行。
项目天然带有 .gradle,本身就支持在 AS 中直接打开、编译并执行 apk。
我们来看内置到系统时是如何处理的:首先在 tlrobothome 应用内新增 android.bp 文件(Android 系统编译旧版本使用 .mk,现在高版本都是 .bp,AOSP 也正逐渐将系统源码中的 .mk 切换为 .bp):
python
//
// 桌面/Launcher 应用 tlrobothome。
//
// 双构建:
// * AOSP 整编:本 Android.bp 由 Soong 编译,打进镜像(system app)。
// * 独立编译:同目录下保留 Gradle 工程(app/ 等),可在 Android Studio 打开。
// 两套 source 共用 app/src/main/*。
//
// 说明:aapt2 要求 manifest 带 package 属性,而 Gradle(AGP 8/9) 用 namespace、
// 禁止 manifest 写 package。因此整编侧使用模块根的 AndroidManifest.xml(带 package),
// Gradle 侧使用 app/src/main/AndroidManifest.xml(无 package),两文件保持同步。
android_app {
name: "tlrobothome",
// 整编专用 manifest(aapt2 要求 package 属性,见 AndroidManifest.xml 头注释)
manifest: "AndroidManifest.xml",
srcs: ["app/src/main/java/**/*.java"],
resource_dirs: ["app/src/main/res"],
static_libs: [
"com.google.android.material_material",
],
// 作为系统 Launcher,使用系统 API 并平台签名,方便后续加持久化/系统权限
sdk_version: "system_current",
min_sdk_version: "31",
certificate: "platform",
optimize: {
enabled: false,
},
}
需要注意:
- 为保留双编译 的能力,同时兼顾 AGP 与 aapt 打包的差异,
manifest需要准备两份:一份供 AS 使用,一份供 Soong 编译打包使用。 - 此外,在 Gradle 中新增的内容,也需要在
android.bp中同步一份。
在设备配置文件 device/rockchip/rk3576/tspi_3m/tspi_3m.mk 中将其加入编译产物:
makefile
PRODUCT_PACKAGES += tlrobothome
这是最关键的增量动作------没有它,out/target/product/.../system/app/tlrobothome 不会生成,也就不会进入 system 分区;/system/app/tlrobothome/tlrobothome.apk 正是靠这一行才被打进固件的。
默认开机桌面
我们的自定义桌面虽然已经内置进去,但尚未被设为默认桌面,开机时仍会弹出桌面选择:

这不是我们想要的,应该如何修改呢?删除原来的 launcher3 肯定是不可行的,因为我们还需要使用 launcher3 的 recent task 等功能。
方案 1 :取消 launcher3 的桌面配置。在 packages/apps/Launcher3/quickstep/AndroidManifest-launcher.xml 中删除 android.intent.category.HOME。
该方案并不可行,启动时会发生崩溃,报错如下:
bash
2026-09-14 13:22:09.199 3400-3400 AndroidRuntime com.android.launcher3 D Shutting down VM
2026-09-14 13:22:09.200 3400-3400 AndroidRuntime com.android.launcher3 E FATAL EXCEPTION: main
Process: com.android.launcher3, PID: 3400
java.lang.RuntimeException: Unable to create service com.android.quickstep.TouchInteractionService: java.lang.NullPointerException: Attempt to read from field 'android.content.pm.ActivityInfo android.content.pm.ResolveInfo.activityInfo' on a null object reference in method 'void com.android.quickstep.OverviewComponentObserver.<init>(android.content.Context, com.android.quickstep.RecentsAnimationDeviceState)'
at android.app.ActivityThread.handleCreateService(ActivityThread.java:4705)
at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2289)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.NullPointerException: Attempt to read from field 'android.content.pm.ActivityInfo android.content.pm.ResolveInfo.activityInfo' on a null object reference in method 'void com.android.quickstep.OverviewComponentObserver.<init>(android.content.Context, com.android.quickstep.RecentsAnimationDeviceState)'
at com.android.quickstep.OverviewComponentObserver.<init>(OverviewComponentObserver.java:86)
at com.android.quickstep.TouchInteractionService.onUserUnlocked(TouchInteractionService.java:530)
at com.android.quickstep.TouchInteractionService$$ExternalSyntheticLambda6.run(Unknown Source:2)
at com.android.launcher3.util.RunnableList.add(RunnableList.java:36)
at com.android.launcher3.util.LockedUserState.runOnUserUnlocked(LockedUserState.kt:72)
at com.android.quickstep.TouchInteractionService.onCreate(TouchInteractionService.java:487)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:4692)
at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2289)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
2026-09-14 13:22:09.204 715-753 ActivityManager system_server I Showing crash dialog for package com.android.launcher3 u0
2026-09-14 13:22:09.239 715-753 CoreBackPreview system_server D Window{f888fbb u0 Application Error: com.android.launcher3}: Setting back callback OnBackInvokedCallbackInfo{mCallback=android.window.WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper@b53eef0, mPriority=0, mIsAnimationCallback=false}
2026-09-14 13:22:29.142 715-761 ActivityManager system_server W Timeout executing service: ServiceRecord{3ece4df u0 com.android.launcher3/com.android.quickstep.TouchInteractionService}
2026-09-14 13:22:29.148 715-3503 ActivityManager system_server I Crashing app skipping ANR: com.android.server.am.ProcessErrorStateRecord@b455886 executing service com.android.launcher3/com.android.quickstep.TouchInteractionService
2026-09-14 13:22:29.149 715-3503 ActivityManager system_server D Completed ANR of com.android.launcher3 in 3ms, latency 0ms
2026-09-14 13:22:29.155 3400-3405 droid.launcher3 com.android.launcher3 I Thread[6,tid=3405,WaitingInMainSignalCatcherLoop,Thread*=0xb400007ca949df50,peer=0x12c48020,"Signal Catcher"]: reacting to signal 3
我们的需求是:以自定义桌面替换 com.android.launcher3,作为镜像首次烧录后的默认桌面;同时不弹出 Launcher 选择弹窗;且不能移除原来的 com.android.launcher3,因为最近任务 / TouchInteractionService 等大量功能仍依赖它。
最终的解决方案是:利用 *.rc 文件的触发器,在接收到 sys.boot_completed 触发器时,以 cmd 命令更改默认的 Launcher 程序(过程中需注意绕开 SELinux 检查)。
两个新创建的文件分别是:
device/rockchip/rk3576/tspi_3m/init.set_default_home.rc
rc
#
# Copyright 2026 The Android Open-Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# 将 tlrobothome 设为默认桌面(Home)角色。
# 本产品同时安装 tlrobothome 与 com.android.launcher3 两个桌面;Launcher3 还需要保留
# 来承载"最近任务/Recents"功能,因此不能删除其 HOME 声明,否则首次开机系统会发现存在
# 多个 HOME 候选而弹出"选择默认桌面"对话框。
#
# 注意:
# 1. add-role-holder 命令行语法: cmd role add-role-holder [--user USER_ID] ROLE PACKAGE [FLAGS]
# --allowOverride 不是本命令的合法 token。
# 2. rc 解析器不允许 service 命令跨物理行,因此下面的 sh -c '...' 必须写成单行(用 ; 分隔)。
# 3. 用 start(异步)+重试: sys.boot_completed 触发那瞬间直接 add-role-holder 常失败(启动竞态),
# 故最多重试10次、间隔3s,直到成功(幂等)。成功/失败痕迹记录到 /data/local/tmp/ 便于排查。
on property:sys.boot_completed=1
# 异步 start + 独立脚本文件实现。用 `sh /system/bin/set_default_home.sh` 而非内联
# `sh -c '单行'`:实测 init 的 rc 解析器会破坏内联单行里的 $(date)/>> /2>&1/;) 等,
# 使 sh 拿到的脚本残缺并 8ms 内退出(status 6)。独立脚本文件则完全避免该问题。
# 脚本内部自带重试(最多10次、间隔3s, 幂等),sys.boot_completed 瞬间的启动竞态可被覆盖。
start set_default_home
service set_default_home /system/bin/sh /system/bin/set_default_home.sh
class late_start
user root
group root
# 必须显式给出 seclabel:init(u:r:init:s0) 直接 exec /system/bin/sh 时策略里没有
# init->shell 的 domain transition,会报 "no domain transition from u:r:init:s0" 而拒绝启动。
# 声明 u:r:shell:s0 让 init 将子进程 setcon 到 shell 域(与 adb shell 手工执行等价),
# shell 域持有 MANAGE_ROLE_HOLDERS,可调用 cmd role。
seclabel u:r:shell:s0
oneshot
disabled
device/rockchip/rk3576/tspi_3m/set_default_home.sh
sh
#!/system/bin/sh
#
# 开机后将 ROLE_HOME 设为 com.tlrobot.home(默认桌面)。
# 由 init 的 set_default_home 服务通过 `sh /system/bin/set_default_home.sh` 调用。
# 用独立脚本文件,避免 init 的 rc 解析器对内联 `sh -c '单行'` 的引号/重定向破坏。
#
# 幂等并可重试:sys.boot_completed 触发瞬间直接 add-role-holder 常失败(启动竞态),
# 故最多重试 10 次、间隔 3s。每次结果的真实 rc/stderr 记录到 /data/local/tmp/ 便于排查。
LOG=/data/local/tmp/set_default_home.log
echo "== boot $(date) ==" > "$LOG"
i=1
while [ "$i" -le 10 ]; do
/system/bin/cmd role add-role-holder --user 0 android.app.role.HOME com.tlrobot.home 0 >> "$LOG" 2>&1
rc=$?
echo "attempt=$i rc=$rc" >> "$LOG"
if [ "$rc" -eq 0 ]; then
echo "OK, ROLE_HOME=com.tlrobot.home" >> "$LOG"
exit 0
fi
i=$((i + 1))
sleep 3
done
exit 1
device/rockchip/rk3576/tspi_3m/tspi_3m.mk 的变更部分
bash
# 可选桌面(Launcher)应用 tlrobothome。
# 既有独立 Gradle 工程(packages/apps/tlrobothome),也可经由此处整编进镜像。
PRODUCT_PACKAGES += tlrobothome
# 开机后将 tlrobothome 设为默认桌面(Home)角色,避免首次开机弹出"选择默认桌面"对话框。
# com.android.launcher3 仍需保留以承载"最近任务/Recents"功能。
# 注意:逻辑放在独立脚本文件 set_default_home.sh(而非 rc 内联 sh -c),避免 init 的
# rc 解析器破坏内联单行脚本。
PRODUCT_COPY_FILES += \
device/rockchip/rk3576/tspi_3m/init.set_default_home.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/init.set_default_home.rc \
device/rockchip/rk3576/tspi_3m/set_default_home.sh:$(TARGET_COPY_OUT_SYSTEM)/bin/set_default_home.sh
至此,我们解决了全部问题,包括开机后弹出「选择桌面」弹窗、闪现 fallbackhome 等问题,同时也完整保留了原本 com.android.launcher3 中仍需要使用的功能。