android 崩溃日志收集以及上传服务器

前言

一般情况下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.
         * <p>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 <tt>Thread</tt> abruptly
* terminates due to an uncaught exception.
* 处理接口,当一个线程由于未捕获的异常突然停止的时候调用。
*
* <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.
* 当一个线程由于一个未捕获的异常即将崩溃的时候,Java虚拟机将会通过【getUncaughtExceptionHandler()】方法,来
* 查询这个线程的【UncaughtExceptionHandler】,并且会调用他的【uncaughtException()】方法,并且把当前线程
* 和异常作为参数传进去。
*
* 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}.
*如果一个线程没有设置他的【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<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()
    }

    /**
     * 获取系统未捕捉的错误信息
     */
    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()
        // 上传到服务器
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容