屏蔽Crash 提示框的兩種方式

在Android應用開發的過程中,有時候我們總覺得自己寫的代碼天衣無縫,根本不會有bug。。。(一切都是幻覺),但在後期的版本迭代中總會讓你猝不及防的報各種crash,我們稱之爲“崩潰”。出錯的原因一般都千奇百怪。

《結合源碼深入理解Android Crash處理流程》中可知:當發生crash時,系統會kill掉正在執行的程序,並彈一個crash提示框給用戶去選擇。

在繼續寫之前,先說下前提:我是做ROM開發的,在公司負責一個“應用管控”的apk,主要作用就是對系統中的應用程序一些行爲進行管控,這個apk沒有一個界面顯示,並且有persistent屬性。如果對persistent屬性不是太瞭解的朋友,可以看下我的《談談Android中的persistent屬性》一文。由於前不久對它進行了重構,現在處於迭代的階段。但最近有用戶報應用管控apk的crash提示框,如下所示:

在這裏插入圖片描述

報crash彈框對用戶體驗不好,有個別用戶直接報到客服那邊,然後我總監和經理都知道了,有點尷尬。。。因爲我的apk沒有界面顯示,用戶根本不會去進行交互操作,且具有persistent屬性。然後還報crash彈框,這確實有點說不過去!所以我的修改宗旨是:apk你可以crash,當你不要給我彈框,然後將crash信息上傳到後臺就行了。

結合上面的報錯場景和修改宗旨,下面我將提供兩種屏蔽crash彈框的方案。

1. 從Framework層去修改

我是做ROM開發的,有直接修改framework層的代碼。從《結合源碼深入理解Android Crash處理流程》中可知:AMS.crashApplication方法中會通過mUiHandler發送message,且消息的msg.what=SHOW_ERROR_MSG,然後交由mUiHandler中的handleMessage去處理。這裏面會創建crash提示框:

final class UiHandler extends Handler {
    public UiHandler() {
        super(com.android.server.UiThread.get().getLooper(), null, true);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case SHOW_ERROR_MSG: {
            HashMap<String, Object> data = (HashMap<String, Object>) msg.obj;
            boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
                    Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
            synchronized (ActivityManagerService.this) {
                ProcessRecord proc = (ProcessRecord)data.get("app");
                AppErrorResult res = (AppErrorResult) data.get("result");

				...省略...

                if (mShowDialogs && !mSleeping && !mShuttingDown) {
					//創建crash提示框,等待用戶選擇,等待時間爲5分鐘
                    Dialog d = new AppErrorDialog(mContext,
                            ActivityManagerService.this, res, proc);
                    d.show();
                    proc.crashDialog = d;
                } 
            }
            ensureBootCompleted();
        } break;

		...省略...
    }
}

修改思路:

在上面有ProcessRecord對象,那我們就可以拿到app對應的processName,那我們就可以自定義一個類似於黑名單的字符串數組,將不要顯示crash彈框的進程名(一般都是包名)寫在數組中,如下所示:

private String[]  dontShowDialogsP = {"com.pptv.terminalmanager","com.pptv.launcher"};

然後我們在顯示crash Dialog前,判斷要報錯的進程名是否在上面定義的字符串數組中?

* 如果進程名在定義的字符串數組黑名單中,則不走彈crash框邏輯

* 如果進程名不在定義的字符串數組黑名單中,走原來的邏輯,彈框

實現方案:

代碼修改前:

if (mShowDialogs && !mSleeping && !mShuttingDown) {
    Dialog d = new AppErrorDialog(mContext,
            ActivityManagerService.this, res, proc);
    d.show();
    proc.crashDialog = d;
} else {
    if (res != null) {
        res.set(0);
    }
}

代碼修改後:

if (mShowDialogs && !mSleeping && !mShuttingDown) {
    boolean showReally = true;
    for (String itemDontShow : dontShowDialogsP){
        if (proc.processName.equals(itemDontShow)){
            showReally = false;
        }
    }
    if (showReally){
        Dialog d = new AppErrorDialog(mContext,
                ActivityManagerService.this, res, proc);
        d.show();
        proc.crashDialog = d;
    } 
}else {
    if (res != null){
        res.set(0);
    }
}

這樣我們就可以從AMS中徹底斷了顯示Crash彈框的邏輯,從而達到在界面上看不到Crash報錯框了。

備註:上面的流程我是結合我當前的項目用的Android6.0去跟蹤分析的,我看了下Android8.0的代碼,略有不同,但修改的思路和方案跟上面一樣,只是代碼添加的地方有所不同而已。

2. 使用CrashHandler

當在用戶那邊發生crash時,如果我們想去解決這個crash時,就需要知道用戶當時的crash信息。Android提供瞭解決這類問題的方法。在Thread中的setDefaultUncaughtExceptionHandler方法可以設置系統默認異常處理器。當發生crash時,系統就會回調UncaughtExceptionHandler的uncaughtException方法,因此我們在uncaughtException方法中就可以獲取到異常信息,可以將異常信息存在SD卡中,然後通過網絡將crash信息上傳到服務器上,這樣開發就可以分析用戶crash場景並在後續的版本中修復。

《結合源碼深入理解Android Crash處理流程》一文中,我們知道在AMS—>handleAppCrashLocked方法中有一處會判斷如果App中存在crash的Handler,那麼就交給App中的Handler處理。

結合上面的分析,我們可以在App內部獲取到應用crash的信息,並可以屏蔽Crash彈框。

修改思路:

  • 實現一個UncaughtExceptionHandler對象,在它的uncaughtException方法中獲取crash信息,並將其保存到SD卡,然後通過網絡將crash信息上傳到服務器

  • 調用Thread的setDefaultUncaughtExceptionHandler方法將它設置爲線程默認的異常處理器。由於默認異常處理是Thread類的靜態成員,所以當前進程的所有線程都可以使用

  • 不讓走默認異常信息處理邏輯,直接kill當前進程。這樣就不會顯示crash彈框。(備註:因爲我的Apk沒有任何與用戶交互的界面,且有persistent屬性,所以可以直接kill掉,如果是與用戶有交互的App,則自定義一個dialog,讓用戶去做選擇,然後根據不同的選擇去做不同的邏輯,可以參考微信彈的dialog!!!)

實現方案:

下面我將我在公司負責的“應用管控”apk的異常處理方案實現出來,僅供參考!!!

1. 實現UncaughtExceptionHandler對象

/**
 * UncaughtException處理類,當程序發生Uncaught異常時,由該類來處理
 * Created by salmonzhang on 2019/6/18.
 */

public class CrashHandlerManager implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandlerManager";

    //日誌保存路徑
    public static final String PATH = Environment.getExternalStorageDirectory().getPath()+"/terminalmanager/crashLog/";
    public static final String FILE_NAME = "crash_";
    public static final String FILE_NAME_SUFFIX = ".txt";
    //系統默認的UncaughtException處理類
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private volatile static CrashHandlerManager instance;
    private Context mContext;

    private CrashHandlerManager() {
    }

    //單例模式
    public static CrashHandlerManager getInstance() {
        if (instance == null) {
            synchronized (CrashHandlerManager.class) {
                if (instance == null) {
                    instance = new CrashHandlerManager();
                }
            }
        }
        return instance;
    }

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

    /**
     * 當程序有未捕獲的異常時,系統自動調用該方法
     * @param thread 出現未捕獲異常的線程
     * @param ex 未捕獲的異常
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        boolean isWriteSuccess = true;
        try {
            //將異常信息寫入到sd卡中
            isWriteSuccess = writeExceptionToSDcard(ex);
            //將異常信息上傳到服務器
            uploadExceptionToServer();
        } catch (IOException e) {
            e.printStackTrace();
        }

        /**
         * 交由系統處理就會由ROM去控制是否彈“停止運行”框
         * 直接kill掉相應進程,就不會彈“停止運行”框
         */
        if (!isWriteSuccess && mDefaultHandler != null) {
            //如果用戶沒有處理,則讓系統默認的異常處理器來處理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }
    }

    private boolean writeExceptionToSDcard(Throwable ex) throws IOException{
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Log.w(TAG, "No SD card");
            return true;
        } else {
            File dir = new File(PATH);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            //清空上次保存的文件,確保每次只保存一份txt文件在sdcard中
            File[] listFiles = dir.listFiles();
            for (File listFile : listFiles) {
                listFile.delete();
            }
            long currentData = System.currentTimeMillis();
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(currentData));
            File file = new File(PATH + FILE_NAME + time.replace(" ", "_") + FILE_NAME_SUFFIX);
            Log.d(TAG, "crash file path : " + file.getAbsolutePath());
            try {
                PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
                printWriter.println(time);//寫入時間
                televisionInformation(printWriter);//寫入電視信息
                printWriter.println();
                ex.printStackTrace(printWriter);//異常信息
                printWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "writer carsh log failed");
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } finally {
                return true;
            }
        }
    }

    //獲取電視基本信息
    private void televisionInformation(PrintWriter pw) throws PackageManager.NameNotFoundException {
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
        pw.println("App versionName : " + pi.versionName + " versionCode : " + pi.versionCode);
        pw.println("OS Version : " + Build.VERSION.RELEASE + " SDK : " + Build.VERSION.SDK_INT);
        pw.println("Model : " + Build.MODEL);
    }

    /**
     * 異常上傳服務器
     */
    private void uploadExceptionToServer() {
        //按照自己公司後臺提供的接口寫相應的邏輯
    }
}

從上面的代碼可以看出:

  • 當應用崩潰時,CrashHandler會將異常信息和電視的基本信息保存到SD卡中

  • 將異常信息上傳到公司服務器(由於公司暫時沒接口,後續添加)

  • 爲了屏蔽crash彈框,crash信息保存成功後,我們將異常不交給系統處理,而是直接kill掉當前應用進程並退出

2. 如何使用定義好的CrashHandler對象

定義好CrashHandler對象後,我們選擇在Application初始化的時候爲線程設置CrashHandler,如下所示:

public class TmApplication extends Application {
    private static final String TAG = TmApplication.class.getSimpleName();
    public static TmApplication tmApplication;
    @Override
    public void onCreate() {

        initCrashHandlerManager();//初始化CrashHandlerManager
    }

    //初始化CrashHandlerManager
    private void initCrashHandlerManager() {
        CrashHandlerManager crashHandlerManager = CrashHandlerManager.getInstance();
        crashHandlerManager.init(tmApplication);
    }
}

結合上面的兩個步驟,我們就可以獲取到crash信息了,並且再也不會給用戶彈crash提示框了。

3. 測試驗證

爲了證明上面方案的有效性,我們需要測試驗證下。

3.1 靜態註冊一個廣播

到AndroidManifest.xml中去註冊一個靜態廣播:

<application
    android:allowBackup="true"
    android:persistent="true"
    android:icon="@mipmap/ic_launcher"
    android:name=".application.TmApplication"
    android:label="@string/app_name"
    android:supportsRtl="true">

    <receiver
        android:name=".receiver.CommonReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="com.pptv.terminalmanager.MY_BROADCAST"/>
        </intent-filter>
    </receiver>

</application>

3.2 到廣播接收者中去製造一個異常

public class CommonReceiver extends BroadcastReceiver {

@Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if ("com.pptv.terminalmanager.MY_BROADCAST".equals(action)) {
            Toast.makeText(context,"received in MY_BROADCAST",Toast.LENGTH_LONG).show();
            String temp = null;
            int length = temp.length();
        }
    }
}

從上面的代碼可以看出,當我們接收到com.pptv.terminalmanager.MY_BROADCAST廣播後,會有一個空指針異常。

3.3 通過命令觸發異常

在觸發異常之前,我們先看下應用管控的進程號:

root@mangosteen:/ # ps | grep  -i com.pptv.terminalmanager
system    7274  1689  875404 29632 SyS_epoll_ 00f6ef7d74 S com.pptv.terminalmanager

可以看到進程號是7274。

通過命令發送廣播:

am broadcast -a com.pptv.terminalmanager.MY_BROADCAST

通過上面的命令,就會觸發App中的空指針異常。

通過現象可以看到系統沒有彈出crash提示框,並再次查看下應用管控的進程號:

root@mangosteen:/ # ps | grep  -i com.pptv.terminalmanager                     
system    25784 1689  875504 29736 SyS_epoll_ 00f6ef7d74 S com.pptv.terminalmanager

可以看到此時進程號是25784,已經發生了改變。因爲帶有persistent屬性,所以kill後,會自啓。

3.4 查看crash信息

在上面觸發空指針異常後,會保存crash信息到SD卡中,路徑如下:

/storage/emulated/0/terminalmanager/crashLog/crash_2019-07-04_20:12:56.txt

打開crash_2019-07-04_20:12:56.txt文件查看下crash信息:

2019-07-04 20:12:56
App versionName : 3.0 versionCode : 1003
OS Version : 6.0 SDK : 23
Model : PPTV-N55U07

java.lang.RuntimeException: Unable to start receiver com.pptv.terminalmanager.receiver.CommonReceiver: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
        at android.app.ActivityThread.handleReceiver(ActivityThread.java:2732)
        at android.app.ActivityThread.-wrap14(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1421)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:5417)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:731)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:621)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
        at com.pptv.terminalmanager.receiver.CommonReceiver.onReceive(CommonReceiver.java:56)
        at android.app.ActivityThread.handleReceiver(ActivityThread.java:2725)
        ... 8 more

這裏我們可以看到crash信息,如果通過網絡上傳到服務器端,開發就可以很好的定位問題。這樣就可以達到我們的目的:屏蔽crash提示框的同時,可以獲取到用戶場景下的crash信息。

非常感謝您的耐心閱讀,希望我的文章對您有幫助。歡迎點評、轉發或分享給您的朋友或技術羣。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章