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();
    }
}
小提示:Android 原生也提供了 StrictMode,你可以用它检测主线程的磁盘读写和网络访问。在 Launcher 的 Application 里开启 StrictMode,开发阶段就能提前发现问题。

1.3 如何避免 ANR

嗯,这里要注意几个关键点:

  • 图标加载必须异步:用 GlideCoil 加载应用图标,别自己写 BitmapFactory.decodeResource。
  • 数据库操作放子线程:Launcher 有自己的数据库(比如存储桌面布局),查询和写入都用 AsyncTaskCoroutine
  • 减少布局层级:能用 ConstraintLayout 就别嵌套 LinearLayout。我见过一个 Launcher 的桌面布局嵌套了 8 层,低端机直接 ANR。
  • 预加载数据:应用列表、快捷方式这些数据,在 Launcher 启动时就加载到内存里,别等用户滑动到那一屏才去读。
避坑指南:我曾经在 Launcher 的 onResume 里做了一次全量数据库查询,结果在低端机上直接 ANR。后来改成增量更新,只查询变化的部分,问题就解决了。

二、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 权限问题有两种方式:

  1. 修改 SELinux 策略:在 system/sepolicy 目录下添加规则。比如允许 Launcher 读取 proc:
# launcher.te
allow launcher proc:file read;
  1. 使用特权 API:如果 Launcher 是系统应用(platform 签名),可以使用 ActivityManager 等系统 API 获取信息,避免直接访问文件系统。
注意:修改 SELinux 策略需要重新编译系统镜像。如果你只是做应用层开发,建议优先使用系统 API。我见过有人为了省事直接 setenforce 0,这在开发机上可以,但正式发布绝对不行。

四、知识体系总览

下面这张图总结了 Launcher 稳定性的三个核心维度,以及它们之间的关系:

Launcher 稳定性知识体系 ANR 监控与避免 Crash 捕获与上报 SELinux 权限问题 主线程耗时操作 Binder 调用阻塞 锁竞争 / 布局复杂 全局异常捕获 崩溃信息收集 崩溃后恢复策略 avc: denied 日志排查 修改 SELinux 策略 使用特权 API 绕过 三者共同保障 Launcher 的稳定运行

这三个方面其实是相互关联的。ANR 如果没处理好,可能演变成 Crash;Crash 如果没捕获,用户就看不到任何反馈;SELinux 权限问题则可能同时导致 ANR 和 Crash。所以,做 Launcher 开发,这三个点一个都不能漏。

我的建议:在项目初期就把 ANR 监控、Crash 上报和 SELinux 权限检查这三个模块搭好。别等到上线了用户反馈问题才去加,那时候就晚了。

公众号:蓝海资料掘金营,微信deep3321