23、Launcher的稳定性:ANR的监控与避免、Crash的捕获与上报、SELinux权限问题
Launcher 这东西,说白了就是 Android 系统的门面。用户每天解锁手机,第一眼看到的就是它。如果 Launcher 动不动就卡死、闪退,那体验感直接归零。我在做 Launcher 开发的头两年,踩过的坑比走过的路还多。今天咱们就聊聊 Launcher 稳定性的三个核心话题:ANR、Crash 和 SELinux。
一、ANR 的监控与避免
ANR 全称是 Application Not Responding。系统会监控你的主线程,如果 5 秒内没响应输入事件,或者 10 秒内没处理完广播,系统就会弹出一个对话框:「XXX 无响应」。用户看到这个,基本就准备卸载了。
1.1 ANR 的常见原因
我总结了一下,Launcher 的 ANR 主要来自这几个地方:
- 主线程做了耗时操作:比如加载图标、读取数据库、解析 XML。
- Binder 调用阻塞:比如跨进程获取应用列表,对方进程卡住了。
- 锁竞争:主线程和子线程争同一把锁,子线程占着不放。
- 布局过于复杂:一次 measure/layout 耗时超过 5 秒,这在低端机上很常见。
1.2 如何监控 ANR
我个人习惯在 Launcher 里埋一个「看门狗」线程。思路很简单:主线程每隔 1 秒往一个共享变量里写时间戳,看门狗线程每隔 3 秒检查一次。如果发现时间戳超过 4 秒没更新,就说明主线程卡住了。
// 看门狗监控示例
class Watchdog extends Thread {
private long lastTick = SystemClock.uptimeMillis();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
public void run() {
while (true) {
long now = SystemClock.uptimeMillis();
if (now - lastTick > 4000) {
// 主线程卡死超过4秒,收集堆栈
Log.w("LauncherWatchdog", "Possible ANR detected");
dumpMainThreadStack();
}
SystemClock.sleep(1000);
}
}
public void tick() {
lastTick = SystemClock.uptimeMillis();
}
}
StrictMode,你可以用它检测主线程的磁盘读写和网络访问。在 Launcher 的 Application 里开启 StrictMode,开发阶段就能提前发现问题。
1.3 如何避免 ANR
嗯,这里要注意几个关键点:
- 图标加载必须异步:用
Glide或Coil加载应用图标,别自己写 BitmapFactory.decodeResource。 - 数据库操作放子线程:Launcher 有自己的数据库(比如存储桌面布局),查询和写入都用
AsyncTask或Coroutine。 - 减少布局层级:能用
ConstraintLayout就别嵌套LinearLayout。我见过一个 Launcher 的桌面布局嵌套了 8 层,低端机直接 ANR。 - 预加载数据:应用列表、快捷方式这些数据,在 Launcher 启动时就加载到内存里,别等用户滑动到那一屏才去读。
二、Crash 的捕获与上报
Crash 比 ANR 更直接——程序直接挂了。Launcher 作为系统级应用,Crash 会导致桌面重启,用户会看到一瞬间的黑屏。这体验,你想想看,多糟糕。
2.1 全局异常捕获
Android 提供了 Thread.setDefaultUncaughtExceptionHandler,我们可以用它捕获所有未处理的异常。我一般在 Launcher 的 Application 里注册一个全局处理器:
public class LauncherApp extends Application {
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
}
private static class CrashHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
// 收集崩溃信息
String crashInfo = collectCrashInfo(e);
// 保存到本地文件
saveCrashToFile(crashInfo);
// 上报到服务器
uploadCrash(crashInfo);
// 让系统默认处理器处理(或者自己重启 Launcher)
android.os.Process.killProcess(android.os.Process.myPid());
}
}
}
2.2 崩溃信息的收集
收集的信息要足够详细,不然你根本没法定位问题。我一般会收集这些:
| 信息项 | 说明 |
|---|---|
| 异常堆栈 | 完整的 StackTrace,包括 cause chain |
| 设备信息 | 型号、Android 版本、屏幕分辨率 |
| 内存状态 | 总内存、可用内存、堆内存使用量 |
| 当前 Activity | 崩溃时用户正在哪个界面 |
| 日志缓冲区 | 最近 100 行 logcat 日志 |
核心要点:崩溃上报要轻量。别在 uncaughtException 里做网络请求,因为主线程已经挂了。我建议先把崩溃信息写到本地文件,下次启动时再上报。
2.3 崩溃后的恢复策略
Launcher 崩溃后,系统会重新启动它。但如果你不做任何处理,用户会看到桌面重新加载,所有图标闪烁一下。我常用的做法是:
- 保存桌面状态:每次用户操作完(比如移动图标),就把当前布局序列化到 SharedPreferences。
- 崩溃后恢复:Launcher 启动时,先读取上次保存的状态,快速恢复桌面布局。
- 延迟加载:非核心数据(比如壁纸、主题)等主界面显示后再加载。
三、SELinux 权限问题
SELinux 是 Android 安全模型的核心。说白了,它就是给每个进程和文件打上标签,然后根据规则决定谁能访问谁。Launcher 作为系统应用,经常需要访问一些敏感资源,这时候 SELinux 就会跳出来说「不」。
3.1 常见的 SELinux 权限问题
我在项目中遇到过几次 Launcher 因为 SELinux 权限问题导致功能异常的情况:
- 无法读取 /proc 下的进程信息:Launcher 需要获取正在运行的应用列表,但 SELinux 阻止了。
- 无法写入 /data/system 下的文件:Launcher 想保存一些系统级配置,但没权限。
- 无法访问其他应用的 data 目录:比如读取第三方应用的图标缓存。
3.2 如何排查 SELinux 问题
排查 SELinux 问题,最直接的方法就是看 logcat。SELinux 的拒绝日志会以 avc: denied 开头。你可以这样过滤:
adb logcat | grep "avc: denied"
举个例子,如果 Launcher 读取 /proc 被拒绝,你会看到类似这样的日志:
avc: denied { read } for pid=1234 comm="com.android.launcher3"
scontext=u:r:launcher:s0 tcontext=u:object_r:proc:s0 tclass=file
这条日志告诉我们:Launcher 进程(scontext=launcher)想读取(read)一个类型为 proc 的文件(tcontext=proc),但被拒绝了。
3.3 如何解决 SELinux 权限问题
解决 SELinux 权限问题有两种方式:
- 修改 SELinux 策略:在
system/sepolicy目录下添加规则。比如允许 Launcher 读取 proc:
# launcher.te
allow launcher proc:file read;
- 使用特权 API:如果 Launcher 是系统应用(platform 签名),可以使用
ActivityManager等系统 API 获取信息,避免直接访问文件系统。
四、知识体系总览
下面这张图总结了 Launcher 稳定性的三个核心维度,以及它们之间的关系:
这三个方面其实是相互关联的。ANR 如果没处理好,可能演变成 Crash;Crash 如果没捕获,用户就看不到任何反馈;SELinux 权限问题则可能同时导致 ANR 和 Crash。所以,做 Launcher 开发,这三个点一个都不能漏。