屏蔽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 data = (HashMap) 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中去注册一个静态广播:


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信息。

非常感谢您的耐心阅读,希望我的文章对您有帮助。欢迎点评、转发或分享给您的朋友或技术群。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部