對於我們編寫程序來說,現如今有很多的手機系統都是基於安卓系統開發的,所以就會造成安裝Android系統的手機版本和設備有着很多差異,於是乎就會有在模擬器上運行良好的程序安裝到某款手機上出現崩潰的現象,但是我們沒有那個精力去把所有的手機型號都試一遍,所以當我們把程序發佈以後,如果程序存在奔潰的現象,我們作爲開發者要及時的獲取到導致奔潰的信息,然後在之後更新的版本把這個Bug修復,對於這個情況我們來說下Android中常用到的異常集以及對應的解決方案。(學習自IMOOC:傳送門)
異常詮釋
異常是指在程序運行過程中所出現的錯誤。這些錯誤會干擾到指令的正常執行,從而導致程序異常退出。這些異常出現的場景有:文件找不到,網絡連接失敗,非法參數等。
異常來源
就Java語言來說,所有的異常都繼承自Throwable,看下面異常(代表)的關係圖:
- Error是指無法處理的錯誤,一般是於上層沒有關係的,是因爲底層系統或者說是Java虛擬機的原因,當Error出現後,上層程序是沒有辦法控制的。
- Exception是指代碼不規範造成的錯誤,如空指針引用,除零,數組越界等錯誤
異常分類
大致可以分爲以下兩類:
- 編譯時錯誤
- 運行時錯誤
來對這兩種異常進行舉例吧。
編譯期異常(以找不到類爲例)
Caused by後面就是報出了異常的名稱,再之後是對該異常的解釋,我們在對於這一類異常的修正是找到第一個關於你編寫的代碼段的報錯,然後後面所有報出你編寫的代碼是異常的都是源於第一個報錯的代碼段,然後點擊藍色的鏈接就跳到發生異常的地方了。
運行期異常(以空指針爲例)
第一個黃框就是報出了異常的名稱以及對該異常的解釋,我們在對於這一類異常的修正是找到第一個關於你編寫的代碼段的報錯,然後後面所有報出你編寫的代碼是異常的都是源於第一個報錯的代碼段,然後點擊藍色的鏈接就跳到發生異常的地方了。
捕獲全局異常
下面這個Dialog一般是當你的程序沒有捕獲到異常的時候,是由系統默認彈出的一個強制退出的對話框,這是我們不想看到的,因爲這樣給了用戶一個及其不友好的體驗,並且對於以後我們對程序的修復是沒有一點幫助的。
這樣我們就需要一個全局的異常捕獲器。這時候我們就需要提到一個接口:UncaughtExceptionHandler,它是定義在Thread中的一個接口,我們可以看到下面這些註釋,說明了當線程因未捕獲的異常而終止的時候調用,任何被該方法拋出的異常都會被虛擬機忽略。
所以我們就要新建一個類去繼承這個接口,然後就是實現他的uncaughtException方法,然後我們來說下我們想要怎麼實現異常捕獲:
- 收集錯誤信息
- 保存錯誤信息
- 上傳到服務器
我們來一步一步的說明。
我們記得要進行初始化,並且以單例的模式創建對象
private CrashHandler(){
}
public static CrashHandler getInstance(){
if(mInstance==null){
synchronized (CrashHandler.class){
if(mInstance==null){
mInstance = new CrashHandler();
}
}
}
return mInstance;
}
/**
* 初始化
* @param context
*/
public void init(Context context){
mContext=context;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
我們要先判斷是否已經處理過了異常,因爲這樣就有兩個不同的後效性,如果異常是未處理的,我們依照註釋中說的就要調用系統默認的處理器處理,也就是我們就不喜歡的那種方法,彈出系統默認的強制退出的對話框,如果我們是已經處理過了的話,我們只要把線程關閉了,並且強制關閉應用就好了。
public void uncaughtException(Thread t, Throwable e) {
if(!handleException(e)){
//未處理,調用系統默認的處理器處理
if(mDefaultHandler!=null){
mDefaultHandler.uncaughtException(t,e);//彈出系統默認的強制退出的對話框
}
}
else{
//已經人爲處理
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Process.killProcess(Process.myPid());
System.exit(1);
}
}
handleException方法就是用來處理異常的,沒有特殊情況都會處理成功。
然後我們就在裏面來進行我們一開始說的那三步,因爲我們沒有服務器,所以我們就不要上傳服務器了。當然我們要先判斷這個異常是不是有信息的,如果是有信息的我們肯定要給用戶一個提示,但是因爲CrashHandler必須要在UI線程的,所以這個操作就要在一個新的線程中進行了。
/**
* 人爲處理異常
* @param e
* @return true:已經處理 false:沒有處理
*/
private boolean handleException(Throwable e) {
if(e==null){
return false;
}
//Toast提示
new Thread(){//因爲CrashHandler必須要在UI線程的,所以要new一個線程
@Override
public void run() {//如果線程中使用Looper.prepare()和Looper.loop()創建了消息隊列就可以讓消息處理在該線程中完成。
Looper.prepare();//關聯一個Looper對象
Toast.makeText(mContext, "UncaughtException", Toast.LENGTH_SHORT).show();
Looper.loop();//讓Looper開始工作,從消息隊列裏取消息,處理消息。
}
}.start();
//收集錯誤信息(設備型號,設備品牌,系統版本,應用版本,異常信息)
collectErrorInfo();
//保存錯誤信息
saveErrorInfo(e);
//上傳到服務器
return false;
}
然後就是開始收集錯誤信息了,錯誤信息就是我們之前說的我們會因爲版本以及手機型號導致出不同的異常,所以我們的錯誤信息要包括設備型號,設備品牌,系統版本,應用版本,異常信息。所以我們就要獲取到我們的版本名字以及版本號,然後我們通過反射獲取信息,通過HashMap存儲這些信息。
private void collectErrorInfo() {
PackageManager pm = mContext.getPackageManager();//獲取應用程序信息
try {
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(),PackageManager.GET_ACTIVITIES);
if(pi!=null){
String versionName = TextUtils.isEmpty(pi.versionName)?"未設置版本名稱":pi.versionName;//版本名稱,有可能未設置
String versionCode = pi.versionCode+"";//版本號,有可能未設置,默認是0
mInfo.put("versionName",versionName);
mInfo.put("versionCode",versionCode);
}
//反射類中的Field
Field[] fields = Build.class.getFields();
if(fields!=null&&fields.length>0){
for(Field field:fields){
field.setAccessible(true);
mInfo.put(field.getName(),field.get(null).toString());
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
最後就要進行信息的存儲了,因爲我們要不斷的拼接字符串,所以我們就用StringBuffer來存儲信息,我們先把所有存在HashMap中的數據取出來,然後我們通過Writer和PrintWriter,然後通過循環把異常寫進去,然後讀取成字符串就好了,最後把這個字符串以文件的形式存入SD卡中。
當然PrintWriter和OutputStreamWriter有什麼區別呢?
- PrintWriter:以字符爲單位,支持漢字
- OutputStreamWriter:以字節爲單位,不支持漢字
當然存入SD卡要記得判斷有沒有SD卡,並且看路徑有沒有存在,如果沒有存在就要新建該路徑。然後通過流的形式存入文件中。
private void saveErrorInfo(Throwable e) {
StringBuffer stringBuffer = new StringBuffer();
for (Map.Entry<String,String> entry:mInfo.entrySet()){
String keyname = entry.getKey();
String value = entry.getValue();
stringBuffer.append(keyname+"="+value+"\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
e.printStackTrace(printWriter);
Throwable cause = e.getCause();
while (cause!=null){
cause.printStackTrace(printWriter);
cause = e.getCause();
}
printWriter.close();
String result = writer.toString();
stringBuffer.append(result);
long curTime = System.currentTimeMillis();
String time = dateFormat.format(curTime);
String fileName = "crash-"+time+"-"+curTime+".log";
//判斷有沒有SD卡
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
String path = "/sdcard/crash/";
File dir = new File(path);
if(!dir.exists()){
dir.mkdirs();//新建
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(path+fileName);
fos.write(stringBuffer.toString().getBytes());
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
finally {
try {
fos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
我們說過他是一個全局的處理器,所以我們要在Application中去調用它。
package com.gin.xjh.androiderrors;
import android.app.Application;
/**
* Created by Gin on 2018/2/24.
*/
public class CrashApplication extends Application {
private CrashHandler mCrashHandler;
@Override
public void onCreate() {
super.onCreate();
mCrashHandler = CrashHandler.getInstance();
mCrashHandler.init(this);
}
}
最後在AndroidManifest中定義下Application就大功告成了:
android:name=".CrashApplication"
然後我們把這個文件上傳服務器就好了。
錯誤分析工具Bugly
其實我們可以直接用騰訊的這個平臺Bugly,他很好的幫助了我們來分析異常,怎麼使用我們就按照他的開發文檔進行操作就好了。Bugly官網:戳這裏