Android 保存崩潰日志

在Android應(yīng)用上線后趾撵,或多或少地都會出現(xiàn)各種問題,尤其是應(yīng)用崩潰最讓人崩潰施籍,如果前期沒有做好異常的捕獲居扒、崩潰日志的保存和上傳的功能,那就很難定位到Bug的位置丑慎,久而久之喜喂,程序猿的頭發(fā)又更少了...


要實現(xiàn)崩潰日志工具類,主要是要考慮兩個方面的功能:

  • 在出現(xiàn)崩潰時保存錯誤信息到日志文件
  • 在某一時段上傳錯誤日志(考慮到用戶體驗竿裂,所以放在下次打開應(yīng)用時自動上傳)

然后考慮到保存和上傳的時機玉吁,大概的流程圖應(yīng)該就是這樣:


崩潰日志流程圖
1. 保存日志文件

所謂的崩潰都是由于Exception(常見)和Error(不常見)引起的。眾所周知:ExceptionError的父類都是Throwable腻异,所以只要在報錯的位置捕獲到Throwable进副,然后輸出日志到文件即可。

  • 這里有個問題悔常,如何在不知道報錯的位置情況下捕獲到日志呢影斑?這里就要用到Thread.UncaughtExceptionHandler接口和Thread.setDefaultUncaughtExceptionHandler()方法了给赞。

Thread.UncaughtExceptionHandler的官網(wǎng)解釋是:當(dāng)線程由于未捕獲的異常突然終止時調(diào)用的處理器的接口。

當(dāng)線程由于未捕獲的異常而即將終止時矫户,Java虛擬機將使用Thread.getUncaughtExceptionHandler()在線程中查詢其UncaughtExceptionHandler并將調(diào)用處理程序的uncaughtException()方法片迅,將線程和異常作為該方法的參數(shù)傳遞。如果未顯式設(shè)置線程的UncaughtExceptionHandler皆辽,則其ThreadGroup對象將充當(dāng)其UncaughtExceptionHandler障涯。如果ThreadGroup對象對處理異常沒有特殊要求,則可以將調(diào)用轉(zhuǎn)發(fā)到默認(rèn)的未捕獲異常處理器膳汪。
ThreadGroup:顧名思義就是線程所在的線程組唯蝶,詳細(xì)可以點擊查看。

Thread.setDefaultUncaughtExceptionHandler()官網(wǎng)解釋是:設(shè)置默認(rèn)的異常處理器的全局靜態(tài)方法遗嗽,傳入的必須是Thread.UncaughtExceptionHandler的實現(xiàn)類粘我。

未捕獲的異常處理首先由線程控制,然后由線程的ThreadGroup對象控制痹换,最后由默認(rèn)的未捕獲的異常處理器控制征字。如果線程沒有設(shè)置顯式的未捕獲異常處理器,并且線程的線程組(包括父線程組)未專門設(shè)置其uncaughtException()方法娇豫,則將調(diào)用默認(rèn)處理器的uncaughtException()方法匙姜。
通過設(shè)置默認(rèn)的未捕獲異常處理器,應(yīng)用程序可以更改那些已經(jīng)接受系統(tǒng)提供的“默認(rèn)”行為的線程的未捕獲異常處理方式(例如冯痢,記錄到特定設(shè)備或文件)氮昧。
請注意,默認(rèn)的未捕獲異常處理器通常不應(yīng)遵從線程的ThreadGroup對象浦楣,因為這可能導(dǎo)致無限遞歸袖肥。

這樣,全局捕獲異常的問題算是解決了振劳,接下來新建工具類椎组,實現(xiàn)Thread.UncaughtExceptionHandler接口,這里通過lazy延遲屬性历恐,使用雙重校驗鎖實現(xiàn)單例寸癌。

class CrashHandler : Thread.UncaughtExceptionHandler {
    companion object {
        //雙重校驗鎖實現(xiàn)單例
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler()
        }
    }

    fun init(context: Context) {
        // 設(shè)置CrashHandler為應(yīng)用的默認(rèn)異常處理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
          //在此中解析exception
    }
}

這里如何獲取Throwable中的信息呢?答案是用StringWriterPrintWriter弱贼,在Throwable實例的printStackTrace()方法中獲取到堆棧信息蒸苇。

private fun getExceptionInfo(exception: Throwable?): String {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception?.printStackTrace(pw)
        return sw.toString()
    }

報錯日志拿到了,但是不能夠去影響到系統(tǒng)處理異常哮洽,該報錯還是得報錯填渠,所以在設(shè)置默認(rèn)異常處理器前要通過Thread.getDefaultUncaughtExceptionHandler()方法獲取原來的系統(tǒng)默認(rèn)處理器,并在保存文件之后,將異常信息原封不動地傳給原來的系統(tǒng)默認(rèn)處理器氛什。

    private var mDefaultCrashHandler: Thread.UncaughtExceptionHandler? = null

    fun init(context: Context) {
        //注意要在設(shè)置前獲取
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
        // 設(shè)置CrashHandler為應(yīng)用的默認(rèn)異常處理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
         //在此中解析exception莺葫,保存日志文件,可開啟子線程寫入文件或者使用kotlin的協(xié)程
        
        // 系統(tǒng)默認(rèn)處理
        mDefaultCrashHandler?.uncaughtException(thread, exception)
    }

至此枪眉,基本的崩潰日志保存就完成了捺檬。什么?怎么保存到文件贸铜?直接新建文件夾堡纬,寫入獲取到的堆棧信息到文件,再詳細(xì)的話歡迎百度蒿秦。

2. 上傳日志文件

上傳日志文件這個其實不用多說烤镐,用Okhttp或者Retrofit就完事了。
這里主要是考慮上傳文件的時機棍鳖,如果在應(yīng)用崩潰時保存文件并上傳炮叶,而且可能等待日志是否上傳成功,在這種情況下會導(dǎo)致應(yīng)用無法操作卡頓后一段時間才崩潰渡处,這樣肯定是不行的镜悉,所以上傳日志文件放在初始化時上傳比較好。

3. 日志信息的完善和可自定義

要完善崩潰日志工具類医瘫,可能就以下幾點:

  • 增加手機基本信息
  • 可控制的日志文件數(shù)量
  • 文件存儲的位置

獲取手機信息然后加入日志文件中侣肄,能了解到更多相關(guān)信息。日志文件數(shù)量可以調(diào)節(jié)醇份,為0時不保存錯誤日志稼锅。自定義錯誤日志保存的目錄,方便自測時查看被芳。然后缰贝,大概就是下面這樣子:

/**
 * 崩潰日志處理類
 * @author JPlus
 * @date 2019/3/14.
 */

class CrashHandler : Thread.UncaughtExceptionHandler {
    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler()
        }
    }

    private var mDefaultCrashHandler: Thread.UncaughtExceptionHandler? = null
    private var mContext: Context? = null
    private var mDirPath: String? = null
    private var mMaxNum = 0
    /**
     * 初始化
     * @param context 上下文
     * @param maxNum 最大保存文件數(shù)量,默認(rèn)為1
     * @param dir 存儲文件的目錄畔濒,默認(rèn)為應(yīng)用私有文件夾下crash目錄
     */
    fun init(context: Context, maxNum: Int = 1, dir: String = FileUtils.writePrivateDir("crash", context).absolutePath) {
        mContext = context
        mDirPath = dir
        mMaxNum = maxNum
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    /**
     * 獲取最新崩潰日志
     * @return 最新文件
     */
    fun getNewFile(): File? {
        //篩選出最近最新的一次崩潰日志
        return FileUtils.getDirFiles(File(mDirPath))?.let {
            if (it.size>0) it.reversed()[0] else null
        }
    }

    private fun writeNewFile(path: String, name: String, body: String) {
        FileUtils.getDirFiles(File(mDirPath))?.let {
            if (it.size >= mMaxNum) {
                //大于設(shè)置的數(shù)量則刪除最舊文件
                FileUtils.delFileOrDir(it.sorted()[0])
            }
            //繼續(xù)存崩潰日志,新線程寫入文件
            GlobalScope.launch{
                FileUtils.writeFile(File(path, name), body, false)
            }
        }
    }

    /**
     * 當(dāng)系統(tǒng)中有未被捕獲的異常锣咒,系統(tǒng)將會自動調(diào)用 uncaughtException 方法
     * @param thread
     * @param exception
     */
    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
        val name = AppUtils.instance.getDeviceImei(mContext!!) + "_" + DateUtils.getDateTimeByMillis(false).replace(":", "-")
        val exceptionInfo = StringBuilder(name + "\n\n" + getSysInfo() + "\n\n" + exception?.message)
        exceptionInfo.append("\n" + getExceptionInfo(exception))
        mDirPath?.let {
            if (mMaxNum > 0) {
                writeNewFile(it, "$name.log", exceptionInfo.toString())
            }
        }
        // 系統(tǒng)默認(rèn)處理
        mDefaultCrashHandler?.uncaughtException(thread, exception)
    }

    private fun getSysInfo(): String {
        val map = hashMapOf<String, String>()
        map["versionName"] = AppUtils.instance.getAppVersionName(mContext)
        map["versionCode"] = "" + AppUtils.instance.getAppVersionCode(mContext)
        map["androidApi"] = "" + AppUtils.instance.getOsLevel()
        map["product"] = "" + AppUtils.instance.getDeviceProduct()
        map["mobileInfo"] = AppUtils.instance.getDeviceInfo()
        map["cpuABI"] = AppUtils.instance.getCpuABI()
        val str = StringBuilder("=".repeat(10) + "PhoneInfo" + "=".repeat(10) + "\n")
        for (item in map) {
            str.append(item.key).append(" = ").append(item.value).append("\n")
        }
        str.append("=".repeat(10) + "=".repeat(10) + "\n")
        return str.toString()
    }

    private fun getExceptionInfo(exception: Throwable?): String {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception?.printStackTrace(pw)
        return sw.toString()
    }

}

至此侵状,一個簡單的崩潰日志工具類實現(xiàn)了,可能或多或少有待改進(jìn)的地方毅整,歡迎批評指正趣兄。

完整項目地址:baselibrary/CrashHandler

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市悼嫉,隨后出現(xiàn)的幾起案子艇潭,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹋凝,死亡現(xiàn)場離奇詭異鲁纠,居然都是意外死亡,警方通過查閱死者的電腦和手機鳍寂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門改含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人迄汛,你說我怎么就攤上這事捍壤。” “怎么了鞍爱?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵鹃觉,是天一觀的道長。 經(jīng)常有香客問我睹逃,道長盗扇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任唯卖,我火速辦了婚禮粱玲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拜轨。我一直安慰自己抽减,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布橄碾。 她就那樣靜靜地躺著卵沉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪法牲。 梳的紋絲不亂的頭發(fā)上史汗,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音拒垃,去河邊找鬼停撞。 笑死,一個胖子當(dāng)著我的面吹牛悼瓮,可吹牛的內(nèi)容都是我干的戈毒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼横堡,長吁一口氣:“原來是場噩夢啊……” “哼埋市!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起命贴,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤道宅,失蹤者是張志新(化名)和其女友劉穎食听,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體污茵,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡樱报,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了省咨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肃弟。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖零蓉,靈堂內(nèi)的尸體忽然破棺而出笤受,到底是詐尸還是另有隱情,我是刑警寧澤敌蜂,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布箩兽,位于F島的核電站,受9級特大地震影響章喉,放射性物質(zhì)發(fā)生泄漏汗贫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一秸脱、第九天 我趴在偏房一處隱蔽的房頂上張望落包。 院中可真熱鬧,春花似錦摊唇、人聲如沸咐蝇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽有序。三九已至,卻和暖如春岛请,著一層夾襖步出監(jiān)牢的瞬間旭寿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工崇败, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盅称,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓后室,卻偏偏與公主長得像微渠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咧擂,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345