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