应用场景,我们经常遇到这样的情况,有些bug是偶现的,实际遇到的时候可能来不及获取log,那么我们可以自己写个异常捕获的类,将捕获的异常输出到文件,当然最好能上传到服务器,因为我们并不一定能拿到用户的机器。
这次我们先简单处理,将文件保存在本地。
大致思路
1.构建自己的异常捕获类
2.获取崩溃信息 手机版本号 apk版本信息 利用StringWriter StringBuilder等辅助类进行拼装
3.在内部存储创建文件 存储上面的那些信息(为避免应用产生的文件吃内存,在创建之前会判断当前是否存在crash文件夹,如果存在 删除并重新创建,这样始终只会有一个crash文件)
4.在Application初始化异常捕获类
实现过程
1.构建自己的异常捕获类
public class ExceptionCrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "ExceptionCrashHandler";
private static ExceptionCrashHandler mInstance;
// 留下原来异常处理器的引用,便于开发的时候调试
private Thread.UncaughtExceptionHandler mDefaultHandler;
// 上下文 用于获取版本信息和手机信息
private WeakReference<Context> mContextRef;
public static ExceptionCrashHandler getInstance() {
if (mInstance == null) {
synchronized (ExceptionCrashHandler.class) {
if (mInstance == null) {
mInstance = new ExceptionCrashHandler();
}
}
}
return mInstance;
}
public void init(Context context) {
Log.e(TAG, " ExceptionCrashHandler init ");
/*
* 官方解释
* Set the handler invoked when this thread abruptly terminates
* due to an uncaught exception.
*
* 当前线程由于未捕获异常而终止的时候 调用当前的handler处理异常
**/
Thread.currentThread().setUncaughtExceptionHandler(this);
// 获取系统默认的UncaughtException处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
this.mContextRef = new WeakReference<>(context);
}
private ExceptionCrashHandler() {
}
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
Log.e(TAG, "拦截到闪退信息" + e.getMessage());
// 1. 获取信息
// 1.1 崩溃信息
// 1.2 手机信息
// 1.3 版本信息
// 2.写入文件
String crashFileName = saveInfoToCache(e);
Log.e(TAG, "fileName --> " + crashFileName);
// 让系统默认处理 否则应用发生crash后没有任何反应
mDefaultHandler.uncaughtException(t, e);
}
/**
* 得到获取的 软件信息,设备信息和出错信息
* 保存在机身内存
*/
private String saveInfoToCache(Throwable ex) {
String fileName = null;
StringBuilder stringBuffer = new StringBuilder();
//遍历手机信息 格式化后输出
for (Map.Entry<String, String> entry : obtainSimpleInfo(mContextRef.get())
.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
stringBuffer.append(key).append(" = ").append(value).append("\n");
}
stringBuffer.append(obtainExceptionInfo(ex));
//这里原课程有问题 为什么获取外部存储的可用状态 最后存储在内部存储 因此我注释掉了最外层的判断
//if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) {
// 获取crash文件夹
File dir = new File(mContextRef.get().getFilesDir() + File.separator + "crash"
+ File.separator);
// 先删除之前的异常信息
if (dir.exists()) {
deleteDir(dir);
}
// 再重新创建文件夹
if (!dir.exists()) {
boolean res = dir.mkdir();
if (!res) {
Log.e(TAG, "saveInfoToSD: create dir failed ");
return "";
}
}
// 创建文件
try {
fileName = dir.toString()
+ File.separator
+ getAssignTime() + ".txt";
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(stringBuffer.toString().getBytes());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
//}
return fileName;
}
/**
* 返回当前日期根据格式
**/
private String getAssignTime() {
DateFormat dataFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.US);
long currentTime = System.currentTimeMillis();
return dataFormat.format(currentTime);
}
public static void deleteDir(File file) {
boolean res;
if (file.isFile()) {
res = file.delete();
if (!res){
Log.e(TAG, "deleteDir: failed when delete "+ file);
}
return;
}
if (file.isDirectory()) {
File[] childFile = file.listFiles();
if (childFile == null || childFile.length == 0) {
res = file.delete();
if (!res){
Log.e(TAG, "deleteDir: failed when delete "+ file);
}
return;
}
for (File f : childFile) {
deleteDir(f);
}
res = file.delete();
if (!res){
Log.e(TAG, "deleteDir: failed when delete "+ file);
}
}
}
/**
* 获取系统未捕捉的错误信息 将其转化为字符串
*/
private String obtainExceptionInfo(Throwable throwable) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
throwable.printStackTrace(printWriter);
printWriter.close();
return stringWriter.toString();
}
/**
* 获取一些简单的信息,软件版本,手机版本,型号等信息存放在HashMap中
*
*/
private HashMap<String, String> obtainSimpleInfo(Context context) {
HashMap<String, String> map = new HashMap<>();
PackageManager mPackageManager = context.getPackageManager();
PackageInfo mPackageInfo = null;
try {
mPackageInfo = mPackageManager.getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
map.put("versionName", mPackageInfo.versionName);
map.put("versionCode", "" + mPackageInfo.versionCode);
map.put("MODEL", "" + Build.MODEL);
map.put("SDK_INT", "" + Build.VERSION.SDK_INT);
map.put("PRODUCT", "" + Build.PRODUCT);
map.put("MOBLE_INFO", getMobileInfo());
return map;
}
/**
* 获取机器信息
*/
public static String getMobileInfo() {
StringBuilder stringBuilder = new StringBuilder();
try {
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
String value = field.get(null).toString();
stringBuilder.append(name).append("=").append(value).append("\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
}
2.BaseApplication相关处理
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化异常处理类
ExceptionCrashHandler.getInstance().init(this);
}
}
在清单文件配置BaseApplication
android:name=".BaseApplication"
3.在activity中模拟异常
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv:
int x = 2 / 0;
Toast.makeText(this, "text view clicked" + x, Toast.LENGTH_SHORT).show();
break;
case R.id.btn:
Toast.makeText(this, "button clicked", Toast.LENGTH_SHORT).show();
break;
}
}
当crash发生时会有log和文件产生:
2021-02-19 10:28:51.544 13270-13270/com.example.learneassyjoke E/ExceptionCrashHandler: ExceptionCrashHandler init
2021-02-19 10:28:53.324 13270-13270/com.example.learneassyjoke E/ExceptionCrashHandler: 拦截到闪退信息divide by zero
2021-02-19 10:28:53.344 13270-13270/com.example.learneassyjoke E/ExceptionCrashHandler: fileName --> /data/user/0/com.example.learneassyjoke/files/crash/2021_02_19_10_28.txt
generic_x86:/data/user/0/com.example.learneassyjoke/files # ls -l
total 4
drwx------ 2 u0_a81 u0_a81 4096 2021-02-19 10:28 crash
generic_x86:/data/user/0/com.example.learneassyjoke/files # cd crash/
generic_x86:/data/user/0/com.example.learneassyjoke/files/crash # ls
2021_02_19_10_28.txt
generic_x86:/data/user/0/com.example.learneassyjoke/files/crash # cat 2021_02_19_10_28.txt
SDK_INT = 27
MOBLE_INFO = BOARD=unknown
BOOTLOADER=unknown
BRAND=google
CPU_ABI=x86
CPU_ABI2=
DEVICE=generic_x86
DISPLAY=sdk_gphone_x86-userdebug 8.1.0 OSM1.180201.037 6739391 dev-keys
FINGERPRINT=google/sdk_gphone_x86/generic_x86:8.1.0/OSM1.180201.037/6739391:userdebug/dev-keys
HARDWARE=ranchu
HOST=abfarm-01134
ID=OSM1.180201.037
IS_CONTAINER=false
IS_DEBUGGABLE=true
IS_EMULATOR=true
IS_ENG=false
IS_TREBLE_ENABLED=true
IS_USER=false
IS_USERDEBUG=true
MANUFACTURER=Google
MODEL=Android SDK built for x86
PERMISSIONS_REVIEW_REQUIRED=false
PRODUCT=sdk_gphone_x86
RADIO=unknown
SERIAL=EMULATOR30X3X5X0
SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@f54b11
SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@5765476
SUPPORTED_ABIS=[Ljava.lang.String;@a621a77
TAG=Build
TAGS=dev-keys
TIME=1596676149000
TYPE=userdebug
UNKNOWN=unknown
USER=android-build
PRODUCT = sdk_gphone_x86
MODEL = Android SDK built for x86
versionName = 1.0
versionCode = 1
java.lang.ArithmeticException: divide by zero
at com.example.learneassyjoke.MainActivity.onClick(MainActivity.java:68)
at android.view.View.performClick(View.java:6294)
at android.view.View$PerformClick.run(View.java:24770)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
这样我们可以获取异常发生的时间,异常发生的原因,异常发生的机器类型,apk版本信息。这样我们分析crash问题就方便多了。