Android 性能优化(四)Crash治理之路——AndroidCrashX开源库

目录

前言

一、治理原则

二、治理实践

(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。

https://github.com/aiyangtianci/AndroidCrashX

猜你喜欢

转载自blog.csdn.net/csdn_aiyang/article/details/105054241