当我们编写程序的时候 , 遇到会抛出异常的方法的时候 , 我们一般会采取 try … catch 的方式:
try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
但是万一我们捕捉不到的时候 , 程序就会FC , 弹出 某某某应用程序已停止运行 的对话框. 然后用户在受到打击之后还要重新点开应用 这从两点来说都是很不好的 , 也是这就我们这次要实现的功能
说个题外的小技巧
手动获取日志的方法 (需要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;
...
}
(继承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){
...
}
/** * 重新启动应用程序 * @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);
}
/** * 创建日志文件 * @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;
}
在方法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());
}