Android崩潰處理
我們寫程序的時候都希望能寫出一個沒有任何Bug的程序,期望在任何情況下都不會發生程序崩潰。但沒有一個程序員能保證自己寫的程序絕對不會出現異常崩潰。特別是當你用戶數達到一定數量級后,你也更容易發現應用不同情況下的崩潰。
對于還沒發布的應用程序,我們可以通過測試、分析Log的方法來收集崩潰信息。但對已經發布的程序,我們不可能讓用戶去查看崩潰信息然后再反饋給開發者。所以,設計一個對于小白用戶都可以輕松實現反饋的應用就顯得很重要了。我這里結合我自己寫的一個Demo,來分析從崩潰開始到崩潰信息反饋到我們服務器,我們程序都需要做什么。
當我們的程序因未捕獲的異常而突然終止時,系統會調用處理程序的接口UncaughtExceptionHandler。如果我們想處理未被程序正常捕獲的異常,只需實現這個接口里的uncaughtException方法,uncaughtException方法回傳了Thread 和 Throwable兩個參數。通過這兩個參數,我們來對異常進行我們需要的處理。
綜上,我對異常處理方式的思路是這樣的:
1.我們需要首先收集產生崩潰的手機信息,因為Android的樣機種類繁多,很可能某些特定機型下會產生莫名的bug。
2.將手機的信息和崩潰信息寫入文件系統中。這樣方便后續處理。3.崩潰的應用需要可以自動重啟。重啟的頁面設置成反饋頁面,詢問 用戶是否需要上傳崩潰報告。
4.用戶同意后,即將2中寫入的崩潰信息文件發送到自己的服務器。
</blockquote>通過上面的步驟,我們就可以寫出大概的偽代碼:
handleException() { collectDeviceInfo(context); //手機手機信息 writeCrashInfoToFile(ex); //寫入崩潰文件 restart(); //應用重啟 }最后,在重啟頁面通過AsyncTask將崩潰信息上傳服務器。
有了以上思路,我們一步一步的寫出每個偽函數的具體代碼。
1.收集手機的信息:
/*
- @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); 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); } } }</pre>
2.崩潰和手機信息寫入文件:
/**- @param ex
- 將崩潰寫入文件系統 / 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); //這里把剛才異常堆棧信息寫入SD卡的Log日志里面 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { String sdcardPath = Environment.getExternalStorageDirectory().getPath(); String filePath = sdcardPath + "/cym/crash/"; localFileUrl = writeLog(sb.toString(), filePath); } } /**
- @param log
- @param name
- @return 返回寫入的文件路徑
- 寫入Log信息的方法,寫入到SD卡里面 */ 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 { Log.d("TAG", "寫入到SD卡里面"); // 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; } }</pre>
3.重啟應用:
注:我嘗試過好多種應用重啟的方法,最終選擇采用PendingIntent的方式。 private void restart(){ try{}</pre>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); /** * 這里重點注意: * name里面的值為服務器端需要key 只有這個key 才可以得到對應的文件 * filename是文件的名字,包含后綴名的 比如:abc.png */ 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(); /** * 獲取響應碼 200=成功 * 當響應成功,獲取響應的流 */ int res = conn.getResponseCode(); Log.e(TAG, "response code:"+res); // if(res==200) // { Log.e(TAG, "request success"); InputStream input = conn.getInputStream(); StringBuffer sb1= new StringBuffer(); int ss ; while((ss=input.read())!=-1) { sb1.append((char)ss); } result = sb1.toString(); Log.e(TAG, "result : "+ result); // }
// else{ // Log.e(TAG, "request error"); // } } }catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
return result; }</pre>
整個流程基本走完,我們來看一下最終效果。(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)總結
通過上面的文件,我們就可以分析什么時候產生崩潰,什么機型下會產生崩潰。
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。
來自:http://blog.saymagic.cn/2014/09/25/Android%E5%B4%A9%E6%BA%83%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%9E%90.html