Android崩潰處理

jopen 9年前發布 | 35K 次閱讀 Android 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{ 
    
         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秒鐘后重啟應用 
    
    }</pre>

    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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!