對於已經發布出去的程式,一旦出現崩潰,就很難調查原因。收集崩潰日誌就尤為重要。
像是騰訊Bugly就是提供類似的服務,但是我覺得這樣一個很精簡的功能沒有必要再引入別家的服務了。
網上有很多類似功能的程式碼,在程式崩潰的時候把日誌寫入到本地檔案。
我這裡分享一個kotlin版本:
/**
* Created by GreendaMi on 2018/5/3.
*/
class CrashHandler :Thread.UncaughtExceptionHandler {
lateinit var mDefaultHandler: Thread.UncaughtExceptionHandler
lateinit var mContext: Context
// 儲存手機資訊和異常資訊
private val mMessage = HashMap<String,String>()
companion object {
var sInstance: CrashHandler? = null
fun getInstance(): CrashHandler? {
if (sInstance == null) {
synchronized(CrashHandler::class.java) {
if (sInstance == null) {
synchronized(CrashHandler::class.java) {
sInstance = CrashHandler()
}
}
}
}
return sInstance
}
}
override fun uncaughtException(t: Thread?, e: Throwable?) {
if (!handleException(e)) {
// 未經過人為處理,則呼叫系統預設處理異常,彈出系統強制關閉的對話方塊
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(t, e)
}
} else {
// 已經人為處理,系統自己退出
try {
Thread.sleep(1000)
} catch (e1: InterruptedException) {
e1.printStackTrace()
}
//重啟
var intent = mContext?.packageManager?.getLaunchIntentForPackage(mContext?.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
mContext?.startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid())
}
}
/**
* 初始化預設異常捕獲
*
* @param context context
*/
fun init(context: Context) {
mContext = context
// 獲取預設異常處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
// 將此類設為預設異常處理器
Thread.setDefaultUncaughtExceptionHandler(this)
}
/**
* 是否人為捕獲異常
*
* @param e Throwable
* @return true:已處理 false:未處理
*/
private fun handleException(e: Throwable?): Boolean {
if (e == null) {// 異常是否為空
return false
}
object : Thread() {
// 在主執行緒中彈出提示
override fun run() {
Looper.prepare()
Toast.makeText(mContext, "程式發生未知異常,將重啟。", Toast.LENGTH_SHORT).show()
Looper.loop()
}
}.start()
collectErrorMessages()
saveErrorMessages(e)
return false
}
private fun collectErrorMessages() {
val pm = mContext?.packageManager
try {
val pi = pm?.getPackageInfo(mContext?.packageName, PackageManager.GET_ACTIVITIES)
if (pi != null) {
val versionName = if (TextUtils.isEmpty(pi.versionName)) "null" else pi.versionName
val versionCode = "" + pi.versionCode
mMessage["versionName"] = versionName
mMessage["versionCode"] = versionCode
}
// 通過反射拿到錯誤資訊
val fields = Build::class.java!!.fields
if (fields != null && fields.isNotEmpty()) {
for (field in fields!!) {
field.isAccessible = true
try {
mMessage[field.name] = field.get(null).toString()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
private fun saveErrorMessages(e: Throwable) {
val sb = StringBuilder()
for (entry in mMessage) {
val key = entry.key
val value = entry.value
sb.append(key).append("=").append(value).append("\n")
}
val writer = StringWriter()
val pw = PrintWriter(writer)
e.printStackTrace(pw)
var cause: Throwable? = e.cause
// 迴圈取出Cause
if (cause != null) {
cause.printStackTrace(pw)
}
pw.close()
val result = writer.toString()
sb.append(result)
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(Date())
val fileName = "crash-" + time + "-" + System.currentTimeMillis() + ".log"
// 有無SD卡
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
val path = AppConfig.crashPath
val dir = File(path)
if (!dir.exists()) dir.mkdirs()
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(path + fileName)
fos!!.write(sb.toString().toByteArray())
} catch (e1: Exception) {
e1.printStackTrace()
} finally {
if (fos != null) {
try {
fos!!.close()
} catch (e1: IOException) {
e1.printStackTrace()
}
}
}
}
}
}
複製程式碼
在Application裡面初始化一下:
//異常收集
CrashHandler.getInstance()?.init(this@App)
複製程式碼
溫馨提示:這裡的寫入檔案都是同步的,如果需要將錯誤日誌資訊上傳到伺服器,因為一般這種網路通訊都是非同步的,所以需要將
if (!handleException(e)) {
// 未經過人為處理,則呼叫系統預設處理異常,彈出系統強制關閉的對話方塊
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(t, e)
}
}
複製程式碼
中的
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(t, e)
}
複製程式碼
部分移動到上傳結束動作的回撥中,防止上傳動作還沒有結束,程式就被意外終止。