问题
Android应用不可避免会发生Crash,不管你的代码写得有多风骚,在这个复杂的网络环境中,Crash还是时常的会发生。也就是常说的应用程序发生崩溃。常见表现就是闪屏然后退出。
原因
有些Crash是只在特定网络环境中才会出现,比如说网络环境为2g的时候。而Crash是发生在用户手机端,如果我们不对这些异常信息进行收集,那我们就没办法分析出Crash的原因,也就无法进行修复。下面就介绍一下如何收集这些Crash信息。
问题分析:
首先,Crash是发生在客户端,并且带有不确定性,即有的用户可能发生而又的不发生。所以,我们可以在应用程序发生Crash后,对Crash信息进行收集,保存在本地客户端(如写入的sd卡),然后在合适的时候(如网络环境好)将Crash文件上传到服务器进行统计分析。
技术实现:
Thread中提供了一个方法叫做setDefaultUncaughtUncaughtExceptionHandler(UncaughtExceptionHandler handler)的方法,用来设置一个UncaughtExceptionHandler对象。
这个对象有什么用呢?
作用:当Crash发生的时候,系统就会回调UncaughtExceptionHandler 中的uncaughtException方法。所以我们就可以在这个方法中去保存crash日志。然后在时机好的时候发送至服务器。
具体逻辑
下面代码中,主要是创建了一个实现Thread.UncaughtExceptionHandler接口的类CrashHandler。
然后以单例的形式向外提供支持。
主要的方法有:
- init (Context) 进行初始化
- uncaughtException(Thread , Throwable ) 复写的方法,每次Crash之后被调用
- handleException(Throwable)处理Crash的逻辑
- uploadExceptionToServer()上传到服务器的实现了
- dumpExceptionToSDCard(Throwable )保存到sd卡的实现类
- dumpPhoneInfo(PrintWriter)获取收集信息
实现的逻辑
在Application中调用init()方法初始化CrashHandler,当发生Crash的时候CrashHandler的uncaughtException方法被调用。然后在这个方法中调用handleException方法,handleException方法中再去组织具体逻辑(调用具体实现类)。uploadExceptionToServer、dumpExceptionToSDCard、dumpPhoneInfo这三个方法则是具体逻辑实现。
代码实现:
- CrashHandler 类:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".txt";
private static CrashHandler sInstance = new CrashHandler();
private Context mContext;
//私有构造器
private CrashHandler() {
}
//单例模式
public static CrashHandler getInstance() {
return sInstance;
}
//初始化
public void init(Context context) {
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context;
}
/**
* 重写uncaughtException
* @param t 发生Crash的线程
* @param ex Throwale对象
*/
@Override
public void uncaughtException(Thread t, Throwable ex) {
//处理逻辑需要开启一个子线程,用于文件的写入操作
handleException(ex);
//在程序关闭之前休眠2秒,以避免在文件写入的操作完
//成。之前进程被杀死。
//也可以考虑弹出对话框友好提示用户
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
// Process.killProcess(Process.myPid());
}
//处理异常
private void handleException(final Throwable ex) {
try {
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
dumpExceptionToSDCard(ex);
uploadExceptionToServer();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将异常信息上传至服务器
*/
private void uploadExceptionToServer() {
}
/**
* 将异常信息写入sd卡
* @param ex
*/
private void dumpExceptionToSDCard(Throwable ex) {
//判断是否支持SD卡
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.i(TAG, "sdcard unfind ,skip dump exception");
return;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date(current));
File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
//将抛出的异常信息写入到文件
ex.printStackTrace(pw);
pw.close();
} catch (Exception e) {
Log.d(TAG, "dump Exception Exception" + e.getMessage());
e.printStackTrace();
}
}
/**
*
* 获取手机信息
* @param pw 写入流
* @throws PackageManager.NameNotFoundException 异常
*/
private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.print("App Version : ");
pw.print(pi.versionName);
Log.d(TAG,"name : "+pi.versionName);
pw.print('_');
pw.println(pi.versionCode);
pw.print("OS Version : ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT );
pw.print("Vendor : ");
pw.println(Build.MANUFACTURER);
pw.print("Model : ");
pw.println(Build.MODEL);
pw.print("Cpu ABI : ");
pw.println(Build.CPU_ABI);
}
}
- Application中初始化:
public class App extends Application {
private Context mContext;
private static App sInstance;
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
mContext = getApplicationContext();
}
public Context getContext() {
return mContext;
}
public static App getsInstance() {
return sInstance;
}
}
大致实现就是这样,主要是学习 Thread.UncaughtExceptionHandler和了解uncaughtException方法在进程Crash后会被调用。然后在这个基础上去处理逻辑(重写uncaughtException方法)。
后记
大家可以自己抛出一个异常,去测试一下是否成功。比如在Activity中使用以下代码:
throw new RuntimeException("自定义异常");
成功生成的文件内容:
遇到的坑
- 文件创建失败,可能是文件的路径不规范
- 文件打开权限不足(Read Only),6.0以上需要动态权限申请
- 开启一个子线程去处理文件的读写,并且将当前线程sleep一定的时间。才能保证在文件写完后,程序才被杀死。
- 文件找不到,代码中显示已经生成,而使用DDMS查看或手机上自带的文件管理程序查看的时候却没有。可以考虑直接使用adb shell查看或者使用RE文件管理器。
参考书籍
- 《Android开发与艺术探索》