28、SystemUI的功耗与性能优化:SystemUI的ANR分析,过度绘制(Overdraw)的检测与优化

SystemUI 的优化,说白了就是跟「卡顿」和「耗电」这两件事死磕。我做了这么多年 Android 框架,见过太多因为 SystemUI 拖垮整个手机体验的案例。你想想看,用户每天解锁几百次,下拉状态栏几十次,每次操作都卡一下,这手机基本就废了。

今天咱们重点聊两个老大难问题:ANR过度绘制。这两个问题看似独立,其实底层都跟主线程的负载和 GPU 的渲染压力有关。

28.1 SystemUI 的 ANR 分析

ANR 全称 Application Not Responding,说白了就是你的应用在 5 秒内没响应输入事件,或者 BroadcastReceiver 在 10 秒内没执行完。SystemUI 作为系统级应用,一旦 ANR,整个手机的上半屏就死了——通知拉不下来,快捷开关点不动,用户的第一反应就是「这破手机又卡了」。

28.1.1 常见的 ANR 场景

我在项目中遇到过最典型的场景是这样的:

  • 通知栏批量更新:微信一下子来了 50 条消息,SystemUI 要在主线程里逐个解析通知、绑定视图、做动画。如果中间插了一个磁盘 IO 或者 Binder 调用超时,直接 ANR。
  • 快捷开关的复杂操作:用户点一下「手电筒」,SystemUI 要跨进程调用 CameraService,如果 CameraService 卡住了,主线程就堵死了。
  • 锁屏界面加载:锁屏时如果 KeyguardViewMediator 里做了太多初始化工作,比如加载壁纸、读取用户配置,也容易触发 ANR。

28.1.2 ANR 的定位方法

定位 ANR 其实不难,关键是要看对文件。SystemUI 的 ANR 信息会写到 /data/anr/traces.txt 里。我个人的习惯是:

  1. 先看主线程的堆栈,找到卡住的那个方法。
  2. 看是不是在等锁——很多 ANR 其实是死锁或者锁竞争导致的。
  3. 看有没有 Binder 调用——跨进程调用如果对端挂了,主线程会一直等。

核心原则:SystemUI 的主线程绝对不能做任何可能阻塞的操作。所有网络请求、磁盘读写、复杂计算,必须扔到子线程或者用 HandlerThread 处理。

28.1.3 实战:用 StrictMode 抓 ANR 隐患

其实不用等到 ANR 发生,我们可以在开发阶段就用 StrictMode 提前发现隐患。在 SystemUI 的 Application 类里加上:

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .detectNetwork()
            .penaltyLog()
            .penaltyFlashScreen()
            .build());
}

这样一旦主线程做了磁盘 IO 或者网络请求,Logcat 里会直接打印警告,屏幕还会闪一下。我曾经用这个方法在项目里抓到了十几个隐藏的 ANR 风险点,大部分都是开发人员不小心在 onBindViewHolder 里做了文件读取。

小技巧:SystemUI 的 ANR 有时候不是自己的问题,而是系统服务卡住了。比如 SystemServer 里的 WindowManager 线程死锁,SystemUI 去调它的接口就会 ANR。这时候看 traces.txt 要特别留意「blocked on」后面的线程名。

28.2 过度绘制(Overdraw)的检测与优化

过度绘制,说白了就是同一个像素被画了多次。GPU 每多画一层,就要多花一份时间和电量。SystemUI 作为常驻进程,它的过度绘制问题会被放大——你想想看,状态栏每秒刷新 60 次,每次多画一层,一天下来多耗的电可不是小数目。

28.2.1 过度绘制的检测方法

检测过度绘制最直接的方法就是打开「开发者选项」里的「调试 GPU 过度绘制」。打开之后屏幕会变成这样:

颜色 含义 可接受吗?
无色 没有过度绘制 完美
蓝色 过度绘制 1 次 可接受
绿色 过度绘制 2 次 一般
粉色 过度绘制 3 次 需要优化
红色 过度绘制 4 次及以上 必须优化

我一般会在 SystemUI 的各个页面上截图,然后数颜色。如果某个区域大面积是粉色或红色,那就要重点排查了。

28.2.2 SystemUI 中常见的过度绘制问题

根据我的经验,SystemUI 里最容易出现过度绘制的地方有这几个:

  • 状态栏背景:很多定制 ROM 会在状态栏上叠三层背景——系统默认的、主题的、还有用户自定义的。其实只需要一层就够了。
  • 通知卡片:每条通知都有自己的背景,如果列表本身也有背景,那通知区域就变成了「列表背景 + 通知背景 + 内容背景」三层。
  • 快捷开关面板:每个开关图标、文字、背景都是独立的 View,叠在一起很容易产生过度绘制。

28.2.3 优化方法:减少层级、裁剪背景

优化过度绘制,核心就两招:减少 View 层级去掉不必要的背景

举个例子,状态栏的布局文件经常写成这样:

<LinearLayout
    android:background="@color/status_bar_bg">
    <LinearLayout
        android:background="@drawable/gradient">
        <TextView
            android:background="@color/white_alpha_10"/>
    </LinearLayout>
</LinearLayout>

你看,三层背景叠在一起,每个像素至少被画了三次。优化后应该只保留最外层的一层背景:

<LinearLayout
    android:background="@drawable/status_bar_bg_combined">
    <TextView/>
</LinearLayout>

把渐变和半透明效果合到一张 drawable 里,GPU 一次就画完了。

注意:不要盲目去掉所有背景。有些背景是为了防止内容穿透,比如白色文字在白色壁纸上会看不见。优化过度绘制的前提是不能影响视觉效果。

28.2.4 用 Hierarchy Viewer 分析布局深度

除了 GPU 过度绘制,布局深度也会影响性能。我习惯用 Hierarchy Viewer(或者 Android Studio 的 Layout Inspector)来看 SystemUI 的布局树。如果某个节点的深度超过了 10 层,那基本上每次渲染都要做大量的 measure/layout 计算。

举个例子,通知栏的布局以前经常有 15 层以上,后来 Google 用 ConstraintLayout 重构之后降到了 5-6 层,性能提升非常明显。

28.3 知识体系总览

下面这张图总结了 SystemUI 功耗与性能优化的核心逻辑:

SystemUI 功耗与性能优化知识体系 ANR 分析 常见场景:通知批量更新 / 快捷开关 / 锁屏 定位方法:traces.txt / 锁分析 / Binder 调用 预防:StrictMode / 子线程化 / HandlerThread 过度绘制 (Overdraw) 优化 检测:开发者选项 / GPU 过度绘制调试 常见问题:状态栏背景 / 通知卡片 / 快捷开关 优化:减少层级 / 合并背景 / 裁剪无用绘制 目标:主线程无阻塞 + GPU 每像素只画一次

28.4 性能优化的实战建议

最后,我分享几个在实际项目中验证过的优化建议:

  • 能用 ViewStub 就别用 View.GONE:ViewStub 在初始化时不占用资源,只有 inflate 时才加载。SystemUI 里很多不常用的面板(比如高级设置)都可以用 ViewStub 延迟加载。
  • 动画用属性动画,别用 ViewAnimation:属性动画直接操作对象的属性,不会触发 layout,性能好很多。
  • 列表用 RecyclerView,别用 ListView:RecyclerView 的 ViewHolder 复用机制比 ListView 高效得多,尤其在通知列表这种频繁更新的场景下。
  • 减少不必要的 invalidate:每次调用 invalidate() 都会触发重绘。我见过有些代码在 onTick 里每秒钟 invalidate 整个状态栏,其实只需要更新时间那一个 TextView 就够了。

避坑指南:我曾经在优化一个定制 ROM 的 SystemUI 时,发现下拉状态栏总是掉帧。查了半天,原来是快捷开关面板里有一个自定义 View,它的 onDraw 方法里 new 了一个 Paint 对象。每次绘制都 new 对象,GC 频繁触发,导致掉帧。改成成员变量复用之后,帧率直接从 40fps 升到了 55fps。

嗯,关于 SystemUI 的 ANR 和过度绘制,今天就聊这么多。这两个问题其实没有太多高深的理论,关键是要养成「时刻关注性能」的习惯。每次提交代码之前,打开 GPU 过度绘制看一眼,用 Systrace 跑一遍,很多问题在开发阶段就能发现。

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