屏蔽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);}@Overridepublic 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 未捕獲的異常*/@Overridepublic 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;@Overridepublic void onCreate() {initCrashHandlerManager();//初始化CrashHandlerManager}//初始化CrashHandlerManagerprivate void initCrashHandlerManager() {CrashHandlerManager crashHandlerManager = CrashHandlerManager.getInstance();crashHandlerManager.init(tmApplication);} }結合上面的兩個步驟,我們就可以獲取到crash信息了,并且再也不會給用戶彈crash提示框了。
3. 測試驗證
為了證明上面方案的有效性,我們需要測試驗證下。
3.1 靜態注冊一個廣播
到AndroidManifest.xml中去注冊一個靜態廣播:
<applicationandroid:allowBackup="true"android:persistent="true"android:icon="@mipmap/ic_launcher"android:name=".application.TmApplication"android:label="@string/app_name"android:supportsRtl="true"><receiverandroid: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 {@Overridepublic 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-N55U07java.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 referenceat 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 referenceat com.pptv.terminalmanager.receiver.CommonReceiver.onReceive(CommonReceiver.java:56)at android.app.ActivityThread.handleReceiver(ActivityThread.java:2725)... 8 more這里我們可以看到crash信息,如果通過網絡上傳到服務器端,開發就可以很好的定位問題。這樣就可以達到我們的目的:屏蔽crash提示框的同時,可以獲取到用戶場景下的crash信息。
非常感謝您的耐心閱讀,希望我的文章對您有幫助。歡迎點評、轉發或分享給您的朋友或技術群。
總結
以上是生活随笔為你收集整理的屏蔽Crash 提示框的两种方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 结合源码深入理解Android Cras
- 下一篇: 生成和合入patch的两种方式