Android 应用稳定性优化实战指南
对稳定性的理解
应用稳定性是衡量 APP 质量的核心指标之一,也是构建体系中的基本盘。如果应用的稳定性出现问题,对产品口碑和用户留存造成的伤害往往是致命的。本文将从稳定性指标、Crash 处理流程、全链路治理策略、高可用性建设以及面试常见问题等方面,系统性地整理 Android 稳定性优化的知识体系。
需要说明的是,广义的稳定性不仅仅包含崩溃问题,还包括卡顿(Jank)、耗电异常、发热严重等性能指标。但本文主要聚焦于崩溃率(Crash Rate)的角度进行深入探讨,因为这是最直接影响用户体验和留存的因素。
稳定性常见指标分类
异常分类:Exception 与 ANR
Android 的崩溃问题主要分为两大类:Exception 和 ANR。
- Exception:应用内部代码发生未捕获的异常。
- JE (Java Exception):在 Java 代码中发生的未捕获异常,如 NullPointerException, ClassCastException 等。
- NE (Native Exception):在 Native 代码中访问非法地址,程序主动 abort 等导致的崩溃。
- ANR (Application Not Responding):应用无响应。通常是因为主线程被阻塞超过一定时间(默认 5 秒),不一定是代码逻辑直接导致的 Crash,但会导致用户感知到应用卡死。
JE、NE、ANR 既可以独立统计,也可以汇总后作为总体崩溃率进行监控。
PV 与 UV
PV 和 UV 是衡量应用软件使用量的基础指标,也是计算崩溃率的分母依据。
- PV (Page View):指页面的全部展示量,不区分用户。如果一名用户打开了 100 次页面,PV=100。
- UV (Unique Visitor):指对用户去重后的访问量统计。如果一名用户打开了 100 次页面,UV=1。
因此,崩溃率在 PV 和 UV 两个维度上都可以进行统计:
- PV Crash:评估问题影响严重程度,反映单位请求下的失败概率。
- UV Crash:评估问题在用户群体内的影响范围,反映有多少比例的用户遇到了问题。
增量崩溃率与存量崩溃率
- 增量崩溃:由当前新增代码或近期变更导致的崩溃。这是导致大盘崩溃率发生波动的主要原因。处理策略应为重要且紧急,需早发现早解决,避免带到线上成为存量崩溃。
- 存量崩溃:由存量代码引起,通常是历史遗留问题。是需要持续跟进的问题,解决存量崩溃有助于拉低线上大盘的崩溃率。处理策略为重要不紧急,需制定计划定期整治。
崩溃率评价指标
统计口径通常为:
- 分子:
JE + NE + ANR发生总次数 - 分母:PV(日活跃用户数或启动次数)
行业通用标准参考:
<2‰为合格<1‰(万级)为优秀
处理 Crash 的一般步骤
在处理 Crash 时,核心分为采集现场数据和分析崩溃原因两个步骤。能够复现的崩溃才是好崩溃,现场信息保留着排查问题的关键线索。
采集现场数据
根据层级的不同,将现场信息分为崩溃本身、运行时状态、系统状态三个层级。成熟的崩溃采集平台(如 Bugly、Sentry)可以覆盖大部分内容,但应用开发者仍需上传用户操作日志等自定义信息以辅助定位。
崩溃本身
主要是崩溃堆栈和进程、线程信息。
- 崩溃堆栈:这是最重要的信息,反映了崩溃时函数的调用栈。如果代码经过混淆,堆栈信息里的类名和方法名需要经过反混淆后才能进行分析,因此保存打包时的 mapping 文件至关重要。
- 进程与线程信息:崩溃的进程是哪一个,处于前台还是后台,线程是 UI 线程还是工作线程。
运行时状态
- 内存信息:
- 系统内存占用情况:
/proc/meminfo记录了系统实时的内存状态。当系统可用内存低于总内存的 10% 时,会频繁发生 GC,导致 OOM、ANR 等问题。 - 应用内存使用情况:PSS (Proportional Set Size)、RSS (Resident Set Size),得知应用当时物理内存占用情况;虚拟内存的信息则保存在
/proc/self/status中,其具体分布则需要查看/proc/self/maps。
- 系统内存占用情况:
- 应用操作路径:应用本身应该通过埋点、日志等记录用户的操作路径,当前打开的页面,正在运行的服务等。这对于复现问题至关重要。
- 应用信息:应用版本,是否经过热修复,CPU 架构(armeabi-v7a, arm64-v8a 等)。
- 文件信息:文件句柄 fd 的打开量。单进程最大允许打开的 fd 数量是 1024,超过 800 则处于危险状态,需要将所有 fd 即对应的文件名进行记录并上报。
- 线程信息:单个线程会占用大约 2MB 虚拟内存。线程总量超过 400 则危险,需要上报所有线程 id、name 及状态。
系统状态
- 系统硬件信息:CPU 型号,ABI,内存总量,网络连接状态(WiFi/4G/弱网)。
- 系统软件信息:Android 版本、Linux 内核版本、WebView 内核版本,OEM 软件版本(ROM),是否 root,是否模拟器。
- 系统日志:Logcat 日志片段,EventLog 等。
分析崩溃原因
这一步也就是解析 Crash,在分析时有种仿佛化身为一名侦探,抽丝剥茧侦查犯罪现场,定位嫌疑人的感觉。
第一步:单点突破
针对单条崩溃日志进行细致分析。
- 确认严重程度:是会导致应用闪退,还是当前页面功能不可用,接口请求失败,又或者是用户根本无感知。
- 确认优先级:根据严重程度确定处理优先级,优先级高的应当先处理。
- 观察收集到的崩溃基本信息:注意那些 Android 版本兼容性导致的异常,可能在新版本不会有问题,但老版本 Android 就会崩溃。根据崩溃类型不同,浏览信息的侧重点有所不同:
- JE:90% 的异常通过堆栈可以找出调用关系。特别指出,对于 OOM 则要关注内存占用情况,检查是否有大对象未及时释放。
- NE:观察 signal、code、fault addr 等内容。崩溃信号 signal 的定义可见其中 SIGSEGV(空指针、非法指针)和 SIGABRT(ANR、abort() 调用)较为常见。
- ANR:先观察 trace.txt 文件,重点查看主线程状态、是否持有锁,IO 信息和 CPU 占用信息,以及 GC 回收前后的状态。
- 观察 Logcat 日志:尤其是 Warning、Error 级别的。对于 ANR 则需要搜索
am_anr的关键字。 - 资源使用情况:如内存(物理 + 虚拟)、文件句柄、线程数量。
第二步:群体聚合
对于后台聚合完成的崩溃信息,查看它们有没有共性,可用于排查的共性有:机型,Android 版本,Rom 版本,厂商,ABI,是否 root,是否虚拟机,网络状态,当前打开的页面,进程状态,后台正在运行的服务等。尤其是 Android 系统版本,很容易出现由于版本变更导致原来可用的代码发生崩溃,或者一个功能只在高版本 Android 正常运行,在低版本则有概率崩溃。例如 Toast 显示时偶现 BadTokenException,就是在 8.0 以下才会发生的特定场景。
稳定性长期治理
根据所处流程,采取不同的稳定性优化策略,将质量左移。
开发阶段
- 开发是质量的第一道关卡,问题发现得越早,修复成本就越低。
- 统一编码规范,增强编码安全教育,增强代码评审(Code Review),进行结对编程等。
- 代码架构优化,对常用能力、底层功能进行模块封装与复用,设计单元测试,对于接口返回失败等情况进行统一的收口与错误处理,避免异常向上抛出导致崩溃。
测试阶段
- 新功能测试:本次新增功能,以及新增功能所影响到的部分。
- 主流程回归测试:核心业务流程必须保证稳定。
- 覆盖安装测试:覆盖安装老版本过程中,缓存、数据库等应当兼容,防止因数据结构变更导致崩溃。
- 兼容性测试:不仅是本公司手机,还有安装应用到第三方手机的场景,特别是低端机和定制 ROM。
- 边界条件:如服务器宕机、返回数据异常、弱网和无网情况下的容错处理。
代码合并阶段
- 冲突处理:如果代码有冲突则优先处理冲突,尤其是依赖的第三方库,如果不同的分支引入了不同版本,在合入代码时应当确认最终版本能够满足不同分支的需求。
- 编译检查:处理完冲突后,打包安装并回归主流程。
- 静态扫描:借助 lint 等工具进行代码静态扫描,发现潜在的风险点,如空指针风险、资源泄漏风险。
- 自动化测试:如果项目有集成 Appium 等自动化测试框架,则应当在合并代码后自动执行。
发布阶段
- 采取灰度策略,先在小范围小量级内投放新版本升级,逐步扩大升级覆盖的用户范围。当灰度版本的稳定性、业务数据等指标合格后(不低于大盘 5%),才进行全量发布。进行多轮灰度,并且可以根据特定机型、OS 版本进行专门灰度,防止特定条件下的问题发生。
- 采取 ABTest 策略:灰度时通过发布一个新功能包 + 一个对比包,来进行稳定性、业务数据的比对。之所以不采用线上全量版本作为对比包,是因为两者量级不同,在一些数据上的表现会有差异。而 ABTest 则通过控制两者具有相同的量级,防止这方面差异的产生。同时,可以在指定的系统版本、机型、用户群体上进行有针对性的 ABTest。
运营阶段
- 应用上线后持续关注线上稳定性波动,采用日报等方式进行监控,当崩溃率在超出阈值、或趋势发生波动时,及时报警通知相关方。
- 当发生无法规避的异常时,采取回滚和降级策略(参考下文的'业务高可用性方案建设'一节)。
- 版本前期关注增量异常,在处理完增量异常后,定期对存量异常进行整治,以期降低大盘崩溃率。
业务高可用性方案建设
稳定性优化不仅仅要降低崩溃率,其根本目标是保证业务的高可用性。有时代码里发生异常后,虽然应用没有崩溃,但业务上处于一个不可用的状态,比如页面跳转失败、接口请求失败后无重试,等等。这些场景也是我们竭力要避免的。
要提高业务的可用性,有以下思路:
- 梳理业务流程,对于关键和核心路径进行埋点统计,尤其是页面加载成功率、下载安装成功率等。
- 通过 AOP 等方式进行无侵入数据采集,既可以全面覆盖防止遗漏,还能降低开发成本。
- 建立数据大盘,采用 IM 消息、每日邮件等方式,推送给利益相关者。数据可以分为业务数据和技术数据两类。
- 建立报警策略,通常有以下几种:
- 阈值报警:某项指标的绝对值,如登录成功率、支付成功率、未导致崩溃的异常发生率。
- 趋势报警:相比同期的变化,如崩溃率环比上涨超过 10%。
- 特定指标报警:单次发生即上报,如支付失败、订单创建失败。
- 针对特定用户的问题,如果在测试、开发环境难以复现,可以对其采取全量日志回捞的方式进行信息采集。
- 发现异常后,采用兜底策略降低损失:
- 在配置中心通过设置开关,关闭相应功能入口。
- 如果是服务器下发的跳转参数,可以在服务器下发数据时,修改为跳转到正常页面。
- 采用热修复等方式替换掉异常逻辑。
客户端容灾方案建设
针对异常导致的崩溃,如果我们在收到线上用户反馈后,采用本地调试、开发、测试、上线、灰度、全量的处理流程,周期是比较长的,在这期间问题的影响可能会进一步增大,而问题影响人数是线上事故定级的重要参考指标,因此不能完全依赖传统开发流程来做容灾方案。
- 新功能配置开关:对于新开发的重要功能,可以在全局配置接口增加开关,当线上该功能发生稳定性问题(不一定是客户端导致,也有可能是服务器、网络运营商等原因),关闭开关,隐藏功能入口。
- 动态配置路由:动态下发路由表,将问题页面导向默认的错误处理页等。
- 热修复:通过热修复来替换掉问题类,快速修复线上 Bug。
- 动态化:如果项目使用的是 RN、Weex 或者 Flutter,则借助其动态化能力进行更新,绕过应用商店审核。
- 安全模式兜底:当应用在启动阶段就发生异常时,进行计数,达到阈值则清空应用数据或进入安全模式,防止无限循环崩溃。
面试常见问题与解答
做了哪些稳定性方面的优化?
主要是三方面的优化:
- Crash 长期治理:我们把 Crash 分为增量和存量两部分,对于开发版本引入的增量 Crash 采取重要且紧急的节奏处理;对于历史版本就存在的存量 Crash,则定期进行专项整治,利用周报复盘机制推动解决。
- 性能优化:主要是内存方面,因为业务场景里有比较多的图片信息流展示,对内存比较敏感,因此在开发时格外注意内存的波动变化,尤其是内存泄漏问题。对于图片,则采用一些手段来控制其内存占用,例如缓存清理、大图分区域展示、低端设备显示降级等。我们引入了 LeakCanary 进行开发期检测,并在生产环境使用轻量级监控方案。
- 业务稳定性优化:在关键业务路径采用 AOP 等无侵入的方法进行埋点,分为业务指标和技术指标。业务指标与用户使用统计情况挂钩,如用户活跃、留存、下载成功率等;技术指标则记录了接口访问成功率、访问耗时、启动耗时、Exception 发生率等。
通过以上优化措施,实现了应用的高可用性,在日活千万量级的前提下,线上崩溃率控制在 0.2%(千分之 2)以内。
具体是怎么做的?
可以展开讲性能优化里的内存优化部分。例如,针对 List 视图中的图片加载,我们采用了 Glide 或 Picasso 等库,并配置了合适的内存缓存策略。对于 Bitmap 对象,确保在使用完毕后及时 recycle。此外,通过 Systrace 和 MAT (Memory Analyzer Tool) 分析堆转储文件,定位具体的泄漏源头,如静态 Context 引用、监听器未注销等。
发生线上异常如何快速止损?
参考上文的《客户端容灾方案建设》。核心原则是'先恢复,后定位'。通过配置开关关闭故障功能,利用热修复技术快速下发补丁,必要时进行版本回滚。
如何长期保障业务稳定性?
参考上文的《业务高可用性方案建设》。建立完善的监控报警体系,落实 Code Review 制度,推行自动化测试,定期进行稳定性演练(Chaos Engineering),确保团队具备应对突发故障的能力。
总结
Android 稳定性优化是一个系统工程,涉及从开发规范、测试验证、发布流程到线上运维的全生命周期管理。通过建立科学的指标体系,完善的数据采集与分析能力,以及高效的容灾预案,可以显著提升应用的质量水平,保障用户体验。开发者应持续关注行业动态,学习大厂的最佳实践,不断提升自身的稳定性治理能力。

