1. 概述
本文主要讲解如何自定义 Android 全局异常捕获,以及如何通过 Dialog 展示异常信息并将异常信息上传至服务器。
下面是最终的效果图:
主要涉及的知识点有:
- Thread.UncaughtExceptionHandler 原理分析
- Android 6.0 权限
- 自定义Dialog
2. 实现原理分析
Thread.UncaughtExceptionHandler 是 Android 实现全局异常捕获功能的核心,尽管此类在实现 Android 全局捕获异常中有举足轻重的地位,但它并不是 Google 的开发工程师创建的,它属于 Java lang 包。
Thread.UncaughtExceptionHandler 是 Java 虚拟机处理由于未捕获异常而导致程序停止运行的接口——当一个线程由于未捕获异常而停止运行的时候,Java 虚拟机就会通过 Thread 的 getUncaughtExceptionHandler() 方法拿到当前 Thread 的 Thread.UncaughtExceptionHandler,并调用该 Thread.UncaughtExceptionHandler 的 uncaughtException 方法。
uncaughtException 方法的具体定义如下:
uncaughtException方法定义uncaughtException 方法的第二个参数是导致程序终止运行的异常对象,异常对象都拿到了,至于后面怎么处理,IT IS UP TO YOU!
异常执行的具体流程:
异常执行流程3. 具体实现步骤
3.1 青铜版——直接在 Application 中实现 Thread.UncaughtExceptionHandler 接口
3.1.1 创建 MainActivity 类
创建 MainActivity 类,在对应的布局文件(activity_main)中添加 Button 按钮,为 Button 添加点击事件,在 Button 点击事件中手动抛出一个未捕获异常。
PS:XML 布局文件中关于 Dimension、String、Color 等的定义都在相应的文件中(dimens.xml、strings.xml、colors.xml 等),由于这些资源文件在此处为非关键必要内容,故不贴出。
建议大家将 Dimension、String、Color 等数据定义在相应的资源文件中,进行统一管理,以便将来需要修改数据的时候容易查找。
//源码 MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void throwRuntimeException(View view){
throw new RuntimeException(CommonConstant.TIAN_WANG_GAI_DI_HU);
}
}
//源码 activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="@dimen/padding_small"
android:paddingLeft="@dimen/padding_medium"
android:paddingRight="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
tools:context=".MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@string/throw_runtime_exception_method"
android:paddingBottom="@dimen/padding_medium"
android:paddingLeft="@dimen/padding_icon"
android:paddingRight="@dimen/padding_icon"
android:paddingTop="@dimen/padding_medium"
android:text="@string/throw_runtime_exception"
android:textSize="@dimen/font_micro"
android:textStyle="bold" />
</LinearLayout>
Android Exception Static
3.1.2 自定义 Application 类
自定义 Application 类,并实现 Thread.UncaughtExceptionHandler 接口中定义的方法 uncaughtException,并在 uncaughtException 方法中将异常信息输出。
在 Application 的 onCreate 方法中通过 Thread.setDefaultUncaughtExceptionHandler() 方法将实现了 Thread.UncaughtExceptionHandler 接口的 Application 设置为当前 Application 默认的 Thread.UncaughtExceptionHandler。
//源码 BaseApplication
public class BaseApplication extends Application implements Thread.UncaughtExceptionHandler{
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(final Thread thread, final Throwable throwable) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
try {
String result = writer.toString();
Log.e(CommonConstant.TAG,"\r\nCurrentThread:" + Thread.currentThread() + "\r\nException:\r\n" + result);
printWriter.close();
writer.close();
}catch (Exception e){
e.printStackTrace();
}
//Toast 提示用户
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(BaseApplication.this, getString(R.string.exception_occurs_prompt), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
//关闭应用程序
SystemClock.sleep(1000);
android.os.Process.killProcess(android.os.Process.myPid());
}
}
3.1.3 在 AndroidManifest 文件中声明自定义的 Application
//源码 AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smart.exceptionanalyse">
<application
android:name=".BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
最终的效果就是下面这个样子:
Android Exception 青铜版//执行结果
08-18 18:45:18.878 5045-5045/com.smart.exceptionanalyse E/TAG: CurrentThread:Thread[main,5,main]
Exception:
java.lang.IllegalStateException: Could not execute method for android:onClick
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:390)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:385)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
Caused by: java.lang.RuntimeException: 天王盖地虎
at com.smart.exceptionanalyse.MainActivity.throwRuntimeException(MainActivity.java:68)
at java.lang.reflect.Method.invoke(Native Method)
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:385)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
到此青铜版的 Android 异常捕获功能就实现了,是不是很简单?
上面程序中的异常是我们手动抛出的,异常信息为“天王盖地虎”,异常抛出的位置在异常信息中已经明确标出,至于后面怎么处理,IT IS UP TO YOU!
3.1.4 青铜版之番外篇——当异常出现在子线程中时会出现什么情况?
上面异常抛出的地方在主线程中,那如果异常出现在子线程中,Thread.UncaughtExceptionHandler 还会不会被调用呢?
将 MainActivity 类中的 throwRuntimeException 方法做部分修改,其他均不变:
//源码 MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button mExecute;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void throwRuntimeException(View view){
new Thread(){
@Override
public void run() {
super.run();
throw new RuntimeException(CommonConstant.TIAN_WANG_GAI_DI_HU);
}
}.start();
}
}
//执行结果
08-18 19:24:01.978 6599-6626/com.smart.exceptionanalyse E/TAG: CurrentThread:Thread[Thread-4,5,main]
Exception:
java.lang.RuntimeException: 天王盖地虎
at com.smart.exceptionanalyse.MainActivity$1.run(MainActivity.java:72)
由上面的执行结果可知:
即使异常发生在子线程中,Thread.UncaughtExceptionHandler 依然会被调用,只不过异常信息没有那么详细了,而且当前异常线程为 Thread-4 而不是 main。
3.2 白银版——自定义 Thread.UncaughtExceptionHandler 实现类,并将异常信息保存至 SD 卡
3.2.1 创建 MainActivity 类
因为需要将异常信息保存至 SD 卡,因此在 MainActivity 初始化的时候需要申请手机存储权限(android.permission.WRITE_EXTERNAL_STORAGE)。
//源码 MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button mExecute;
private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 0x0001024;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showToast(getString(R.string.permission_granted));
} else {
showToast(getString(R.string.permission_denied));
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void showToast(String string){
Toast.makeText(MainActivity.this,string,Toast.LENGTH_SHORT).show();
}
public void throwRuntimeException(View view){
throw new RuntimeException(CommonConstant.TIAN_WANG_GAI_DI_HU);
}
}
//源码 activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="@dimen/padding_small"
android:paddingLeft="@dimen/padding_medium"
android:paddingRight="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
tools:context=".MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@string/throw_runtime_exception_method"
android:paddingBottom="@dimen/padding_medium"
android:paddingLeft="@dimen/padding_icon"
android:paddingRight="@dimen/padding_icon"
android:paddingTop="@dimen/padding_medium"
android:text="@string/throw_runtime_exception"
android:textSize="@dimen/font_micro"
android:textStyle="bold" />
</LinearLayout>
Android_Exception_Static.jpg
3.2.2 自定义 Thread.UncaughtExceptionHandler 实现类——CrashHandler
自定义 Thread.UncaughtExceptionHandler 实现类——CrashHandler,在 CrashHandler 中实现 uncaughtException 方法。在 uncaughtException 方法中将异常信息存储至 SD 卡。
//源码 CrashHandler
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static CrashHandler INSTANCE = new CrashHandler();
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultHandler;
private CrashHandler() {}
public static CrashHandler getInstance() {
return INSTANCE;
}
public void initCrashHandler(Context context) {
mContext = context;
//获取系统默认的 UncaughtExceptionHandler
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置该 CrashHandler 为程序的默认 UncaughtExceptionHandler
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
if (!handleException(throwable) && mDefaultHandler != null) {
//如果用户没有处理则让系统默认的 UncaughtExceptionHandler 来处理
mDefaultHandler.uncaughtException(thread, throwable);
} else {
SystemClock.sleep(1000);
//退出程序
android.os.Process.killProcess(android.os.Process.myPid());
}
}
/**
* Des: 将异常信息保存至 SD 卡
*
* Time: 2018/8/15 上午12:33
*/
private boolean handleException(Throwable throwable){
if(throwable == null){
return false;
}
//使用Toast来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, mContext.getString(R.string.exception_occurs_prompt), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
Throwable cause = throwable.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
try {
writer.close();
}catch (Exception e){
e.printStackTrace();
}
try {
StringBuffer sb = new StringBuffer();
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
sb.append("应用版本:" + pi.versionName + "\n");
sb.append("应用版本号:" + pi.versionCode + "\n");
sb.append("品牌:" + Build.MANUFACTURER + "\n");
sb.append("机型:" + Build.MODEL + "\n");
sb.append("Android 版本:" + Build.VERSION.RELEASE + "\n");
sb.append("系统版本:" + Build.DISPLAY + "\n");
long timestamp = System.currentTimeMillis();
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = formatter.format(new Date());
sb.append("报错时间:" + time + "\n");
sb.append("异常信息:" + "\n" + result);
String fileName = "crash_" + timestamp + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory()+ File.separator + mContext.getPackageName() + File.separator + "Crash";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(path,fileName);
if(!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file.getPath());
fos.write(sb.toString().getBytes());
fos.close();
}
mExceptionMessage = sb.toString();
}
} catch (Exception e) {
Log.e(CommonConstant.TAG, "An error occured when collect package info", e);
}
return true;
}
}
3.2.3 自定义 Application——BaseApplication
自定义 Application——BaseApplication,并在 BaseApplication 中初始化 CrashHandler。
//源码 BaseApplication
public class BaseApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}
}
3.2.4 在 AndroidManifest 文件中声明自定义的 Application 及存储权限
//源码 AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smart.exceptionanalyse">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
最终的效果就是下面这个样子:
Android Exception 白银版.gif Android Exception 白银版 异常文件 Android Exception 白银版 文件详情3.3 黄金版——自定义 Thread.UncaughtExceptionHandler 实现类,将异常信息展示在 Dialog 中,并将异常信息提交至服务器
3.3.1 创建 MainActivity 类
同 3.2.1 一样,故在此不赘述。
3.3.2 自定义 Thread.UncaughtExceptionHandler 实现类——CrashHandler
自定义 Thread.UncaughtExceptionHandler 实现类——CrashHandler,在 CrashHandler 中实现 uncaughtException 方法。在 uncaughtException 方法中将异常信息存储至 SD 卡、通过 Intent 将异常信息发送至新的 Activity——CrashInfoActivity。
//源码 CrashHandler
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static CrashHandler INSTANCE = new CrashHandler();
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultHandler;
private String mExceptionMessage;
private CrashHandler() {}
public static CrashHandler getInstance() {
return INSTANCE;
}
public void initCrashHandler(Context context) {
mContext = context;
//获取系统默认的 UncaughtExceptionHandler
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置该 CrashHandler 为程序的默认 UncaughtExceptionHandler
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
if (!handleException(throwable) && mDefaultHandler != null) {
//如果用户没有处理则让系统默认的 UncaughtExceptionHandler 来处理
mDefaultHandler.uncaughtException(thread, throwable);
} else {
Intent intent = new Intent(mContext, CrashInfoActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(CommonConstant.EXCEPTION_MESSAGE, mExceptionMessage);
mContext.startActivity(intent);
SystemClock.sleep(1000);
//退出程序
android.os.Process.killProcess(android.os.Process.myPid());
}
}
/**
* Des: 将异常信息保存至 SD 卡
*
* Time: 2018/8/15 上午12:33
*/
private boolean handleException(Throwable throwable){
if(throwable == null){
return false;
}
//Toast 提示用户
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, mContext.getString(R.string.exception_occurs_prompt), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
Throwable cause = throwable.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
try {
writer.close();
}catch (Exception e){
e.printStackTrace();
}
try {
StringBuffer sb = new StringBuffer();
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
sb.append("应用版本:" + pi.versionName + "\n");
sb.append("应用版本号:" + pi.versionCode + "\n");
sb.append("品牌:" + Build.MANUFACTURER + "\n");
sb.append("机型:" + Build.MODEL + "\n");
sb.append("Android 版本:" + Build.VERSION.RELEASE + "\n");
sb.append("系统版本:" + Build.DISPLAY + "\n");
long timestamp = System.currentTimeMillis();
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = formatter.format(new Date());
sb.append("报错时间:" + time + "\n");
sb.append("异常信息:" + "\n" + result);
String fileName = "crash_" + timestamp + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory()+ File.separator + mContext.getPackageName() + File.separator + "Crash";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(path,fileName);
if(!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file.getPath());
fos.write(sb.toString().getBytes());
fos.close();
}
mExceptionMessage = sb.toString();
}
} catch (Exception e) {
Log.e(CommonConstant.TAG, "An error occured when collect package info", e);
}
return true;
}
}
3.3.3 创建 CrashInfoActivity
创建 CrashInfoActivity,在对应的布局文件(activity_crash_info)中创建两个按钮——重启应用、异常信息。重启应用按钮用于重启 App,异常信息按钮用于弹出对话框。此处的对话框并不是系统自带的,而是自定义的,这样的目的是为了控制对话框的尺寸,因为对话框要显式异常信息,有时异常信息会特别长,如果用系统自带的对话框,就会占满整个屏幕。
PS:
- 自定义 Dialog 在此处为非关键必要因素,故不贴出
- 此处的上传功能并未真正实现,是用 Handler 通过倒计时模拟的
//源码 CrashInfoActivity
public class CrashInfoActivity extends AppCompatActivity implements View.OnClickListener{
private Button mRestartApp,mShowExceptionInfo;
private String mExceptionMessageInfo;
private SmartProgressDialog mSmartDialog;
private static final int UPLOAD_EXCEPTION_MESSAGE = 0X00001;
private static final int COUNT_DOWN = 5;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case UPLOAD_EXCEPTION_MESSAGE:
int countDown = (Integer) msg.obj;
countDown--;
if(countDown == 4){
uploadExceptionMessage();
}else if(countDown == 0){
uploadSucceed();
}
if(countDown > 0){
Message message = new Message();
message.what = UPLOAD_EXCEPTION_MESSAGE;
message.obj = countDown;
mHandler.sendMessageDelayed(message,1000);
}
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crash_info);
initView();
initData();
}
private void initView(){
mRestartApp = findViewById(R.id.app_restart);
mShowExceptionInfo = findViewById(R.id.show_exception_info);
mRestartApp.setOnClickListener(this);
mShowExceptionInfo.setOnClickListener(this);
}
private void initData(){
Intent intent = getIntent();
mExceptionMessageInfo = intent.getStringExtra(CommonConstant.EXCEPTION_MESSAGE);
}
@Override
public void onClick(View v) {
int id = v.getId();
switch (id){
case R.id.app_restart:
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
this.finish();
break;
case R.id.show_exception_info:
final SmartNormalDialog smartDialog = new SmartNormalDialog(this);
smartDialog.setTitle(getResources().getString(R.string.exception_dialog_title));
smartDialog.setContent(mExceptionMessageInfo);
smartDialog.setCancelButtonText(getResources().getString(R.string.exception_dialog_cancel));
smartDialog.setConfirmButtonText(getResources().getString(R.string.exception_dialog_confirm));
smartDialog.setOnDialogClickListener(new SmartDialog.OnDialogClickListener() {
@Override
public void onConfirmClick() {
smartDialog.dismiss();
}
@Override
public void onCancelClick() {
smartDialog.dismiss();
Message message = new Message();
message.what = UPLOAD_EXCEPTION_MESSAGE;
message.obj = COUNT_DOWN;
mHandler.sendMessageDelayed(message,300);
}
});
smartDialog.show();
break;
}
}
private void uploadExceptionMessage(){
if(mSmartDialog == null){
mSmartDialog = new SmartProgressDialog(this);
}
mSmartDialog.setContent(getResources().getString(R.string.exception_upload_dialog_content));
mSmartDialog.show();
}
private void uploadSucceed(){
if(mSmartDialog != null){
mSmartDialog.dismiss();
}
Toast.makeText(this, getString(R.string.exception_upload_succeed), Toast.LENGTH_SHORT).show();
}
}
//源码 activity_crash_info
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".CrashInfoActivity">
<ImageView
android:id="@+id/exception_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/customactivityoncrash_error_image" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="@dimen/padding_icon"
android:paddingTop="@dimen/padding_icon"
android:text="@string/exception_occurs"
android:textSize="@dimen/font_medium"
android:textStyle="bold" />
<Button
android:id="@+id/app_restart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_micro"
android:text="@string/app_restart" />
<Button
android:id="@+id/show_exception_info"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_micro"
android:text="@string/show_exception_info"
android:textColor="@color/blue_a400" />
</LinearLayout>
Crash Info Activity
3.3.4 自定义 Application——BaseApplication
同 3.2.3 一样,故在此不赘述。
3.3.5 在 AndroidManifest 文件中声明自定义的 Application 、存储权限以及 CrashInfoActivity
//源码 AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smart.exceptionanalyse">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".CrashInfoActivity"
android:label="@string/exception_message" />
</application>
</manifest>
最终的效果就是下面这个样子:
Android_Exception_黄金版.gif到此黄金版的 Android 异常捕获功能就实现了,是不是很简单?
如果你足够的细心的话,一定会发现我们的 Dialog 显式、异常信息上传都是在新的 Activity 中进行,那能不能直接在 CrashHandler 中显式呢?这个问题就交给你们了。
4. 应用
全局异常捕获是 Android 应用程序不可或缺的一个功能,因为它可以方便将异常信息收集起来,以便开发者在后期的开发工作中改掉一些测试工作人员未发现但用户在使用的过程中出现的 Bug。鉴于此,建议大家将此功能封装进自己的 BaseLibrary,这样就可以避免重复造轮子了。
5. 关联知识
除了自定义全局异常捕获之外,我们还可以使用第三方提供的库,如:
之所以不用此类库是因为它们里面会包含其他不是我所需要的服务,但这些第三方可能比我们自定义的异常捕获要全面,毕竟它们已经迭代了那么多次。所以到底要如何选择,IT IS UP TO YOU!
参考文档
1)Thread.UncaughtExceptionHandler Java
2)Thread.UncaughtExceptionHandler Android Developer
3)Android开发之全局异常捕获
4)Android 6.0 权限申请
5)CustomActivityOnCrash
网友评论