在實際項目開發中,會出現很多的異常直接導致程序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