因为在开发或者测试阶段不能做到100%的问题解决,因为 app 上线之后会有你想不到的各种各样的使用的场景,而发生问题时用户只能描述一下怎么怎么怎么就出现了问题。也许反馈到开发这边可以100%复现那就可以得到解决,但是也有可能在本地复现不了,只有在用户的手机上可以出现,这可能和用户使用的场景(温度太高导致CPU限速,温度太低等),手机的内存,CPU,老年机等等都有关系。
在 android 里面,奔溃可以分为二大类,一个为 java 奔溃,一个为 native 奔溃。对于这二种奔溃需要用不同的方式去捕获。
一个合格的异常捕获组件也要能达到以下目的:
A : 支持在crash时进行更多扩展操作, 例如 :
1、打印logcat和应用日志
2、上报crash次数
3、对不同的crash做不同的恢复措施
B : 可以针对业务不断改进和适应
可查的异常(checked exceptions):
编译器要求必须处置的异常(使用 try…catch…finally 或者 throws )。在方法中要么用try-catch语句捕获它并处理,要么用 throws 子句声明抛出它,否则编译不会通过。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。
不可查的异常(unchecked exceptions):
包括运行时异常(RuntimeException与其子类)和错误(Error)。在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。
在 Applicaiton中进行初始化崩溃收集器
public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); //初始化崩溃收集器 CollectCrashUtils.initColleteCrash(); } }
收集java层崩溃和native层崩溃
public class CollectCrashUtils { public static void initColleteCrash() { //初始化Handler,收集java层崩溃 MyJavaCrashHandler handler = new MyJavaCrashHandler(); Thread.setDefaultUncaughtExceptionHandler(handler); //收集native层崩溃 File file = new File("sdcard/Crashlog"); if (!file.exists()) { file.mkdirs(); } NativeBreakpad.init(file.getAbsolutePath()); } }
native层的崩溃收集可以使用编译好的breakpad.so
java层崩溃实现Thread.UncaughtExceptionHandler接口进行收集
public class MyJavaCrashHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { Log.e("程序出现异常了", "Thread = " + t.getName() + "\nThrowable = " + e.getMessage()); String stackTraceInfo = getStackTraceInfo(e); Log.e("stackTraceInfo", stackTraceInfo); saveThrowableMessage(stackTraceInfo); } /** * 获取错误的信息 * * @param throwable * @return */ private String getStackTraceInfo(final Throwable throwable) { PrintWriter pw = null; Writer writer = new StringWriter(); try { pw = new PrintWriter(writer); throwable.printStackTrace(pw); } catch (Exception e) { return ""; } finally { if (pw != null) { pw.close(); } } return writer.toString(); } private String logFilePath = "sdcard/Crashlog"; private void saveThrowableMessage(String errorMessage) { if (TextUtils.isEmpty(errorMessage)) { return; } File file = new File(logFilePath); if (!file.exists()) { boolean mkdirs = file.mkdirs(); if (mkdirs) { writeStringToFile(errorMessage, file); } } else { writeStringToFile(errorMessage, file); } } private void writeStringToFile(final String errorMessage, final File file) { new Thread(new Runnable() { @Override public void run() { FileOutputStream outputStream = null; try { ByteArrayInputStream inputStream = new ByteArrayInputStream(errorMessage.getBytes()); outputStream = new FileOutputStream(new File(file, System.currentTimeMillis() + ".txt")); int len = 0; byte[] bytes = new byte[1024]; while ((len = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, len); } outputStream.flush(); Log.e("程序出异常了", "写入本地文件成功:" + file.getAbsolutePath()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } }
Thread中存在两个UncaughtExceptionHandler:
- 一个是静态的defaultUncaughtExceptionHandler:来自所有线程中的Exception在抛出并且未捕获的情况下,都会从此路过。进程fork的时候设置的就是这个静态的defaultUncaughtExceptionHandler,管辖范围为整个进程。
- 另一个是非静态uncaughtExceptionHandler:为单个线程设置一个属于线程自己的uncaughtExceptionHandler,辖范围比较小。
Thread类的异常处理变量声明 :
//成员变量,线程独有的 // null unless explicitly set private volatile UncaughtExceptionHandler uncaughtExceptionHandler; //静态变量,用于所有线程 // null unless explicitly set private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
当一个线程由于未捕获异常即将终止时,Java虚拟机将使用Thread的getuncaughtexceptionhandler()方法查询线程的uncaughtException处理程序,并调用处理程序的uncaughtException方法,将线程和异常作为参数传递。
一个线程如果没有设置uncaughtExceptionHandler,将使用线程所在的线程组来处理这个未捕获异常。线程组ThreadGroup实现了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); } } }
线程组处理未捕获异常的逻辑是:
- 首先将异常消息通知给父线程组处理。
- 否则,尝试利用一个默认的defaultUncaughtExceptionHandler来处理异常。
- 如果没有默认的异常处理器则将错误信息输出到System.err。
在Android中,运行时异常(属于不可查的异常),如果没有在try-catch语句捕获并处理,就会产生Crash,导致程序崩溃。
Crash是App稳定性的一个重要指标,大量的Crash是非常差的用户体验,会导致用户的流失。Crash发生之后,我们应该及时处理并解决,然后发版对其进行修复。
那么,问题来了,我们想要解决Crash,但是Crash发生在用户手机上,我们如何能拿到我们想要的错误信息呢?
Android中添加全局的Crash监控实战
在Android中,我们同样可以使用UncaughtExceptionHandler来添加运行时异常的回调,来监控Crash的发生。
1、创建一个自定义UncaughtExceptionHandler类
public class CrashHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread thread, Throwable ex) { //回调函数,处理异常 //在这里将崩溃日志读取出来,然后保存到SD卡,或者直接上传到日志服务器 //如果我们也想继续调用系统的默认处理,可以先把系统UncaughtExceptionHandler存下来,然后在这里调用。 } }
2. 设置全局监控
CrashHandler crashHandler = new CrashHandler(); Thread.setDefaultUncaughtExceptionHandler(crashHandler);
完成以上2个步骤,我们就可以实现全局的Crash监控了。这里所说的全局,是指针对整个进程生效。
上面我们了解了怎么在Android中捕获Crash,实现Crash的监控、上报。那么,在Android中,系统是如何处理、分发Crash的呢?
异常处理的注册
App启动时,会通过zygote进程fork一个进程,然后创建VM虚拟机,然后会调用到zygoteInit进行初始化工作。
zygoteInit方法
frameworks/base/core/java/com/android/internal/os/ZygoteInit.java的zygoteInit方法:
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) { …… RuntimeInit.commonInit(); …… }
zygoteInit调用了RuntimeInit.commonInit()方法。
RuntimeInit.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(); RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler); Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); }
逻辑解析:
- LoggingHandler用于处理打印日志,我们不做详细分析,感兴趣的可以自己看下。
- Thread.setDefaultUncaughtExceptionHandler用于注册系统默认异常处理的逻辑。
这里的RuntimeHooks.setUncaughtExceptionPreHandler方法,其实是调用了Thread的setUncaughtExceptionPreHandler方法。
Android 8.0中,Thread类增加了一个接口叫setUncaughtExceptionPreHandler,它会注册在分发异常处理时的回调,用于Android系统(平台)使用。
我们先来看异常的分发逻辑,后面再分析KillApplicationHandler中对异常的处理逻辑。
异常的分发
Thread的dispatchUncaughtException负责处理异常的分发逻辑。
Thread的dispatchUncaughtException
public final void dispatchUncaughtException(Throwable e) { // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform. 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 } } // END Android-added: uncaughtExceptionPreHandler for use by platform. getUncaughtExceptionHandler().uncaughtException(this, e); }
这里首先处理setUncaughtExceptionPreHandler注册的异常处理方法,然后在调用通过setDefaultUncaughtExceptionHandler或setUncaughtExceptionHandler方法注册的异常处理方法。
Thread的getUncaughtExceptionHandler()方法
public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; }
逻辑解析:
- 当uncaughtExceptionHandler不为空时,返回uncaughtExceptionHandler。uncaughtExceptionHandler是通过setUncaughtExceptionHandler方法注册异常处理Handler。
- 否则,返回group。
- 这里的group,其实就是当前线程所在的线程组。并且线程组ThreadGroup同样实现了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); } } }
逻辑解析:
线程组处理未捕获异常的逻辑是:
- 首先将异常消息通知给父线程组处理。
- 否则,尝试利用一个默认的defaultUncaughtExceptionHandler来处理异常。
- 如果没有默认的异常处理器则将错误信息输出到System.err。
系统默认异常处理逻辑在KillApplicationHandler类的uncaughtException方法中,我们来看代码
KillApplicationHandler类的uncaughtException方法
public void uncaughtException(Thread t, Throwable e) { try { //确保LoggingHandler的执行 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(); } //调用AMS,展示弹出等逻辑 // 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); } }
逻辑解析:
- 调用ensureLogging(t, e)确保LoggingHandler的执行(有去重逻辑,不用担心重复执行)。
- 然后调用了ActivityManager.getService().handleApplicationCrash方法来进行处理。
- 最后调用Process.killProcess(Process.myPid())来杀死进程,并且退出VM。
AMS的handleApplicationCrash方法
我们继续来看ActivityManagerService的handleApplicationCrash方法:
位置:/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.javapublic void handleApplicationCrash(IBinder app, ApplicationErrorReport.ParcelableCrashInfo crashInfo) { ProcessRecord r = findAppProcess(app, "Crash"); final String processName = app == null ? "system_server" : (r == null ? "unknown" : r.processName); handleApplicationCrashInner("crash", r, processName, crashInfo); }
这里通过Application的binder,取得进程的ProcessRecord对象,然后调用handleApplicationCrashInner方法。
AMS的handleApplicationCrashInner方法
final AppErrors mAppErrors; void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName, ApplicationErrorReport.CrashInfo crashInfo) { /* 处理一些错误日志相关的逻辑 */ //调用 mAppErrors.crashApplication(r, crashInfo); }
这里处理一些错误日志相关的逻辑,然后调用AppErrors的crashApplication方法。
AppErrors的crashApplication方法
/frameworks/base/services/core/java/com/android/server/am/AppErrors.java
void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) { final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { crashApplicationInner(r, crashInfo, callingPid, callingUid); } finally { Binder.restoreCallingIdentity(origId); } }
crashApplication调用crashApplicationInner方法,处理系统的crash弹框等逻辑 .
Crash是App性能的一个非常重要的指标,我们要尽可能的减少Crash,增加App的稳定性,以下是几点实践经验:
- 要有可靠的Crash日志收集方式:可以自己实现,也可以集成第三方SDK来采集分析。
- 当一个Crash发生了,我们不但需要针性的解决这一个Crash,而且要考虑这一类Crash怎么去解决和预防,只有这样才能使得该类Crash真正的解决,而不是反复出现。
- 不能随意的使用try-catch,这样只会隐蔽真正的问题,要从根本上了解Crash的原因,根据原因去解决。
- 增加代码检测,预防常规可检测的代码问题的产生,预防胜于治理。
- Java异常可分为:可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。
- Java中,可以通过设置Thread类的uncaughtExceptionHandler属性或静态属性defaultUncaughtExceptionHandler来设置不可查异常的回调处理。
- 在Android中,运行时异常(属于不可查的异常),如果没有在try-catch语句捕获并处理,就会产生Crash,导致程序崩溃。
- 在Android中,我们同样可以使用UncaughtExceptionHandler来添加运行时异常的回调,来监控Crash的发生。
- 通过Thread的静态方法setDefaultUncaughtExceptionHandler方法,可以注册全局的默认Crash监控。通过Thread的setUncaughtExceptionHandler方法来注册某个线程的异常监控。
- setDefaultUncaughtExceptionHandler方法和setUncaughtExceptionHandler方法有注册顺序的问题,多次注册后,只有最后一次生效。
- Android系统中,默认的Crash处理Handler,是在进程创建时,通过RuntimeInit.commonInit()方法进行注册的。
- Android 8.0中,Thread类增加了一个接口叫setUncaughtExceptionPreHandler,它会注册在分发异常处理时的回调,用于Android系统(平台)使用。
- Thread的dispatchUncaughtException负责处理异常的分发逻辑。
- Android中,异常分发顺序为:
- 首先处理setUncaughtExceptionPreHandler注册的异常处理方法;
- 然后处理线程私有的(uncaughtExceptionHandler)Handler异常处理方法;
- 如果私有Handler不存在,则处理ThreadGroup的Handler异常处理方法;
- ThreadGroup中,优先调用父线程组的处理逻辑,否则,调用通过setUncaughtExceptionHandler方法注册异常处理Handler。
- 系统默认异常处理逻辑在KillApplicationHandler类的uncaughtException方法中,系统默认Crash弹框等逻辑是通过AMS的handleApplicationCrash方法执行的。
- Crash是App性能的一个非常重要的指标,我们要尽可能的减少Crash,增加App的稳定性。