Android系统碎片化造成应用程序崩溃严重,在模拟器上运行良好的程序安装到某款手机上说不定就会出现崩溃的现象。而且,往往都是程序发布之后在用户端出现了崩溃现象。所以在程序发布出去之后,如果出现了崩溃现象,开发者应该及时获取在该设备上导致崩溃的信息,这对于下一个版本的bug修复帮助极大。以下将讲述如何捕获异常崩溃信息保存至本地并上传至服务器。

Android崩溃机制

常见的Android崩溃有两类,一类是Java Exception异常,一类是Native Signal异常。我们将围绕这两类异常进行。对于很多基于Unity、Cocos平台的游戏,还会有C#、JavaScript、Lua等的异常,这里不做讨论。Android应用程序的开发是基于Java语言的,所以主要分析第一类Android崩溃Java Exception。

Java的异常可以分为两类:Checked Exception和UnChecked Exception。所有RuntimeException类及其子类的实例被称为Runt ime异常,即UnChecked Exception,不是RuntimeException类及其子类的异常实例则被称为Checked Exception。

Checked异常又称为编译时异常,即在编译阶段被处理的异常。编译器会强制程序处理所有的Checked异常,也就是用try…catch显式的捕获并处理,因为Java认为这类异常都是可以被处理(修复)的。在Java API文档中,方法说明时,都会添加是否throw某个exception,这个exception就是Checked异常。如果没有try…catch这个异常,则编译出错,错误提示类似于“Unhandled

exception type xxxxx”。

该类异常捕获的流程是:

执行try块中的代码出现异常,系统会自动生成一个异常对象,并将该异常对象提交给Java运行环境,这个就是异常抛出(throw)阶段;

当Java运行环境收到异常对象时,会寻找最近的能够处理该异常对象的catch块,找到之后把该异常对象交给catch块处理,这个就是异常捕获(catch)阶段。

Checked异常一般是不引起Android App Crash的,注意是“一般”,这里之所以介绍Checked异常,有两个原因:

形成系统的了解,更好地对比理解UnCheckedException;

对于一些Checked Exception,虽然我们在程序里面已经捕获并处理了,但是如果能同时将该异常收集并发送到后台,将有助于提升App的健壮性。比如修改代码逻辑回避该异常,或者捕获后采用更好的方法去处理该异常。至于应该收集哪些Checked Exception,则取决于App的业务逻辑和开发者的经验了。

UnChecked异常又称为运行时异常,即Runtime-Exception,最常见的莫过于NullPointerException。UnChecked异常发生时,由于没有相应的try…catch处理该异常对象,所以Java运行环境将会终止,程序将退出,也就是我们所说的Crash。当然,你可能会说,那我们把这些异常也try…catch住不就行了。理论上确实是可以的,但有两点会导致这种方案不可行:

无法将所有的代码都加上try…catch,这样对代码的效率和可读性将是毁灭性的;

UnChecked异常通常都是较为严重的异常,或者说已经破坏了运行环境的。比如内存地址,即使我们try…catch住了,也不能明确知道如何处理该异常,才能保证程序接下来的运行是正确的。

没有try…catch住的异常,即Uncaught异常,都会导致应用程序崩溃。那么面对崩溃,我们是否可以做些什么呢?比如程序退出前,弹出个性化对话框,而不是默认的强制关闭对话框,或者弹出一个提示框安慰一下用户,甚至重启应用程序等。

其实Java提供了一个接口给我们,可以完成这些,这就是UncaughtExceptionHandler,该接口含有一个纯虚函数:public abstract void uncaughtException (Thread thread, Throwableex)。

Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常,然后便会调用uncaughtException函数。如果该handler没有被显式设置,则会调用对应线程组的默认handler。如果我们要捕获该异常,必须实现我们自己的handler,并通过以下函数进行设置:

public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)

实现自定义的handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方法即可。

static class MyCrashHandler implements UncaughtExceptionHandler{

@Override

public void uncaughtException(Thread thread, final Throwable throwable) {

// Deal this exception

}

}

在任何线程中,都可以通过setDefaultUncaughtExceptionHandler来设置handler,但在Android应用程序中,全局的Application和Activity、Service都同属于UI主线程,线程名称默认为“main”。所以,在Application中应该为UI主线程添加UncaughtExceptionHandler,这样整个程序中的Activity、Service中出现的UncaughtException事件都可以被处理。

捕获异常实现

接下来我们就来实现这一机制,不过首先我们还是来了解以下两个类:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。

Application:用来管理应用程序的全局状态。在应用程序启动时Application会首先创建,然后才会根据情况(Intent)来启动相应的Activity和Service。本示例中将在自定义加强版的Application中注册未捕获异常处理器。

Thread.UncaughtExceptionHandler:线程未捕获异常处理器,用来处理未捕获异常。如果程序出现了未捕获异常,默认会弹出系统中强制关闭对话框。我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。

我们实现Thread.UncaughtExceptionHandler,是我们用来处理未捕获异常的主要逻辑,代码如下:

package com.scott.crash;

import java.io.File;

import java.io.FileOutputStream;

import java.io.PrintWriter;

import java.io.StringWriter;

import java.io.Writer;

import java.lang.Thread.UncaughtExceptionHandler;

import java.lang.reflect.Field;

import java.text.DateFormat;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

import android.content.Context;

import android.content.pm.PackageInfo;

import android.content.pm.PackageManager;

import android.content.pm.PackageManager.NameNotFoundException;

import android.os.Build;

import android.os.Environment;

import android.os.Looper;

import android.util.Log;

import android.widget.Toast;

/**

* UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告.

*

* @author user

*

*/

public class CrashHandler implements UncaughtExceptionHandler {

public static final String TAG = "CrashHandler";

//系统默认的UncaughtException处理类

private Thread.UncaughtExceptionHandler mDefaultHandler;

//CrashHandler实例

private static CrashHandler INSTANCE = new CrashHandler();

//程序的Context对象

private Context mContext;

//用来存储设备信息和异常信息

private Map infos = new HashMap();

//用于格式化日期,作为日志文件名的一部分

private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

/** 保证只有一个CrashHandler实例 */

private CrashHandler() {

}

/** 获取CrashHandler实例 ,单例模式 */

public static CrashHandler getInstance() {

return INSTANCE;

}

/**

* 初始化

*

* @param context

*/

public void init(Context context) {

mContext = context;

//获取系统默认的UncaughtException处理器

mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();

//设置该CrashHandler为程序的默认处理器

Thread.setDefaultUncaughtExceptionHandler(this);

}

/**

* 当UncaughtException发生时会转入该函数来处理

*/

@Override

public void uncaughtException(Thread thread, Throwable ex) {

if (!handleException(ex) && mDefaultHandler != null) {

//如果用户没有处理则让系统默认的异常处理器来处理

mDefaultHandler.uncaughtException(thread, ex);

} else {

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

Log.e(TAG, "error : ", e);

}

//退出程序

android.os.Process.killProcess(android.os.Process.myPid());

System.exit(1);

}

}

/**

* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.

*

* @param ex

* @return true:如果处理了该异常信息;否则返回false.

*/

private boolean handleException(Throwable ex) {

if (ex == null) {

return false;

}

//使用Toast来显示异常信息

new Thread() {

@Override

public void run() {

Looper.prepare();

Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();

Looper.loop();

}

}.start();

//收集设备参数信息

collectDeviceInfo(mContext);

//保存日志文件

saveCrashInfo2File(ex);

return true;

}

/**

* 收集设备参数信息

* @param ctx

*/

public void collectDeviceInfo(Context ctx) {

try {

PackageManager pm = ctx.getPackageManager();

PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);

if (pi != null) {

String versionName = pi.versionName == null ? "null" : pi.versionName;

String versionCode = pi.versionCode + "";

infos.put("versionName", versionName);

infos.put("versionCode", versionCode);

}

} catch (NameNotFoundException e) {

Log.e(TAG, "an error occured when collect package info", e);

}

Field[] fields = Build.class.getDeclaredFields();

for (Field field : fields) {

try {

field.setAccessible(true);

infos.put(field.getName(), field.get(null).toString());

Log.d(TAG, field.getName() + " : " + field.get(null));

} catch (Exception e) {

Log.e(TAG, "an error occured when collect crash info", e);

}

}

}

/**

* 保存错误信息到文件中

*

* @param ex

* @return 返回文件名称,便于将文件传送到服务器

*/

private String saveCrashInfo2File(Throwable ex) {

StringBuffer sb = new StringBuffer();

for (Map.Entry entry : infos.entrySet()) {

String key = entry.getKey();

String value = entry.getValue();

sb.append(key + "=" + value + "\n");

}

Writer writer = new StringWriter();

PrintWriter printWriter = new PrintWriter(writer);

ex.printStackTrace(printWriter);

Throwable cause = ex.getCause();

while (cause != null) {

cause.printStackTrace(printWriter);

cause = cause.getCause();

}

printWriter.close();

String result = writer.toString();

sb.append(result);

try {

long timestamp = System.currentTimeMillis();

String time = formatter.format(new Date());

String fileName = "crash-" + time + "-" + timestamp + ".log";

if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {

String path = "/sdcard/crash/";

File dir = new File(path);

if (!dir.exists()) {

dir.mkdirs();

}

FileOutputStream fos = new FileOutputStream(path + fileName);

fos.write(sb.toString().getBytes());

fos.close();

}

return fileName;

} catch (Exception e) {

Log.e(TAG, "an error occured while writing file...", e);

}

return null;

}

}

在收集异常信息时,我们也可以使用Properties,因为Properties有一个很便捷的方法properties.store(OutputStream out, String comments),用来将Properties实例中的键值对外输到输出流中,但是在使用的过程中发现生成的文件中异常信息打印在同一行,看起来极为费劲,所以换成Map来存放这些信息,然后生成文件时稍加了些操作。

完成这个CrashHandler后,我们需要在一个Application环境中让其运行,为此,我们继承android.app.Application,添加自己的代码,CrashApplication.java代码如下:

package com.scott.crash;

import android.app.Application;

public class CrashApplication extends Application {

@Override

public void onCreate() {

super.onCreate();

CrashHandler crashHandler = CrashHandler.getInstance();

crashHandler.init(getApplicationContext());

}

}

最后,为了让我们的CrashApplication取代android.app.Application的地位,在我们的代码中生效,我们需要修改AndroidManifest.xml:

因为我们上面的CrashHandler中,遇到异常后要保存设备参数和具体异常信息到SDCARD,所以我们需要在AndroidManifest.xml中加入读写SDCARD权限:

这些信息对于开发者来说帮助极大,所以我们需要将此日志文件上传到服务器。目前以及有许多第三方移动平台崩溃收集分析SDK,详情参照移动平台崩溃收集分析系统之 --crashlytics、友盟、bugly、网易云捕对比

参考文章链接:

android 多线程 崩溃,Android异常崩溃捕获相关推荐

  1. android studio try catch自动生成,Android Studio:Try-catch异常崩溃了应用程序

    我正在测试代码中的一些漏洞,并尝试通过在用户输入无效时抛出异常来修复它们.现在当我实现try-catch并在我的手机上运行应用程序时,当我输入无效输入时它会崩溃. 我认为我的代码没有从addData方 ...

  2. android 多线程封装,Android线程池封装库

    目录介绍 1.遇到的问题和需求 1.1 遇到的问题有哪些 1.2 遇到的需求 1.3 多线程通过实现Runnable弊端 1.4 为什么要用线程池 2.封装库具有的功能 2.1 常用的功能 3.封装库 ...

  3. Android多线程优劣,Android 开发中用到的几个多线程解析

    在开发工程中线程可以帮助我们提高运行速度,Android开发中我知道的线程有四个一个是老生长谈的Thread,第二个是asyncTask,第三个:TimetTask,第四个是Looper,四个多线程各 ...

  4. android多线程文章,Android 多线程处理之多线程用法大集合

    类型:服务器区大小:21KB语言:中文 评分:6.6 标签: 立即下载 第 4 页 全部源码 全部源码: 1 package com.bvin.exec; 2 3 import java.io.IOE ...

  5. android 多线程 js,android中的jstack,见见线程都在干嘛

    android中的jstack,看看线程都在干嘛 之前做java开发的时候,遇到进程卡住的情况都会用jstack来打印一个进程里的线程活动情况.到了安卓开发,发现没有这个命令了,很不习惯. googl ...

  6. android 多线程 加锁,android 多线程 — 从一个小例子再次品位多线程

    今天回味 volatile 时看到了别人的一个 Demo: class VolatileDemo() { var flag: Boolean = false fun read() { while (! ...

  7. android 多线程封装,Android 线程池的封装

    GlobalThreadPools.java: /** * 全局使用的线程池 */ public class GlobalThreadPools { private static String TAG ...

  8. android 多线程类,Android 多线程处理之多线程用法大集合

    类型:服务器区大小:21KB语言:中文 评分:6.6 标签: 立即下载 第 3 页 ExecutorServie线程池 5.ExecutorServie线程池 通过Executors的静态方法来创建, ...

  9. android 多线程 场景,Android多线程总结

    场景一:假如APP需要访问两个接口得到数据,在两个接口数据返回时再进行操作下一步. 是不是第一时间想到就是写两个线程就完事了? 上面的解决方案弊端很明显是吧~,~囧.线程无法得知另一个线程的状态 解决 ...

  10. Android 多线程之阻塞队列

    Android 多线程系列 Android 多线程之几个基本问题 阻塞队列 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程.阻塞队列就是生产者存放元素 ...

最新文章

  1. Tutorial——使用Maven开发Cloud Driver
  2. 中兴智能视觉大数据公交车专用道移动智能电子警察系统功能、特点及优势详细介绍...
  3. sobel算子_OpenCV 学习:4 Sobel算子
  4. nodejs+vue.js+webpack
  5. (数据科学学习手札03)Python与R在随机数生成上的异同
  6. Java-静态方法、非静态方法
  7. RabbitMQ官方教程一 Hello World!
  8. golangd 报错信息梳理
  9. Faster RCNN详解
  10. 用C 语言实现斐波那契数列
  11. 计算机怎么连接手机网络,电脑怎么连接手机的热点上网?
  12. mysql网络投票系统设计_学生在线投票表决系统设计与开发(JSP,MySQL)
  13. linux shell写cgi,shell写cgi脚本
  14. 蓝牙扫描startLeScan测试
  15. valgrind安装及使用
  16. 校园网服务器系统方案设计,校园网服务器系统项目设计方案.pdf
  17. QT:黑白棋的吃子规则(七)
  18. Java 实现高并发秒杀
  19. html文本环绕’,css如何使文字环绕显示
  20. 量化初步-《python与量化投资从基础到实战》——优矿策略

热门文章

  1. 博客,跳出日志的围墙[转]
  2. Django笔记7(通用视图)
  3. Java里什么是面向对象?
  4. 图的深度优先遍历和广度优先遍历_图的深度优先遍历(DFS)与广度优先遍历(BFS)的c语言实现...
  5. android判断密码字符串,逆向分析苏宁易购安卓客户端加密到解密获取明文密码(附demo验证) | WooYun...
  6. ant design pro模板_ant design pro 当中改变ant design 组件的样式和 数据管理
  7. C#使用oledb操作excel文件的方法
  8. [译]Javascript中的mutators
  9. jquery日期和时间的插件精确到秒
  10. CCF2014123集合竞价(C语言版)