App为什么会crash?

前言

看到这个问题,马上就可以回答出来:因为抛出异常就会 crash。
那么为什么抛出异常就会 crash 呢?
有没有办法不让 App crash 呢?
接下来我们进入正题吧


先探讨一下第一个问题吧:为什么抛出异常就会 crash。

首先我们看下线程中抛出异常以后的处理逻辑吧:
一旦代码抛出异常,并且我们没有捕捉的情况下,JVM 会调用 Thread 的 dispatchUncaughtException 方法。

    public final void dispatchUncaughtException(Throwable e) {
        Thread.UncaughtExceptionHandler initialUeh =
                Thread.getUncaughtExceptionPreHandler();
        if (initialUeh != null) {
            try {
                initialUeh.uncaughtException(this, e);
            } catch (RuntimeException | Error ignored) {
                // Throwables thrown by the initial handler are ignored
            }
        }
        //这里会获取对应的 UncaughtExceptionHandler 对象,然后调用对应的 uncaughtException 方法
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        //可以看到当 uncaughtExceptionHandler 没有赋值的时候,会返回 ThreadGroup 对象
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

看上述代码,如果 App 中并没有设置 uncaughtExceptionHandler 对象,那么会执行 ThreadGroup的uncaughtException 方法:

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

然后调用 Thread.getDefaultUncaughtExceptionHandler() 获取默认的 UncaughtExceptionHandler ,然后调用 uncaughtException 方法,既然名字是默认的 uncaughtExceptionHandler 对象,那么必然有初始化的地方,这就需要从系统初始化开始说起,不过初始化流程特别复杂,也不是本篇重点,所以就直接从 RuntimeInit 的 main 方法开始吧。

    public static final void main(String[] argv) {
        enableDdms();
        if (argv.length == 2 && argv[1].equals("application")) {
            if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
            redirectLogStreams();
        } else {
            if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");
        }

        commonInit();

        /*
         * Now that we're running in interpreted code, call back into native code
         * to run the system.
         */
        nativeFinishInit();

        if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");
    }

作为 java 类, main 方法一直都是执行的入口。从上述代码可以看出, main 方法中会调用 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));

        ...代码省略...

        initialized = true;
    }

此处会给 Thread 设置一个 KillApplicationHandler 对象,我们可以看到这个 KillApplicationHandler 是实现了 Thread.UncaughtExceptionHandler 这个接口的,所以自然会重写 uncaughtException 方法。

        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);

                // 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) {
                    ActivityThread.currentActivityThread().stopProfiling();
                }

                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } 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());
                System.exit(10);
            }
        }

在代码的最后执行了 System.exit(10) ;这个方法就会直接干掉当前进程,也就是所谓的 App crash 了。
所以我们一旦抛出异常,并且没有捕捉的话,程序就会被强制干掉。

第二个问题:能否让 App 不要 crash

答案自然是肯定的,我们刚才在看代码的时候也看到下面这段代码:

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        //可以看到当uncaughtExceptionHandler没有赋值的时候,会返回ThreadGroup对象
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

只有在我们没有设置 UncaughtExceptionHandler 的时候,才会调用 defaultUncaughtExceptionHandler 对象,所以自然而然的就想到了实现这个类,然后在这里面做相应的处理。
说干就干试试吧:
我们先试一下主动抛出异常的效果吧,先是在 MainActivity 里面放置一个 Button,让它点击可以主动抛出异常:

package com.netease.demo;

import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void click(View view) throws Exception{
        throw new Exception("主动抛出异常");
    }
}

来看一下执行效果:


crash.gif

不出意料程序崩溃了


那我们接下来写一个 CrashHandler 的类实现了 Thread.UncaughtExceptionHandler 接口:

package com.netease.demo;

import android.util.Log;

// Created by chendanfeng on 2020-08-19.
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    @Override
    public void uncaughtException(Thread t, Throwable e) {
       
        Toast.makeText(MyApplication.sApp,"uncaughtException : " + e.getMessage(),Toast.LENGTH_SHORT).show();
    }
}

然后在 MyApplication 里面对这个 Handler 进行设置:

package com.netease.demo;

import android.app.Application;

// Created by chendanfeng on 2020-08-19.
public class MyApplication extends Application {
    public static MyApplication sApp;
    @Override
    public void onCreate() {
        super.onCreate();
        sApp = this;
        Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());
    }
}

然后再看下效果:


crashHandler.gif

我们发现确实 App 已经不会 crash 了,但是又出现了另外一个问题,那就是 App 卡死在了这个界面,点击无效了。
那么这到底是怎么一回事呢?其实这也不难理解,我们的页面启动的入口是在 ActivityThread 的 main 方法:

public static void main(String[] args) {
        ...代码省略...

        Looper.prepareMainLooper();

       ...代码省略...
        Looper.loop();

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

在这里面进行初始化主线程的 Loop ,然后执行 loop 循环,我们知道 Looper 是用来循环遍历消息队列的,一旦消息队列中存在消息,那么就会执行里面的操作。整个 Android 系统就是基于事件驱动的,而事件主要就是基于 Looper 来获取的。所以如果这里一旦出现 crash,那么就直接会跳出整个 main 方法,自然 loop 循环也就跳出了,那么自然而然事件也就接收不到,更没法处理,所以整个 App 就会卡死在这里。


既然如此,那有没有其他办法可以保证 App 在抛出异常不 crash 的情况下,又能保证不会卡死呢?
既然 looper 是查询事件的核心类,那么我们是否可以不让跳出 loop 循环呢,乍一想好像没办法做到,我们没法给 loop 方法 try-catch 。但是我们可以给消息队列发送一个 loop 循环,然后给这个 loop 做一个 try-catch ,一旦外层的 loop 检测到这个事件,就会执行我们自己创建的 loop 循环,这样以后 App 内的所有事件都会在我们自己的 loop 循环中处理。一旦抛出异常,跳出 loop 循环以后,我们也可以在 loop 外层套一层 while 循环,让自己的 loop 再次工作。
还是一句老话"Talk is cheap,show me the code",没有什么比代码验证来的更直接的:

package com.netease.demo;

import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

// Created by chendanfeng on 2020-08-19.
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Handler handler = new Handler(getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        Looper.loop();
                    } catch (Exception e) {
                        Toast.makeText(MyApplication.this,"抛出了异常",Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }
}

执行以下效果:


loop循环.gif

这样就解决了抛出异常导致 App crash 的问题了


不过事情当然没有那么快就结束,这里给主线程的Looper 发送 loop 循环都是主线程操作的,那么子线程如果抛出异常怎么办呢,这么处理应该也是会 crash 吧,那就再做个实验吧:

package com.netease.demo;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void click(View view) throws Exception{
        new Thread(new Runnable() {
            @Override
            public void run() {
                TextView tv = null;
                tv.setText("hello,word");
            }
        }).start();
    }
}

这段代码TextView没有初始化,然后看下效果:

子线程crash.gif

没错,确实是直接crash的,那这个时候该怎么办呢?刚才说的Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());似乎也不行,这是设置当前 Thread 的方法,总不能给每个 Thread 都设置一个吧,这肯定不可取。不过 Thead 里面貌似还有个全局静态的 UncaughtExceptionHandler 对象被遗忘了

    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

ThreadGroup 里面最终会调用到他的方法,一开始在 RunTimeInit 里面初始化的。既然这样,那我们直接覆盖这个对象应该就可以了吧?那就试试吧:

package com.netease.demo;

import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

// Created by chendanfeng on 2020-08-19.
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Handler handler = new Handler(getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        Looper.loop();
                    } catch (Exception e) {
                        Toast.makeText(MyApplication.this,"抛出了异常",Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
          //代码其实也很简单,只需要这里加上这么一句话
        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
    }
}

另外,这里还稍微改造了一下 CrashHandler :

package com.netease.demo;

import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

// Created by chendanfeng on 2020-08-19.
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    @Override
    public void uncaughtException(Thread t, final Throwable e) {
        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MyApplication.sApp,"uncaughtException : " + e.getMessage(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

通过 Handler 将 toast 抛到主线程弹出,这个问题这里就不展开讲了,并不是本文重点。
好了,那么试试看效果:


子线程crashHandler.gif

这样就解决了子线程抛出异常而crash的问题了。


总结

不过虽然这种方法可以阻止系统 crash,但毕竟不是正常途径的方式。另外如果本该 crash 的地方最终没有 crash ,说不定会导致后续一连串的 App 问题发生。所以这种方式看看就好,最好的方式就是控制代码质量,尽量减少 crash 的发生。

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