問題
Android應用不可避免會發生Crash,不管你的代碼寫得有多風騷,在這個複雜的網絡環境中,Crash還是時常的會發生。也就是常說的應用程序發生崩潰。常見表現就是閃屏然後退出。
原因
有些Crash是隻在特定網絡環境中才會出現,比如說網絡環境爲2g的時候。而Crash是發生在用戶手機端,如果我們不對這些異常信息進行收集,那我們就沒辦法分析出Crash的原因,也就無法進行修復。下面就介紹一下如何收集這些Crash信息。
問題分析:
首先,Crash是發生在客戶端,並且帶有不確定性,即有的用戶可能發生而又的不發生。所以,我們可以在應用程序發生Crash後,對Crash信息進行收集,保存在本地客戶端(如寫入的sd卡),然後在合適的時候(如網絡環境好)將Crash文件上傳到服務器進行統計分析。
技術實現:
Thread中提供了一個方法叫做setDefaultUncaughtUncaughtExceptionHandler(UncaughtExceptionHandler handler)的方法,用來設置一個UncaughtExceptionHandler對象。
這個對象有什麼用呢?
作用:當Crash發生的時候,系統就會回調UncaughtExceptionHandler 中的uncaughtException方法。所以我們就可以在這個方法中去保存crash日誌。然後在時機好的時候發送至服務器。
具體邏輯
下面代碼中,主要是創建了一個實現Thread.UncaughtExceptionHandler接口的類CrashHandler。
然後以單例的形式向外提供支持。
主要的方法有:
- init (Context) 進行初始化
- uncaughtException(Thread , Throwable ) 複寫的方法,每次Crash之後被調用
- handleException(Throwable)處理Crash的邏輯
- uploadExceptionToServer()上傳到服務器的實現了
- dumpExceptionToSDCard(Throwable )保存到sd卡的實現類
- dumpPhoneInfo(PrintWriter)獲取收集信息
實現的邏輯
在Application中調用init()方法初始化CrashHandler,當發生Crash的時候CrashHandler的uncaughtException方法被調用。然後在這個方法中調用handleException方法,handleException方法中再去組織具體邏輯(調用具體實現類)。uploadExceptionToServer、dumpExceptionToSDCard、dumpPhoneInfo這三個方法則是具體邏輯實現。
代碼實現:
- CrashHandler 類:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".txt";
private static CrashHandler sInstance = new CrashHandler();
private Context mContext;
//私有構造器
private CrashHandler() {
}
//單例模式
public static CrashHandler getInstance() {
return sInstance;
}
//初始化
public void init(Context context) {
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context;
}
/**
* 重寫uncaughtException
* @param t 發生Crash的線程
* @param ex Throwale對象
*/
@Override
public void uncaughtException(Thread t, Throwable ex) {
//處理邏輯需要開啓一個子線程,用於文件的寫入操作
handleException(ex);
//在程序關閉之前休眠2秒,以避免在文件寫入的操作完
//成。之前進程被殺死。
//也可以考慮彈出對話框友好提示用戶
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
// Process.killProcess(Process.myPid());
}
//處理異常
private void handleException(final Throwable ex) {
try {
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
dumpExceptionToSDCard(ex);
uploadExceptionToServer();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 將異常信息上傳至服務器
*/
private void uploadExceptionToServer() {
}
/**
* 將異常信息寫入sd卡
* @param ex
*/
private void dumpExceptionToSDCard(Throwable ex) {
//判斷是否支持SD卡
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.i(TAG, "sdcard unfind ,skip dump exception");
return;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date(current));
File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
//將拋出的異常信息寫入到文件
ex.printStackTrace(pw);
pw.close();
} catch (Exception e) {
Log.d(TAG, "dump Exception Exception" + e.getMessage());
e.printStackTrace();
}
}
/**
*
* 獲取手機信息
* @param pw 寫入流
* @throws PackageManager.NameNotFoundException 異常
*/
private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.print("App Version : ");
pw.print(pi.versionName);
Log.d(TAG,"name : "+pi.versionName);
pw.print('_');
pw.println(pi.versionCode);
pw.print("OS Version : ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT );
pw.print("Vendor : ");
pw.println(Build.MANUFACTURER);
pw.print("Model : ");
pw.println(Build.MODEL);
pw.print("Cpu ABI : ");
pw.println(Build.CPU_ABI);
}
}
- Application中初始化:
public class App extends Application {
private Context mContext;
private static App sInstance;
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
mContext = getApplicationContext();
}
public Context getContext() {
return mContext;
}
public static App getsInstance() {
return sInstance;
}
}
大致實現就是這樣,主要是學習 Thread.UncaughtExceptionHandler和了解uncaughtException方法在進程Crash後會被調用。然後在這個基礎上去處理邏輯(重寫uncaughtException方法)。
後記
大家可以自己拋出一個異常,去測試一下是否成功。比如在Activity中使用以下代碼:
throw new RuntimeException("自定義異常");
成功生成的文件內容:
遇到的坑
- 文件創建失敗,可能是文件的路徑不規範
- 文件打開權限不足(Read Only),6.0以上需要動態權限申請
- 開啓一個子線程去處理文件的讀寫,並且將當前線程sleep一定的時間。才能保證在文件寫完後,程序才被殺死。
- 文件找不到,代碼中顯示已經生成,而使用DDMS查看或手機上自帶的文件管理程序查看的時候卻沒有。可以考慮直接使用adb shell查看或者使用RE文件管理器。
參考書籍
- 《Android開發與藝術探索》