前言
一般情况下app开发完成最后一道工序就是日志收集、奔溃信息查找,我们需要根据打印的错误日志来定位,分析,解决错误。现在一般我们会使用市场上一些第三方平台替我们做了这些事情,比如腾讯的Bugly,和友盟的统计等。但是我们对于一些特殊的app,比如支付、加密、军工……对数据保密性要求严格的应用就的自己搞了,接下来我们一起看看怎么搞定它
定位
这些Java的异常类,对于编译器来说,可以分为两大类:
unCheckedException(非检查异常):Error和RuntimeException以及他们各自的子类,都是非检查异常。换句话说,当我们编译程序的时候,编译器并不会提示我们这些异常。要么我们在编程的时候,对于可能抛出异常的代码加上try…catch,要么就等着运行的时候崩溃就好了。
checkedException(检查异常):除了UncheckedException之外,其他的都是checkedExcption。对于这种异常,我们的代码通常都无法进行编译,因为as都会提示我们出错了。这个时候要强制加上try…catch,或者将异常throw。
其实最主要的就是下面这个类:Thread.UncaughtExceptionHandler
这个接口很简单代码如下
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* 当传过来的【Thread】因为穿过来的未捕获的异常而停止时候调用这个方法。
* 所有被这个方法抛出的异常,都将会被java虚拟机忽略。
*
* @param t the thread
* @param e the exception
*/
void uncaughtException(@RecentlyNonNull Thread var1, @RecentlyNonNull Throwable var2);
}
简单解释一下Google 上的描述
/**
* Interface for handlers invoked when a Thread abruptly
* terminates due to an uncaught exception.
* 处理接口,当一个线程由于未捕获的异常突然停止的时候调用。
*
*When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* UncaughtExceptionHandler using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* uncaughtException method, passing the thread and the
* exception as arguments.
* 当一个线程由于一个未捕获的异常即将崩溃的时候,Java虚拟机将会通过【getUncaughtExceptionHandler()】方法,来
* 查询这个线程的【UncaughtExceptionHandler】,并且会调用他的【uncaughtException()】方法,并且把当前线程
* 和异常作为参数传进去。
*
* If a thread has not had its UncaughtExceptionHandler
* explicitly set, then its ThreadGroup object acts as its
* UncaughtExceptionHandler. If the ThreadGroup object
* has no
* special requirements for dealing with the exception, it can forward
* the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
* default uncaught exception handler}.
*如果一个线程没有设置他的【UncaughtExceptionHandler】,那么他的ThreadGroup对象就会作为他的
*【UncaughtExceptionHandler】。如果【ThreadGroup】没有特殊的处理异常的需求,那么就会转调
*【getDefaultUncaughtExceptionHandler】这个默认的处理异常的handler。
*(线程组的东西我们先不管,我们只需要知道,如果Thread没有设置【UncaughtExceptionHandler】的话,那么
*最终会调用【getDefaultUncaughtExceptionHandler】获取默认的【UncaughtExceptionHandler】来处理异常)
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
所有我们如果仅仅不想让app崩溃可以直接在application中调用:
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
//你倒是奔溃呀
}
但这种操作容易被领导优化调,接下来我就附上至少被领导优化掉之前稍微犹豫一下的代码
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)
}
// 再从新创建文件夹
if (!dir.exists()) {
dir.mkdir()
}
try {
//这里文件名字可以自己根据项目修改
fileName = (dir.toString()
+ File.separator
+ getAssignTime("yyyy_MM_dd_HH_mm") + ".txt")
val fos = FileOutputStream(fileName)
//把字符串转成字节数组
fos.write(sb.toString().toByteArray())
fos.flush()
fos.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
return fileName
}
/**
* 获取一些简单的信息,软件版本,手机版本,型号等信息存放在HashMap中
* 这里缺什么可以自己补全
*/
private fun obtainSimpleInfo(context: Context): HashMap {
val map = HashMap()
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()
}
/**
* 获取系统未捕捉的错误信息
*/
private fun obtainExceptionInfo(throwable: Throwable): String? {
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
throwable.printStackTrace(printWriter)
printWriter.close()
return stringWriter.toString()
}
/**
* 获取设备信息
*
*/
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
}
/**
* 返回当前日期根据格式
*/
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)
}
}
调用方式applicaiton 中:
class App : Application() {
override fun onCreate() {
super.onCreate()
CrashManager.instance.init(this)
}
}
测试:Activity onCreate中调用:
// 获取上次的崩溃信息(拿到file你就可以肆意妄为了)
val crashFile = CrashManager.instance.getCrashFile()
// 上传到服务器