您的位置:首页 > 移动开发 > Android开发

未处理异常处理器 UncaughtExceptionHandler 实现 崩溃日志保存 与 重启应用

2016-05-26 13:59 746 查看

前言

当我们编写程序的时候 , 遇到会抛出异常的方法的时候 , 我们一般会采取 try … catch 的方式:

try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}


但是万一我们捕捉不到的时候 , 程序就会FC , 弹出 某某某应用程序已停止运行 的对话框. 然后用户在受到打击之后还要重新点开应用 这从两点来说都是很不好的 , 也是这就我们这次要实现的功能

当APP在用户使用的时候 , 我们无法处理得到的异常

应用在崩溃之后没有响应的处理

说个题外的小技巧

手动获取日志的方法 (需要ROOT权限)

/data/system/dropbox 下存有所有应用的异常日志文件 , 非常时候可以手动提取

生成的日志文件:



崩溃后的提示:



重启后显示上次的异常日志:



开始编写异常处理器

要使用到的一些参数

/**
* Created by OCWVAR
* Package: com.ocwvar.surfacetest.ExceptionHandler
* Date: 2016/5/25  9:20
* Project: SurfaceTest
* 未处理异常接收器
*
*  在重新启动Activity时会传递数据包 Bundle
*  也可直接使用  OCExceptionHandler.handleIncomingBundle()  方法进行处理
*
*  数据:
*  IsRecover   .布尔类型.                          区别这个数据是否为崩溃重启的数据 永远为  true
*  hasLogs     .布尔类型.                          是否成功生成了日志文件
*  Throwable  .Serializable序列化类型.       上次崩溃的异常对象
*
*
* 参数:
* SLEEPTIME_RESTART_ACTIVITY        重新启动应用程序指定Activity间隔时间.  毫秒.  1000ms = 1s
* RESTART_ACTIVITY                         重新启动的Activity类
* LOG_NAME_HEAD                             日志文件名开头
* LOG_SAVE_FOLDER                          日志保存目录
* SAVE_LOGS                                    是否生成日志
*/
public class OCExceptionHandler{

private final static long SLEEPTIME_RESTART_ACTIVITY = 2000;
private final static Class RESTART_ACTIVITY = MainActivity.class;
private final static String LOG_NAME_HEAD = "OCLog";
private final static String LOG_SAVE_FOLDER = "/log/";
private final static boolean SAVE_LOGS = true;

public final static String THROWABLE_OBJECT = "Throwable";
public final static String IS_RECOVERY = "IsRecover";
public final static String HAS_LOGS = "hasLogs";

private boolean logsCreated = false;

...
}


1.引用 Thread.UncaughtExceptionHandler 接口 继承 Application

(继承Application只是为了设置 setDefaultUncaughtExceptionHandler , 和使用 ApplicationContext 而已 , 如果可以有其他实现方式可以不继承)

这是就是我们一开始最基础的配置

public class OCExceptionHandler extends Application implements Thread.UncaughtExceptionHandler {

@Override
public void onCreate() {
super.onCreate();
//设置捕捉全局未处理异常为我们这个类
Thread.setDefaultUncaughtExceptionHandler(this);
}

@Override
public void uncaughtException(Thread thread, Throwable ex){
...
}


2.在程序崩溃之后 , 启动对应的Activity

/**
* 重新启动应用程序
* @param activityClass 要启动的Activity
*/
private void restartActivity(Class activityClass , Throwable throwable){

//创建用于启动的 Intent , 与对应的数据
Intent intent = new Intent(getApplicationContext(),activityClass);
intent.putExtra("IsRecover",true);
intent.putExtra("hasLogs",logsCreated);
intent.putExtra("Throwable",throwable);

PendingIntent pendingIntent = PendingIntent.getActivity(
getApplicationContext(),
0,
intent,
PendingIntent.FLAG_ONE_SHOT
);

//获取闹钟管理器 , 用于定时执行我们的启动任务
AlarmManager mgr = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
//设置执行PendingIntent的时间是当前时间+SLEEPTIME_RESTART_ACTIVITY 参数的值
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + SLEEPTIME_RESTART_ACTIVITY , pendingIntent);
}


3.在程序崩溃的时候记录下日志文件

/**
* 创建日志文件
* @param throwable 要记录的异常
* @return  执行结果
*/
private boolean createLogs(Throwable throwable){
if (throwable == null || !SAVE_LOGS){
return false;
}

if (Build.VERSION.SDK_INT >= 23 && !checkPermission()){
Log.e("异常处理--保存日志", "保存失败 , Android 6.0+ 系统. 内存卡读写权限没有获取" );
return false;
}

if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
//如果内存卡或内置储存已经挂载

//创建储存目录
String savePath = Environment.getExternalStorageDirectory().getPath()+LOG_SAVE_FOLDER;
File file = new File(savePath);
file.mkdirs();

if (file.canWrite()){
//如果目录可以写入

FileWriter fileWriter;
PrintWriter printWriter;

//得到当前的日期与时间 , 精确到秒
String exceptionTime = DateFormat.format("yyyy-MM-dd hh:mm:ss", new Date()).toString();

//创建日志文件对象
file = new File(savePath + LOG_NAME_HEAD + "  " + exceptionTime + "  .log");
try {
if (file.createNewFile()){
//如果文件创建成功 , 则写入文件
fileWriter = new FileWriter(file,true);
printWriter = new PrintWriter(fileWriter);
printWriter.println("Date:"+exceptionTime+"\n");
printWriter.println("Exception Class Name: ");
printWriter.println(throwable.getStackTrace()[0].getClassName());
printWriter.println("");
printWriter.println("Exception Class Position: ");
printWriter.println("Line number: "+throwable.getStackTrace()[0].getLineNumber());
printWriter.println("");
printWriter.println("Exception Cause: ");
printWriter.println(throwable.getMessage());
printWriter.println("");
printWriter.println("-----------------------------------\nException Message: \n");
for (int i = 0; i < throwable.getStackTrace().length; i++) {
printWriter.println(throwable.getStackTrace()[i]);
}
//清空与关闭用到的流
printWriter.flush();
fileWriter.flush();
printWriter.close();
fileWriter.close();
Log.w("异常处理--保存日志", "日志保存成功" );
return true;
}else {
Log.e("异常处理--保存日志", "保存失败 , 存在相同名称的日志文件" );
return false;
}
} catch (IOException e) {
Log.e("异常处理--保存日志", "保存失败 , 无法创建日志文件或写入流失败" );
return false;
}

}else {
//目录不可写入 , 操作失败
Log.e("异常处理--保存日志", "保存失败 , 无法写入目录" );
file = null;
return false;
}

}else {
Log.e("异常处理--保存日志", "保存失败 , 储存未挂载" );
return false;
}

}

/**
* 检查内存卡读写权限 针对Android 6.0+
* @return  是否有权限
*/
@TargetApi(23)
private boolean checkPermission(){
return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}


4.结尾工作 , 使用我们创建的方法

在方法public void uncaughtException(Thread thread, Throwable ex)中调用我们的方法

@Override
public void uncaughtException(Thread thread, Throwable ex) {
//记录日志生成结果 , 用于给Intent传递日志生存的结果
logsCreated = createLogs(ex);
restartActivity(RESTART_ACTIVITY , ex);
//应用已经崩溃, 需要先终止当前的应用线程. 否则会ANR
System.exit(2);
}


然后在配置文件中使用我们这个类 (如果你是用你自己的Application类就不需要了):

<application
android:icon="@mipmap/ic_launcher"
android:label="TEST TEST TEST"
android:theme="@style/AppTheme"

//在这里进行注册Application类
android:name=".ExceptionHandler.OCExceptionHandler">

...

</application>


注意

有的大佬可能会想在程序崩溃的时候显示一个Toast或对话框 , 但这是不行的 , 无论你是直接执行还是用Handler .我看了下Stack Overflow上的QA , 有个人提出的观点 , 也是我最认同的观点 :

当应用崩溃的时候 , 已经没有能用的ApplicationContext了 ,所以你用了之后都是没反应的.

但我发现如果你用的是Activity.Context就能达到目的 , 但是这会导致Activity无法被回收的风险 , 所以非常不建议这么用. 毕竟为了显示一句话而导致内存泄漏这就捡了芝麻丢了西瓜.

弥补缺点

我们既然不方便在 UncaughtExceptionHandler 里面进行提示 , 但我们既然会传 Intent 给启动的Activity , 那么我们直接用它来做就行了.

/**
* 处理上次崩溃重启传回Activity的Bundle数据
* @param bundle    传入的Bundle数据
* @return  True: 处理成功  False:不是上次崩溃时传入的数据
*/
public static boolean handleIncomingBundle(@NonNull Bundle bundle , Context context){

//判断这个Bundle是不是我们崩溃后传回的数据
if (bundle.get(IS_RECOVERY) != null){
if (bundle.getBoolean(HAS_LOGS)){
Toast.makeText(context, "程序已恢复 , 崩溃日志已生成", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(context, "程序已恢复", Toast.LENGTH_SHORT).show();
}
Throwable throwable = (Throwable)bundle.getSerializable(THROWABLE_OBJECT);
if (throwable != null){
Log.e("上次崩溃日志", "---------------------------------------------");
for (int i = 0; i < throwable.getStackTrace().length; i++) {
System.out.println(throwable.getStackTrace()[i]);
}
Log.e("上次崩溃日志", "---------------------------------------------");
}else {
Log.e("上次崩溃日志", "日志丢失 或 记录失败 !");
}
return true;
}else {
return false;
}
}


在要启动的Activity中进行使用即可:

if (getIntent().getExtras() != null){
OCExceptionHandler.handleIncomingBundle(getIntent().getExtras(),getApplicationContext());
}


大佬们看完有啥要说的啊 ~~~

点个顶呗 QAQ

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息