App为什么会crash?一篇文章带你探究根本原因 ,事情没有你想得那么简单!

2020-09-17 10:29:01 浏览数 (1)

前言

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

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

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

代码语言:javascript复制
    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 方法:

代码语言:javascript复制
    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 方法开始吧。

代码语言:javascript复制
    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 方法:

代码语言:javascript复制
    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 方法。

代码语言:javascript复制
        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

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

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

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

代码语言:javascript复制
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("主动抛出异常");
    }
}

来看一下执行效果:

不出意料程序崩溃了。。。

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

代码语言:javascript复制
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 进行设置:

代码语言:javascript复制
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());
    }
}

然后再看下效果:

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

代码语言:javascript复制
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",没有什么比代码验证来的更直接的:

代码语言:javascript复制
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();
                    }
                }
            }
        });
    }
}

执行以下效果:

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

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

代码语言:javascript复制
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的,那这个时候该怎么办呢?刚才说的Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());似乎也不行,这是设置当前 Thread 的方法,总不能给每个 Thread 都设置一个吧,这肯定不可取。不过 Thead 里面貌似还有个全局静态的 UncaughtExceptionHandler 对象被遗忘了

代码语言:javascript复制
    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

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

代码语言:javascript复制
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 :

代码语言:javascript复制
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 抛到主线程弹出,这个问题这里就不展开讲了,并不是本文重点。 好了,那么试试看效果:

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

总结

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

最后我在这里分享一下这段时间从朋友,大佬那里收集到的一些2019-2020BAT 面试真题解析,里面内容很多也很系统,包含了很多内容:Android 基础、Java 基础、Android 源码相关分析、常见的一些原理性问题等等,可以很好地帮助我们深刻理解Android相关知识点的原理以及面试相关知识

这份资料把大厂面试中常被问到的技术点整理成了 PDF ,包知识脉络 诸多细节;还有 高级架构技术进阶脑图 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

以上内容均放在了开源项目:github 中已收录,里面包含不同方向的自学Android路线、面试题集合/面经、及系列技术文章等,资源持续更新中...

0 人点赞