当APP发生Crash后虽然无法让其再继续运行,但能直到程序crash的原因,就可以修复错误(比如热修复)。当APP上线后,APP发生了crash,这个crash信息是很难被获取到的。而通过CrahHandler来监视应用的crash信息,给程序设置一个CrashHandler,这样当程序crash的时候就会调用CrahHandler的uncaughtException(),在这个方法中可以获取crash信息并上传到服务器或保留到手机的SD卡中,这种方式后台就可监控程序的运行状况并进行后续的修复。
发生crash的原因可能是系统底层的BUG,可能是机型适配,可能是网络状况等。当crash发生时,系统会kill掉正在执行的程序,造成常见的“闪退”。
Android提供了处理这类问题的方法:(Thread类中[xx:\AndroidSDK\sources\android-28\java\lang\Thread.java])
根据注释得知可以设置系统的默认异常处理器。这个方法就可解决线上的app的crash问题.
当Android发生carsh的时候,系统就会回调uncaughtException(),在此方法中就可以获取到异常信息,然后剩下的至于怎么处理它就是开发人员的事情了,比如:可把异常信息存储到SD卡中,当有网络的时候发送到服务器上,然后在新版本中去修复它,甚至可以在crash的时候给用户一个对话框告知程序crash了。
1.实现一个UncaughtExceptionHandler对象,在该对象的uncaughtException()中获取异常信息并将其存储到SD卡中或者上传到服务器,然后调用Thread的setDefaultUncaughtExceptionHandler()将它设置为线程默认的异常处理器。因为默认的异常处理器是Thread类的静态成员,它的作用对象是当前进程的所有线程。
package com.yinlei.crashhandler
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.util.Log
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.text.SimpleDateFormat
import java.util.*
import kotlin.contracts.contract
/**
* 没用kotlon中的单例
*/
class CrashHandler private constructor() : Thread.UncaughtExceptionHandler{
private lateinit var mDefaultCrashHandler: Thread.UncaughtExceptionHandler
private lateinit var mContext: Context
companion object{
val TAG ="yinlei"
private val DEBUG = true
private val PATH = "${Environment.getExternalStorageDirectory().path}/CrashTest/log/"
private const val FILE_NAME = "crash"
private const val FILE_NAME_SUFFIX = ".trace"
private var sInstance = CrashHandler()
fun getInstance(): CrashHandler = sInstance
}
/**
* 初始化工作准备
*/
fun initWork(context: Context){
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
mContext = context
Log.d(TAG,"当前的FIle路径为:$PATH")
}
/**
* 当APP中有未被捕获的异常,系统将会自动调用此方法
* @param t 为出现未捕获异常的线程
* @param e 为未捕获的异常,通过这个e就可以得到异常信息
*/
override fun uncaughtException(t: Thread?, e: Throwable?) {
try{
//导出异常信息到SD卡中
dumpExceptionToSDCard(e)
Log.d(TAG,"我进入了uncaughtException方法" )
//上传异常信息到服务器便于开发者分析日志解决bug等。。。【开发者自己处理】
}catch (e: Exception){
e.printStackTrace()
}
}
/***
* 导出异常信息到SDCard中
* @param e 为未捕获的异常,通过这个e就可以得到异常信息
*/
private fun dumpExceptionToSDCard(e: Throwable?) {
Log.d(TAG,"我进入了dumpExceptionToSDCard方法" )
//判断SDCard是否挂载
if(Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED){//没挂载SD卡
if(DEBUG){
Log.d(TAG,"SD卡未挂载,跳过导出异常信息到SD卡中的操作。")
return
}
}
//挂载SD了的..
val dir = File(PATH)
if(!dir.exists()){
dir.mkdirs()
}
val current = System.currentTimeMillis()
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date(current))
val file = File(PATH+ FILE_NAME+time+ FILE_NAME_SUFFIX)
Log.d(TAG,"文件位于:${file.absolutePath}" )
try{
PrintWriter(BufferedWriter(FileWriter(file))).use {
it.println(time)
dumpPhoneInfo(it)
it.println()
e!!.printStackTrace(it)
}
}catch (ex: Exception){
Log.d(TAG,"导出异常信息失败")
ex.printStackTrace()
}
}
/**
* 导出手机信息
*/
private fun dumpPhoneInfo(pw: PrintWriter) {
var pm = mContext.packageManager
var pi = pm.getPackageInfo(mContext.packageName,PackageManager.GET_ACTIVITIES)
pw.print("APP版本:")
pw.print(pi.versionName)
pw.println("_")
pw.println(pi.versionCode)
pw.print("Android版本号:")
pw.print(Build.VERSION.RELEASE)
pw.println("_")
pw.print(Build.VERSION.SDK_INT)
pw.print("手机制造商(Vendor): ")
pw.println(Build.MANUFACTURER)
pw.print("手机型号(Model): ")
pw.println(Build.MODEL)
pw.print("CPU架构:")
pw.println(Build.CPU_ABI)
}
}
当应用崩溃的时候,上述代码的作用是将异常信息和设备信息写到SD卡中,接着异常交给系统处理,系统会帮助我们终止程序,如果系统没有默认的异常处理机制,那么就自行终止。
2.使用编写的自定义CrashHandler:Application初始化的时候为线程设置CrashHandler
package com.yinlei.crashhandler
import android.app.Application
/**
* 此处kotlin写创建kt文件的时候要类名首字母大写,才有class + 类名 的提示
*/
class MyApplication : Application(){
companion object{
private lateinit var sInstance: MyApplication
fun getInstance(): MyApplication = sInstance
}
override fun onCreate() {
super.onCreate()
sInstance = this
//在这里为应用设置异常处理,然后程序才能获取未处理的异常
var crashHandler = CrashHandler.getInstance()
crashHandler.initWork(this)
}
}
3.自己模拟一个异常出来看看程序是否能线上捕获到[此处模拟通过在SD卡是否能看到异常文件即可判断是否有效代码]
注意Android 6.0动态权限适配:
package com.yinlei.crashhandler
import android.Manifest
import android.content.pm.PackageManager
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import com.yinlei.crashhandler.CrashHandler.Companion.FILE_NAME
import com.yinlei.crashhandler.CrashHandler.Companion.FILE_NAME_SUFFIX
import com.yinlei.crashhandler.CrashHandler.Companion.PATH
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
checkAndroidPermission()
testTryCatch()
ProduceException.setOnClickListener {
//模拟异常抛出情况,抛出一个运行时异常
throw RuntimeException("我是一个运行时异常!")
}
}
/**
* 危险权限适配
*/
private fun checkAndroidPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
0)
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
0)
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
// Permission has already been granted
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
when (requestCode) {
0 -> {
// If request is cancelled, the result arrays are empty.
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
Log.d(CrashHandler.TAG,"授权成功")
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
Log.d(CrashHandler.TAG,"权限被否决了")
}
return
}
// Add other 'when' lines to check for other
// permissions this app might request.
else -> {
// Ignore all other requests.
Log.d(CrashHandler.TAG,"检测到其他权限")
}
}
}
private fun testTryCatch(){
try{
thread {
try{
var result: CharSequence = (3/0).toString()
ShowResult.append(result)
}catch (e: Exception){
e.printStackTrace()
}
/**
* 当然kotlin提供了空安全机制,采用如下代码就可以避免空指针异常,并将判空交给开发者自己去处理
*/
// var file1: File? = null
// file1!!.absolutePath
}
}catch (e: Exception){
e.printStackTrace()
}
}
}
4.验证:[这里我打包给了真机运行,模拟了线上环境]
运行APP点击按钮抛出运行时异常,然后在手机文件管理器中查找或者搜索CrashTest,可以发现如下: