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