转载请标明出处: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()
源码
很简单,其实就是调用了 Throwable
的 printStackTrace
方法。
除此之外, 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 {
}
}
}
这样就同时兼容了 bugly
等 crash
捕捉工具。
惊喜
最近看到一个很出名的异常捕获工具,翻了它的源码,发现和我的做法一致,因此可以证明,我的做法没有问题。下面截一个图给大家看看,这个工具是怎么处理的?
扩展设备信息
像 bugly
一样,每一个 crash
日志都会包含设备信息,app
版本号等。其实这个也很简单,把设备信息写入文件就行了,我在 github
上已经完善了,这里就不展示了。
这里展示一个完整的 crash
日志: