一、应用保活与启动优化
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。在DecorViewattachToWindow前,可通过View.post()将任务Runnables存放到HandlerActionQueue中。当DecorViewattachToWindow时,会遍历并执行这些任务。 -
关键源码逻辑:
- 在
View.dispatchAttachedToWindow()时,mAttachInfo被赋值; - 此后,
View.post()实际就是直接调用Handler.post()执行任务; -
performResumeActivity()在渲染之前先执行,说明只有在onResume()或之前调用View.post()才有效。
- 在
-
二次延迟技巧:
- 在
View.post()的Runnable的run()方法中再延迟一个任务; - 从
performTraversals()调用顺序看,该任务刚好在渲染完成后执行。
- 在
-
代码示例:
getWindow().getDecorView().post(new Runnable() { @Override public void run() { sHandler.post(runnable); } });
方案四:解决冷启动白屏/黑屏
-
方法:使用透明主题。
<activity android:name=".MainActivity" android:theme="@style/TranslucentTheme" /> - 参考资料:
二、ANR(Application Not Responding)机制
2.1 ANR 的四种触发场景
-
Service TimeOut:
- Service 未在规定时间内执行完成。
- 前台服务: 20秒
- 后台服务: 200秒
-
BroadcastQueue TimeOut:
- 未在规定时间内处理完广播。
- 前台广播: 10秒内
- 后台广播: 60秒内
-
ContentProvider TimeOut:
-
publish在 10秒内没有完成。
-
-
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 -
原理简述:
-
RefWatcher.watch()创建一个KeyedWeakReference用于观察对象。 - 在后台线程中,检测引用是否被清除,并且是否没有触发 GC。
- 如果引用仍然没有被清除,则将堆栈信息保存为
.hprof文件。 -
HeapAnalyzerService在独立进程中启动,使用 HAHA 库解析 heap dump。 - 根据
referenceKey找到KeyedWeakReference并定位泄露的引用。 - 计算到 GC Roots 的最短强引用路径,建立泄露链。
- 结果传回 app 进程,通过通知展示。
-
-
官方简化解释:
- 在
Activity执行完onDestroy()后,将其放入WeakReference中,并与ReferenceQueue关联。 - 检查
ReferenceQueue中是否有该对象,如果没有,执行 GC 后再次检查。 - 若仍无,则判定为内存泄露,并用 HAHA 库分析 heap dump。
- 在
-
工作流程细节:
- LeakCanary 在判定有内存泄漏时,首先会生成一个内存快照文件(
.hprof文件),通常有 10+MB。 - 然后根据
referenceKey找出泄漏实例,在快照堆中使用 BFS 找到实例所在节点,并反向生成最小引用链。 - 生成引用链后,将其保存在
AnalysisResult对象中,然后写入.hprof.result文件(仅几十 KB)。 - 最后,在
DisplayLeakActivity的onResume中读取所有.hprof.result文件并显示。
- LeakCanary 在判定有内存泄漏时,首先会生成一个内存快照文件(
- 参考资料:
3.2 内存抖动(Memory Churn)
- 问题现象:没有内存泄漏,但依然发生 OOM(Out Of Memory)。
- 根本原因:多半是内存抖动在作祟。
-
影响:
- 导致程序莫名卡顿,甚至 Crash 和 OOM。
-
产生机制:
- 内存的频繁分配和回收导致内存不稳定。
-
典型症状:
- 频繁 GC;
- 内存曲线呈锯齿状。
-
卡顿原理:
- 频繁的 GC 会导致 GC 线程在采集垃圾时挂起主线程及其他工作线程,造成用户操作无响应。
-
OOM 原理:
- 频繁申请和回收内存会产生大量内存碎片;
- 内存不连续,导致在创建需要连续内存空间的对象(如大数组、长字符串)时失败,引发 OOM。
3.3 Bitmap 优化
3.4 稳定性优化
- 参考资料:深入探索Android稳定性优化
3.5 耗时方法定位
- 工具/方法:让你的Android应用快速定位耗时方法
四、网络优化
4.1 测试与监控工具
-
测试工具:
Network ProfilerCharles-
Stetho(可以链接 Android 和 Chrome)
-
线上监控:
-
OkHttp 事件监听器:
- 自定义事件监听器;
-
GlideModule(监控 Glide 加载图片); - 最大并发请求数;
- 区分前后台流量。
-
流量统计:
-
NetworkStatsManager:- 可获取某时段或不同网络类型的流量消耗;
- 不足:需要用户开启“查看使用情况”权限,用户体验差。
-
TrafficStats:- 统计手机上次重启后的流量消耗;
- 局限:无法统计重启前的流量。
-
-
OkHttp 事件监听器:
4.2 流量优化方案
- 数据缓存
-
数据压缩:
- Gzip
- 压缩请求头
- 合并请求
-
图片压缩:
- 缩略图
- WebP
- Luban
-
网络请求质量优化:
- 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 为什么需要兜底框架?
以下场景尤其需要这种可配置的崩溃拦截能力:
-
系统级崩溃:
- 例如,臭名昭著的 Android 7.x 版本中由 Toast 引发的
BadTokenException。这类问题源于系统底层,应用层难以规避。
- 例如,臭名昭著的 Android 7.x 版本中由 Toast 引发的
-
第三方库的“无痛”崩溃:
- 对于公司内部广泛使用但无法或不愿修改源码的大型第三方框架(如 React Native),其 UI 操作(如动画)可能在特定条件下抛出难以复现的异常。兜底框架可以避免因这些非核心路径的崩溃导致整个 App 退出。
-
特殊资源型崩溃:
- 例如,因磁盘空间不足引发的
No space left on device异常。兜底框架可以在捕获此类异常时,主动清理应用的磁盘缓存,然后尝试让应用继续运行,而不是直接崩溃。
- 例如,因磁盘空间不足引发的
-
其他未知场景:
- 为应对线上复杂多变的环境,提供一个通用的、可动态调整的崩溃防护层。
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)的有效补充。后者侧重于崩溃后的信息收集与分析,而前者则侧重于崩溃发生时的实时干预与恢复,两者结合可构建更健壮的应用稳定性体系。