前段时间在编写一个图片上传应用的时候,最后发布第一版的时候还是会出现各种各样的程序崩溃现象,由于测试与调试的不同步,所以就需要记录程序所有的异常信息用于优化改进;这个时候就遇到一个问题,如何捕获所有的异常信息;这个就是我们今天要讨论的内容了。

        今天主要介绍以下几点内容:

  • logcat未主动捕获日志信息演示
  • 未捕获异常Android处理机制对于的API
  • 结合前两点保存logcat异常日志信息到本地

 

1,logcat未主动捕获日志信息演示

当我们在一个按钮的点击事件中添加如下代码:

int a=0;
  int b=1;
  int c=b/a;

logcat输入如下日志信息并且应用给出提示弹窗或者异常退出:

android kill触发gc_android kill触发gc

android kill触发gc_android kill触发gc_02

 

如果我把上面的代码改为:

int a=0;
        int b=1;
        try {
            int c = b / a;
        }
        catch (ArithmeticException e){
            e.printStackTrace();
            Log.e(TAG, "catchExceptionTest1: 主动捕获异常" );
        }

点击按钮,logcat输入日志信息:

android kill触发gc_Android_03

 

        所以我们就可以得出一个结论,系统是帮助我们捕获了这些异常信息,只是没有保存这些信息并且处理的方式可能不是我们想要的。

 

2,未捕获异常Android处理机制对于的API

      Android内部所有未主动捕获的异常都会走UncaughtExceptionHandler的uncaughtException方法  。为了体现这句话,我们直接看一下效果:

(1)第一步:新建一个未捕获异常处理类实现UncaughtExceptionHandler

package com.hfut.operationcrashhandler;

import android.os.Process;
import android.util.Log;

/**
 * author:why
 * created on: 2018/12/22 22:55
 * description:
 */
public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "MyCrashHandler";

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.e(TAG, "uncaughtException: 默认异常捕获处理");
        Process.killProcess(Process.myPid());//杀死当前进程
    }

}

(2)第二步:设置默认未捕获异常处理器;在测试的Activity的onCreate中添加如下代码:

Thread.setDefaultUncaughtExceptionHandler(new MyCrashHandler());

(3)第三步:点击未主动捕获异常时的按钮,logcat日志如下:

android kill触发gc_异常_04

(4)第四步:点击主动捕获异常时的按钮,logcat日志如下:

android kill触发gc_Android_05

上面几步的操作证明了一下几个观点:

  • 未主动捕获的异常系统会在UncaughtExceptionHandler的uncaughtException方法中处理
  • 主动捕获的异常不会在UncaughtExceptionHandler的uncaughtException方法中处理
  • 在无法主动获取全部异常信息时(Crash时);我们可以通过自定义一个异常处理器实现UncaughtExceptionHandler来记录异常信息和处理异常Crash来分别实现收集Crash信息以及优化用户体验的目的(下一部分介绍)。

 

3,结合前两点保存logcat异常日志信息到本地

    3.1 分析 

保存未捕获异常logcat窗口输出的异常信息到本地。我们首先分析一下:

(1)首先保存的信息除了上面说到的还需要设备以及应用的相关信息,所以自定义异常处理器就需要传入当前应用的上下文对象用于获取的这些当中的部分信息

(2)因为我们通过Thread的setDefaultUncaughtExceptionHandler方法设置的异常处理器是一个静态全局变量,所以它是针对进程的,所以应该在Application初始化的时候就该完成

// null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
         defaultUncaughtExceptionHandler = eh;
     }

(3)因为(2)的原因,我们的异常处理器应该被设计成单例模式

下面就来看看具体的实现,本部分代码很多参考《Android开发艺术探索》;想更多详细内容可参考原书。

 

3.2 实现

MyCrashHandler1.java代码:

package com.hfut.operationcrashhandler;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Process;
import android.util.Log;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * author:why
 * created on: 2018/12/23 9:39
 * description: crashInfo manage class
 */
public class MyCrashHandler1 implements Thread.UncaughtExceptionHandler {

    private static final String TAG = "MyCrashHandler1";
    private static final MyCrashHandler1 singleInstance = new MyCrashHandler1();
    private final String crashStorageDir = "/mnt/sdcard/crashInfo/";
    private final String crashStorageFile = "crash.txt";
    private Context mContext;
    private Thread.UncaughtExceptionHandler mUncaughtExceptionHandler;

    private MyCrashHandler1() {

    }

    /**
     * 获取单例
     *
     * @return
     */
    public static MyCrashHandler1 getSingleInstance() {
        return singleInstance;
    }

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

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

    /**
     * 保存crash信息到SDcard
     *
     * @param e
     */
    private void saveCrashInfo2SD(Throwable e) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {// No sdcard
            Log.e(TAG, "saveCrashInfo2SD: sdcard unmounted,can not save crash info");
            return;
        }
        File file = new File(crashStorageDir);
        if (!file.exists()) {
            file.mkdirs();
        }
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
        try {
            //time = time.replace("-", "_").replace(":", "_").replace(" ", "_");
            File file1 = new File(crashStorageDir + time + crashStorageFile);
            if (!file1.exists()) {
                file1.createNewFile();
            }
            PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(file1)));
            writer.print(time);
            saveDeviceInfo(writer);
            writer.println();//换行
            e.printStackTrace(writer);
            writer.close();//关闭输出流
            Process.killProcess(Process.myPid());//关闭应用进程
        } catch (Exception e1) {
            Log.e(TAG, "saveCrashInfo2SD: save crash exception");
        }

    }

    private void saveDeviceInfo(PrintWriter writer) throws Exception{
        PackageManager packageManager=mContext.getPackageManager();
        PackageInfo packageInfo=packageManager.getPackageInfo(mContext.getPackageName(),
                PackageManager.GET_ACTIVITIES);
        writer.print("APP Version: ");
        writer.print(packageInfo.versionName+"_");
        writer.println(packageInfo.versionCode);

        //Android OS
        writer.print("OS Version: ");
        writer.print(Build.VERSION.RELEASE+"_");
        writer.println(Build.VERSION.SDK_INT);

        //phone maker
        writer.print("Vendor: ");
        writer.println(Build.MANUFACTURER);

        //phone type
        writer.print("Model: ");
        writer.println(Build.MODEL);

        //cpu 架构
        writer.print("CPU ABI: ");
        writer.println(Build.CPU_ABI);
    }
}

MyApplication.java代码:

package com.hfut.operationcrashhandler;

import android.app.Application;

/**
 * author:why
 * created on: 2018/12/23 11:46
 * description:
 */
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        MyCrashHandler1.getSingleInstance().init(this);//初始化异常捕获
    }
}

MainActivity.java测试代码:

package com.hfut.operationcrashhandler;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * @author why
 * @date 2018-12-22 22:41:20
 */
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //Thread.setDefaultUncaughtExceptionHandler(new MyCrashHandler());
    }


    //测试默认异常捕获
    public void catchExceptionTest(View view){
        int a=0;
        int b=1;
        int c=b/a;
    }

    //测试主动异常捕获
    public void catchExceptionTest1(View view){
        int a=0;
        int b=1;
        try {
            int c = b / a;
        }
        catch (ArithmeticException e){
            e.printStackTrace();
            Log.e(TAG, "catchExceptionTest1: 主动捕获异常" );
        }
    }
}

测试结果

在点击第一个按钮之后,找到对应的Crash信息保存目录“/mnt/sdcard/crashInfo”找到日志文件打开:

android kill触发gc_Android_06

2018-12-22 22:56:31APP Version: 1.0_1
OS Version: 5.1_22
Vendor: unknown
Model: Google Nexus 7 - 5.1.0 - API 22 - 800x1280
CPU ABI: x86

java.lang.IllegalStateException: Could not execute method for android:onClick
	at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:389)
	at android.view.View.performClick(View.java:4780)
	at android.view.View$PerformClick.run(View.java:19866)
	at android.os.Handler.handleCallback(Handler.java:739)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:135)
	at android.app.ActivityThread.main(ActivityThread.java:5254)
	at java.lang.reflect.Method.invoke(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:372)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
Caused by: java.lang.reflect.InvocationTargetException
	at java.lang.reflect.Method.invoke(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:372)
	at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:384)
	... 10 more
Caused by: java.lang.ArithmeticException: divide by zero
	at com.hfut.operationcrashhandler.MainActivity.catchExceptionTest(MainActivity.java:27)
	... 13 more

可见异常信息已经全都完整的保留下来了,后续可上传至服务器用于产品优化。好了到这里关于应用crash第一部分就介绍完成了。这里在测试的时候需要注意一下:

(1)自定义的MyApplication需要在配置文件中的Application标签下面添加一下android:name属性,如下:

android kill触发gc_异常_07

 

(2)因为涉及到读写SD卡权限,6.0及以上请添加权限申请部分代码,可参考Android权限介绍。