在Android應(yīng)用上線后趾撵,或多或少地都會出現(xiàn)各種問題,尤其是應(yīng)用崩潰最讓人崩潰施籍,如果前期沒有做好異常的捕獲居扒、崩潰日志的保存和上傳的功能,那就很難定位到Bug的位置丑慎,久而久之喜喂,程序猿的頭發(fā)又更少了...
要實現(xiàn)崩潰日志工具類,主要是要考慮兩個方面的功能:
- 在出現(xiàn)崩潰時保存錯誤信息到日志文件
- 在某一時段上傳錯誤日志(考慮到用戶體驗竿裂,所以放在下次打開應(yīng)用時自動上傳)
然后考慮到保存和上傳的時機玉吁,大概的流程圖應(yīng)該就是這樣:
1. 保存日志文件
所謂的崩潰都是由于Exception
(常見)和Error
(不常見)引起的。眾所周知:Exception
和Error
的父類都是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
中的信息呢?答案是用StringWriter
和PrintWriter
弱贼,在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