对于我们编写程序来说,现如今有很多的手机系统都是基于安卓系统开发的,所以就会造成安装Android系统的手机版本和设备有着很多差异,于是乎就会有在模拟器上运行良好的程序安装到某款手机上出现崩溃的现象,但是我们没有那个精力去把所有的手机型号都试一遍,所以当我们把程序发布以后,如果程序存在奔溃的现象,我们作为开发者要及时的获取到导致奔溃的信息,然后在之后更新的版本把这个Bug修复,对于这个情况我们来说下Android中常用到的异常集以及对应的解决方案。(学习自IMOOC:传送门)
异常诠释
异常是指在程序运行过程中所出现的错误。这些错误会干扰到指令的正常执行,从而导致程序异常退出。这些异常出现的场景有:文件找不到,网络连接失败,非法参数等。
异常来源
就Java语言来说,所有的异常都继承自Throwable,看下面异常(代表)的关系图:
- Error是指无法处理的错误,一般是于上层没有关系的,是因为底层系统或者说是Java虚拟机的原因,当Error出现后,上层程序是没有办法控制的。
- Exception是指代码不规范造成的错误,如空指针引用,除零,数组越界等错误
异常分类
大致可以分为以下两类:
- 编译时错误
- 运行时错误
来对这两种异常进行举例吧。
编译期异常(以找不到类为例)
Caused by后面就是报出了异常的名称,再之后是对该异常的解释,我们在对于这一类异常的修正是找到第一个关于你编写的代码段的报错,然后后面所有报出你编写的代码是异常的都是源于第一个报错的代码段,然后点击蓝色的链接就跳到发生异常的地方了。
运行期异常(以空指针为例)
第一个黄框就是报出了异常的名称以及对该异常的解释,我们在对于这一类异常的修正是找到第一个关于你编写的代码段的报错,然后后面所有报出你编写的代码是异常的都是源于第一个报错的代码段,然后点击蓝色的链接就跳到发生异常的地方了。
捕获全局异常
下面这个Dialog一般是当你的程序没有捕获到异常的时候,是由系统默认弹出的一个强制退出的对话框,这是我们不想看到的,因为这样给了用户一个及其不友好的体验,并且对于以后我们对程序的修复是没有一点帮助的。
这样我们就需要一个全局的异常捕获器。这时候我们就需要提到一个接口:UncaughtExceptionHandler,它是定义在Thread中的一个接口,我们可以看到下面这些注释,说明了当线程因未捕获的异常而终止的时候调用,任何被该方法抛出的异常都会被虚拟机忽略。
所以我们就要新建一个类去继承这个接口,然后就是实现他的uncaughtException方法,然后我们来说下我们想要怎么实现异常捕获:
- 收集错误信息
- 保存错误信息
- 上传到服务器
我们来一步一步的说明。
我们记得要进行初始化,并且以单例的模式创建对象
private CrashHandler(){
}
public static CrashHandler getInstance(){
if(mInstance==null){
synchronized (CrashHandler.class){
if(mInstance==null){
mInstance = new CrashHandler();
}
}
}
return mInstance;
}
/**
* 初始化
* @param context
*/
public void init(Context context){
mContext=context;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
我们要先判断是否已经处理过了异常,因为这样就有两个不同的后效性,如果异常是未处理的,我们依照注释中说的就要调用系统默认的处理器处理,也就是我们就不喜欢的那种方法,弹出系统默认的强制退出的对话框,如果我们是已经处理过了的话,我们只要把线程关闭了,并且强制关闭应用就好了。
public void uncaughtException(Thread t, Throwable e) {
if(!handleException(e)){
//未处理,调用系统默认的处理器处理
if(mDefaultHandler!=null){
mDefaultHandler.uncaughtException(t,e);//弹出系统默认的强制退出的对话框
}
}
else{
//已经人为处理
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Process.killProcess(Process.myPid());
System.exit(1);
}
}
handleException方法就是用来处理异常的,没有特殊情况都会处理成功。
然后我们就在里面来进行我们一开始说的那三步,因为我们没有服务器,所以我们就不要上传服务器了。当然我们要先判断这个异常是不是有信息的,如果是有信息的我们肯定要给用户一个提示,但是因为CrashHandler必须要在UI线程的,所以这个操作就要在一个新的线程中进行了。
/**
* 人为处理异常
* @param e
* @return true:已经处理 false:没有处理
*/
private boolean handleException(Throwable e) {
if(e==null){
return false;
}
//Toast提示
new Thread(){//因为CrashHandler必须要在UI线程的,所以要new一个线程
@Override
public void run() {//如果线程中使用Looper.prepare()和Looper.loop()创建了消息队列就可以让消息处理在该线程中完成。
Looper.prepare();//关联一个Looper对象
Toast.makeText(mContext, "UncaughtException", Toast.LENGTH_SHORT).show();
Looper.loop();//让Looper开始工作,从消息队列里取消息,处理消息。
}
}.start();
//收集错误信息(设备型号,设备品牌,系统版本,应用版本,异常信息)
collectErrorInfo();
//保存错误信息
saveErrorInfo(e);
//上传到服务器
return false;
}
然后就是开始收集错误信息了,错误信息就是我们之前说的我们会因为版本以及手机型号导致出不同的异常,所以我们的错误信息要包括设备型号,设备品牌,系统版本,应用版本,异常信息。所以我们就要获取到我们的版本名字以及版本号,然后我们通过反射获取信息,通过HashMap存储这些信息。
private void collectErrorInfo() {
PackageManager pm = mContext.getPackageManager();//获取应用程序信息
try {
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(),PackageManager.GET_ACTIVITIES);
if(pi!=null){
String versionName = TextUtils.isEmpty(pi.versionName)?"未设置版本名称":pi.versionName;//版本名称,有可能未设置
String versionCode = pi.versionCode+"";//版本号,有可能未设置,默认是0
mInfo.put("versionName",versionName);
mInfo.put("versionCode",versionCode);
}
//反射类中的Field
Field[] fields = Build.class.getFields();
if(fields!=null&&fields.length>0){
for(Field field:fields){
field.setAccessible(true);
mInfo.put(field.getName(),field.get(null).toString());
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
最后就要进行信息的存储了,因为我们要不断的拼接字符串,所以我们就用StringBuffer来存储信息,我们先把所有存在HashMap中的数据取出来,然后我们通过Writer和PrintWriter,然后通过循环把异常写进去,然后读取成字符串就好了,最后把这个字符串以文件的形式存入SD卡中。
当然PrintWriter和OutputStreamWriter有什么区别呢?
- PrintWriter:以字符为单位,支持汉字
- OutputStreamWriter:以字节为单位,不支持汉字
当然存入SD卡要记得判断有没有SD卡,并且看路径有没有存在,如果没有存在就要新建该路径。然后通过流的形式存入文件中。
private void saveErrorInfo(Throwable e) {
StringBuffer stringBuffer = new StringBuffer();
for (Map.Entry<String,String> entry:mInfo.entrySet()){
String keyname = entry.getKey();
String value = entry.getValue();
stringBuffer.append(keyname+"="+value+"\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
e.printStackTrace(printWriter);
Throwable cause = e.getCause();
while (cause!=null){
cause.printStackTrace(printWriter);
cause = e.getCause();
}
printWriter.close();
String result = writer.toString();
stringBuffer.append(result);
long curTime = System.currentTimeMillis();
String time = dateFormat.format(curTime);
String fileName = "crash-"+time+"-"+curTime+".log";
//判断有没有SD卡
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
String path = "/sdcard/crash/";
File dir = new File(path);
if(!dir.exists()){
dir.mkdirs();//新建
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(path+fileName);
fos.write(stringBuffer.toString().getBytes());
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
finally {
try {
fos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
我们说过他是一个全局的处理器,所以我们要在Application中去调用它。
package com.gin.xjh.androiderrors;
import android.app.Application;
/**
* Created by Gin on 2018/2/24.
*/
public class CrashApplication extends Application {
private CrashHandler mCrashHandler;
@Override
public void onCreate() {
super.onCreate();
mCrashHandler = CrashHandler.getInstance();
mCrashHandler.init(this);
}
}
最后在AndroidManifest中定义下Application就大功告成了:
android:name=".CrashApplication"
然后我们把这个文件上传服务器就好了。
错误分析工具Bugly
其实我们可以直接用腾讯的这个平台Bugly,他很好的帮助了我们来分析异常,怎么使用我们就按照他的开发文档进行操作就好了。Bugly官网:戳这里