背景
- 平時玩應用的時候,遇到bug,應用會彈出一個“很抱歉,“xx”已停止運行”的對話框,當按下確定的時候,程序會強制退出,退回到上一個頁面或者直接返回到桌面。這是android給我們提供的一種程序拋出異常結束應用默認的處理方式。開發測試中,我們可以查看到FC的原因。一旦應用發佈後,用戶體驗時FC的日誌,在不使用第三方框架捕獲的情況下我們是無法獲取到的。那麼android有沒有提供一些方法去解決這個問題呢。通過網上查找資料,在推酷上看到一篇文章:Android去除煩人的默認閃退Dialog。原來android已經預留一個線程異常退出中止前給我們提供了一個接口UnCaughtExceptionHandler,讓我們去坐一些善後的工作。
瞭解UnCaughtExceptionHandler
- 從官網我們可以瞭解到,很簡單的一個接口,只有一個抽象方法:
public abstract void uncaughtException (Thread thread, Throwable ex)`
提供了兩個參數 ,thread指的是拋出異常即將中止的線程;Throwable 指的是中止線程的一些原因和信息,可以通過getMessage(),getCause()去獲取。
使用UnCaughtExceptionHandler
使用UnCaughtExceptionHandler有兩種方式,一種可以在Application實現該接口處理,還有一種方法就是在BaseActivity裏面實現該接口處理。處理方法都是下面3步,只是實現Thread.UncaughtExceptionHandler地方放的不同。
- Application或者BaseActivity實現Thread.UncaughtExceptionHandler接口
- onCreate()方法裏面添加一句代碼:Thread.setDefaultUncaughtExceptionHandler(this);
- 在Thread.UncaughtExceptionHandler接口要實現的抽象方法 uncaughtException(Thread thread, Throwable ex)裏面做一些處理。
看起來很簡單的樣子,那就來一個簡單的例子:
簡單的例子
需求是這樣的,我們還是使用系統默認的閃退對話框,類似下圖,但是我們要去獲取手機閃退的信息。
這裏定義BaseActivity爲抽象類,方便其他Activity繼承,代碼如下:
public abstract class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler{
public Context mContext;
private Thread.UncaughtExceptionHandler defalutHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this ;
defalutHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* @param thread 拋出異常的線程
* @param ex 拋出異常的一些信息
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
HandleException(thread,ex);
}
/*
* 默認處理保存信息
*/
public void HandleException(Thread thread, Throwable ex){
//打印出日誌,方便調試的時候查看,否則不拋出異常
Log.d("BaseActivity",thread.getName()+"exception==="+ex.getMessage());
defalutHandler.uncaughtException(thread,ex);
//判斷是否有網
if(CommUtil.checkNetwork(mContext)!=Const.NO_NETWORK){
collectDeviceInfo(ex);
}else{
HashMap<String,String> map = new HashMap<>();
map.put(Const.DEVICE_ID, Build.DEVICE);
map.put(Const.CURRENT_VERSION,CommUtil.getCurrentVersion(mContext));
map.put(Const.EXCEPTION_CAUSE,ex.getMessage());
new SaveFileLogUtils().saveCrashInfo2File(mContext,ex,map);
}
}
//測試使用,拋出的異常
public void throwException(){
throw new NullPointerException("珍愛生命,遠離Exception");
}
private void collectDeviceInfo(Throwable ex){
Intent intent = new Intent(mContext, UploadLogService.class);
intent.putExtra(Const.DEVICE_ID, Build.DEVICE);
intent.putExtra(Const.CURRENT_VERSION, CommUtil.getCurrentVersion(mContext));
intent.putExtra(Const.EXCEPTION_CAUSE, ex.getMessage());
startService(intent);
}
}
代碼其實就是先實現上面3步,然後通過Thread.getDefaultUncaughtExceptionHandler()去獲取系統閃退的Dialog,返回的是一個Thread.UncaughtExceptionHandler類型的defalutHandler對象。然後在uncaughtException方法裏面調用defalutHandler.uncaughtException(thread,ex),然後使用一個HandleException方法在BaseActivity裏面進行默認處理,方便其他類去繼承重寫這個方法。收集閃退信息思路就是先判斷當前網絡環境是否有網,有網的情況下就是start一個service,把獲取閃退的基本信息和手機基本信息通過Intent傳遞給service,在Service裏面通過接口上傳信息到服務器。如果沒網的情況下,就先把信息保存到手機本地文件,在有網的情況下,把當前文件進行上傳,文件上傳具體步驟根據自己情況來處理就好了。
上傳信息的Service代碼:
public class UploadLogService extends IntentService{
public UploadLogService() {
super("UploadLogService");
}
@Override
protected void onHandleIntent(Intent intent) {
String deviceId = intent.getStringExtra(Const.DEVICE_ID);
String currentVersion = intent.getStringExtra(Const.CURRENT_VERSION);
String exception = intent.getStringExtra(Const.EXCEPTION_CAUSE);
Log.d("zgx","deviceId======="+deviceId);
Log.d("zgx","currentVersion======="+currentVersion);
Log.d("zgx","exception======="+exception);
try {
//模擬接口上傳時間
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//接口上傳完成後,結束當前service
stopSelf();
}
}
保存信息到手機本地的代碼:
SaveFileLogUtils.class
public class SaveFileLogUtils {
//用於格式化日期,作爲日誌文件名的一部分
private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
public void saveCrashInfo2File(Context context,Throwable ex, HashMap<String,String> hashMap){
StringBuilder sb = new StringBuilder();
for(Map.Entry<String,String> entry :hashMap.entrySet()){
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause() ;
if(cause!=null){
cause.printStackTrace(printWriter);
}
printWriter.close();
String result = writer.toString();
sb.append(result);
try {
long timestamp = System.currentTimeMillis();
String time = formatter.format(new Date());
String fileName = "crash-" + time + "-" + timestamp + ".log";
String path = StorageUtils.getCacheDirectory(context).getAbsolutePath()+"/crash";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(dir.getAbsolutePath() +"/"+fileName);
fos.write(sb.toString().getBytes());
fos.close();
} catch (Exception e) {
}
}
}
StorageUtils.class
public final class StorageUtils {
private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE";
private StorageUtils() {
}
public static File getCacheDirectory(Context context) {
return getCacheDirectory(context, true);
}
public static File getCacheDirectory(Context context, boolean preferExternal) {
File appCacheDir = null;
String externalStorageState;
try {
externalStorageState = Environment.getExternalStorageState();
} catch (NullPointerException var5) {
externalStorageState = "";
} catch (IncompatibleClassChangeError var6) {
externalStorageState = "";
}
if(preferExternal && "mounted".equals(externalStorageState) && hasExternalStoragePermission(context)) {
appCacheDir = getExternalCacheDir(context);
}
if(appCacheDir == null) {
appCacheDir = context.getFilesDir();
}
if(appCacheDir == null) {
String cacheDirPath = "/data/data/" + context.getPackageName() + "/data/";
appCacheDir = new File(cacheDirPath);
}
return appCacheDir;
}
private static File getExternalCacheDir(Context context) {
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "data");
if(!appCacheDir.exists()) {
if(!appCacheDir.mkdirs()) {
return null;
}
try {
(new File(appCacheDir, ".nomedia")).createNewFile();
} catch (IOException var4) {
}
}
return appCacheDir;
}
private static boolean hasExternalStoragePermission(Context context) {
int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION);
return perm == 0;
}
}
簡單寫一個測試activity,代碼如下:
CrashActivity.class
public class CrashActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exit_app);
throwException();
}
}
這個例子基本上就完成了。
有時候,突然想自己去實現這個閃退Dialog。那怎麼辦呢。這可怎麼搞,其實基於上面的例子就很簡單了。我們只需要在CrashActivity 裏面重寫HandleException方法。彈一個自定義對話框出來就行了。代碼如下:
@Override
public void HandleException(Thread thread, Throwable ex) {
super.HandleException(thread, ex);
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
new AlertDialog.Builder(mContext).setTitle("提示").setCancelable(false)
.setMessage("oo,我掛掉了...").setNeutralButton("再來一次,讓我重啓復活吧", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//第一種方式,結束fc的activity,直接返回activity棧上一層
finish();
System.exit(0);
//第二種方式,結束所有的activity,返回桌面
/*
finish();//結束當前fc的activity
Intent home = new Intent(Intent.ACTION_MAIN);
home.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
home.addCategory(Intent.CATEGORY_HOME);
startActivity(home);
exitAllActivity();
System.exit(1);*/
}
})
.create().show();
Looper.loop();
}
}).start();
}
然後在BaseActivity添加部分代碼:
定義一個LinkedList,用來保存activity。
public static LinkedList<BaseActivity> allActivity = new LinkedList<>();
在onCreate()添加代碼
allActivity.add(this);
在onDestroy()添加代碼
allActivity.remove(this);
添加方法
public void exitAllActivity(){
for(BaseActivity activity:allActivity){
if(activity!=null){
activity.finish();
}
}
}
HandleException方法裏面修改一句代碼
defalutHandler.uncaughtException(thread,ex);
替換爲
if(ex==null){
defalutHandler.uncaughtException(thread,ex);
return;
}
因爲main線程已經中止了(背景是黑色的原因),而HandleException運行在UI線程。這樣我們就不能直接彈框。所以這裏創建一個帶消息體的線程,來處理彈框消息,效果如下圖1,同時我們也可以測試創建一個新線程裏面來拋異常,類似這樣
new Thread(new Runnable() {
@Override
public void run() {
throwException();
}
}).start();
則會產生下圖2的效果。
圖1:
圖2:
onClick以兩種方式退出,一種是直接finish掉crash的activity,返回到棧上一個activity,也就是上一個頁面。第二種方式就是直接退出整個應用。同時也可以在這裏一定時間後重啓應用,通過方法:
Intent intent = new Intent();
intent.setClassName("包名", MainActivity.class.getName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent restartIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
添加幾個權限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
最後,上傳源碼,開發環境爲Android studio 2.1 Preview4。注意修改build.gradle的classpath