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

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

猜你喜欢

转载自blog.csdn.net/salmon_zhang/article/details/94653248