Android 打造异常崩溃捕获工具

转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/112476811
本文出自【赵彦军的博客】

前言

因为疫情原因,今年的年会取消了,对于这个年会期待已久,心里还是有点失落。疫情当前,祝福所有人平安。

本文所有代码示例都上传至:https://github.com/zyj1609wz/AndroidCrash

Thread.dumpStack()

打印当前线程调用堆栈, 这个在调试时特别好用,举例如下:

Util.java

public class Util {

    public static void print(){
        Thread.dumpStack();
    }
}

MainActivity.java

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Util.print()
    }
}

效果如下:
在这里插入图片描述
看到这个日志,你想到什么? 肯定是想到崩溃日志,是吧?

线程堆栈日志清晰的标明了当前发生的类及其行号,更重要的是显示了方法调用路径。这个很重要,对于排查问题,调试项目提供了很好的帮助。

下次遇到问题需要调试时,可以试试这个方法,很有用?

如何把线程堆栈日志保存到文件

我们先看看 Thread.dumpStack() 源码

在这里插入图片描述
很简单,其实就是调用了 ThrowableprintStackTrace 方法。
在这里插入图片描述
除此之外, Throwable 还有一个方法,允许外部传入一个 PrintWriter

在这里插入图片描述
完整的代码如下:

package com.cootek.remoteapp;

import android.content.Context;
import android.os.Environment;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Locale;

/**
 * @author yanjun.zhao
 * @time 2021/1/11 4:44 PM
 * @desc
 */
public class Util {

    private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault());

    /**
     * 打印当前线程堆栈,保存到本地文件
     *
     * @param con
     */
    public static void print(Context con) {
        Context context = con.getApplicationContext();
        String fileName = getFileDir(context) + FORMAT.format(System.currentTimeMillis()) + ".txt";
        if (createOrExistsFile(fileName)) {
            PrintWriter pw = null;
            try {
                pw = new PrintWriter(new FileWriter(fileName, false));
                new Exception("Stack trace").printStackTrace(pw);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (pw != null) {
                    pw.close();
                }
            }
        }
    }

    /**
     * 创建文件
     * @param filePath
     * @return
     */
    private static boolean createOrExistsFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            return file.isFile();
        }
        if (!createOrExistsDir(file.getParentFile())) {
            return false;
        }
        try {
            return file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    private static boolean createOrExistsDir(File file) {
        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
    }

    /**
     * 获取堆栈日志存储目录
     *
     * @param context
     * @return
     */
    private static String getFileDir(Context context) {
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                && context.getExternalCacheDir() != null) {
            return context.getExternalCacheDir() + File.separator + "crash" + File.separator;
        } else {
            return context.getCacheDir() + File.separator + "crash" + File.separator;
        }
    }
}

MainActivity 如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Util.print(this)
    }
}

允许起来,看一下效果:

在这里插入图片描述
可以看到文件已经写入了,打开看看 :
在这里插入图片描述
很完美啊 !!!

如何捕捉Crash

没有 try…catch 住的异常,即 Uncaught 异常,都会导致应用程序崩溃。那么面对崩溃,我们是否可以做些什么呢?比如程序退出前,弹出个性化对话框,而不是默认的强制关闭对话框,或者弹出一个提示框安慰一下用户,甚至重启应用程序等。

其实Java提供了一个接口给我们,可以完成这些,这就是 UncaughtExceptionHandler,该接口含有一个纯虚函数:public abstract void uncaughtException (Thread thread, Throwableex)
在这里插入图片描述

Uncaught 异常发生时会终止线程,此时,系统便会通知 UncaughtExceptionHandler ,告诉它被终止的线程以及对应的异常,然后便会调用 uncaughtException 函数。如果该 handler 没有被显式设置,则会调用对应线程组的默认 handler 。如果我们要捕获该异常,必须实现我们自己的handler,并通过以下函数进行设置:

public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)

实现自定义的 handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方法即可。

package com.cootek.remoteapp;

import android.content.Context;
import android.os.Environment;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Locale;

/**
 * @author yanjun.zhao
 * @time 2021/1/11 4:44 PM
 * @desc
 */
public class CrashUtil implements Thread.UncaughtExceptionHandler {

    private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault());
    private Context mContext;
    private static CrashUtil INSTANCE = new CrashUtil();

    /**
     * 保证只有一个CrashHandler实例
     */
    private CrashUtil() {
    }

    /**
     * 获取CrashHandler实例 ,单例模式
     */
    public static CrashUtil getInstance() {
        return INSTANCE;
    }

    public void init(Context context) {
        this.mContext = context.getApplicationContext();
        
        //这一句,至关重要,一定要设置 
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 打印当前线程堆栈,保存到本地文件
     */
    public void print(Throwable throwable) {
        String fileName = getFileDir(mContext) + FORMAT.format(System.currentTimeMillis()) + ".txt";
        if (createOrExistsFile(fileName)) {
            PrintWriter pw = null;
            try {
                pw = new PrintWriter(new FileWriter(fileName, false));
                throwable.printStackTrace(pw);
            } catch (IOException ioException) {

            } finally {
                if (pw != null) {
                    pw.close();
                }
            }
        }
    }

    /**
     * 创建文件
     *
     * @param filePath
     * @return
     */
    private static boolean createOrExistsFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            return file.isFile();
        }
        if (!createOrExistsDir(file.getParentFile())) {
            return false;
        }
        try {
            return file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    private static boolean createOrExistsDir(File file) {
        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
    }

    /**
     * 获取堆栈日志存储目录
     *
     * @param context
     * @return
     */
    private static String getFileDir(Context context) {
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                && context.getExternalCacheDir() != null) {
            return context.getExternalCacheDir() + File.separator + "crash" + File.separator;
        } else {
            return context.getCacheDir() + File.separator + "crash" + File.separator;
        }
    }

    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
        print(e);
    }
}

MyApp 代码:

/**
 * @author yanjun.zhao
 * @time 2021/1/11 7:52 PM
 * @desc
 */
public class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()

        CrashUtil.getInstance().init(this)

    }
}

主要代码,我们就写完了,下面我们来测试一下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<TextView>(R.id.bt).setOnClickListener {
            //制造一个crash
             5 / 0
        }
    }
}

允许起来,点击 button , 人为制造一个 crash ,发现 Android 应用程序没有崩溃,再点开crash 日志目录,发现已经生成了日志,打开看看:

在这里插入图片描述

异常传递

在上面的例子中,我们人为的制造了一个 crash , 并且成功的捕捉了,把 crash 日志写入本地文件。

一个直观的感觉是:app 不会崩溃了。

但是也有一个问题,其他 crash 捕捉器就捕捉不到了,比如 :bugly 。如何才能解决这个问题。

第一步,在 Thread.setDefaultUncaughtExceptionHandler 之前,先获取 defaultUncaughtExceptionHandler

 public void init(Context context) {
        this.mContext = context.getApplicationContext();
        defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
}

在处理异常的地方,先处理自己的逻辑,然后把异常向后传递

@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {

        //优先处理自己的逻辑,把crash日志存起来
        print(e);

        if (defaultUncaughtExceptionHandler != null) {
            //如果原来的 Thread 有自己的 handler , 就把 crash 传递下去,
            //比如:如果集成了bugly , 那就传给bugly 处理
            defaultUncaughtExceptionHandler.uncaughtException(t, e);
        } else {

        }
}

完整的 CrashUtil 类如下:

package com.cootek.remoteapp;

import android.content.Context;
import android.os.Environment;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Locale;

/**
 * @author yanjun.zhao
 * @time 2021/1/11 4:44 PM
 * @desc
 */
public class CrashUtil implements Thread.UncaughtExceptionHandler {

    private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault());
    private Context mContext;
    private static CrashUtil INSTANCE = new CrashUtil();
    private Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    /**
     * 保证只有一个CrashHandler实例
     */
    private CrashUtil() {
    }

    /**
     * 获取CrashHandler实例 ,单例模式
     */
    public static CrashUtil getInstance() {
        return INSTANCE;
    }

    public void init(Context context) {
        this.mContext = context.getApplicationContext();
        defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 打印当前线程堆栈,保存到本地文件
     */
    public void print(Throwable throwable) {
        String fileName = getFileDir(mContext) + FORMAT.format(System.currentTimeMillis()) + ".txt";
        if (createOrExistsFile(fileName)) {
            PrintWriter pw = null;
            try {
                pw = new PrintWriter(new FileWriter(fileName, false));
                throwable.printStackTrace(pw);
            } catch (IOException ioException) {

            } finally {
                if (pw != null) {
                    pw.close();
                }
            }
        }
    }

    /**
     * 创建文件
     *
     * @param filePath
     * @return
     */
    private static boolean createOrExistsFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            return file.isFile();
        }
        if (!createOrExistsDir(file.getParentFile())) {
            return false;
        }
        try {
            return file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    private static boolean createOrExistsDir(File file) {
        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
    }

    /**
     * 获取堆栈日志存储目录
     *
     * @param context
     * @return
     */
    private static String getFileDir(Context context) {
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                && context.getExternalCacheDir() != null) {
            return context.getExternalCacheDir() + File.separator + "crash" + File.separator;
        } else {
            return context.getCacheDir() + File.separator + "crash" + File.separator;
        }
    }

    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {

        //优先处理自己的逻辑,把crash日志存起来
        print(e);

        if (defaultUncaughtExceptionHandler != null) {
            //如果原来的 Thread 有自己的 handler , 就把 crash 传递下去,
            //比如:如果集成了bugly , 那就传给bugly 处理
            defaultUncaughtExceptionHandler.uncaughtException(t, e);
        } else {

        }
    }
}

这样就同时兼容了 buglycrash捕捉工具。

惊喜

最近看到一个很出名的异常捕获工具,翻了它的源码,发现和我的做法一致,因此可以证明,我的做法没有问题。下面截一个图给大家看看,这个工具是怎么处理的?
在这里插入图片描述

扩展设备信息

bugly 一样,每一个 crash日志都会包含设备信息,app 版本号等。其实这个也很简单,把设备信息写入文件就行了,我在 github 上已经完善了,这里就不展示了。

这里展示一个完整的 crash日志:

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值