Android 性能优化体系详解

一、应用保活与启动优化

1.1 Android 活动等级与保活策略

  • Android 活动等级:5级
  • 保活手段
    • 使用 [JobScheduler](https://www.jianshu.com/p/1f2103d3d2a2) 进行保活。
  • 必要权限
    • 允许应用后台运行;
    • 允许应用自启动。

1.2 冷启动耗时测量

在 Android Studio Logcat 中过滤关键字 “Displayed”,可查看如下日志:

2019-07-03 01:49:46.748 1678-1718/? I/ActivityManager: Displayed com.tencent.qqmusic/.activity.AppStarterActivity: +12s449ms
  • 日志末尾的 12s449ms 即为冷启动耗时

1.3 冷启动优化方案

方案一:利用 IdleHandler 延迟初始化

  • 原理IdleHandler 列表中的任务只有在 MessageQueue 队列为空时才会执行,即所在线程任务已执行完、处于空闲状态时。
  • 代码示例
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            // 页面启动所需耗时初始化
            doSomething();
            return false; // 执行一次后移除
        }
    });
    

方案二:在 onWindowFocusChanged 中通过 Handler 延后任务

打点顺序
  • 原因:直接在 onWindowFocusChanged 中执行任务,其打点时间早于系统日志 “Displayed”;通过 Handler.post() 延后一个任务,可确保在 “Displayed” 日志之后执行。
  • 调用流程分析
    • 渲染调用 requestLayout() 会增加任务监听;
    • 只有 SurfaceFlinger 渲染信号返回时才会触发渲染;
    • 因此延后一个任务,刚好在其之后执行。
  • 代码示例
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (onCreateFlag && hasFocus) {
            onCreateFlag = false;
            sHandler.post(new Runnable() {
                @Override
                public void run() {
                    doSomething();
                }
            });
        }
    }
    

方案三:通过 DecorView.post() 延迟任务

  • 原理View 内部维护了一个 HandlerActionQueue。在 DecorView attachToWindow 前,可通过 View.post() 将任务 Runnables 存放到 HandlerActionQueue 中。当 DecorView attachToWindow 时,会遍历并执行这些任务。
  • 关键源码逻辑
    1. View.dispatchAttachedToWindow() 时,mAttachInfo 被赋值;
    2. 此后,View.post() 实际就是直接调用 Handler.post() 执行任务;
    3. performResumeActivity() 在渲染之前先执行,说明只有在 onResume() 或之前调用 View.post() 才有效。
  • 二次延迟技巧
    • View.post()Runnablerun() 方法中再延迟一个任务;
    • performTraversals() 调用顺序看,该任务刚好在渲染完成后执行。
  • 代码示例
    getWindow().getDecorView().post(new Runnable() {
        @Override
        public void run() {
            sHandler.post(runnable);
        }
    });
    

方案四:解决冷启动白屏/黑屏


二、ANR(Application Not Responding)机制

2.1 ANR 的四种触发场景

  1. Service TimeOut:

    • Service 未在规定时间内执行完成。
    • 前台服务: 20秒
    • 后台服务: 200秒
  2. BroadcastQueue TimeOut:

    • 未在规定时间内处理完广播。
    • 前台广播: 10秒内
    • 后台广播: 60秒内
  3. ContentProvider TimeOut:

    • publish 在 10秒内没有完成。
  4. Input Dispatching timeout:

    • 5秒内未响应键盘输入、触摸屏幕等事件。

【重要澄清】
Activity 的生命周期回调阻塞并不在触发 ANR 的场景里,因此不会直接触发 ANR。但是,死循环阻塞了主线程后,如果系统再发生上述四种事件之一,就无法在规定时间内处理,从而间接触发 ANR

2.2 ANR 产生机制详解

1. 输入事件超时 (5s)

a. InputDispatcher 发送 key 事件给对应进程的 Focused Window。若出现以下情况则发生 ANR:

  • 对应的 window 不存在;
  • 处于暂停态;
  • 通道(input channel)占满、未注册或异常;
  • 5s 内没有处理完一个事件。
    b. InputDispatcher 发送 MotionEvent 事件有个例外:
  • 当对应 Touched Window 的 input waitQueue 中有超过 0.5s 的事件,inputDispatcher 会暂停该事件,并等待 5s。
  • 如果仍旧没有收到 window 的 ‘finish’ 事件,则触发 ANR。
    c. 下一个事件到达,发现有一个超时事件才会触发 ANR。

2. 广播类型超时(前台15s,后台60s)

a. 静态注册的广播和有序广播会 ANR,动态注册的非有序广播并不会 ANR
b. 广播发送时,会判断该进程是否存在,不存在则创建,创建进程的耗时也算在超时时间里
c. 只有当进程存在前台显示的 Activity 才会弹出 ANR 对话框,否则会直接杀掉当前进程
d. 当 onReceive 执行超过阈值(前台15s,后台60s),将产生 ANR。
e. 如何发送前台广播Intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)

3. 服务超时(前台20s,后台200s)

a. Service 的以下方法都会触发 ANR:

  • onCreate(), onStartCommand(), onStart(), onBind(), onRebind(), onTaskRemoved(), onUnbind(), onDestroy().
    b. 前台 Service 超时时间为 20s,后台 Service 超时时间为 200s。
    c. 如何区分前台、后台执行:当前 APP 处于用户态,此时执行的 Service 则为前台执行。
    d. 用户态定义:有前台 activity、有前台广播在执行、有 foreground service 执行。

4. ContentProvider 类型

a. ContentProvider 创建发布超时并不会 ANR
b. 使用 ContentProviderClient 来访问 ContentProvider 可以自主选择触发 ANR,超时时间自己定:

client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);

5. Activity 生命周期超时会不会 ANR?

  • 经测试并不会

2.3 导致 ANR 的根本原因

1. 应用层导致 ANR(耗时操作)

a. 函数阻塞:如死循环、主线程 IO、处理大数据。
b. 锁出错:主线程等待子线程的锁。
c. 内存紧张:系统分配给应用的内存有上限,长期内存紧张会导致频繁内存交换,进而导致操作超时。

2. 系统导致 ANR

a. CPU 被抢占:例如前台在玩游戏,可能导致后台广播被抢占 CPU。
b. 系统服务无法及时响应:如获取系统联系人,系统服务(Binder 机制)服务能力有限,可能长时间不响应。
c. 其他应用占用大量内存

2.4 参考资料


三、内存优化

3.1 内存泄漏排查

  • 工具LeakCanary
  • 原理简述
    1. RefWatcher.watch() 创建一个 KeyedWeakReference 用于观察对象。
    2. 在后台线程中,检测引用是否被清除,并且是否没有触发 GC。
    3. 如果引用仍然没有被清除,则将堆栈信息保存为 .hprof 文件。
    4. HeapAnalyzerService 在独立进程中启动,使用 HAHA 库解析 heap dump。
    5. 根据 referenceKey 找到 KeyedWeakReference 并定位泄露的引用。
    6. 计算到 GC Roots 的最短强引用路径,建立泄露链。
    7. 结果传回 app 进程,通过通知展示。
  • 官方简化解释
    • Activity 执行完 onDestroy() 后,将其放入 WeakReference 中,并与 ReferenceQueue 关联。
    • 检查 ReferenceQueue 中是否有该对象,如果没有,执行 GC 后再次检查。
    • 若仍无,则判定为内存泄露,并用 HAHA 库分析 heap dump。
  • 工作流程细节
    • LeakCanary 在判定有内存泄漏时,首先会生成一个内存快照文件(.hprof 文件),通常有 10+MB。
    • 然后根据 referenceKey 找出泄漏实例,在快照堆中使用 BFS 找到实例所在节点,并反向生成最小引用链。
    • 生成引用链后,将其保存在 AnalysisResult 对象中,然后写入 .hprof.result 文件(仅几十 KB)。
    • 最后,在 DisplayLeakActivityonResume 中读取所有 .hprof.result 文件并显示。
  • 参考资料

3.2 内存抖动(Memory Churn)

  • 问题现象:没有内存泄漏,但依然发生 OOM(Out Of Memory)。
  • 根本原因:多半是内存抖动在作祟。
  • 影响
    • 导致程序莫名卡顿,甚至 Crash 和 OOM。
  • 产生机制
    • 内存的频繁分配和回收导致内存不稳定。
  • 典型症状
    • 频繁 GC;
    • 内存曲线呈锯齿状。
  • 卡顿原理
    • 频繁的 GC 会导致 GC 线程在采集垃圾时挂起主线程及其他工作线程,造成用户操作无响应。
  • OOM 原理
    • 频繁申请和回收内存会产生大量内存碎片
    • 内存不连续,导致在创建需要连续内存空间的对象(如大数组、长字符串)时失败,引发 OOM。

3.3 Bitmap 优化

3.4 稳定性优化

3.5 耗时方法定位


四、网络优化

4.1 测试与监控工具

  • 测试工具
    • Network Profiler
    • Charles
    • Stetho(可以链接 Android 和 Chrome)
  • 线上监控
    • OkHttp 事件监听器
      • 自定义事件监听器;
      • GlideModule(监控 Glide 加载图片);
      • 最大并发请求数;
      • 区分前后台流量。
    • 流量统计
      • NetworkStatsManager
        • 可获取某时段或不同网络类型的流量消耗;
        • 不足:需要用户开启“查看使用情况”权限,用户体验差。
      • TrafficStats
        • 统计手机上次重启后的流量消耗;
        • 局限:无法统计重启前的流量。

4.2 流量优化方案

  1. 数据缓存
  2. 数据压缩
    • Gzip
    • 压缩请求头
    • 合并请求
  3. 图片压缩
    • 缩略图
    • WebP
    • Luban
  4. 网络请求质量优化
    • HttpDNS
    • Http 协议版本优化

4.3 参考资料


五、性能分析工具

5.1 主流工具集

5.2 内存类别详解(Profiler 视角)

  • Java: 从 Java 或 Kotlin 代码中分配的对象的内存。
  • Native: 从 C 或 C++ 代码中分配的对象的内存。即使 App 未使用 C++,也可能看到此内存,因为 Android 框架使用 Native 内存处理图像等任务。
  • Graphics: 用于图形缓冲区队列的内存,包括 GL 表面、GL 纹理等。(注意:这是与 CPU 共享的内存,非专用 GPU 内存)
  • Stack: 应用程序中 Native 和 Java 栈使用的内存,与线程数相关。
  • Code: 应用程序用于代码和资源的内存,如 dex 字节码、编译后的代码、库和字体。
  • Other: 应用程序使用的、系统无法分类的内存。
  • Allocated: 应用程序分配的 Java/Kotlin 对象的数量(不包含 C/C++ 对象)。

【注意】
当前应用程序中,native 内存统计值可能会偏大,因为分析工具自身的内存(多达 10MB)也被计入。在未来版本中,这些数字将被过滤掉。

5.3 其他工具

  • Perfetto:
    • Android 10 后引入,适用 9.0 以上机型。
  • Emmagee:
    • 网易出品,已不维护,7.0 之后版本不支持。
    • 监控维度:PSS 内存占用比、CPU 使用率、流量、电量、温度等。
  • wetest:商用产品。
  • GT:
    • 腾讯出品的手机端工具。
  • MAT 工具:
  • 通用提醒:
    • 所有性能测试工具本身都需要占用资源,会影响测试结果

5.4 参考资料


六、综合性能优化实践

6.1 启动优化案例

6.2 必知必会清单


七、Android 稳定性:可远程配置化的 Looper 兜底框架

7.1 崩溃处理机制的核心原理

在 Android 应用中,当一个未被捕获的异常(即崩溃)被抛出时,系统会调用 Thread#dispatchUncaughtException(throwable) 方法进行处理。

  • 默认行为

    • 在进程初始化阶段,RuntimeInit#commonInit 方法会注入一个默认的 UncaughtExceptionHandler,即 KillApplicationHandler
    • 如果开发者没有实现自定义的 UncaughtExceptionHandler,那么 dispatchUncaughtException 最终会走到 KillApplicationHandler 中,直接杀死当前进程,从而产生一次用户可感知的崩溃。
  • 自定义兜底逻辑

    • 通过实现自定义的 UncaughtExceptionHandler,我们可以在应用真正崩溃退出前,拦截异常并执行自定义逻辑(如上报、清理、尝试恢复等),从而提升应用的稳定性和用户体验。

7.2 为什么需要兜底框架?

以下场景尤其需要这种可配置的崩溃拦截能力:

  1. 系统级崩溃

    • 例如,臭名昭著的 Android 7.x 版本中由 Toast 引发的 BadTokenException。这类问题源于系统底层,应用层难以规避。
  2. 第三方库的“无痛”崩溃

    • 对于公司内部广泛使用但无法或不愿修改源码的大型第三方框架(如 React Native),其 UI 操作(如动画)可能在特定条件下抛出难以复现的异常。兜底框架可以避免因这些非核心路径的崩溃导致整个 App 退出。
  3. 特殊资源型崩溃

    • 例如,因磁盘空间不足引发的 No space left on device 异常。兜底框架可以在捕获此类异常时,主动清理应用的磁盘缓存,然后尝试让应用继续运行,而不是直接崩溃。
  4. 其他未知场景

    • 为应对线上复杂多变的环境,提供一个通用的、可动态调整的崩溃防护层。

7.3 可远程配置化能力

一个强大的兜底框架必须具备动态、精细化的控制能力。这可以通过网络下发配置来实现,允许运维或开发人员在不发版的情况下,对特定崩溃进行策略调整。

可配置的维度包括

  • throwable class name:异常的全类名(如 java.lang.NullPointerException)。
  • throwable message:异常的详细信息。
  • throwable stacktrace:完整的堆栈跟踪信息。
  • Android version:发生崩溃的设备 Android 版本。
  • app version:发生崩溃的应用版本号。
  • model / brand:发生崩溃的设备型号和品牌。

通过组合以上条件,可以实现非常精准的崩溃拦截策略。例如:“仅在 Android 7.0 的华为 P10 上,对 BadTokenException 且消息包含 'Unable to add window' 的崩溃进行静默处理”。

7.4 实现与参考

【补充】
此兜底方案是对传统 Crash Report(如 Bugly、Firebase Crashlytics)的有效补充。后者侧重于崩溃后的信息收集与分析,而前者则侧重于崩溃发生时的实时干预与恢复,两者结合可构建更健壮的应用稳定性体系。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容