前言
一般情況下app開發(fā)完成最后一道工序就是日志收集、奔潰信息查找,我們需要根據(jù)打印的錯誤日志來定位,分析,解決錯誤?,F(xiàn)在一般我們會使用市場上一些第三方平臺替我們做了這些事情,比如騰訊的Bugly,和友盟的統(tǒng)計等。但是我們對于一些特殊的app,比如支付、加密、軍工……對數(shù)據(jù)保密性要求嚴(yán)格的應(yīng)用就的自己搞了,接下來我們一起看看怎么搞定它
定位
這些Java的異常類,對于編譯器來說,可以分為兩大類:
unCheckedException(非檢查異常):Error和RuntimeException以及他們各自的子類,都是非檢查異常。換句話說,當(dāng)我們編譯程序的時候,編譯器并不會提示我們這些異常。要么我們在編程的時候,對于可能拋出異常的代碼加上try…catch,要么就等著運行的時候崩潰就好了。
checkedException(檢查異常):除了UncheckedException之外,其他的都是checkedExcption。對于這種異常,我們的代碼通常都無法進(jìn)行編譯,因為as都會提示我們出錯了。這個時候要強制加上try…catch,或者將異常throw。

其實最主要的就是下面這個類:Thread.UncaughtExceptionHandler
這個接口很簡單代碼如下
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* 當(dāng)傳過來的【Thread】因為穿過來的未捕獲的異常而停止時候調(diào)用這個方法。
* 所有被這個方法拋出的異常,都將會被java虛擬機忽略。
*
* @param t the thread
* @param e the exception
*/
void uncaughtException(@RecentlyNonNull Thread var1, @RecentlyNonNull Throwable var2);
}
簡單解釋一下Google 上的描述
/**
* Interface for handlers invoked when a <tt>Thread</tt> abruptly
* terminates due to an uncaught exception.
* 處理接口,當(dāng)一個線程由于未捕獲的異常突然停止的時候調(diào)用。
*
* <p>When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* <tt>UncaughtExceptionHandler</tt> using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* <tt>uncaughtException</tt> method, passing the thread and the
* exception as arguments.
* 當(dāng)一個線程由于一個未捕獲的異常即將崩潰的時候,Java虛擬機將會通過【getUncaughtExceptionHandler()】方法,來
* 查詢這個線程的【UncaughtExceptionHandler】,并且會調(diào)用他的【uncaughtException()】方法,并且把當(dāng)前線程
* 和異常作為參數(shù)傳進(jìn)去。
*
* If a thread has not had its <tt>UncaughtExceptionHandler</tt>
* explicitly set, then its <tt>ThreadGroup</tt> object acts as its
* <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
* has no
* special requirements for dealing with the exception, it can forward
* the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
* default uncaught exception handler}.
*如果一個線程沒有設(shè)置他的【UncaughtExceptionHandler】,那么他的ThreadGroup對象就會作為他的
*【UncaughtExceptionHandler】。如果【ThreadGroup】沒有特殊的處理異常的需求,那么就會轉(zhuǎn)調(diào)
*【getDefaultUncaughtExceptionHandler】這個默認(rèn)的處理異常的handler。
*(線程組的東西我們先不管,我們只需要知道,如果Thread沒有設(shè)置【UncaughtExceptionHandler】的話,那么
*最終會調(diào)用【getDefaultUncaughtExceptionHandler】獲取默認(rèn)的【UncaughtExceptionHandler】來處理異常)
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
所有我們?nèi)绻麅H僅不想讓app崩潰可以直接在application中調(diào)用:
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
//你倒是奔潰呀
}
但這種操作容易被領(lǐng)導(dǎo)優(yōu)化調(diào),接下來我就附上至少被領(lǐng)導(dǎo)優(yōu)化掉之前稍微猶豫一下的代碼
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.PrintWriter
import java.io.StringWriter
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class CrashManager private constructor() : Thread.UncaughtExceptionHandler {
private val TAG = "CrashManager"
private var mDefaultHandler: Thread.UncaughtExceptionHandler? = null
private lateinit var mContext: Context
companion object {
val instance = CrashManager.holder
}
private object CrashManager {
val holder = CrashManager()
}
fun init(context: Context) {
Thread.currentThread().uncaughtExceptionHandler = this
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
mContext = context
}
override fun uncaughtException(p0: Thread, p1: Throwable) {
val crashFileName = saveCatchInfo2File(p1)
Log.e(TAG, "fileName --> $crashFileName")
cacheCrashFile(crashFileName)
mDefaultHandler?.uncaughtException(p0, p1)
}
/**
* 信息保存
*/
private fun saveCatchInfo2File(ex: Throwable): String? {
var fileName: String? = ""
val sb = StringBuffer()
for ((key, value) in obtainSimpleInfo(mContext).entries) {
sb.append(key).append(" = ").append(value).append("\n")
}
sb.append(obtainExceptionInfo(ex))
if (Environment.getExternalStorageState() ==
Environment.MEDIA_MOUNTED
) {
val dir = File(
mContext.filesDir.toString() + File.separator + "crash"
+ File.separator
)
// 先刪除之前的異常信息
if (dir.exists()) {
deleteDir(dir)
}
// 再從新創(chuàng)建文件夾
if (!dir.exists()) {
dir.mkdir()
}
try {
//這里文件名字可以自己根據(jù)項目修改
fileName = (dir.toString()
+ File.separator
+ getAssignTime("yyyy_MM_dd_HH_mm") + ".txt")
val fos = FileOutputStream(fileName)
//把字符串轉(zhuǎn)成字節(jié)數(shù)組
fos.write(sb.toString().toByteArray())
fos.flush()
fos.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
return fileName
}
/**
* 獲取一些簡單的信息,軟件版本,手機版本,型號等信息存放在HashMap中
* 這里缺什么可以自己補全
*/
private fun obtainSimpleInfo(context: Context): HashMap<String, String> {
val map = HashMap<String, String>()
val mPackageManager = context.packageManager
var mPackageInfo: PackageInfo? = null
try {
mPackageInfo = mPackageManager.getPackageInfo(
context.packageName, PackageManager.GET_ACTIVITIES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
map["versionName"] = mPackageInfo?.versionName ?: ""
map["versionCode"] = "" + mPackageInfo?.versionCode
map["MODEL"] = "" + Build.MODEL
map["SDK_INT"] = "" + Build.VERSION.SDK_INT
map["PRODUCT"] = "" + Build.PRODUCT
map["MOBILE_INFO"] = getMobileInfo()
return map
}
/**
* 緩存崩潰日志文件
*/
private fun cacheCrashFile(fileName: String?) {
val sp = mContext.getSharedPreferences("crash", Context.MODE_PRIVATE)
sp.edit().putString("CRASH_FILE_NAME", fileName).apply()
}
/**
* 獲取系統(tǒng)未捕捉的錯誤信息
*/
private fun obtainExceptionInfo(throwable: Throwable): String? {
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
throwable.printStackTrace(printWriter)
printWriter.close()
return stringWriter.toString()
}
/**
* 獲取設(shè)備信息
*
*/
private fun getMobileInfo(): String {
val sb = StringBuffer()
try {
val fields = Build::class.java.declaredFields
for (field in fields) {
field.isAccessible = true
val name = field.name
val value = field[null].toString()
sb.append("$name=$value")
sb.append("\n")
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return sb.toString()
}
/**
* 刪除文件
*/
private fun deleteDir(dir: File): Boolean {
if (dir.isDirectory) {
val iterator = dir.list().iterator()
while (iterator.hasNext()) {
val success = deleteDir(File(dir, iterator.next()))
if (!success) {
return false
}
}
}
// 目錄此時為空,可以刪除
return true
}
/**
* 返回當(dāng)前日期根據(jù)格式
*/
private fun getAssignTime(str: String): String? {
val dataFormat: DateFormat = SimpleDateFormat(str)
val currentTime = System.currentTimeMillis()
return dataFormat.format(currentTime)
}
/**
* 獲取崩潰文件名稱
*
* @return
*/
fun getCrashFile(): File? {
val crashFileName = mContext.getSharedPreferences(
"crash",
Context.MODE_PRIVATE
).getString("CRASH_FILE_NAME", "")
return File(crashFileName)
}
}
調(diào)用方式applicaiton 中:
class App : Application() {
override fun onCreate() {
super.onCreate()
CrashManager.instance.init(this)
}
}
測試:Activity onCreate中調(diào)用:
// 獲取上次的崩潰信息(拿到file你就可以肆意妄為了)
val crashFile = CrashManager.instance.getCrashFile()
// 上傳到服務(wù)器