Android程序crash處理

       轉載請註明出處:http://blog.csdn.net/allen315410/article/details/41444053 

       在實際項目開發中,會出現很多的異常直接導致程序crash掉,在開發中我們可以通過logcat查看錯誤日誌,Debug出現的異常,讓程序安全的運行,但是在開發中有些異常隱藏的比較深,直到項目發佈後,由於各種原因,譬如android設備不一致等等,android版本不同,實際上我們在測試的時候不可能在市場上所有的Android設備上都做了測試,當用戶安裝使用時被暴露出來,導致程序直接crash掉,這顯然對於用戶是不OK的!這些在用戶設備上導致crash的異常我們是不知道的,要想知道這些異常出現的一些信息,我們還是得自己通過程序捕獲到異常,並且將其記錄下來(本地保存或者上傳服務器),方便項目維護。

        先來看一下,我自己“故意”定義出來的一個異常,在MainActivity,java中:

package com.example.crash;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		int i = 1;
		System.out.println(i/0);
	}
}
以上程序報出一個數學運算的除0異常,顯然程序被“崩潰”,不能繼續執行的。看一下運行效果截圖:



       運行結果如上圖所示,這種直接崩潰的效果對於用戶來說是很不OK的,用戶不知道發生了什麼,程序就停止了,會讓用戶對程序有種不想繼續使用的想法。 對於程序中未捕獲的異常,我們可以做哪些操作呢!我們需要的是軟件有一個全局的異常捕獲器,當出現一個我們沒有發現的異常時,捕獲這個異常,並且將異常信息記錄下來,上傳到服務器公開發這分析出現異常的具體原因,這是一種最佳實踐,那麼我們接下來就必須要熟悉兩個類別,一個是android提供的Application,另一個是Java提供的Thread.UncaughtExceptionHandler。

      Application:這是android程序管理全局狀態的類,Application在程序啓動的時候首先被創建出來,它被用來統一管理activity、service、broadcastreceiver、contentprovider四大組件以及其他android元素,這裏可以打開android工程下的Mainifest.xml文件查看一下。我們除了使用android默認的Application來處理程序,也可以自定義一個Application處理一些需要在全局狀態下控制程序的操作,例如本文講到的處理程序未知異常時,這是一種最佳實踐。

     Thread.UncaughtExceptionHandler:關於這個概念的解釋,我在JDK1.6的文檔中找到一些科學的解釋。

       當 Thread 因未捕獲的異常而突然終止時,調用處理程序的接口。
      當某一線程因未捕獲的異常而即將終止時,Java 虛擬機將使用 Thread.getUncaughtExceptionHandler() 查詢該線程以獲得其 UncaughtExceptionHandler 的線程,並調用處理程序的 uncaughtException 方法,將線程和異常作爲參數傳遞。如果某一線程沒有明確設置其 UncaughtExceptionHandler,則將它的 ThreadGroup 對象作爲其 UncaughtExceptionHandler。如果 ThreadGroup 對象對處理異常沒有什麼特殊要求,那麼它可以將調用轉發給默認的未捕獲異常處理程序。

     Thread.UncaughtExceptionHandler是一個接口,它提供如下的方法,讓我們自定義處理程序。

     void uncaughtException(Thread t,Throwable e)  

    當給定線程因給定的未捕獲異常而終止時,調用該方法。Java 虛擬機將忽略該方法拋出的任何異常。參數:t - 線程  e - 異常

       一句話,線程未捕獲異常處理器,用來處理未捕獲異常。如果程序出現了未捕獲異常,默認會彈出系統中強制關閉對話框。我們需要實現此接口,並註冊爲程序中默認未捕獲異常處理。這樣當未捕獲異常發生時,就可以做一些個性化的異常處理操作。所以接下來,我們要做的就是自定義一個CrashHandler類去實現Thread.UncaughtExceptionHandler,並且在實現的方法中做一些相關的操作。

package com.example.crash;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;

public class CrashHandler implements UncaughtExceptionHandler {
	public static final String TAG = "CrashHandler";

	// 系統默認的UncaughtException處理類
	private Thread.UncaughtExceptionHandler mDefaultHandler;
	// CrashHandler實例
	private static CrashHandler INSTANCE = new CrashHandler();
	// 程序的Context對象
	private Context mContext;
	// 用來存儲設備信息和異常信息
	private Map<String, String> infos = new HashMap<String, String>();
	// 用於格式化日期,作爲日誌文件名的一部分
	private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

	/** 保證只有一個CrashHandler實例 */
	private CrashHandler() {
	}

	/** 獲取CrashHandler實例 ,單例模式 */
	public static CrashHandler getInstance() {
		return INSTANCE;
	}

	/**
	 * 初始化
	 * 
	 * @param context
	 */
	public void init(Context context) {
		mContext = context;
		// 獲取系統默認的UncaughtException處理器
		mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
		// 設置該CrashHandler爲程序的默認處理器
		Thread.setDefaultUncaughtExceptionHandler(this);
	}

	/**
	 * 當UncaughtException發生時會轉入該函數來處理
	 */
	@Override
	public void uncaughtException(Thread thread, Throwable ex) {
		if (!handleException(ex) && mDefaultHandler != null) {
			// 如果用戶沒有處理則讓系統默認的異常處理器來處理
			mDefaultHandler.uncaughtException(thread, ex);
		} else {
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				Log.e(TAG, "error : ", e);
			}
			// 退出程序
			android.os.Process.killProcess(android.os.Process.myPid());
			System.exit(1);
		}
	}

	/**
	 * 自定義錯誤處理,收集錯誤信息 發送錯誤報告等操作均在此完成.
	 * 
	 * @param ex
	 * @return true:如果處理了該異常信息;否則返回false.
	 */
	private boolean handleException(Throwable ex) {
		if (ex == null) {
			return false;
		}
		// 使用Toast來顯示異常信息
		new Thread() {
			@Override
			public void run() {
				Looper.prepare();
				Toast.makeText(mContext, "很抱歉,程序出現異常,即將退出.", Toast.LENGTH_LONG)
						.show();
				Looper.loop();
			}
		}.start();
		// 收集設備參數信息
		collectDeviceInfo(mContext);
		// 保存日誌文件
		saveCrashInfo2File(ex);
		return true;
	}

	/**
	 * 收集設備參數信息
	 * 
	 * @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);
			}
		} 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);
			}
		}
	}

	/**
	 * 保存錯誤信息到文件中
	 * 
	 * @param ex
	 * @return 返回文件名稱,便於將文件傳送到服務器
	 */
	private String saveCrashInfo2File(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);
		try {
			long timestamp = System.currentTimeMillis();
			String time = formatter.format(new Date());
			String fileName = "crash-" + time + "-" + timestamp + ".log";
			if (Environment.getExternalStorageState().equals(
					Environment.MEDIA_MOUNTED)) {
				String path = "/sdcard/crash/";
				File dir = new File(path);
				if (!dir.exists()) {
					dir.mkdirs();
				}
				FileOutputStream fos = new FileOutputStream(path + fileName);
				fos.write(sb.toString().getBytes());
				fos.close();
			}
			return fileName;
		} catch (Exception e) {
			Log.e(TAG, "an error occured while writing file...", e);
		}
		return null;
	}
}

完成了這個CrashHandler類之後,還需要自定義一個全局Application來啓動管理異常收集,以下是自定義的Application類,很簡單:

package com.example.crash;

import android.app.Application;

public class CrashApplication extends Application {

	@Override
	public void onCreate() {
		// TODO Auto-generated method stub
		super.onCreate();
		CrashHandler crashHandler = CrashHandler.getInstance();
		crashHandler.init(getApplicationContext());
	}

}
最後,爲了讓程序在啓動時使用我們自定義的Application,必須在Mainifest.xml的Application節點上,聲明出我們自定義的Application:

<application
        android:name=".CrashApplication" ...>
      .....
    </application>

配置SDCard寫文件的權限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
運行以下程序:


在SD卡中找到crash文件夾,打開文件夾:


到處這個log日誌,用notepad打開,查看內容如下:

TIME=1385535270000
......
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.crash/com.example.crash.MainActivity}: 
        java.lang.ArithmeticException: divide by zero
	......
Caused by: java.lang.ArithmeticException: divide by zero
	at com.example.crash.MainActivity.onCreate(MainActivity.java:13)
	at android.app.Activity.performCreate(Activity.java:5243)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2140)
	... 11 more
java.lang.ArithmeticException: divide by zero
	at com.example.crash.MainActivity.onCreate(MainActivity.java:13)
......

好了,程序中未捕獲的異常被及時捕捉到,保存在SD卡中,並且給用戶良好的提示信息,被沒有一下子crash掉,通過SD卡中的錯誤日誌,我們可以很快定義到錯誤的根源,方便我們及時對程序進行修正。當然了,這裏我由於做的是個Demo,所以相關錯誤日誌僅僅保存在了SD卡上,其實好的做法是將錯誤日誌上傳到服務器中,以便我們收集來自四面八方用戶的日誌,爲程序進行更新迭代升級。

       注:該文是我學習筆記,裏面會有一些Bug。程序僅作爲參考實例,不能直接使用到真實項目中,請諒解!

       參考資料:http://www.cjsdn.net/Doc/JDK60/java/lang/Thread.UncaughtExceptionHandler.html

                         http://www.cnblogs.com/draem0507/archive/2013/05/25/3099461.html

                         http://blog.csdn.net/liuhe688/article/details/6584143

                     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章