Android崩溃处理

  我们写程序的时候都希望能写出一个没有任何Bug的程序,期望在任何情况下都不会发生程序崩溃。但没有一个程序员能保证自己写的程序绝对不会出现异常崩溃。特别是当你用户数达到一定数量级后,你也更容易发现应用不同情况下的崩溃。

  对于还没发布的应用程序,我们可以通过测试、分析Log的方法来收集崩溃信息。但对已经发布的程序,我们不可能让用户去查看崩溃信息然后再反馈给开发者。所以,设计一个对于小白用户都可以轻松实现反馈的应用就显得很重要了。我这里结合我自己写的一个Demo,来分析从崩溃开始到崩溃信息反馈到我们服务器,我们程序都需要做什么。

当我们的程序因未捕获的异常而突然终止时,系统会调用处理程序的接口UncaughtExceptionHandler。如果我们想处理未被程序正常捕获的异常,只需实现这个接口里的uncaughtException方法,uncaughtException方法回传了Thread 和 Throwable两个参数。通过这两个参数,我们来对异常进行我们需要的处理。

综上,我对异常处理方式的思路是这样的:

1.我们需要首先收集产生崩溃的手机信息,因为Android的样机种类繁多,很可能某些特定机型下会产生莫名的bug。 2.将手机的信息和崩溃信息写入文件系统中。这样方便后续处理。

3.崩溃的应用需要可以自动重启。重启的页面设置成反馈页面,询问 用户是否需要上传崩溃报告。

4.用户同意后,即将2中写入的崩溃信息文件发送到自己的服务器。

通过上面的步骤,我们就可以写出大概的伪代码:

handleException() {
	  collectDeviceInfo(context);
	  writeCrashInfoToFile(ex);
	  restart(); //应用重启
}

最后,在重启页面通过AsyncTask将崩溃信息上传服务器。

有了以上思路,我们一步一步的写出每个伪函数的具体代码。

1.收集手机的信息:

 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);
    infos.put("crashTime", formatter.format(new Date()));
   }
  } 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);
   }
  }
 }

2.崩溃和手机信息写入文件:

 private void writeCrashInfoToFile(Throwable ex) {
  StringBuffer sb = new StringBuffer();
  for (Map.Entry<String, String> 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);	  if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
  {
   String sdcardPath = Environment.getExternalStorageDirectory().getPath();
   String filePath = sdcardPath + "/cym/crash/";
   localFileUrl = writeLog(sb.toString(), filePath);
  }
 }

 private String writeLog(String log, String name)
 {
  CharSequence timestamp = new Date().toString().replace(" ", "");
  timestamp = "crash";
  String filename = name + timestamp + ".log";
  File file = new File(filename);
  if(!file.getParentFile().exists()){
   file.getParentFile().mkdirs();
  }
  try
  {
   //	 FileOutputStream stream = new FileOutputStream(new File(filename));
   //	 OutputStreamWriter output = new OutputStreamWriter(stream);
   file.createNewFile();
   FileWriter fw=new FileWriter(file,true);
   BufferedWriter bw = new BufferedWriter(fw);
   //写入相关Log到文件
   bw.write(log);
   bw.newLine();
   bw.close();
   fw.close();
   return filename;
  }
  catch (IOException e)
  {
   Log.e(TAG, "an error occured while writing file...", e);
   e.printStackTrace();
   return null;
  }
 }

3.重启应用:

注:采用PendingIntent的方式进行重启。
private void restart(){
   try{
             Thread.sleep(2000);
         }catch (InterruptedException e){
             Log.e(TAG, "error : ", e);
         }
         Intent intent = new Intent(context.getApplicationContext(), SendCrashActivity.class);
         PendingIntent restartIntent = PendingIntent.getActivity(
           context.getApplicationContext(), 0, intent,
                 Intent.FLAG_ACTIVITY_NEW_TASK);
         //退出程序
         AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
         mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 1000,
                 restartIntent); // 1秒钟后重启应用
 }

4.上传崩溃

应用重启后来到的是SendCrashActivity界面,在这里我设置了一个简单的按钮,点击后即可上传崩溃信息。代码比较多,这里列一个比较有用的上传方法吧:

public static String uploadFile(File file,String requestUrl){
  String result = null;
  String BOUNDARY = UUID.randomUUID().toString(); 	  String PREFIX = "--" ;
  String LINE_END = "\r\n";
  String CONTENT_TYPE = "multipart/form-data";	  try{
   URL url = new URL(requestUrl);
   HttpURLConnection conn = (HttpURLConnection) url.openConnection();
   conn.setReadTimeout(TIME_OUT);
   conn.setConnectTimeout(TIME_OUT);
   conn.setDoInput(true);
   conn.setDoOutput(true);
   	conn.setUseCaches(false);
   conn.setRequestMethod("POST");
   conn.setRequestProperty("Charset", CHARSET);
   conn.setRequestProperty("connection", "keep-alive");
   conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY);


   if(file!=null)
   {
    DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
    StringBuffer sb = new StringBuffer();
    sb.append(PREFIX);
    sb.append(BOUNDARY);
    sb.append(LINE_END);
    sb.append("Content-Disposition: form-data; name=\"uploadcrash\"; filename=\""+file.getName()+"\""+LINE_END);
    sb.append("Content-Type: application/octet-stream; charset="+CHARSET+LINE_END);
    sb.append(LINE_END);
    dos.write(sb.toString().getBytes());
    InputStream is = new FileInputStream(file);
    byte[] bytes = new byte[1024];
    int len = 0;
    while((len=is.read(bytes))!=-1)
    {
     dos.write(bytes, 0, len);
    }
    is.close();
    dos.write(LINE_END.getBytes());
    byte[] end_data = (PREFIX+BOUNDARY+PREFIX+LINE_END).getBytes();
    dos.write(end_data);
    dos.flush();
    int res = conn.getResponseCode();
    InputStream input = conn.getInputStream();
    StringBuffer sb1= new StringBuffer();
    int ss ;
    while((ss=input.read())!=-1)
    {
     sb1.append((char)ss);
    }
    result = sb1.toString();
   }
  }catch (MalformedURLException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }

  return result;
 }

整个流程基本走完,我们来看一下最终效果。(MainActivity点击按钮后执行了一个2/0的操作,所以崩溃)

我将崩溃上传到了我的sae服务器的storage里。下图中红色圈起来的文件即是我们上传的崩溃文件。

我把这个文件下载下来,内容如下:

TIME=1383016889000
FINGERPRINT=generic/sdk/generic:4.4/KRT16L/892118:eng/test-keys
HARDWARE=goldfish
UNKNOWN=unknown
RADIO=unknown
BOARD=unknown
versionCode=1
PRODUCT=sdk
versionName=1.0
DISPLAY=sdk-eng 4.4 KRT16L 892118 test-keys
USER=android-build
HOST=vpak27.mtv.corp.google.com
DEVICE=generic
TAGS=test-keys
MODEL=sdk
BOOTLOADER=unknown
crashTime=2014-09-24 05:39:21
CPU_ABI=armeabi-v7a
CPU_ABI2=armeabi
IS_DEBUGGABLE=true
ID=KRT16L
SERIAL=unknown
MANUFACTURER=unknown
BRAND=generic
TYPE=eng
java.lang.IllegalStateException: Could not execute method of the activity
 at android.view.View$1.onClick(View.java:3814)
 at android.view.View.performClick(View.java:4424)
 at android.view.View$PerformClick.run(View.java:18383)
 at android.os.Handler.handleCallback(Handler.java:733)
 at android.os.Handler.dispatchMessage(Handler.java:95)
 at android.os.Looper.loop(Looper.java:137)
 at android.app.ActivityThread.main(ActivityThread.java:4998)
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
 at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.reflect.InvocationTargetException
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at android.view.View$1.onClick(View.java:3809)
 ... 11 more
Caused by: java.lang.ArithmeticException: divide by zero
 at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
 ... 14 more
java.lang.reflect.InvocationTargetException
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at android.view.View$1.onClick(View.java:3809)
 at android.view.View.performClick(View.java:4424)
 at android.view.View$PerformClick.run(View.java:18383)
 at android.os.Handler.handleCallback(Handler.java:733)
 at android.os.Handler.dispatchMessage(Handler.java:95)
 at android.os.Looper.loop(Looper.java:137)
 at android.app.ActivityThread.main(ActivityThread.java:4998)
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
 at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ArithmeticException: divide by zero
 at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
 ... 14 more
java.lang.ArithmeticException: divide by zero
 at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at android.view.View$1.onClick(View.java:3809)
 at android.view.View.performClick(View.java:4424)
 at android.view.View$PerformClick.run(View.java:18383)
 at android.os.Handler.handleCallback(Handler.java:733)
 at android.os.Handler.dispatchMessage(Handler.java:95)
 at android.os.Looper.loop(Looper.java:137)
 at android.app.ActivityThread.main(ActivityThread.java:4998)
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
 at dalvik.system.NativeStart.main(Native Method)

第三方工具

市面上还是有很多不错的第三方崩溃统计产品来供我们使用,这里安利几款:

bughd:这是国内Fir.im的一款产品,使用起来非常简单。

fabric: Twitter出品,非常强大。

当然,如果仔细寻找,这类产品还是相当多的,如果项目中集成了Umeng,它会自带崩溃统计;听云的移动监控也很是不错;腾讯出品的能监控ANR的bugly。这些产品我也只是尝试过,并未过多使用,不做过多评价。

总结

通过上面的文件,我们就可以分析什么时候产生崩溃,什么机型下会产生崩溃。

Android里有一种比较常见的问题,那就是ANR,关于ANR的相关知识可以阅读我的另一篇博文http://blog.saymagic.cn/2014/03/25/anr-analyze.html