1. 程式人生 > >android 自定義閃退Dialog 收集閃退資訊

android 自定義閃退Dialog 收集閃退資訊

背景

  • 平時玩應用的時候,遇到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地方放的不同。

  1. Application或者BaseActivity實現Thread.UncaughtExceptionHandler介面
  2. onCreate()方法裡面新增一句程式碼:Thread.setDefaultUncaughtExceptionHandler(this);
  3. 在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

程式碼下載