通过源码分析 Bugly 和 CrashHandler 实现应用重启潜在问题分析

1. 功能说明

在文章开头处,先对要实现的功能进行说明,以方便大家对该文章想要分析的问题有一个大致的了解。

首先,我们需要集成 Bugly 组件用于应用崩溃后的错误信息捕获。也就是当应用出现崩溃后,会将错误日志立即传递到 Bugly 平台,便于开发者第一时间发现并修复问题。

其次,通过自定义实现 Thread.UncaughtExceptionHandler 接口来对应用崩溃后,接收崩溃信息并根据具体的产品设计对应用崩溃后进行相关处理。在该文章里,产品的需求是对应用进行重启。

总结一下就是需要实现下面两个功能:

  1. 使用 Bugly 进行崩溃捕获
  2. 使用自定义的 Thread.UncaughtExceptionHandler 来实现应用崩溃后的重启工作

2. 实现功能

2.1 集成 Bugly 进行日志捕获

这个没什么可说的,根据 Bugly 官方提供的集成文档进行集成就好。

    private fun initBugly() {
        val strategy = CrashReport.UserStrategy(this)
        CrashReport.initCrashReport(this, Bugly.APP_ID, true, strategy)
    }

2.2 实现应用崩溃后重启的功能

代码比较简单,直接上代码即可。

class CrashHandler : Thread.UncaughtExceptionHandler {

    fun init() {
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(t: Thread, e: Throwable) {
        Log.e("TAG", "crash exception")

        try {
            Thread.sleep(2000)
        } catch (ex: Exception) {
            Log.e("TAG", ex.message)
        }

        // restart application
        Log.i("TAG", "restart application")
        val intent = Intent(appCtx, MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        appCtx.startActivity(intent)

        // exit previous process
        exitProcess(10)
    }
}

2.3 将两者集成在同一个项目里

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        initBugly()
        CrashHandler().init()
    }
}

2.4 小插曲:Bugly 不支持 Android P 及以上系统版本

在写完上面的测试例子,使用 Android P(9.0)的时候,发现 Bugly 运行不成功,仔细查看日志后发现了下面的报错:

CrashReport: java.io.IOException: Cleartext HTTP traffic to android.bugly.qq.com not permitted

意思是说 Bugly 的明文 HTTP 传输是不被允许的。Google 了一番后发现,原来是 Android 系统从 9.0 开始默认不再支持 HTTP 明文传输,需要使用 Https 或者 Http2 来进行服务的请求。

那怎么办呢?其实我们可以通过配置来改变应用对某些域名的网络安全配置级别,来关闭这个默认的限制的。具体点击这里进行了解。

具体的配置如下:

  1. 在 res 目录下创建名称为 xml的文件夹(只能是这个文件名)
  2. 针对 bugly 的域名按照下面方式进行配置


    
        android.bugly.qq.com
    

  1. 应用网络安全配置

配置完成后,运行测试程序发现已经可以正常上传崩溃日志了。

2.5 存在问题说明

按照上面的方法集成后,应用可以重新启动,但发现 bugly 并没有上传对应的崩溃日志。这是为什么呢?这就是我在开发过程中,遇到的问题。接下来我们就一起来分析一下发生这个事情的原因吧。

3. Application Crash 实现机制分析

当遇到上面的问题后,Google 了一下,试了一些方法,如:在自定义的 CrashHandler 异常处理器的方法里,将当前异常再次传递给系统默认异常处理器,具体代码如下所示:

class CrashHandler : Thread.UncaughtExceptionHandler {
    lateinit var defaultExceptionHandler: Thread.UncaughtExceptionHandler

    fun init() {
        // to get default handler value must be at the first line
        defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler(this)
    }
    
        override fun uncaughtException(t: Thread, e: Throwable) {
        Log.e("TAG", "crash exception")
        e.printStackTrace()

        try {
            Thread.sleep(2000)
        } catch (ex: Exception) {
            Log.e("TAG", ex.message)
        }

        // restart application
        Log.i("TAG", "restart application")
        val intent = Intent(appCtx, MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        appCtx?.startActivity(intent)
        // dispatch the exception to system
        defaultExceptionHandler.uncaughtException(t, e)

        // exit previous process
        android.os.Process.killProcess(android.os.Process.myPid())
        exitProcess(1)
    }

主要是使用 defaultExceptionHandler.uncaughtException(t, e) 方法将具体的异常在跳转后又交给了系统来处理。

此时,上报成功了,但是提示应用已崩溃的对话框(如下图所示)已经弹出来了。

image

出现这种情况,猜想是 CrashHandler 中关闭当前进程的方法没有被执行。因为当应用崩溃后,系统默认会弹出对话框让用户选择是否立即结束应用,或者默认 5 分钟后关闭对话框并关闭当前进程。

        // exit previous process
        android.os.Process.killProcess(android.os.Process.myPid())
        exitProcess(1)

为了验证这个猜想,这里通过 debug 的方法在杀掉当前进程的方法前面加了个断点。果然,退出当前进程的方法并没有执行。这是为什么呢?正常来讲我们通过 defaultExceptionHandler.uncaughtException(t, e) 方法将异常传递给了系统,系统的方法执行完成后,应该还要回来执行我们关闭线程的方法才对的呀?

为了搞清楚这个问题,就不得不对系统异常处理的机制进行分析了。

在分析之间,我们再将问题聚焦一下。当前要分析的问题是:Bugly 的崩溃日志已经可以上报成功了,但弹出了系统默认的错误信息提示对话框。

3.1 系统注册默认异常处理器流程

为了弄懂对 uncaughtException 在 Android 系统中的处理,我们需要先从应用启动开始,来看看 Android 系统中是如何启动一个应用的,并找到系统是从哪里注册应用默认的 uncaughtExceptionHandler 处理器的。

由于每个 Android 系统版本在实现上会有差异,这里使用的是 Android 23 来进行分析的。

在系统启动的时候,会开启一个 Zygote 进程用于后面应用启动时,负责为应用 fork 进程。我们来看看 Zygote 的创建过程中做了什么事情:

    // ZygoteInit.java
    public static void main(String argv[]) {
        try {
            ...
            registerZygoteSocket(socketName);
            ...
            runSelectLoop(abiList);

            closeServerSocket();
        } catch (MethodAndArgsCaller caller) {
            caller.run();
        } catch (RuntimeException ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }

可以看到,Zygote 在执行过程中,会调用 registerZygoteSocket(socketName) 方法。

    // ZygoteInit.java
    private static void registerZygoteSocket(String socketName) {
            ...
            sServerSocket = new LocalServerSocket(fd);
            ...
        }
    }
    
    // LocalServerSocket.java
    public LocalServerSocket(FileDescriptor fd) throws IOException {
        impl = new LocalSocketImpl(fd);
        impl.listen(LISTEN_BACKLOG);
        localAddress = impl.getSockAddress();
    }

从上面的代码可以看出,Zygote 进程启动后,创建了 Socket 本地服务对象并通过 Socket 端口监听发过来的消息(主要是各种服务如:ActivityManagerService等)。注册完监听后,又做了什么呢,我们再来看看 runSelectLoop(String abiList) 方法做了什么事情。

    private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
        ...
        while(true) {
            ...
            boolean done = peers.get(i).runOnce();
            if (done) {
                peers.remove(i);
                fds.remove(i);
            }
        }
    }

runSelectLoop 方法是一个死循环,当有其它服务与 Zygote 进程建立了 Socket 连接后,就会调用 runOnce() 方法去读取来看其它服务发送过来的命令。

    // ZygoteConnection.java
    boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
        ...
        // 通过 nativeForkAndSpecialize 方法 fork 进程
        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
                parsedArgs.appDataDir);
        ...
        // 对目标进程进程处理
        handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
        ...
    }
    
    private void handleChildProc(Arguments parsedArgs,
        FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
        throws ZygoteInit.MethodAndArgsCaller {
        /**
         * By the time we get here, the native code has closed the two actual Zygote
         * socket connections, and substituted /dev/null in their place.  The LocalSocket
         * objects still need to be closed properly.
         */
    
        closeSocket();
        ZygoteInit.closeServerSocket();
        ...
        RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,
                parsedArgs.remainingArgs, null /* classLoader */);
        ...
    }

对于 Zygote 进程来说读取的指令就是通过 fork 的方式创建进程,并切换到目标进程中,进行进程启动过程中的一些初始化操作。同时,也可以看到,Zygote 的 Socket 连接通信都是一次性的,即需要创建进程的服务(AMS)先与 Zygote 建立连接,将一些参数封装后,通过连接传递给 Zygote 进程,Zygote 进程创建完进程后,立即关闭了 Socket 连接。接下来要看下 RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,parsedArgs.remainingArgs, null /* classLoader */) 方法里面做了些什么。

// RuntimeInit.java
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
        throws ZygoteInit.MethodAndArgsCaller {
    ...
    commonInit();
    nativeZygoteInit();
    applicationInit(targetSdkVersion, argv, classLoader);
}

private static final void commonInit() {
    ...
    /* set default handler; this applies to all threads in the VM */
    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
    ...
}

今天的主角登场了,在 commonInit() 方法里,当前应用中的默认未捕获异常处理器 UncaughtHandler 被注册了。到此为此,整个应用从启动到注册默认异常处理器的执行流程如下图所示:

image

我们只需要搞清楚 UncaughtHandler 到底做了什么,就知道系统对应用崩溃的处理了。在分析之前,我们需要搞清楚一件事情:AMS 是在什么时候与 Zygote 进程建立 Socket 连接并发送创建进程的指令的呢?

3.2 AMS 何时向 Zygote 发送创建进程的消息

针对 3.1 中提到的问题,我们应该这样去思考,就是假如我们通过 startActivity 来启动一个 Activity,而这个 Activity 附属于一个还未启动的进程,此时,ActivityManagerService 是怎么处理的呢?废话不多说,我们去看一下 startActivity 后,都做了哪些操作吧。

这里只关注核心代码,一些不重要的方法就直接忽略了。

// Instrumentation.java
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {
    ...
    int result = ActivityManagerNative.getDefault()
        .startActivity(whoThread, who.getBasePackageName(), intent,
                intent.resolveTypeIfNeeded(who.getContentResolver()),
                token, target != null ? target.mEmbeddedID : null,
                requestCode, 0, null, options);
    ...
}

这里通过调用 Instrumentaion 类中的 execStartActivity 方法通过 Binder 的方式来向 ActivityManagerService 发送 startActivity 的指令。这里的 ActivityManagerNative.getDefault() 方法实际上是 ActivityManagerProxy 运行在应用进程的一个 AMS 代理对象,其实现了 IActivityManager 接口并通过 Binder 方法发送指令到 AMS 进程。而 ActivityManagerNative 是一个运行在 AMS 进程的实现了 IActivityManager 接口的 AMS 代理对象,而 ActivityManagerService 继承自 ActivityManagerNative 抽象类,并真正实现了 IActivityManager 接口。ActivityManagerNative 通过 onTransact() 方法接收 ActivityManagerProxy 从应用进程发过来的请求,并调用 IActivityManager 接口中对应的方法,将由真正接口的实现类 ActivityManagerService 来进行处理。

所以,我们直接看下在 ActivityManagerService 中的 startActivity 方法是怎么处理的,应该就能找到 AMS 是在什么时候发送消息给 Zygote 进程来 fork 新的进程了。

    // ActivityStack.java
    private boolean resumeTopActivityInnerLocked(ActivityRecord prev, Bundle options) {
        ...
        ActivityStack lastStack = mStackSupervisor.getLastStack();
        // 要启动的 activity 进程已存在的情况
        if (next.app != null && next.app.thread != null) {
            ...
        } else {
            ...
            mStackSupervisor.startSpecificActivityLocked(next, true, true);
        }
        ...
    }
    
    // ActivityStackSupervisor.java
    void startSpecificActivityLocked(ActivityRecord r,boolean andResume, boolean checkConfig) {
        ...
        // 调用 AMS 为目标 Activity 创建进程
        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
    }

一路跟踪 startActivity() 方法的启动状态到了 ActivityStack 类中的 resumeTopActivityInnerLocked 方法,终于看到了有关目标 Actiivty 对象所对应的进程是否存在的判断了。接下来跟到了 ActivityStackSupervisor 类的 startSpecificActivityLocked() 方法后,终于看到了 AMS 为目标 Activity 创建进程的 startProcessLocked() 方法了。接下来,我们去看看 AMS 的 startProcessLocked() 方法中具体是怎么处理的。

    // ActivityManagerService.java
    private final void startProcessLocked(ProcessRecord app, String hostingType,
            String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {
        ...
        checkTime(startTime, "startProcess: asking zygote to start proc");
        // 请求 Zygote 创建新进程
        Process.ProcessStartResult startResult = Process.start(entryPoint,
                app.processName, uid, uid, gids, debugFlags, mountExternal,
                app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
                app.info.dataDir, entryPointArgs);
        ...
        
    // Process.java
    private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
        if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
            try {
                // 与 zygote 建立 Socket 连接
                primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
            } catch (IOException ioe) {
                throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
            }
        }
        ...
    }
    
    // Process.java
    private static ProcessStartResult zygoteSendArgsAndGetResult(
        ZygoteState zygoteState, ArrayList args)
        throws ZygoteStartFailedEx {
        final BufferedWriter writer = zygoteState.writer;
        final DataInputStream inputStream = zygoteState.inputStream;
        ...
        int sz = args.size();
        // 向 Zygote 发送创建进程的参数
        for (int i = 0; i < sz; i++) {
            String arg = args.get(i);
            if (arg.indexOf('\n') >= 0) {
                throw new ZygoteStartFailedEx(
                        "embedded newlines not allowed");
            }
            writer.write(arg);
            writer.newLine();
        }

        writer.flush();
        
        ProcessStartResult result = new ProcessStartResult();
        // 等待读取 Zygote 创建新进程的结果
        result.pid = inputStream.readInt();
        if (result.pid < 0) {
            throw new ZygoteStartFailedEx("fork() failed");
        }
        result.usingWrapper = inputStream.readBoolean();
        return result;
    } catch (IOException ex) {
        // 关闭 Socket 连接
        zygoteState.close();
        throw new ZygoteStartFailedEx(ex);
    }
}

看到这里,已经看到了 AMS 是如何通知 Zygote 创建新进程的了。其过程是:先通过在 ActivityManagerService 里调用 Process 类的 startProcessLocked() 方法,并调用 openZygoteSocketIfNeeded() 方法来与 Zygote 进程建立 Socket 连接。然后通过建立连接后的 Socket 客户端对象将创建进程需要的参数组装起来并通过 OutputStream 流将参数发送给了 Zygote 来创建新进程。数据发送完毕后,再通过 InputStream 流阻塞的方式等待获取 Zygote 的处理结果。从启动调用 startActivity() 方法到 AMS 发送创建新进程参数的整个跟踪路径如下图所示:

image

3.3 默认异常处理器的处理逻辑

// RuntimeInit.java
private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        try {
            ...
            // Bring up crash dialog, wait for it to be dismissed
            ActivityManagerNative.getDefault().handleApplicationCrash(
                    mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
            ...
        } catch(Throwable t2) {
            ...
        } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
}

从上面的代码可以看出,对于未捕获异常的处理是通过 ActivityManagerProxy 这个应用进程的 AMS 代理类传递给了 AMS 来进行处理的。

// ActivityManagerService.java
private void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
    ...
    
    AppErrorResult result = new AppErrorResult();
    ...
    // If we can't identify the process or it's already exceeded its crash quota,
    // quit right away without showing a crash dialog.
    if (r == null || !makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace)) {
        Binder.restoreCallingIdentity(origId);
        return;
    }
    
    Message msg = Message.obtain();
    msg.what = SHOW_ERROR_MSG;
    HashMap data = new HashMap();
    data.put("result", result);
    data.put("app", r);
    msg.obj = data;
    mUiHandler.sendMessage(msg);
    ...
    
    int res = result.get(); 
    ... 
}

final class AppErrorResult {
    public void set(int res) {
        synchronized (this) {
            mHasResult = true;
            mResult = res;
            notifyAll();
        }
    }

    public int get() {
        synchronized (this) {
            while (!mHasResult) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mResult;
    }

    boolean mHasResult = false;
    int mResult;
}

可以看到 AMS 对 crash 的处理是通过向 mUiHandler 发送 SHOW_ERROR_MSG 消息来进行处理的,同时通过 result.get()
以 wait 的方式阻塞当前线程,直到有其它线程通过调用 set() 方法的方式来结束阻塞。我们再来看下 mUiHandler 是如何对这个消息进行处理的。

    
final class UiHandler extends Handler {
    ...
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case SHOW_ERROR_MSG: {
            ...
            if (mShowDialogs && !mSleeping && !mShuttingDown) {
                // 创建异常崩溃对话框
                Dialog d = new AppErrorDialog(mContext,
                        ActivityManagerService.this, res, proc);
                // 显示对话框
                d.show();
                proc.crashDialog = d;
            } else {
                // The device is asleep, so just pretend that the user
                // saw a crash dialog and hit "force quit".
                if (res != null) {
                    res.set(0);
                    }
                }
            }
            break;   
            ...
        }
    }
}

// AppErrorDialog.java
public AppErrorDialog(Context context, ActivityManagerService service,
        AppErrorResult result, ProcessRecord app) {

    // After the timeout, pretend the user clicked the quit button
    mHandler.sendMessageDelayed(
            mHandler.obtainMessage(FORCE_QUIT),
            DISMISS_TIMEOUT);
}

// AppErrorDialog.java
private final Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {
        ...
        dismiss();
    }
};

// AppErrorDialog.java
@Override
public void dismiss() {
    ...
    mResult.set(FORCE_QUIT);
    ...
}

在 UiHandler 里对异常崩溃的处理是弹出错误对话框,并在创建对话框 dialog 对象的时候,发送了一个 DISMISS_TIMEOUT = 5 min 的延迟消息,并在 5 分钟后关闭该错误提示对话框。此外,在关闭对话框 dismiss() 方法的回调中,通过调用 AppErrorResult 对象的 set() 方法来结束上面代码中的线程阻塞,并继续向下执行。而默认的情况下,crashApplication() 方法执行完成了。然后返回到默认异常处理器 UncaughtHandler 的 uncaughtException() 里继续执行就走到了 finally 代码块,并将当前进程杀掉了。

整个执行过程如下图所示:

image

到此,第 3 章开头出现的 Bugly 日志上传成功,但是 dialog 又弹出来了的原因以及为什么将错误异常交给系统处理后,调用方法后面的方法没有执行的原因就已经都清楚了。

这里还有一个问题没有搞清楚,就是当出现应用程序未捕获的异常后,系统是在哪里调用这个通过 Thread.setDefaultUncaughtExceptionHandler() 所传入的处理器的呢?这个留在下一节进行分析。

3.4 系统如何捕获 uncaughtException

这部分的内容涉及虚拟机的相关的知识点,为了不要过于发散我们直接从 ThreadGroup 的 uncaughtException() 方法来进行分析了。

// ThreadGroup.java
public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else if (Thread.getDefaultUncaughtExceptionHandler() != null) {
        // TODO The spec is unclear regarding this. What do we do?
        Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, e);
    } else if (!(e instanceof ThreadDeath)) {
        // No parent group, has to be 'system' Thread Group
        e.printStackTrace(System.err);
    }
}

一般情况下,一个应用中所使用的线程都是在同一个线程组,而在这个线程组里只要有一个线程出现未被捕获异常的时候,JAVA 虚拟机就会调用当前线程所在线程组中的 uncaughtException() 方法。

在这个方法里,很明显可以看到当通过 getDefaultUncaughtExceptionHandler() 所获取的默认异常处理器不为空时,就会将这个异常转发给注册的异常处理器。如果程序没有指定自定义的异常处理器,那么,当前异常处理器就是应用在被创建过程中,系统所指定的 UncaughtHandler 对象。

由于通过 setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) 方法设置的异常处理器是一个进程内全局唯一的静态变量,所以,只会有一个异常处理器。前面所提到的问题 Bugly 的崩溃日志已经可以上报成功了,但弹出了系统默认的错误信息提示对话框 的问题也就迎刃而解了。这部分内容放到第 4 章来进行说明。

4. 无法同时上传 Bugly 日志和重启应用的问题解决

从第 3 章的分析中我们知道了,每个进程内部只存在一个异常处理器,而异常处理器之间是可以通过分发的方式进行传递的,这里有点类似于职责链模式

回到最开始注册 Bugly 和自定义 CrashHandler 的地方。

override fun onCreate() {
    super.onCreate()
    appCtx = this
    CrashHandler().init()
    initBugly()
}

class CrashHandler : Thread.UncaughtExceptionHandler {
    lateinit var defaultExceptionHandler: Thread.UncaughtExceptionHandler

    fun init() {
        defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

   override fun uncaughtException(t: Thread, e: Throwable) {
        Log.e("TAG", "crash exception")
        e.printStackTrace()

        try {
            Thread.sleep(2000)
        } catch (ex: Exception) {
            Log.e("TAG", ex.message)
        }

        // restart application
        Log.i("TAG", "restart application")
        val intent = Intent(appCtx, SplashActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        appCtx?.startActivity(intent)

        defaultExceptionHandler.uncaughtException(t, e)

        // exit previous process
        android.os.Process.killProcess(android.os.Process.myPid())
        exitProcess(1)
    }
}

可以看到我们先初始化了 Bugly,再注册了自定义 CrashHandler 对象。我们还是看看在 Bugly 初始化过程中,都做了些什么吧。代码被混淆了,看着难受,不过,还是能找到一些蛛丝马迹的。其实只要找到 Thread.setDefaultUncaughtExceptionHandler() 方法就可以了,因为如果 Bugly 要获得未捕获异常处理回调的话,一定会注册这个方法。而且这个方法是不可以被混淆的。果然,在跟踪初始化的过程中,找到了下面方法,其对象是一个实现了 UncaughtExceptionHandler 接口的类。

// com.tencent.bugly.crashreport.crash.e
public final synchronized void a() {
        ...
        if ((var1 = Thread.getDefaultUncaughtExceptionHandler()) != null) {
            String var2 = this.getClass().getName();
            String var3 = var1.getClass().getName();
            if (var2.equals(var3)) {
                return;
            }
    
            if ("com.android.internal.os.RuntimeInit$UncaughtHandler".equals(var1.getClass().getName())) {
                x.a("backup system java handler: %s", new Object[]{var1.toString()});
                this.f = var1;
                this.e = var1;
            } else {
                x.a("backup java handler: %s", new Object[]{var1.toString()});
                // 获取系统前一个被设置的默认的异常处理器并进行保存
                this.e = var1;
            }
        }
        // 将当前类作为默认异常处理器注册到 Thread 里
        Thread.setDefaultUncaughtExceptionHandler(this);
        ...
    }
}

上面的代码虽然被混淆了,看起来非常的头疼,但我们只需要关注关键的几段代码就可以了。首先,在 Bugly 初始化的时候,Bugly 先获取并保存了上一下被设置的默认异常处理器(也就是系统默认的 UncaughtHandler 异常处理器对象),然后,将自己自定义的异常处理器注册到了 Thread 里面。uncaughtException() 方法在代码执行过程中,再讲。

接下来初始化自定义的 CrashHandler 异常处理器。在 init() 初始化方法中,我们先保存了上一个被设置成默认的异常处理器,也就是 Bugly 所设置的异常处理器。然后将自己注册到了 Thread 里,作为系统默认的异常处理器。到这里,其依赖关系如下图所示:

image

从图上可以看出,Thread 中系统默认的全局异常处理器为 CrashHandler,而 CrashHandler 自定义异常处理器中又持有了 Bugly 中的异常处理器,同时,Bugly 的异常处理器类中又持有了系统默认的 UncaughtHandler 异常处理器。

接下来我们再来看看其执行过程。

// CrashHandler.java
override fun uncaughtException(t: Thread, e: Throwable) {
    ...
    // restart application
    Log.i("TAG", "restart application")
    val intent = Intent(appCtx, SplashActivity::class.java)
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    appCtx?.startActivity(intent)

    defaultExceptionHandler.uncaughtException(t, e)
    ...
}

//Bugly com.tencent.bugly.crashreport.crash.e
public final void uncaughtException(Thread var1, Throwable var2) {
    synchronized(i) {
        this.a(var1, var2, true, (String)null, (byte[])null);
    }
}

public final void a(Thread var1, Throwable var2, boolean var3, String var4, byte[] var5) {
    ...
    try {
        ...
    } catch (Throwable var9) {
        ...
    } finally {
        if (var3) {
            // 这里的 e 就是在初始化过程中,保存的上一个默认异常处理器的值
            if (this.e != null && a(this.e)) {
                x.e("sys default last handle start!", new Object[0]);
                this.e.uncaughtException(var1, var2);
                x.e("sys default last handle end!", new Object[0]);
            } else if (this.f != null) {
                x.e("system handle start!", new Object[0]);
                this.f.uncaughtException(var1, var2);
                x.e("system handle end!", new Object[0]);
            } else {
                x.e("crashreport last handle start!", new Object[0]);
                x.e("current process die", new Object[0]);
                Process.killProcess(Process.myPid());
                System.exit(1);
                x.e("crashreport last handle end!", new Object[0]);
            }
        }

    }
}

// UncaughtHandler.java 
public void uncaughtException(Thread t, Throwable e) {
    try {
        ...
        // Bring up crash dialog, wait for it to be dismissed
        ActivityManagerNative.getDefault().handleApplicationCrash(
                mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
    } catch (Throwable t2) {
        ...
    } finally {
        // Try everything to make sure this process goes away.
        Process.killProcess(Process.myPid());
        System.exit(10);
    }
}

当应用出现未捕获异常时,根据注册过程中的依赖关系,CrashHandler 的 uncaughtException() 方法会最先被执行,在方法里,执行了重启的操作,同时调用了 Bugly 的自定义异常处理器的 uncaughtException() 方法。在 Bugly 的异常处理器回调方法里,先执行了异常日志的保存和上报操作,并在 finally 代码块中执行了系统默认异常处理器 UncaughtHandleruncaughtException() 方法。在 UncaughtHandler 的回调方法里,调用了 AMS 的 handleApplicationCrash() 方法,并执行弹出默认显示 5 分钟的错误提示对话框,当对话框显示 5 分钟后会被关闭,并执行杀掉当前进程的操作。整个异常处理的执行过程如下图所示:

image

通过上面的注册和执行的分析,可以明显看出这就是一个职责链模式,一个异常处理器连着另一个处理器,构成一个链。同时,当应用程序出现未捕获的异常时,会将异常在异常处理器链中一个一个的向下传递,走到所有的节点都接收到异常回调并结束当前进程为止。

到此,我们也就彻底明白了 2.5 中的问题是怎么回事了。知道了原因及其执行过程后,我们只需要将其依赖关系颠倒一下并不要执行系统默认的 UncaughtHandler 类(也就是不要弹出错误提示对话框)即可。修改之后的代码如下所示:

class CrashHandler : Thread.UncaughtExceptionHandler {

    fun init() {
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(t: Thread, e: Throwable) {
        Log.e("TAG", "crash exception")
        e.printStackTrace()

        try {
            Thread.sleep(2000)
        } catch (ex: Exception) {
            Log.e("TAG", ex.message)
        }

        // restart application
        Log.i("TAG", "restart application")
        val intent = Intent(appCtx, SplashActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        appCtx?.startActivity(intent)

        // exit previous process
        android.os.Process.killProcess(android.os.Process.myPid())
        exitProcess(1)
    }
}

override fun onCreate() {
    super.onCreate()
    appCtx = this
    CrashHandler().init()
    initBugly()
}

执行上面的代码 Bugly 就可以正常上传崩溃日志了,同时,应用也可以正常重启,而不会再弹出错误提示对话框了。

5. 总结

到这里为止,关于集成 Bugly 并通过自定义 CrashHandler 实现应用重启可能导致的问题以及正确的集成方式已经都讲完了。接下来对于本文中提到的几个知识点,进行简单的总结一下:

5.1 系统默认的异常处理器是在什么时候进行注册的?

在系统启动的过程中,会启动一个 Zygote 进程,Zygote 启动后会执行 ZygoteInit.main() 方法,在这个方法里面,Zygote 将自己作为 LocalSockerServer,并监听来自客户端的连接及创建进程的命令。

当应用中通过 startActivity() 启动应用的时候,如果目标 Activity 所对应的进程并不存在,AMS 会通过 Process.start() 方法先建立与 Zygote 进程的 Socket 连接,然后在通过 Socket 流来传递创建进程所需要的参数,并通知 Zygote 创建新的进程。

在 Zygote 进程 fork 出新的进程之后,会切换到创建后的进程并调用 RuntimeInit.zygoteInit() 进行进程的初始化工作。也就是在这个方法所调用的 RuntimeInit.commonInit() 方法中,通过调用 Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); 方法,将 UncaughtHandler 对象注册到了 Thread 类中,并作为当前应用进程默认的异常处理器。

5.2 系统何时触发调用异常处理器的 uncaughtException() 方法

要完全弄清楚这个问题,需要从虚拟机层面来进行分析。由于篇幅有限,虚拟机中的内容在本文并没有被提到,而是直接给出了在虚拟机里调用并传递异常的方法。这个方法是 ThreadGroup 中的 uncaughtException() 方法,该方法直接在应用出现未捕获异常的时候,被虚拟器调用。

在这个方法里,又调用了 Thread 中 defaultUncaughtExceptionHandler 默认异常处理器中的 uncaughtException() 方法来进行未捕获异常的传递工作。

5.3 Bugly 和自定义 CrashHandler 在实现应用重启功能时,注册及执行顺序是怎样的?

在注册过程中,需要先调用自定义的 CrashHandler 对象的 init() 初始化方法,并将自己设置为默认的异常处理器。然后,调用 Bugly 的 initBugly()方法。在初始化方法中,先持有当前进程中的默认异常处理器,也就是自定义的 CrashHandler 对象,并将自己设置为默认的异常处理器。

在应用出现未被捕获异常的时候,异常首先会被分发给 Bugly 的 uncaughtException() 方法进行业务处理,这里主要是将错误日志进行存储和上报的操作。执行完成后,又调用 CrashHandler 中的 uncaughtException() 方法,进行应用重启的工作,并杀掉当前进程。

整个注册、执行的过程,就是一个典型的职责链模式。在注册的过程中,就是创建链的过程,将一个个处理器相互连接成链。在执行过程中,将未捕获的异常在链上进行传递,直到所有的异常处理器都接收到异常的回调,并关闭进程为此。

你可能感兴趣的:(通过源码分析 Bugly 和 CrashHandler 实现应用重启潜在问题分析)