Android如何通过降低App的Crash提升留存

app的crash大部分是由于代码不健壮或者脏数据造成的,·如何才能最大限度的避免这些crash,提升用户体验,增加留存,下面个人的一些对crash的思考与实践:

先来看一下测试视频,一下每个按钮都会触发异常,按照正常android异常处理机制,在生命周期内发生异常会导致界面黑屏等现象,非生命周期内会再直接kill掉application:

crash.gif

作为一个android开发者基本了解当用户点击launcher上的app图标时,Zygote会fork一个进程,通过classloader加载运行ActivityThread的Main方法,然后bindApplication,由此开启了消息驱动机制来运行这个app。而这个消息驱动的机器便是ActivityThread中Main方法中的Looper:


    public static void main(String[] args) {
       ...
        Looper.prepareMainLooper(); // 创建main looper
        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
  ...

        Looper.loop(); // 开始循环取消息

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

通过以上代码便开启了消息驱动的大幕,activity、service、broadcast、contentprovider、window、view绘制、事件分发这些都是通过该消息驱动来进行事件分发,而日常最常见的一些crash log 基本都有下面红线里面的部分:

在这里插入图片描述

了解Throwable运行机制的同学,应该都看得出在进行一系列方法调用过程中,异常消息在收集异常日志时是从调用方法栈中一层一层地将调用的信息作为异常日志保存到异常log中,而既然app是消息驱动,所以我们的大部分crash都是包含上面红线框中的部分,只要在最开始调用的地方也就是方法调用时最先压栈的方法进行try{} catch{} 处理就能避免crash的发生,而红线中的方法我们能处理的就是Looper了,Thread API中包含UncaughtExceptionHandler这个类,用来专门处理线程在发生异常时的处理,而在Zygote由init进程创建时,系统便实现了该异常处理类,先来看一下Zygote在初始化时的大体逻辑:

App_main.main

int main(int argc, char* const argv[])
{
  ...
    //参数解析
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;
    ++i;
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            //对于64位系统nice_name为zygote64; 32位系统为zygote
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }
  ...
   //设置进程名
    if (!niceName.isEmpty()) {
        runtime.setArgv0(niceName.string());
        set_process_name(niceName.string());
    }
    if (zygote) {
        // 启动AppRuntime 
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        //没有指定类名或zygote,参数错误
        return 10;
    }
}

经过一系列调用到达RuntimeInit.javamain方法中调用的commonInit

 protected static final void commonInit() {
        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

        /*
         * set handlers; these apply to all threads in the VM. Apps can replace
         * the default handler, but not the pre handler.
         */
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); // 设置系统默认异常处理器
       ...
    }

以上代码看出异常处理器为KillApplicationHandler,接下来看一下该类的异常处理逻辑:

      @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);  // 处理异常log的输出

                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                if (mCrashing) return;
                mCrashing = true;

                // Try to end profiling. If a profiler is running at this point, and we kill the
                // process (below), the in-memory buffer will be lost. So try to stop, which will
                // flush the buffer. (This makes method trace profiling useful to debug crashes.)
                if (ActivityThread.currentActivityThread() != null) {  // 结束androidstudio的进程分析 
                    ActivityThread.currentActivityThread().stopProfiling();  
                }

                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));  // 弹出进程dead的弹框
            } catch (Throwable t2) {
                if (t2 instanceof DeadObjectException) {
                    // System process is dead; ignore
                } else {
                    try {
                        Clog_e(TAG, "Error reporting crash", t2);
                    } catch (Throwable t3) {
                        // Even Clog_e() fails!  Oh well.
                    }
                }
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());    // 重点 : 10秒杀死进程
                System.exit(10);   
            }
        }

看到这里应该就明白为啥app中的crash机制了 那我们可以自定义异常处理器就可以让app不至于crash导致用户流失了,结合文章开始的分析我们现在通过两点来完成:

  1. 异常抛出的底层方法由我们自己调用
  2. 自定义异常处理类

首先解决第一点,我们可以自己去往主线程的Looper中添加一个死循环的任务,这样就会消息阻塞导致ANR,既然我们自定义的任务由于让Looper中的消息无法继续for(;;),那可以在自己的任务中去调用Looper.loop(),这样相当于我们该任务是一个阻塞任务替换掉了ActivityThread中Looper.loop() 使得我们主线程的消息驱动时方法异常抛出时由我们的方法代理抛出,我们在该处加上try{}catch{}就能捕获到在消息驱动app过程中导致应用crash的异常,我们将导致应用crash的该异常处理掉就不会导致应用crash:

      new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                while (true) {  // 防止第二次抛出无法捕捉
                    try {
                        Looper.loop();
                    } catch (Throwable e) {
                        if (e instanceof CmCrashException) {  // unregister 时取消该套机制
                            return;
                        }
                        if (handler != null) {  // 交由我们自己处置
                            handler.handlerException(e);
                        }
                    }
                }
            }
        });

解决第二点通过自定义异常处理机制:

  mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); // 设置默认处理类 unregister时设置默认处理
  Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler()
    // 主线程的异常已经被我们try了,所以该处的异常都是子线程异常
        {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                if (handler != null) {
                    handler.handlerException(e); //交给我们自己处理
                }
            }
        });

通过以上分析很捕获到大部分因为代码的不健壮或者脏数据导致的crash的发生,但是对于Android而言,如果异常发生在Activity的生命周期调用时会导致界面黑屏或者界面白屏等现象,这时候我的解决办法就是去finish掉该activity,那如何对系统的activity生命周期调用时加try呢?通过反射出ActivityThreadmH(handler),给该handler添加回调方法,因为在ActivityThread中该handler未实现callback,所有我们可以反射添加一个callback来我们处理关于Activity生命周期调用的方法:

      private static boolean reflectHandlerActivityLife() {
        try {
            Class activityThreadClass = Class.forName("android.app.ActivityThread");
            Object activityThread = activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null);
            Field mhField = activityThreadClass.getDeclaredField("mH");
            mhField.setAccessible(true);
            final Handler mh = (Handler) mhField.get(activityThread);
            final Field callbackField = Handler.class.getDeclaredField("mCallback");
            callbackField.setAccessible(true);
            callbackField.set(mh, new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    switch (msg.what) {
                        case LAUNCH_ACTIVITY: {  // 由于该事件的msg与其他msg的内容不一致单独处理
                            try {
                                mh.handleMessage(msg);
                            } catch (Throwable e) {
                                mHandler.handlerException(e);
                                ActivityCloseManager.getInstance().finish(msg);
                            } finally {
                                return true;
                            }
                        }
                        case RESUME_ACTIVITY:
                        case PAUSE_ACTIVITY:
                        case STOP_ACTIVITY_HIDE:
                        case PAUSE_ACTIVITY_FINISHING:
                        case EXECUTE_TRANSACTION:
                        case NEW_INTENT:
                        case RELAUNCH_ACTIVITY28:
                        case RELAUNCH_ACTIVITY: {
                            try {
                                mh.handleMessage(msg);
                            } catch (Throwable e) {
                                mHandler.handlerException(e);
                                ActivityCloseManager.getInstance().finish(msg);
                            } finally {
                                return true;
                            }
                        }
                        case DESTROY_ACTIVITY: { // 界面已经销毁 无需再继续finish
                            try {
                                mh.handleMessage(msg);
                            } catch (Throwable e) {
                                mHandler.handlerException(e);
                            } finally {
                                return true;
                            }
                        }
                    }
                    return false;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            return false;// 反射失败
        }
        return true;
    }

这样就可以实现在Ativity生命周期调用时异常导致界面黑白屏问题,另外由于android各个版本中activity的启动逻辑的变更,暂时先适配sdk15~28,具体代码github 给个小星星

CrashDefend使用步骤

  1. 添加jetpack仓库
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
  1. 引入到项目
dependencies {
            implementation 'com.github.luweicheng24:CrashDefend:1.0.1'
    }
  1. 自定义Application中初始化:
/**
 * Created by luweicheng on 2019/3/26.
 */
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        registerCmCatcher();
    }

    private void registerCmCatcher() {
        CmCatcher.registerCatcher(this, new CmThrowableHandler() {
            @Override
            public void handlerException(Throwable msg) {
                // 异常上报
                Toast.makeText(MyApplication.this, msg.getMessage(), Toast.LENGTH_LONG);
                Log.e("lwc", "handlerException: " + msg.getMessage());
            }
        });
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275