目录
前言
一、治理原则
二、治理实践
(1)NullPointerException
(2)IndexOutOfBoundsException
三、Crash预防
(1)主线程或子线程抛出异常后,迫使主线程Looper持续loop()
(2)hook Activity生命周期,反射关闭异常页面
(3)当绘制、测量、布局出现问题导致Crash时,关闭异常界面。
前言
Crash率是衡量一个App好坏的重要指标之一。如果你忽略了它的存在,它就会得寸进尺,愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。
上一篇(Android 性能优化(三)认识异常Exception和错误Error)讲到造成Crash的原因却有很多,比如:运行时异常的空指针、数组越界、未实例化、强制类型、低内存机制(Android 性能优化(五)Crash治理之LMK,内存泄漏及OOM检测)等等,有些时候我们在开发测试阶段都没有出现异常崩溃现象,而发布上线后到了用户手机就会出现各种奇怪闪退。所以,我们要去努力实现一个永远打不死的小强 —— 不会出现Crash闪退的APP。
Githup开源地址:
https://github.com/aiyangtianci/AndroidCrashX
当我们遇见一个bug时,不能依赖于拦截异常,然后改一行代码就行了,而是学习《美团外卖Android Crash治理之路》说的“预防胜于治理”。对于Crash的治理,我们尽量遵守以下三点原则:
1、异常不能随便吃掉。
随意的使用try-catch,只会增加业务的分支和隐蔽真正的问题,要了解Crash的本质原因,根据本质原因去解决。catch的分支,更要根据业务场景去兜底,保证后续的流程正常。
2、由点到面。
一个Crash发生了,我们不能只针对这个Crash的去解决,而要去考虑这一类Crash怎么去解决和预防。只有这样才能使得这一类Crash真正被解决。
3、预防胜于治理。
当Crash发生的时候,损失已经造成了,我们再怎么治理也只是减少损失。尽可能的提前预防Crash的发生,可以将Crash消灭在萌芽阶段。
二、治理实践
由于开发人员编写代码不小心而导致的Crash,常见的Crash类型包括:空节点、角标越界、类型转换异常、实体对象没有序列化、数字转换异常、Activity或Service找不到等。这类Crash是App中最为常见的Crash,也是最容易反复出现的。解决这类Crash需要由点到面,根据Crash引发的原因和业务本身,统一集中解决。在获取Crash堆栈信息后,解决这类Crash一般比较简单,更多考虑的应该是如何避免。下面介绍两个我们治理的量比较大的Crash。
(1)NullPointerException
造成这种Crash一般有两种情况:
1、对象本身没有进行初始化或者手动置为null了,然后对其进行操作;
治理方法:
- 对可能为空的对象做判空处理或加try-catch保护。
不要吞掉异常,若为空,对其再次进行初始化。
- 使用@NonNull和@Nullable注解。
标注在方法、字段、参数上,表示对应的值不可以为空或值可以为空,否则IDE会警告。
- 考虑使用Kotlin语言。代码简洁、类型检测、类型转换等。
// 加?表示变量的值可以为null ,否则提示错误❎
var age: String? = "23"
// !!表示抛出空指针异常❌
val ageInt = age!!.toInt()
2、对象已经初始化后,但被虚拟机GC回收,然后对其进行操作。
治理方法:
这种情况大部分是由于Activity销毁或Fragment被移除后,在Message、Runnable、http请求等回调中执行了一些代码导致的。可以将Message、Runnable回调时,判断Activity/Fragment是否销毁或被移除;加try-catch保护;在BaseActivity、BaseFragment的onDestory()里把当前Activity所发的所有请求取消掉。
(2)IndexOutOfBoundsException
这类Crash常见于对数组、集合的操作和多线程下对容器操作。
1、例如,RecycleView列表出现IndexOutOfBoundsException,经常是因为外部也持有了Adapter里数据的引用。这时外部引用对数据更改了(如在Adapter的构造函数里直接赋值),但没有及时调用notifyDataSetChanged(),则有可能造成Crash。对此我们封装了一个BaseAdapter,当有数据更改增删时,统一由Adapter自己维护通知。
2、很多容器是线程不安全的,所以如果在多线程下对其操作就容易引发IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同时也要注意有一些类的内部实现也是用的线程不安全的容器,如Bundle里用的就是ArrayMap。
三、Crash预防
话说回来,这篇说的要想打造一个不会出现Crash的APP。那么,就要让线上的应用出现Crash时:
(1)主线程或子线程抛出异常后,迫使主线程Looper持续loop()。
(2)Activity生命周期中抛出异常,关闭异常页面
。
(3)当绘制、测量、布局出现问题导致Crash时,关闭异常界面。
这么涉及到的Handler机制,第二篇(Android 性能优化(二)Handler运行机制原理,源码分析)就已经详细说过了。不懂的同学可以去看看,尽量非常熟悉这个技术点,以为它非常重要。
(1)主线程或子线程抛出异常后,迫使主线程Looper持续loop()
通常我们会使用 try - catch在代码中拦截异常,在发现容易出现崩溃的代码块,主动加上try-catch 预防异常闪退。但是没加try-catch的代码块出现异常还是闪退该怎么办?
使用系统异常捕获器(uncaughtException),就可以对系统运行中出现的未被捕获的异常。代码如下:
public class CrashCatchHandler implements UncaughtExceptionHandler {
public static final String TAG = "CrashCatchHandler";
private static CrashCatchHandler crashHandler = new CrashCatchHandler();
private Context mContext;
private UncaughtExceptionHandler mDefaultCaughtExceptionHandler;
/**
* 饿汉单例模式(静态)
*/
public static CrashCatchHandler getInstance() {
return crashHandler;
}
public void init(Context context) {
mContext = context;
//获取默认的系统异常捕获器
mDefaultCaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
//把当前的crash捕获器设置成默认的crash捕获器
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
if (!handleException(throwable) && mDefaultCaughtExceptionHandler != null) {
//如果用户没有处理则让系统默认的异常处理器来处理
mDefaultCaughtExceptionHandler.uncaughtException(thread, throwable);
}else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
LogUtils.e(TAG, "error : "+ e);
}
//退出程序
AppUtil.restarteApp(mContext);
}
}
/**
* 自定义错误处理
* @return true:处理了该异常; 否则返回false
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
final String msg = ex.getLocalizedMessage();
if (msg == null) {
return false;
}
//使用Toast来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "异常被拦截,已处理", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
return true;
}
}
Android中虽然可以通过设置 Thread.setDefaultUncaughtExceptionHandler来捕获所有线程的异常,但主线程抛出异常时仍旧会导致Activity闪退。
主线程异常,迫使Looper继续loop。
//由于主线程的异常都被我们catch住了,所以下面的代码拦截到的都是子线程的异常
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (t == Looper.getMainLooper().getThread()){
//主线程异常拦截
while (true) {//循环套循环
try {
Looper.loop();//主线程的异常会从这里抛出
} catch (Throwable e) {
e.printStackTrace();
}
}
}else{
//子线程
e.printStackTrace();
}
}
});
很简单,当主线程出现未捕获的异常,会进入while(true)循环,while中又调用了
Looper.loop()
,这就迫使主线程Looper持续loop(),又开始不断的读取消息队列中的Message并执行。这样就可以保证以后主线程的所有异常都会从我们手动调用的Looper.loop()
处抛出,一旦抛出就会被try{}catch捕获,这样主线程就不会crash了。
(2)hook Activity生命周期,反射关闭异常页面
Android Hook动态代理机制详解
Android 使用Java的反射机制总结
原理很简单:
首先,hook代理
ActivityThread.mH.mCallback,
实现拦截Activity生命周期,直接忽略生命周期的异常的话会导致黑屏。然后,反射调用ActivityManager的“finishActivity”结束掉生命周期抛出异常的Activity。
核心代码:
private static void mHmook() throws Exception{
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Object activityThread = activityThreadClass.
getDeclaredMethod("currentActivityThread").invoke(null);
Field mhField = activityThreadClass.getDeclaredField("mH");
mhField.setAccessible(true);
final Handler mhHandler = (Handler) mhField.get(activityThread);
Field callbackField = Handler.class.getDeclaredField("mCallback");
callbackField.setAccessible(true);
callbackField.set(mhHandler, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY://启动
try {
mhHandler.handleMessage(msg);
} catch (Throwable throwable) {
sActivityKiller.finishLaunchActivity(msg);//关闭
}
return true;
}
return false;
}
});
}
需要注意的是由于Android不同版本,系统源码会有所改动,各版本android的ActivityManager获取方式,finishActivity的参数,token(binder对象)的获取不一样,要注意做好版本兼容,不然反射调用会出异常。
下面是API <= 20 Android 4.4 版本
public class ActivityKiller implements IActivityKiller {
@Override
public void finishLaunchActivity(Message message) {
try {
Object activityClientRecord = message.obj;
Field tokenField = activityClientRecord.getClass().getDeclaredField("token");
tokenField.setAccessible(true);
IBinder binder = (IBinder) tokenField.get(activityClientRecord);
finish(binder);
} catch (Exception e) {
e.printStackTrace();
}
}
private void finish(IBinder binder) throws Exception {
Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = activityManagerNativeClass.getDeclaredMethod("getDefault");
Object activityManager = getDefaultMethod.invoke(null);
Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class);
finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null);
}
}
(3)当绘制、测量、布局出现问题导致Crash时,关闭异常界面。
当view,在 measure 、layout 、draw时抛出异常会导致Choreographer挂掉。通过调用getStackTrace() 方法是得到异常方法栈记录,它会返回一个栈轨迹元素的数组 StackTraceElement[]。
可以查看Android 性能优化(三)认识错误Error和异常Exception及栈轨迹StackTrace
private static void isChoreographerException(Throwable e) {
StackTraceElement[] elements = e.getStackTrace();
if (elements == null) {
return;
}
for (int i = elements.length - 1; i > -1; i--) {
if (elements.length - i > 20) {
return;
}
StackTraceElement element = elements[i];
if ("android.view.Choreographer".equals(element.getClassName())
&& "Choreographer.java".equals(element.getFileName())
&& "doFrame".equals(element.getMethodName())) {
//处理异常
return;
}
}
}
Githup开源地址,欢迎star。