你知道App为什么会Crash吗?
一、概述
目录
前言
每一个Android开发同学在项目开发过程中一定都遇到过各式各样的Crash问题,大家都非常不希望程序发生Crash。
那么问题来了,你真的理解Crash吗?
二、App 为什么会发生Crash?
最近在思考一个问题,为什么Android程序发生空指针等异常时,会导致应用会崩溃,进程结束。
而java web程序发生这些异常,只需有其余线程还在运行,虚拟机就不会关闭,进程也不会结束。
我在App中模拟了一个数组越界异常,Android系统会帮我们打印异常日志。
主线程异常
子****线程异常
每当异常发生的时候,我们往往都会通过查看日志来处理。
那么我们是不是可以通过查看打印异常日志的代码,来找到Android系统是如何抛出这些未捕获的异常,以及Android在出现未捕获异常的时候为什么会发生Crash。
我们找到了com.android.internal.os.RuntimeInit类,这里我们仅贴出我们需要的代码
public class RuntimeInit { final static String TAG = "AndroidRuntime"; .... private static class LoggingHandler implements Thread.UncaughtExceptionHandler { public volatile boolean mTriggered = false; @Override public void uncaughtException(Thread t, Throwable e) { mTriggered = true; if (mCrashing) return; //打印异常日志 if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) { Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e); } else { StringBuilder message = new StringBuilder(); message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n"); final String processName = ActivityThread.currentProcessName(); if (processName != null) { message.append("Process: ").append(processName).append(", "); } message.append("PID: ").append(Process.myPid()); Clog_e(TAG, message.toString(), e); } } } private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler { private final LoggingHandler mLoggingHandler; public KillApplicationHandler(LoggingHandler loggingHandler) { this.mLoggingHandler = Objects.requireNonNull(loggingHandler); } @Override public void uncaughtException(Thread t, Throwable e) { try { ensureLogging(t, e); if (mCrashing) return; mCrashing = true; if (ActivityThread.currentActivityThread() != null) { ActivityThread.currentActivityThread().stopProfiling(); } ActivityManager.getService().handleApplicationCrash( mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e)); } catch (Throwable t2) { if (t2 instanceof DeadObjectException) { } else { try { Clog_e(TAG, "Error reporting crash", t2); } catch (Throwable t3) { } } } finally { //杀死进程 Process.killProcess(Process.myPid()); System.exit(10); } } private void ensureLogging(Thread t, Throwable e) { if (!mLoggingHandler.mTriggered) { try { mLoggingHandler.uncaughtException(t, e); } catch (Throwable loggingThrowable) { } } } .... } protected static final void commonInit() { //设置异常解决回调 LoggingHandler loggingHandler = new LoggingHandler(); Thread.setUncaughtExceptionPreHandler(loggingHandler); Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); .... }
RuntimeInit有两个的内部类,LoggingHandler和KillApplicationHandler。
很显然,LoggingHandler的作用是打印异常日志,而KillApplicationHandler就是App发生Crash的真正起因,其内部调用了Process.killProcess(Process.myPid())来杀死发生Uncaught异常的进程。
我们还发现,这两个内部类都实现了Thread.UncaughtExceptionHandler接口。
分别通过Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法进行注册。
- Thread.setUncaughtExceptionPreHandler,覆盖所有线程,会在回调DefaultUncaughtExceptionHandler之前调用,只能在Android Framework内部调用该方法
- Thread.setDefaultUncaughtExceptionHandler,假如在任意线程中调用就可覆盖所有线程的异常,可以在应用层调用,每次调用传入的Thread.UncaughtExceptionHandler都会覆盖上一次的,即我们可以手动覆盖系统实现的KillApplicationHandler
- new Thread().setUncaughtExceptionHandler(),只可以覆盖当前线程的异常,假如某个Thread有定义UncaughtExceptionHandler,则忽略全局DefaultUncaughtExceptionHandler
小结:Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常, 而后便会调用uncaughtException函数。
假如该handler没有被显式设置,则会调用对应线程组的默认handler。假如我们要捕获该异常,必需实现我们自己的handler。
三、我们能让应用不发生Crash吗?
上面说到了我们可以在应用层调用Thread.setDefaultUncaughtExceptionHandler来实现所有线程的Uncaught异常的监听,并且会覆盖系统的默认实现的KillApplicationHandler,这样我们即可以做到让线程发生Uncaught异常的时候只是当前杀死线程,而不会杀死整个进程。
这适用于我们的子线程发生Uncaught异常,假如我们的主线程发生Uncaught异常呢?
主线程都被销毁了,这和Crash似乎就没什么区别的。
那么我们有办法让主线程发生Uncaught异常也不会发生Crash吗?
答案是有的,但在讲如何实现之前我们先来详情少量知识点。
我们知道Java程序开始于一个Main函数,假如只是顺序执行有限任务很快这个Main函数所在的线程就结束了。
如何来保持Main函数一直存活并不断的解决已知或者未知的任务呢?
- 采用死循环。但是死循环的一次循环需要解决什么任务。假如任务暂时没有,也要程序保持活跃的等待状态怎样办?
- 假如有两个线程或者者多个线程如何来协作以完成一个微型系统任务?
假如熟习Android Handler机制的话,我们会理解到整个Android系统其实是消息驱动的。
Looper内部是一个死循环,不断地MessageQueue内部取出消息,由消息来通知做什么任务。
比方收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创立Activity实例,而后再执行Activity.onCreate()等方法;
再比方收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ... for (;;) { //从消息队列中取出Message Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } //派发发消息到对应的Handler,target就是Handler的实例 msg.target.dispatchMessage(msg); .... //释放消息占据的资源 msg.recycleUnchecked(); }}
那么我们有没有想过一个问题,Looper.loop是在ActiivtyThread被调用的,也就是主线程中,那么主线程中死循环为什么不会导致应用卡死呢?
这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Looper.loop()的queue.next()中的nativePollOnce()方法,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个形容符,当某个形容符就绪(读或者写就绪),则立刻通知相应程序进行读或者写操作,本质同步I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
当收到不同Message时则采用相应措施:一旦退出消息循环,那么你的程序也即可以退出了。从消息队列中取消息可能会阻塞,取到消息会做出相应的解决。假如某个消息解决时间过长,即可能会影响UI线程的刷新速率,造成卡慢的现象
在子线程中,假如手动为其创立了Looper,那么在所有的事情完成以后应该调用quit()方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而假如退出Looper以后,这个线程就会立刻(执行所有方法并)终止,因而建议不需要的时候终止Looper
简单总结一下就是当没有消息时,native层的方法做了阻塞解决,所以Looper.loop()死循环不会卡死应用。
我们整个系统都是基于消息机制,再回过头去看一眼上面的主线程异常日志堆栈信息,是不是会经过Looper.loop(),所以其实我们只要要try catch Looper.loop()就可捕获主线程异常。
代码如下所示
public class CrashCatch { private CrashHandler mCrashHandler; private static CrashCatch mInstance; private CrashCatch(){ } private static CrashCatch getInstance(){ if(mInstance == null){ synchronized (CrashCatch.class){ if(mInstance == null){ mInstance = new CrashCatch(); } } } return mInstance; } public static void init(CrashHandler crashHandler){ getInstance().setCrashHandler(crashHandler); } private void setCrashHandler(CrashHandler crashHandler){ mCrashHandler = crashHandler; //主线程异常阻拦 new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { for (;;) { try { Looper.loop(); } catch (Throwable e) { if (mCrashHandler != null) { //解决异常 mCrashHandler.handlerException(Looper.getMainLooper().getThread(), e); } } } } }); //所有线程异常阻拦,因为主线程的异常都被我们catch住了,所以下面的代码阻拦到的都是子线程的异常 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { if(mCrashHandler!=null){ //解决异常 mCrashHandler.handlerException(t,e); } } }); } public interface CrashHandler{ void handlerException(Thread t,Throwable e); }}
原理很简单,就是通过Handler往主线程的MessageQueue中增加一个Runnable,当主线程执行到该Runnable时,会进入我们的while死循环。
假如while内部是空的就会导致代码卡在这里,最终导致ANR,但我们在while死循环中又调用了Looper.loop(),这就导致主线程又开始不断的读取queue中的Message并执行,这样即可以保证以后主线程的所有异常都会从我们手动调用的Looper.loop()处抛出,一旦抛出就会被try{}catch捕获,这样主线程就不会crash了。
假如没有这个while的话那么主线程下次抛出异常时我们就又捕获不到了,这样App就又crash了,所以我们要通过while让每次crash发生后都再次进入消息循环,while的作用仅限于每次主线程抛出异常后迫使主线程再次进入消息循环。
为什么要通过new Handler.post方式而不是直接在主线程中任意位置执行 while (true) { try { Looper.loop(); } catch (Throwable e) {} }。
这是由于该方法是个死循环,若在主线程中,比方在Activity的onCreate中执行时会导致while后面的代码得不到执行,Activity的生命周期也就不能完整执行,通过Handler.post方式可以保证不影响该条消息中后面的逻辑。
使用起来也非常简单
CrashCatch.getInstance().setCrashHandler(new CrashHandler(){ @Override void handlerException(Thread t,Throwable e){ //try catch 以防handlerException内部再次抛出异常,导致循环调用handlerException try{ //TODO 实现自己的异常解决逻辑 }catch(Exeception e){ } }})
四、总结
很多时候因为少量微不足道的bug导致app崩溃很可惜,android默认的异常杀进程机制简单粗暴,但很多时候让app崩溃其实并不是一个特别好的选择。
有些bug可能是系统bug,对于这些难以意料的系统bug我们不好绕过,还有少量bug是我们自己编码造成的,对于有些bug来说直接忽略掉的话可能只是导致部分不重要的功能没法使用而已,又或者者对客户来说完全没有影响,这种情况总比每次都崩溃要好很多。
我们还可以捕获到异常后做少量自己的逻辑判断。
本文主要讲原理,具体大家如何使用如何取舍,还是视自己项目的实际情况而定。
在这我也分享一份自己收录整理的 Android学习PDF+架构视频+面试文档+源码笔记 ,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点。
总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
假如你有需要的话,可以点赞+评论,关注我, 加Vx:15388039515(备注简书,需要资料)
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 你知道App为什么会Crash吗?