Android開發(fā)之全局異常捕獲

前言

大家都知道,現(xiàn)在安裝Android系統(tǒng)的手機版本和設(shè)備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就出現(xiàn)崩潰的現(xiàn)象,開發(fā)者個人不可能購買所有設(shè)備逐個調(diào)試,所以在程序發(fā)布出去之后,如果出現(xiàn)了崩潰現(xiàn)象,開發(fā)者應(yīng)該及時獲取在該設(shè)備上導致崩潰的信息,這對于下一個版本的bug修復幫助極大,所以今天就來介紹一下如何在程序崩潰的情況下收集相關(guān)的設(shè)備參數(shù)信息和具體的異常信息,并發(fā)送這些信息到服務(wù)器供開發(fā)者分析和調(diào)試程序。


源碼分析


    /**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

源碼并不難,通過源碼可以知道:
Thread.UncaughtExceptionHandler是一個當線程由于未捕獲的異常突然終止而調(diào)用處理程序的接口.
當線程由于未捕獲的異常即將終止時,Java虛擬機將使用它來查詢其UncaughtExceptionHandler的線程Thread.getUncaughtExceptionHandler(),并將調(diào)用處理程序的 uncaughtException方法,將線程和異常作為參數(shù)傳遞。如果一個線程沒有顯示它的UncaughtExceptionHandler,那么它的ThreadGroup對象充當它的 UncaughtExceptionHandler。如果ThreadGroup對象沒有處理異常的特殊要求,它可以將調(diào)用轉(zhuǎn)發(fā)到默認的未捕獲的異常處理程序。

因此我們可以自定一個類去實現(xiàn)該接口,該類主要用于收集錯誤信息和保存錯誤信息.


實現(xiàn)代碼

自己先寫一個錯誤的、會導致崩潰的代碼:

public class MainActivity extends Activity {  
  
    private String s;  
      
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        System.out.println(s.equals("any string"));  
    }  
}  

我們在這里故意制造了一個潛在的運行期異常,當我們運行程序時就會出現(xiàn)以下界面:


這里寫圖片描述

遇到軟件沒有捕獲的異常之后,系統(tǒng)會彈出這個默認的強制關(guān)閉對話框。

我們當然不希望用戶看到這種現(xiàn)象,簡直是對用戶心靈上的打擊,而且對我們的bug的修復也是毫無幫助的。我們需要的是軟件有一個全局的異常捕獲器,當出現(xiàn)一個我們沒有發(fā)現(xiàn)的異常時,捕獲這個異常,并且將異常信息記錄下來,上傳到服務(wù)器公開發(fā)這分析出現(xiàn)異常的具體原因。

剛才在項目的結(jié)構(gòu)圖中看到的CrashHandler.java實現(xiàn)了Thread.UncaughtExceptionHandler,使我們用來處理未捕獲異常的主要成員,代碼如下:

public class CrashHandler implements UncaughtExceptionHandler {  
      
    public static final String TAG = "CrashHandler";  
      
    //系統(tǒng)默認的UncaughtException處理類   
    private Thread.UncaughtExceptionHandler mDefaultHandler;  
    //CrashHandler實例  
    private static CrashHandler INSTANCE = new CrashHandler();  
    //程序的Context對象  
    private Context mContext;  
    //用來存儲設(shè)備信息和異常信息  
    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;  
        //獲取系統(tǒng)默認的UncaughtException處理器  
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();  
        //設(shè)置該CrashHandler為程序的默認處理器  
        Thread.setDefaultUncaughtExceptionHandler(this);  
    }  
  
    /** 
     * 當UncaughtException發(fā)生時會轉(zhuǎn)入該函數(shù)來處理 
     */  
    @Override  
    public void uncaughtException(Thread thread, Throwable ex) {  
        if (!handleException(ex) && mDefaultHandler != null) {  
            //如果用戶沒有處理則讓系統(tǒng)默認的異常處理器來處理  
            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);  
        }  
    }  
  
    /** 
     * 自定義錯誤處理,收集錯誤信息 發(fā)送錯誤報告等操作均在此完成. 
     *  
     * @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, "很抱歉,程序出現(xiàn)異常,即將退出.", Toast.LENGTH_LONG).show();  
                Looper.loop();  
            }  
        }.start();  
        //收集設(shè)備參數(shù)信息   
        collectDeviceInfo(mContext);  
        //保存日志文件   
        saveCrashInfo2File(ex);  
        return true;  
    }  
      
    /** 
     * 收集設(shè)備參數(shù)信息 
     * @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  返回文件名稱,便于將文件傳送到服務(wù)器 
     */  
    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;  
    }  
}  

在收集異常信息時,也可以使用Properties,因為Properties有一個很便捷的方法properties.store(OutputStream out, String comments),用來將Properties實例中的鍵值對外輸?shù)捷敵隽髦?,但是在使用的過程中發(fā)現(xiàn)生成的文件中異常信息打印在同一行,看起來極為費勁,所以換成Map來存放這些信息,然后生成文件時稍加了些操作。

完成這個CrashHandler后,我們需要在一個Application環(huán)境中讓其運行,為此,我們繼承android.app.Application,添加自己的代碼,CrashApplication.java代碼如下:

public class CrashApplication extends Application {  
    @Override  
    public void onCreate() {  
        super.onCreate();  
        CrashHandler crashHandler = CrashHandler.getInstance();  
        crashHandler.init(getApplicationContext());  
    }  
}  

最后,為了讓我們的CrashApplication取代android.app.Application的地位,在我們的代碼中生效,我們需要修改AndroidManifest.xml:

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

因為我們上面的CrashHandler中,遇到異常后要保存設(shè)備參數(shù)和具體異常信息到SDCARD,所以我們需要在AndroidManifest.xml中加入讀寫SDCARD權(quán)限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  

搞定了上邊的步驟之后,我們來運行一下這個項目:

這里寫圖片描述

會發(fā)現(xiàn),在我們的sdcard中生成了一個Log文件

用文本編輯器打開日志文件,看一段日志信息:

Caused by: java.lang.NullPointerException  
    at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)  
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)  
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)  
    ... 11 more 

這些信息對于開發(fā)者來說幫助極大,所以我們需要將此日志文件上傳到服務(wù)器。
這樣就達到了線上如果用戶出問題,就可以抓到對應(yīng)的log問題了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android系統(tǒng)碎片化造成應(yīng)用程序崩潰嚴重,在模擬器上運行良好的程序安裝到某款手機上說不定就會出現(xiàn)崩潰的現(xiàn)象。而...
    YoungTa0閱讀 18,068評論 5 20
  • 穩(wěn)定性測試是保障客戶端穩(wěn)定性的一種手段,致力于提前發(fā)現(xiàn)問題,收集更多異常信息,復現(xiàn)線上閃退。當Android客戶端...
    one_step123閱讀 8,440評論 0 0
  • 進程和線程 進程 所有運行中的任務(wù)通常對應(yīng)一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,256評論 0 23
  • 大家都知道,現(xiàn)在安裝Android系統(tǒng)的手機版本和設(shè)備千差萬別,在模擬器和自己的android手機上運行良好的程序...
    Orz013閱讀 3,869評論 0 4
  • 前言 大家都知道,安裝Android系統(tǒng)的手機版本和設(shè)備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就...
    開發(fā)者小王閱讀 4,224評論 4 4

友情鏈接更多精彩內(nèi)容