Android | Android的异常捕获

前言

一个应用不可避免的会有bug。当一个bug的类型是未捕获异常时,应用就会崩溃。如果应用还在开发阶段,那么情况好一些。因为开发人员可以根据Android Studio打印的崩溃日志来定位问题,并解决问题。如果应用已经上线了,那么开发人员是无法获取到具体的崩溃信息的,也就无法在应用的下一次更新中修复问题。此时,Android应用的异常捕获就显得必不可少了。通常,每个上线的应用都会集成自家的或者第三方的异常上报平台。

基本原理

在Java语言的 Thread 类中有一个叫做 UncaughtExceptionHandler 的接口,它表示一个未捕获异常处理器。具体接口的定义如下所示:

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.
     * @param t the thread
     * @param e the exception
     */
    void uncaughtException(Thread t, Throwable e);
}

我们可以通过Thread类的 setDefaultUncaughtExceptionHandler() 静态方法为线程设置一个默认的未捕获异常处理器。具体方法的定义如下所示:

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

当遇到未捕获异常时,应用就会回调这个默认的未捕获异常处理器的 uncaughtException() 方法。我们可以在这个方法中收集崩溃信息、应用信息、设备信息和其它自定义的信息,然后将这些信息以文件的形式保存到SD卡中,最后在适当的时机将这些崩溃日志上传到指定的服务器。这样,开发人员就可以获取到应用崩溃时的详细日志,就可以清晰地定位问题了,在应用的下一次更新中就可以修复问题。

异常捕获

接下来,我们来实践Android的异常捕获。项目源码地址:https://github.com/chongyucaiyan/CrashHandler

首先,新建一个名为CrashHandler的类,并实现Thread.UncaughtExceptionHandler接口。具体代码和注释如下所示:

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private static final String DIR = "crash";

    // CrashHandler实例
    private static CrashHandler sCrashHandler = new CrashHandler();

    private Context mContext;

    // 默认的未捕获异常处理器
    private Thread.UncaughtExceptionHandler mDefaultHandler;

    // 用来存储应用信息和设备信息
    private Map<String, String> mInfo = new LinkedHashMap<>();

    private CrashHandler() {

    }

    public static CrashHandler getInstance() {
        return sCrashHandler;
    }

    public void init(Context context) {
        mContext = context.getApplicationContext();
        // 获取默认的未捕获异常处理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置CrashHandler为默认的未捕获异常处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    @Override
    public void uncaughtException(Thread t, Throwable ex) {
        // 处理异常
        handleException(ex);

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 结束应用
        if (mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(t, ex);
        } else {
            Process.killProcess(Process.myPid());
            System.exit(1);
        }
    }

    private void handleException(Throwable e) {
        // Toast提示出现异常
        new Thread() {

            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(mContext, R.string.crash_tips, Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }.start();
        // 收集应用信息和设备信息
        collectInfo();
        // 保存崩溃信息到SD卡
        saveInfo(e);
    }

    private void collectClassInfo(Class cls) {
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                mInfo.put(field.getName(), field.get(null).toString());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void collectInfo() {
        // 收集BuildConfig类信息
        collectClassInfo(BuildConfig.class);
        // 收集Build.VERSION类信息
        collectClassInfo(Build.VERSION.class);
        // 收集Build类信息
        collectClassInfo(Build.class);
    }

    private void saveInfo(Throwable ex) {
        // 判断SD卡是否挂载
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Log.e(TAG, "SD card is unmounted, so we can not save crash info!");
            return;
        }

        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : mInfo.entrySet()) {
            stringBuilder.append(entry.getKey() + " = " + entry.getValue() + "\n");
        }

        File dir = new File(Environment.getExternalStorageDirectory() + File.separator + DIR);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 以当前时间来命名崩溃日志文件
        long timestamp = System.currentTimeMillis();
        String time = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(timestamp));
        File file = new File(dir, "crash-" + time + "-" + timestamp + ".log");

        // 保存崩溃信息到文件
        try {
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            printWriter.print(stringBuilder.toString());
            printWriter.println();
            ex.printStackTrace(printWriter);
            printWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后,我们要把CrashHandler使用起来。通常,在应用自定义的 Application 类的onCreate()方法中,我们初始化CrashHandler。具体代码如下所示:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化CrashHandler
        CrashHandler.getInstance().init(this);
    }
}

这样,当遇到未捕获异常时,应用就会回调CrashHandler类的uncaughtException()方法。在这个方法中,我们收集了崩溃信息,并把崩溃信息保存到了SD卡。

接着,我们在MainActivity里放置一个按钮。当点击按钮时,抛出一个异常,以此来验证CrashHandler。具体代码如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final int REQUEST_CODE_PERMISSION = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initContentView();
        checkPermission();
    }

    private void initContentView() {
        findViewById(R.id.btn_main_make_a_crash).setOnClickListener(this);
    }

    private void checkPermission() {
        // 检查写外部存储权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            // 申请写外部存储权限
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSION);
        }
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_main_make_a_crash) {
            // 点击按钮时,抛出一个异常
            throw new RuntimeException("For test CrashHandler!");
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限授予
                Toast.makeText(this, R.string.main_permission_granted_tips, Toast.LENGTH_SHORT).show();
            } else {
                // 权限拒绝
                Toast.makeText(this, R.string.main_permission_denied_tips, Toast.LENGTH_SHORT).show();
            }
        }
    }
}

注意:保存崩溃信息到SD卡中需要应用有写外部存储的权限!

最后,测试一下。点击按钮,应用会Toast提示信息,然后退出。应用退出之后,在SD卡根目录下的crash目录下会生成一个崩溃日志文件。打开崩溃日志文件,文件的内容如下所示:

APPLICATION_ID = com.github.cyc.crashhandler
BUILD_TYPE = debug
DEBUG = true
FLAVOR = 
VERSION_CODE = 1
VERSION_NAME = 1.0
ACTIVE_CODENAMES = [Ljava.lang.String;@3a3e4bd
ALL_CODENAMES = [Ljava.lang.String;@3a82ab2
BASE_OS = 
CODENAME = REL
INCREMENTAL = V8.2.1.0.MXDCNDL
PREVIEW_SDK_INT = 0
RELEASE = 6.0.1
RESOURCES_SDK_INT = 23
SDK = 23
SDK_INT = 23
SECURITY_PATCH = 2017-01-01
BOARD = MSM8974
BOOTLOADER = unknown
BRAND = Xiaomi
CPU_ABI = armeabi-v7a
CPU_ABI2 = armeabi
DEVICE = cancro
DISPLAY = MMB29M
FINGERPRINT = Xiaomi/cancro_wc_lte/cancro:6.0.1/MMB29M/V8.2.1.0.MXDCNDL:user/release-keys
HARDWARE = qcom
HOST = c3-miui-ota-bd41.bj
ID = MMB29M
IS_DEBUGGABLE = false
MANUFACTURER = Xiaomi
MODEL = MI 4LTE
PRODUCT = cancro_wc_lte
RADIO = unknown
SERIAL = 160e94f3
SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@3489a03
SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@628ad80
SUPPORTED_ABIS = [Ljava.lang.String;@52f0fb9
TAG = Build
TAGS = release-keys
TIME = 1484620119000
TYPE = user
UNKNOWN = unknown
USER = builder

java.lang.RuntimeException: For test CrashHandler!
	at com.github.cyc.crashhandler.MainActivity.onClick(MainActivity.java:42)
	at android.view.View.performClick(View.java:5207)
	at android.view.View$PerformClick.run(View.java:21177)
	at android.os.Handler.handleCallback(Handler.java:739)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:148)
	at android.app.ActivityThread.main(ActivityThread.java:5438)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)

如果将这个崩溃日志文件在适当的时机上报到指定的服务器,那么开发人员就可以很清晰地定位并解决问题了。

总结

应用上线之后,如果崩溃,那么开发人员是无法获取到具体的崩溃信息的。因此,对于每个上线的应用来说,异常捕获并上报是必不可少的。

参考

  • Android 7.1.1 (API level 25)
  • https://developer.android.com/training/permissions/requesting.html
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值