簡便地Android崩潰日誌收集

我是綠色大米呀發表於2018-07-03

對於已經發布出去的程式,一旦出現崩潰,就很難調查原因。收集崩潰日誌就尤為重要。

像是騰訊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)
            }
複製程式碼

部分移動到上傳結束動作的回撥中,防止上傳動作還沒有結束,程式就被意外終止。

相關文章