Android APP崩溃上传日志到服务器并且重启!

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

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

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

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

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

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

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

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

 
 
  1. handleException() {
  2. collectDeviceInfo(context); //手机手机信息
  3. writeCrashInfoToFile(ex); //写入崩溃文件
  4. restart(); //应用重启
  5. }

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

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

1.收集手机的信息:

 
 
  1. /**
  2. *
  3. * @param ctx
  4. * 手机设备相关信息
  5. */
  6. public void collectDeviceInfo(Context ctx) {
  7. try {
  8. PackageManager pm = ctx.getPackageManager();
  9. PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
  10. PackageManager.GET_ACTIVITIES);
  11. if (pi != null) {
  12. String versionName = pi.versionName == null ? "null"
  13. : pi.versionName;
  14. String versionCode = pi.versionCode + "";
  15. infos.put("versionName", versionName);
  16. infos.put("versionCode", versionCode);
  17. infos.put("crashTime", formatter.format(new Date()));
  18. }
  19. } catch (NameNotFoundException e) {
  20. Log.e(TAG, "an error occured when collect package info", e);
  21. }
  22. Field[] fields = Build.class.getDeclaredFields();
  23. for (Field field: fields) {
  24. try {
  25. field.setAccessible(true);
  26. infos.put(field.getName(), field.get(null).toString());
  27. Log.d(TAG, field.getName() + " : " + field.get(null));
  28. } catch (Exception e) {
  29. Log.e(TAG, "an error occured when collect crash info", e);
  30. }
  31. }
  32. }

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

 
 
  1. /**
  2. *
  3. * @param ex
  4. * 将崩溃写入文件系统
  5. */
  6. private void writeCrashInfoToFile(Throwable ex) {
  7. StringBuffer sb = new StringBuffer();
  8. for (Map.Entry<String, String> entry: infos.entrySet()) {
  9. String key = entry.getKey();
  10. String value = entry.getValue();
  11. sb.append(key + "=" + value + "\n");
  12. }
  13. Writer writer = new StringWriter();
  14. PrintWriter printWriter = new PrintWriter(writer);
  15. ex.printStackTrace(printWriter);
  16. Throwable cause = ex.getCause();
  17. while (cause != null) {
  18. cause.printStackTrace(printWriter);
  19. cause = cause.getCause();
  20. }
  21. printWriter.close();
  22. String result = writer.toString();
  23. sb.append(result);
  24. //这里把刚才异常堆栈信息写入SD卡的Log日志里面
  25. if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
  26. {
  27. String sdcardPath = Environment.getExternalStorageDirectory().getPath();
  28. String filePath = sdcardPath + "/cym/crash/";
  29. localFileUrl = writeLog(sb.toString(), filePath);
  30. }
  31. }
  32. /**
  33. *
  34. * @param log
  35. * @param name
  36. * @return 返回写入的文件路径
  37. * 写入Log信息的方法,写入到SD卡里面
  38. */
  39. private String writeLog(String log, String name)
  40. {
  41. CharSequence timestamp = new Date().toString().replace(" ", "");
  42. timestamp = "crash";
  43. String filename = name + timestamp + ".log";
  44. File file = new File(filename);
  45. if(!file.getParentFile().exists()){
  46. file.getParentFile().mkdirs();
  47. }
  48. try
  49. {
  50. Log.d("TAG", "写入到SD卡里面");
  51. // FileOutputStream stream = new FileOutputStream(new File(filename));
  52. // OutputStreamWriter output = new OutputStreamWriter(stream);
  53. file.createNewFile();
  54. FileWriter fw=new FileWriter(file,true);
  55. BufferedWriter bw = new BufferedWriter(fw);
  56. //写入相关Log到文件
  57. bw.write(log);
  58. bw.newLine();
  59. bw.close();
  60. fw.close();
  61. return filename;
  62. }
  63. catch (IOException e)
  64. {
  65. Log.e(TAG, "an error occured while writing file...", e);
  66. e.printStackTrace();
  67. return null;
  68. }
  69. }

3.重启应用:

 
 
  1. 注:我尝试过好多种应用重启的方法,最终选择采用PendingIntent的方式。
  2. private void restart(){
  3. try{
  4. Thread.sleep(2000);
  5. }catch (InterruptedException e){
  6. Log.e(TAG, "error : ", e);
  7. }
  8. Intent intent = new Intent(context.getApplicationContext(), SendCrashActivity.class);
  9. PendingIntent restartIntent = PendingIntent.getActivity(
  10. context.getApplicationContext(), 0, intent,
  11. Intent.FLAG_ACTIVITY_NEW_TASK);
  12. //退出程序
  13. AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
  14. mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 1000,
  15. restartIntent); // 1秒钟后重启应用
  16. }

4.上传崩溃

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

 
 
  1. public static String uploadFile(File file,String requestUrl){
  2. String result = null;
  3. String BOUNDARY = UUID.randomUUID().toString(); //边界标识 随机生成
  4. String PREFIX = "--" ;
  5. String LINE_END = "\r\n";
  6. String CONTENT_TYPE = "multipart/form-data"; //内容类型
  7. try{
  8. URL url = new URL(requestUrl);
  9. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  10. conn.setReadTimeout(TIME_OUT);
  11. conn.setConnectTimeout(TIME_OUT);
  12. conn.setDoInput(true); //允许输入流
  13. conn.setDoOutput(true); //允许输出流
  14. conn.setUseCaches(false); //不允许使用缓存
  15. conn.setRequestMethod("POST"); //请求方式
  16. conn.setRequestProperty("Charset", CHARSET); //设置编码
  17. conn.setRequestProperty("connection", "keep-alive");
  18. conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY);
  19. if(file!=null)
  20. {
  21. /**
  22. * 当文件不为空,把文件包装并且上传
  23. */
  24. DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
  25. StringBuffer sb = new StringBuffer();
  26. sb.append(PREFIX);
  27. sb.append(BOUNDARY);
  28. sb.append(LINE_END);
  29. /**
  30. * 这里重点注意:
  31. * name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
  32. * filename是文件的名字,包含后缀名的 比如:abc.png
  33. */
  34. sb.append("Content-Disposition: form-data; name=\"uploadcrash\"; filename=\""+file.getName()+"\""+LINE_END);
  35. sb.append("Content-Type: application/octet-stream; charset="+CHARSET+LINE_END);
  36. sb.append(LINE_END);
  37. dos.write(sb.toString().getBytes());
  38. InputStream is = new FileInputStream(file);
  39. byte[] bytes = new byte[1024];
  40. int len = 0;
  41. while((len=is.read(bytes))!=-1)
  42. {
  43. dos.write(bytes, 0, len);
  44. }
  45. is.close();
  46. dos.write(LINE_END.getBytes());
  47. byte[] end_data = (PREFIX+BOUNDARY+PREFIX+LINE_END).getBytes();
  48. dos.write(end_data);
  49. dos.flush();
  50. /**
  51. * 获取响应码 200=成功
  52. * 当响应成功,获取响应的流
  53. */
  54. int res = conn.getResponseCode();
  55. Log.e(TAG, "response code:"+res);
  56. // if(res==200)
  57. // {
  58. Log.e(TAG, "request success");
  59. InputStream input = conn.getInputStream();
  60. StringBuffer sb1= new StringBuffer();
  61. int ss ;
  62. while((ss=input.read())!=-1)
  63. {
  64. sb1.append((char)ss);
  65. }
  66. result = sb1.toString();
  67. Log.e(TAG, "result : "+ result);
  68. // }
  69. // else{
  70. // Log.e(TAG, "request error");
  71. // }
  72. }
  73. }catch (MalformedURLException e) {
  74. e.printStackTrace();
  75. } catch (IOException e) {
  76. e.printStackTrace();
  77. }
  78. return result;
  79. }

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

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

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

 
 
  1. TIME=1383016889000
  2. FINGERPRINT=generic/sdk/generic:4.4/KRT16L/892118:eng/test-keys
  3. HARDWARE=goldfish
  4. UNKNOWN=unknown
  5. RADIO=unknown
  6. BOARD=unknown
  7. versionCode=1
  8. PRODUCT=sdk
  9. versionName=1.0
  10. DISPLAY=sdk-eng 4.4 KRT16L 892118 test-keys
  11. USER=android-build
  12. HOST=vpak27.mtv.corp.google.com
  13. DEVICE=generic
  14. TAGS=test-keys
  15. MODEL=sdk
  16. BOOTLOADER=unknown
  17. crashTime=2014-09-24 05:39:21
  18. CPU_ABI=armeabi-v7a
  19. CPU_ABI2=armeabi
  20. IS_DEBUGGABLE=true
  21. ID=KRT16L
  22. SERIAL=unknown
  23. MANUFACTURER=unknown
  24. BRAND=generic
  25. TYPE=eng
  26. java.lang.IllegalStateException: Could not execute method of the activity
  27. at android.view.View$1.onClick(View.java:3814)
  28. at android.view.View.performClick(View.java:4424)
  29. at android.view.View$PerformClick.run(View.java:18383)
  30. at android.os.Handler.handleCallback(Handler.java:733)
  31. at android.os.Handler.dispatchMessage(Handler.java:95)
  32. at android.os.Looper.loop(Looper.java:137)
  33. at android.app.ActivityThread.main(ActivityThread.java:4998)
  34. at java.lang.reflect.Method.invokeNative(Native Method)
  35. at java.lang.reflect.Method.invoke(Method.java:515)
  36. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
  37. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
  38. at dalvik.system.NativeStart.main(Native Method)
  39. Caused by: java.lang.reflect.InvocationTargetException
  40. at java.lang.reflect.Method.invokeNative(Native Method)
  41. at java.lang.reflect.Method.invoke(Method.java:515)
  42. at android.view.View$1.onClick(View.java:3809)
  43. ... 11 more
  44. Caused by: java.lang.ArithmeticException: divide by zero
  45. at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
  46. ... 14 more
  47. java.lang.reflect.InvocationTargetException
  48. at java.lang.reflect.Method.invokeNative(Native Method)
  49. at java.lang.reflect.Method.invoke(Method.java:515)
  50. at android.view.View$1.onClick(View.java:3809)
  51. at android.view.View.performClick(View.java:4424)
  52. at android.view.View$PerformClick.run(View.java:18383)
  53. at android.os.Handler.handleCallback(Handler.java:733)
  54. at android.os.Handler.dispatchMessage(Handler.java:95)
  55. at android.os.Looper.loop(Looper.java:137)
  56. at android.app.ActivityThread.main(ActivityThread.java:4998)
  57. at java.lang.reflect.Method.invokeNative(Native Method)
  58. at java.lang.reflect.Method.invoke(Method.java:515)
  59. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
  60. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
  61. at dalvik.system.NativeStart.main(Native Method)
  62. Caused by: java.lang.ArithmeticException: divide by zero
  63. at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
  64. ... 14 more
  65. java.lang.ArithmeticException: divide by zero
  66. at so.cym.crashhandlerdemo.MainActivity.generateAnr(MainActivity.java:20)
  67. at java.lang.reflect.Method.invokeNative(Native Method)
  68. at java.lang.reflect.Method.invoke(Method.java:515)
  69. at android.view.View$1.onClick(View.java:3809)
  70. at android.view.View.performClick(View.java:4424)
  71. at android.view.View$PerformClick.run(View.java:18383)
  72. at android.os.Handler.handleCallback(Handler.java:733)
  73. at android.os.Handler.dispatchMessage(Handler.java:95)
  74. at android.os.Looper.loop(Looper.java:137)
  75. at android.app.ActivityThread.main(ActivityThread.java:4998)
  76. at java.lang.reflect.Method.invokeNative(Native Method)
  77. at java.lang.reflect.Method.invoke(Method.java:515)
  78. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
  79. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
  80. at dalvik.system.NativeStart.main(Native Method)

总结

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

Android里有一种崩溃(严格意义将不叫崩溃)是捕获不到的,那就是ANR,关于ANR的相关知识可以阅读我的另一篇博文http://blog.saymagic.cn/2014/09/25/ANR%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%9E%90.html

如果你对源码感兴趣,欢迎到此处进行star或者fork:https://gitcafe.com/saymagic/AndroidCrashHandler

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值