什么是Crash?
在应用启动及运行中,出现闪退(崩溃),屏幕提示当前程序停止运行的弹窗,类似于windows的应用程序崩溃。
为什么会出现崩溃?
概括来讲,就是程序运行中有未捕获的异常,未被 try-catch,导致进程被杀。
线程中抛出异常后的处理逻辑?
- 一旦线程出现异常,并且代码中为捕获的情况下,JVM 将调用
Thread
的dispatchUncaughtException
方法把异常传递给线程的未捕获异常处理器。 - 默认情况下,线程是没有处理未捕获异常的能力的,线程组处理未捕获异常的逻辑是:首先将异常消息通知给父线程组,然后尝试利用一个默认的
defaultUncaughtExceptionHandler
来处理异常。 - JVM 提供给给我们设置每个线程的具体的未捕获的异常处理器,也提供了设置默认异常处理器的方法。
class Thread implements Runnable {
//线程组
private ThreadGroup group;
//为当前线程单独设置的异常处理器
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
//所有线程通用的默认异常处理器,静态的
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
//当出现未捕获的异常 --》 线程分发异常
public final void dispatchUncaughtException(Throwable e) {
// END Android-added: uncaughtExceptionPreHandler for use by platform.
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
//默认情况下,线程的异常处理都为 null,除非我们手动设置,所有线程的异常都交由线程组统一处理
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
}
-
一旦线程出现未捕获的异常,JVM将调用
Thread
的dispatchUncaughtException
方法将异常传递给线程的未捕获的异常处理器。 -
如果没有设置
UncaughtExceptionHandler
,将使用线程所在的线程组来处理这个未捕获的异常 -
线程
ThreadGroup
实现了UncaughtExceptionHandler
接口来处理异常
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
public void uncaughtException(java.lang.Thread t, Throwable e) {
//1.获取线程默认异常处理器,即在 RuntimeInitJava -> commitInit 方法中设置的 KillApplicationHandler 异常处理器
Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
//1.交由默认处理器来处理
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
//否则就打印在控制台上
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
- 在
RuntimeInit#main
方法入口函数中,调用了commonInit
方法设置了默认的异常处理器。
public class RuntimeInit {
protected static final void commonInit() {
//在这里通过静态调用 为线程设置了默认的异常处理器。
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
}
}
- 在
RuntimeInit#KillApplicationHandler
的内部类中,该类也实现了Thread.UncaughtExceptionHandler
接口。
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(java.lang.Thread t, Throwable e) {
try {
//在这里弹出崩溃弹窗
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
} finally {
// Try everything to make sure this process goes away.
//杀死进程,并退出 10,正常退出为 0.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
}
其实在 fork 出 app 进程的时候,系统已经为 app 设置了默认的一个异常处理,并且最终崩溃后直接杀死 app,并退出。我们可以设置 Thread 的静态方法
Thread.setDefaultUncaughtExceptionHandler 自己设置了 Thread.UncaughtExceptionHandler 方法。
Java Crash监控
- 如何收集
Java_Crash
日志 - 如何重启应用
- 如何还原混淆代码
1.如何收集 Java_Crash 日志
自定义java异常处理,记录当前设备的基础信息,及线程错误信息存储到本地文件中。在需要的时候上传到服务器。
CrashHandler
internal object CrashHandler {
var CRASH_DIR = "crash_dir"
fun init(crashDir: String) {
Thread.setDefaultUncaughtExceptionHandler(CaughtExceptionHandler())
this.CRASH_DIR = crashDir
}
private class CaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private val context = AppGlobals.get()!!
private val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)
private val LAUNCH_TIME = formatter.format(Date())
private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
override fun uncaughtException(t: Thread, e: Throwable) {
if (!handleException(e) && defaultExceptionHandler != null) {
defaultExceptionHandler.uncaughtException(t, e)
}
restartApp()
}
private fun restartApp() {
val intent: Intent? =
context.packageManager?.getLaunchIntentForPackage(context.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
Process.killProcess(Process.myPid())
exitProcess(10)
}
private fun handleException(e: Throwable?): Boolean {
if (e == null) return false
val log = collectDeviceInfo(e)
if (BuildConfig.DEBUG) {
HiLog.e(log)
}
saveCrashInfo2File(log)
return true
}
private fun saveCrashInfo2File(log: String) {
val crashDir = File(CRASH_DIR)
if (!crashDir.exists()) {
crashDir.mkdirs()
}
val crashFile = File(crashDir, formatter.format(Date()) + "-crash.txt")
crashFile.createNewFile()
val fos = FileOutputStream(crashFile)
try {
fos.write(log.toByteArray())
fos.flush()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
fos.close()
}
}
/**
* 设备类型、OS本版、线程名、前后台、使用时长、App版本、升级渠道
CPU架构、内存信息、存储信息、permission权限
*/
private fun collectDeviceInfo(e: Throwable): String {
val sb = StringBuilder()
sb.append("brand=${Build.BRAND}\n")// huawei,xiaomi
sb.append("rom=${Build.MODEL}\n") //sm-G9550
sb.append("os=${Build.VERSION.RELEASE}\n")//9.0
sb.append("sdk=${Build.VERSION.SDK_INT}\n")//28
sb.append("launch_time=${LAUNCH_TIME}\n")//启动APP的时间
sb.append("crash_time=${formatter.format(Date())}\n")//crash发生的时间
sb.append("forground=${ActivityManager.instance.front}\n")//应用处于前后台
sb.append("thread=${Thread.currentThread().name}\n")//异常线程名
sb.append("cpu_arch=${Build.CPU_ABI}\n")//armv7 armv8
//app 信息
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
sb.append("version_code=${packageInfo.versionCode}\n")
sb.append("version_name=${packageInfo.versionName}\n")
sb.append("package_name=${packageInfo.packageName}\n")
sb.append("requested_permission=${Arrays.toString(packageInfo.requestedPermissions)}\n")//已申请到那些权限
//统计一波 存储空间的信息,
val memInfo = android.app.ActivityManager.MemoryInfo()
val ams =
context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
ams.getMemoryInfo(memInfo)
sb.append("availMem=${Formatter.formatFileSize(context, memInfo.availMem)}\n")//可用内存
sb.append("totalMem=${Formatter.formatFileSize(context, memInfo.totalMem)}\n")//设备总内存
val file = Environment.getExternalStorageDirectory()
val statFs = StatFs(file.path)
val availableSize = statFs.availableBlocks * statFs.blockSize
sb.append(
"availStorage=${Formatter.formatFileSize(
context,
availableSize.toLong()
)}\n"
)//存储空间
val write: Writer = StringWriter()
val printWriter = PrintWriter(write)
e.printStackTrace(printWriter)
var cause = e.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
printWriter.close()
sb.append(write.toString())
return sb.toString()
}
}
fun crashFiles(): Array<File> {
return File(
AppGlobals.get()?.cacheDir,
CRASH_DIR
).listFiles()
}
}
2. 重启应用
注意事项:如果进入应用就崩溃,就会进入重复重启的死循环。所以在做重启时候,需要做一些判断:例如1分钟内系统出现两次 crash 就不在重启了,否则这个应用被卸载无疑。
3.混淆日志代码的还原
**工具:**在SDK的的目录下:D:\software_for_code\Android\SDK\tools\proguard\bin
使用方法:
- 双击打开 proguardgui.bat,找到app工程目下的编译后的
mapping file
文件中选择:mapping.txt
,前提是代码开了混淆。将要还原的代码复制到窗口中。
- 点击
ReTrace
即可。
Native crash异常处理
native_crash
,就是 native 层发生了 crash。内部是通过一个NativeCrashListener
线程去监控的。- SystemServer --> ActivityManagerService --> startObservingNativeCrashes();
class ActivityManagerService{
public void startObservingNativeCrashes() {
final NativeCrashListener ncl = new NativeCrashListener(this);
ncl.start();
}
}
直接引入第三方库来处理 native 异常
3.封装工具类
object CrashMgr {
private const val CRASH_DIR_JAVA = "java_crash"
private const val CRASH_DIR_NATIVE = "native_crash"
fun init() {
val javaCrashDir = getJavaCrashDir()
val nativeCrashDir = getNativeCrashDir()
CrashHandler.init(javaCrashDir.absolutePath)
NativeCrashHandler.init(nativeCrashDir.absolutePath)
}
private fun getJavaCrashDir(): File {
val javaCrashFile = File(AppGlobals.get()!!.cacheDir, CRASH_DIR_JAVA)
if (!javaCrashFile.exists()) {
javaCrashFile.mkdirs()
}
return javaCrashFile
}
private fun getNativeCrashDir(): File {
val nativeCrashFile = File(AppGlobals.get()!!.cacheDir, CRASH_DIR_NATIVE)
if (!nativeCrashFile.exists()) {
nativeCrashFile.mkdirs()
}
return nativeCrashFile
}
fun crashFiles(): Array<File> {
return getJavaCrashDir().listFiles() + getNativeCrashDir().listFiles()
}
}