android中捕獲全局異常

android中捕獲全局異常

大傢都知道,安裝Android系統的手機版本和設備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就出現崩潰的現象,開發者個人不可能購買所有設備逐個調試。所以在程序發佈出去之後,如果出現瞭崩潰現象,開發者應該及時獲取在該設備上導致崩潰的信息,這對於下一個版本的bug修復幫助極大,所以今天就來介紹一下如何在程序崩潰的情況下收集相關的設備參數信息和具體的異常信息,並發送這些信息到服務器供開發者分析和調試程序。

從制造異常開始

為瞭演示,我們先制造一個異常。新建一個名為CrashDemo項目,在MainActivity中輸入下面代碼,故意制造瞭一個潛在的運行期異常,在一個null對象上調用方法。

public class MainActivity extends AppCompatActivity {
    private String mStr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        System.out.print(mStr.equals("print AnyThings!"));
    }
}

 

在測試機上發佈,程序運行後直接crash瞭。

遇到軟件沒有捕獲的異常之後,系統會彈出這個默認的強制關閉對話框。我們當然不希望用戶看到這種現象,簡直是對用戶心靈上的打擊,而且對我們的bug的修復也是毫無幫助的。我們需要的是軟件有一個全局的異常捕獲器,當出現一個我們沒有發現的異常時,捕獲這個異常,並且將異常信息記錄下來,上傳到服務器供開發者這分析出現異常的具體原因。

捕獲全局異常的方法

捕獲全局異常主要是靠Thread類中的UncaughtExceptionHandler接口。

public class Thread implements Runnable {
    // ,,,

    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }
}

該接口中僅有一個方法,是用來處理未捕獲異常的,傳入線程和一個可拋出的對象。當線程因未捕獲的異常而要終止的時候會回掉這個接口中的uncaughtException方法。

每個線程對象中有個UncaughtExceptionHandler類型的引用,提供瞭getter和setter方法,這是用策略模式解耦(定義一系列可替換的算法族,可用setter傳入不同的算法)。

public class Thread implements Runnable {
    // ,,,

    private ThreadGroup group;
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        checkAccess();
        uncaughtExceptionHandler = eh;
    }

    // Dispatch an uncaught exception to the handler. This method is
    // intended to be called only by the JVM.
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
}

dispatchUncaughtException方法隻會被JVM調用,發佈異常給handler。getUncaughtExceptionHandler方法將返回uncaughtExceptionHandler對象,而當uncaughtExceptionHandler為null時,也就客戶沒有設置策略的時候,就返回group對象。

group是ThreadGroup類對象,顧名思義就是線程組的意思,在創建一個Thread對象時可以指定線程組。我們來看看這個類中有哪些方法(部分被標記為 @Deprecated的廢棄方法被我去掉瞭)。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    static final ThreadGroup systemThreadGroup = new ThreadGroup();
    static final ThreadGroup mainThreadGroup = new ThreadGroup(systemThreadGroup, "main");
    private final ThreadGroup parent;
    String name;
    int maxPriority;
    boolean destroyed;
    boolean daemon;
    boolean vmAllowSuspension;
    int nUnstartedThreads = 0;
    int nthreads;
    Thread threads[];
    int ngroups;
    ThreadGroup groups[];

    private ThreadGroup();
    public ThreadGroup(String name);
    public ThreadGroup(ThreadGroup parent, String name);
    private ThreadGroup(Void unused, ThreadGroup parent, String name);

    private static Void checkParentAccess(ThreadGroup parent);
    public final String getName();
    public final ThreadGroup getParent();
    public final int getMaxPriority();
    public final boolean isDaemon();
    public synchronized boolean isDestroyed();
    public final void setDaemon(boolean daemon);
    public final void setMaxPriority(int pri);
    public final boolean parentOf(ThreadGroup g);
    public final void checkAccess();
    public int activeCount();
    public int enumerate(Thread list[]);
    public int enumerate(Thread list[], boolean recurse);
    private int enumerate(Thread list[], int n, boolean recurse);
    public int activeGroupCount();
    public int enumerate(ThreadGroup list[]);
    public int enumerate(ThreadGroup list[], boolean recurse);
    private int enumerate(ThreadGroup list[], int n, boolean recurse);
    public final void interrupt();
    private boolean stopOrSuspend(boolean suspend);
    public final void destroy();
    private final void add(ThreadGroup g);
    private void remove(ThreadGroup g);
    void addUnstarted();
    void add(Thread t)
    void threadStartFailed(Thread t);
    void threadTerminated(Thread t);
    private void remove(Thread t);
    public void list();
    void list(PrintStream out, int indent);
    public void uncaughtException(Thread t, Throwable e);
    public String toString();
}

通過源碼,我們瞭解到ThreadGroup包含一個同樣為ThreadGroup類型的父元素parent,也有Thread[]和ThreadGroup[]類型作為其子元素。這關系就像View和ViewGroup,Thread和ThreadGroup構成一棵線程樹。

不出意料ThreadGroup果然實現瞭UncaughtExceptionHandler接口,我們看看uncaughtException方法的實現。

// ThreadGroup.java
public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

可以看出ThreadGroup把異常傳遞給他線程樹上的父親來處理,當沒父親時將會調用線程類裡的默認處理器來處理。這是一種責任鏈模式,兒子遇到無法解決的難題時就把難題交給他的父親來處理,一直傳遞上去,如果最終也沒找到能處理的父親,就把異常交給默認處理器一把處理。

Thread中有個屬於類的defaultUncaughtExceptionHandler,也有對應的getter和setter方法,這就是線程類的默認處理器,傳遞鏈上的最後一棒。註意defaultUncaughtExceptionHandler的uncaughtException不能調用ViewGroup的uncaughtException放法,否則會無限遞歸。

public class Thread implements Runnable {
    // ...

    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
         defaultUncaughtExceptionHandler = eh;
     }

    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
        return defaultUncaughtExceptionHandler;
    }
}

以上分析的流程可以用下面這張圖來概括。

當有未捕獲的異常時,JVM會首先把異常分發給當前線程處理,若當前線程無法處理,則此異常會在鏈上傳遞。

處理全局異常的Demo

知道瞭這些原理,我們也就知道瞭該怎麼捕獲全局異常瞭。我們可以通過設置defaultUncaughtExceptionHandler來統一處理全局異常。下面貼出瞭Demo的源碼。

首先創建一個捕獲處理全局異常的類CrashHandler,實現UncaughtExceptionHandler接口。該類設計為單例,完成錯誤信息的收集和上報,具體的邏輯結合代碼中的註釋不難看懂,這裡我們把錯誤信息存儲在本地,開發過程中可以把錯誤信息上報服務器。

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private Context mContext;
    private static CrashHandler mInstance;
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    // 用來存儲設備信息和異常信息  
    private Map mInfo = new HashMap<>();
    private DateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private CrashHandler() {}

    public static CrashHandler getInstance() {
        if (mInstance == null) {
            synchronized (CrashHandler.class) {
                if (mInstance == null) {
                    mInstance = new CrashHandler();
                }
            }
        }
        return mInstance;
    }

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

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 如果用戶沒有處理則讓系統默認的異常處理器來處理  
        if (!handleException(e) && mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(t, e);
        } else {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e1) {
                Log.e(TAG, "error", e);  
            }
            // 退出程序  
            Process.killProcess(Process.myPid());  
            System.exit(1);
        }
    }

    // 自定義錯誤處理,收集錯誤信息,發送錯誤報告等操作均在此完成. 
    private boolean handleException(Throwable e) {
        if (e == null) {
            return false;
        }
        // 使用Toast來顯示異常信息  
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(mContext, "很抱歉,程序出現異常,即將退出.", Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }.start();

        //收集設備參數信息   
        collectErrorInfo();  
        //保存日志文件   
        saveErrorInfo(e);
        return true;
    }

    // 收集設備參數信息 
    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 + "";
                mInfo.put("versionName", versionName);
                mInfo.put("versionCode", versionCode);
            }

            Field[] fields = Build.class.getFields();
            if (fields != null && fields.length > 0) {
                for (Field field : fields) {
                    field.setAccessible(true);
                    try {
                        mInfo.put(field.getName(), field.get(null).toString());
                    } catch (IllegalAccessException e) {
                        Log.e(TAG, "an error occured when collect crash info", e);  
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "an error occured when collect package info", e);  
        }
    }

    // 保存錯誤信息到文件中 
    private void saveErrorInfo(Throwable e) {
        StringBuffer stringBuffer = new StringBuffer();
        for (Map.Entry entry : mInfo.entrySet()) {
            String keyName = entry.getKey();
            String value = entry.getKey();
            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 = cause.getCause();
        }

        printWriter.close();

        String result = writer.toString();
        stringBuffer.append(result);

        long currentTime = System.currentTimeMillis();
        String time = mDateFormat.format(new Date());
        String fileName = "crash-" + time + "-" + currentTime + ".log";

        // 判斷有沒有SD卡
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            File dir = mContext.getExternalFilesDir("crash");
            if (!dir.exists()) {
                dir.mkdirs();
            }

            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(dir + "/" + fileName);
                fos.write(stringBuffer.toString().getBytes());
            } catch (FileNotFoundException e1) {
                Log.e(TAG, "an error occured due to file not found", e);  
            } catch (IOException e2) {
                Log.e(TAG, "an error occured while writing file...", e);  
            } finally {
                try {
                    fos.close();
                } catch (IOException e1) {
                    Log.e(TAG, "an error occured when close file", e);  
                }
            }
        }
    }
}
完成這個CrashHandler後,我們需要在一個Application環境中讓其初始化,為此,我們繼承android.app.Application,添加自己的代碼,CrashApplication.java代碼如下

public class CrashApplication extends Application {
    private static Context mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        this.mContext = this;
        CrashHandler.getInstance().init(this);
    }
}

最後在AndroidManifest.xml裡將CrashApplication配置成運行的Application就可以瞭,這個非常簡單就不貼代碼瞭。

測試

運行程序,程序在彈出提示Toast之後就退出瞭,在Android/data/[包名]/files/crash目錄下,我們找到瞭記錄的異常信息。

記錄的異常信息。

SUPPORTED_64_BIT_ABIS=SUPPORTED_64_BIT_ABIS
versionCode=versionCode
BOARD=BOARD
BOOTLOADER=BOOTLOADER
TYPE=TYPE
ID=ID
TIME=TIME
BRAND=BRAND
SERIAL=SERIAL
HARDWARE=HARDWARE
SUPPORTED_ABIS=SUPPORTED_ABIS
CPU_ABI=CPU_ABI
RADIO=RADIO
IS_DEBUGGABLE=IS_DEBUGGABLE
MANUFACTURER=MANUFACTURER
SUPPORTED_32_BIT_ABIS=SUPPORTED_32_BIT_ABIS
isOSUpgradeKK2LL=isOSUpgradeKK2LL
TAGS=TAGS
IS_SYSTEM_SECURE=IS_SYSTEM_SECURE
CPU_ABI2=CPU_ABI2
UNKNOWN=UNKNOWN
USER=USER
FINGERPRINT=FINGERPRINT
FOTA_INFO=FOTA_INFO
HOST=HOST
PRODUCT=PRODUCT
versionName=versionName
DISPLAY=DISPLAY
MODEL=MODEL
DEVICE=DEVICE
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sandemarine.crashdemo/com.sandemarine.crashdemo.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3319)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
    at android.app.ActivityThread.access$1100(ActivityThread.java:229)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:7406)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    at com.sandemarine.crashdemo.MainActivity.onCreate(MainActivity.java:14)
    at android.app.Activity.performCreate(Activity.java:6904)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
    ... 9 more
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    at com.sandemarine.crashdemo.MainActivity.onCreate(MainActivity.java:14)
    at android.app.Activity.performCreate(Activity.java:6904)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
    at android.app.ActivityThread.access$1100(ActivityThread.java:229)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:7406)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

You May Also Like