首页 > 极客资料 博客日记
Android 稳定性(二):治理思路篇
2025-01-10 16:00:04极客资料围观2次
本文同步发布于公众号:移动开发那些事:Android 稳定性(二):治理思路篇
一般来讲Android
稳定性包括crash
和ANR
,本文主要围绕crash
(应用的crash
率)来讲述如何来做Android
的稳定性相关的工作。在讲具体的思路之前,我们先来了解一下Android
的异常捕获机制
1 异常捕获机制
Android
中的异常捕获机制从语言层面可以划分为java
层和native(C++)
层。
1.1 java异常捕获机制
1.1.1 基础
Throwable
是所有异常的基类,它有两个重要的子类:
Error
: 严重的系统错误,如OOM
,一般应用程序没有办法对其进行处理Exception
:可被应用程序捕获和处理的异常,如NPE
一般我们在代码里处理的都是Exception
相关的异常,而这些异常里,根据是否需要编译阶段处理,划分为两类:
- 受检异常: 编译阶段就需要处理的,否则代码就无法通过编译(一般通过
try-catch
或者方法签名中使用throws
声明会抛出该异常),如文件操作时的IOException
; - 非受检异常:编译阶段不要求处理,但运行时会出现的异常,包括运行时异常
RuntimeException
及其子类
1.1.2 使用
除了代码中很常用的,通过try-catch
块来进行包裹可能出现异常的问题代码块外,还可以通过 Thread.setDefaultUncaughtExceptionHandler
对应用全局的异常进行捕获。例如在 Application
类中设置此方法,当发生未处理的异常时,能够将异常信息记录下来,方便后续分析。一般是通过自定义类来实现UncaughtExceptionHandler
的接口来实现全局的异常处理:
class ACrash implements Thread.UncaughtExceptionHandler {
public UncaughtExceptionHandler exceptionHandler;
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
// 这里根据异常的类型和线程来做自定义的处理
// 在处理完自定义逻辑后,判断是否要把异常继续给原来的异常处理器
if (exceptionHandler != null) {
exceptionHandler.uncaughtException(t, e)
}
}
}
这里在设置自定义异常处理接口时,有个点需要注意的是,如果有使用第三方的的crash
收集系统,像bugly
,acrc
,此时在设置异常处理器时,需要注意是否已经设置过了:
UncaughtExceptionHandler tmpHandler = Thread.getDefaultUncaughtExceptionHandler()
ACrash aHandle = ACrash()
// 要保留原来的异常处理
aHandle.exceptionHandler = tmpHandler
Thread.setDefaultUncaughtExceptionHandler(aHandle)
1.2 native 异常捕获机制
1.2.1 基础
native
层的异常捕获机制,除了类似的try-catch
和throw
抛出异常外,还有个系统层的信号分发处理机制。系统会通过分发信号来告知异常信息,所以异常的处理就是一个信号的处理,
一般可通过sigaction
函数来注册信息处理函数:
static void signalHandler(int signal, siginfo_t *info, void *reserved) {
// 处理信号
}
void initSignalHandler() {
struct sigaction action;
action.sa_flags = SA_SIGINFO;
action.sa_sigaction = signalHandler;
sigaction(SIGSEGV, &action, NULL); // 捕获段错误
}
常见的信号量:
- SIGSEGV 11 无效的内存引用
- SIGABRT 6 由abort发出的退出指令
- SIGFPE 8 C浮点异常
- SIGILL 4 非法指令
- SIGBUS 10,7,总线错误(内存错误)
- SIGKILL 9 kill 信号
1.2.2 使用
native
异常捕获后,还涉及到minidump
文件的获取和整个堆栈的还原,比较复杂,所以一般我们不直接自己注册信号监听来处理,会使用第三方的解决方案,如bugly
,或使用breakpad
库来处理(bugly
底层也是使用的breakpad
),breakpad
的使用可参考:如何在Android平台使用Google Breakpad
2 分类与治理思路
2.1 分类
除了常规的业务代码的优化外(像常规的npe
,indexoutofboundsexception
等)还可以从操作系统的角度,将稳定性的优化问题可以大概分为以下几类:
- 内存稳定性优化
- 线程稳定性优化
- 系统问题优化
2.2 治理思路
稳定性治理其实最终是为用户服务的,因此整个治理也是围绕提高用户的体验来展开。治理的思路主要是:
- 能修复的,尽量去修复(像npe,oom);
- 从业务上没有办法修复(像系统bug),则尽可能减少对用户的影响,对一些异常进行降级
提高用户的体验,无非就是要降低应用的crash
率,这里有个核心的原则是:主要精力要花在Top 10
,Top20
的问题分析上,把主要的问题解决,顺带解决一些长尾的问题;
2.2.1 内存治理
内存问题的治理主要围绕:尽可能减少运行的内存占用同时避免出现内存泄露,内存溢出的问题。这里需要借助一些工具来辅助我们判断可能的问题是什么:
- leakcanary: Square开源的,检测和诊断 Android 应用中的内存泄漏的工具,用于开发过程;
- KOOM : 快手出的线上内存监控方案,可帮助更好优化应用的内存
- Profiler:Android 自带的性能监控工具,可辅助分析内存
(业内也还有其他的用于内存分析的工具,可以自行选择合适的,能解决问题就行)
一些优化内存的方式,可参考前面的文章Android稳定性(一):内存使用指南
2.2.2 线程治理
线程治理主要围绕:
- 线程复用,尽可能使用线程池的方式来调度(不同的业务会采用不同的线程池策略);
- 线程回收,线程使用完毕后,要及时调用关闭(shutdown),如果有持有线程的变量,也要及时置空
这里笔者在业务中,有尝试过几个优化的方向:
- 限制
OkHttpClient
的最大线程数,避免无限增长,并且尽可能复用同一个OKHttpClient
- 收敛线程,提供几个从线程池获取线程的方法,避免业务直接
new
- 线程池初始化的
core
线程根据不同的业务做差异化的初始化; - 避免线程的实例被某个单例持有,导致线程关闭后也没有办法释放资源;
2.2.3 系统问题治理
由于Android
版本的碎片化问题,会遇到各种只收集到系统堆栈的crash
问题,无法从业务层面解决,这里就只能具体问题具体分析。针对系统问题,有个大的治理(分析)思路:对发生问题的系统版本进行聚类,判断是特定版本的问题还是通用的问题,有个猜测后,再去分析对应版本的源码验证猜测(假设 -> 确定问题->解决问题)
可在线查看Android
源码的地址:Android Code Search
在确定问题后,在思考如何解决问题(系统问题大概率无法根治,只能尽可能减小对用户的影响)时,这里也有个大的框架:
- 能通过
hook
系统接口处理的,就通过hook
系统接口来处理(一般需要在C++
层进行hook
),如:- 扩大系统的限制,如
Android 8.1
系统的文件描述符限制为1024,可通过hook
接口扩充到4096
,可减小由于fd
溢出引起的问题; - 降低系统的级别影响,如将
RenderThread
的问题由abart
的 crash降低到丟帧;
- 扩大系统的限制,如
- 没有办法通过
hook
系统处理的:- 不影响用户的异常,如
DeadSystemException
,FinalizerWatchdogDaemonTimeout
这一类的,则可以直接在业务层catch
住(可参考前面的异常捕获机制) - 影响用户体验的异常,则还是要走
crash
的逻辑
- 不影响用户的异常,如
3 总结
本文围绕Android
应用的crash
率阐述了Android
稳定性相关工作,先介绍异常捕获机制,再提出分类与治理的思路,旨在降低应用 crash 率,提升用户体验。
通过合理的异常捕获机制的设置和优化策略,重点关注应用Top10和Top20的问题,对问题进行分类治理,可以有效提升应用的稳定性。
标签: