android crash log收集總結

一般應用在運行時,不可避免的會發生crash。一般應用crash分為三種:未捕獲的異常、ANR(Application Not Responding)、閃退(NDK引發錯誤)。

  1. 未捕獲的異常:一般系統會將crash log輸入出控制臺,開發人員可以及時查看。對于發布的版本,則可以通過一些方式收集crash log, 這樣方便開發人員分析程序崩潰原因,更好的改進代碼。同時當程序崩潰時,表現出閃退或已停止,可能會給用戶帶來很不好的體驗,這需要我們重新定義程序崩潰時發出一些友情提示。
  2. ANR:系統一般會彈出提示框,讓用戶選擇waiting還是close應用,在/data/anr目錄下還生成一個traces.txt文件,方便程序員查看系統產生anr異常的堆棧和線程信息。
  3. 應用閃退,很可能是由于應用中的NDK引發錯誤,logcat打印出來的那堆日志根據看不懂,很難從日志當中定位錯誤的根源。如果是上線產品,一般很難收集到這些crash log,但也不是沒有辦法。

下面就我們常遇到的crash log收集進行總結:

一、java層崩潰:

對于java層的crash,android中有處理這類問題的方法,在Thread類中的一個方法#setDefaultUncaughtExceptionHandler

public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {  
    Thread.defaultUncaughtHandler = handler;  
}

通過該方法我們可以設置系統的默認異常處理器。其實UncaughtExceptionHandler是一個接口,

 public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

我們可以實現uncaughtException方法,自定義crash收集方法,然后將crash信息保存到本地的文件中就好了。
具體實現步驟:

1? 建立異常處理的CrashHandler,實現系統的UncaughtExceptionHandler

當crash發生的時候,我們可以捕獲到異常信息,把異常信息存儲到SD卡中,然后在合適的時機通過網絡將crash信息上傳到服務器上,這樣開發人員就可以分析用戶crash的場景從而在后面的版本中修復此類crash。具體實現如下:

public class MyCrashHandler implements UncaughtExceptionHandler {

    private static MyCrashHandler crashHandler;
    private static SimpleDateFormat formatter = new          SimpleDateFormat("yyyyMMddHHmmss");
    private static Date curDate = new Date(System.currentTimeMillis());//獲取當前時間
    private static String str = formatter.format(curDate);
    private static String TAG = "MyCrashHandler";
    //系統默認的UncaughtException處理類
    private UncaughtExceptionHandler mDefaultHandler;
    //程序的Context對象
    private Context mContext;
    //用來存儲設備信息和異常信息
    private Map<String, String> infos = new HashMap<String, String>();


    @Override
    public void uncaughtException(Thread thread, Throwable ex) {

        if (!handleException(ex) && mDefaultHandler != null) {
            //如果用戶沒有處理則讓系統默認的異常處理器來處理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                Log.e(TAG, "error : ", e);
            }
            //退出程序
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }
    }

    /**
     * 自定義錯誤處理,收集錯誤信息 發送錯誤報告等操作均在此完成.
     *
     * @param ex
     * @return true:如果處理了該異常信息;否則返回false.
     */
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }
        //使用Toast來顯示異常信息
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();//準備發消息的MessageQueue
                Toast.makeText(mContext, "很抱歉,程序出現異常,即將退出.", Toast.LENGTH_LONG).show();
                Looper.loop();
            }
        }.start();
        //收集設備參數信息
        collectDeviceInfo(mContext);
        //保存日志文件
        saveCrashInfo2File(ex);
        return true;
    }


    /**
     * 保存錯誤信息到文件中
     *
     * @param ex
     * @return 返回文件名稱, 便于將文件傳送到服務器
     */
    private String saveCrashInfo2File(Throwable ex) {

        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : infos.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();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String result = writer.toString();
        sb.append(result);
        try {
            long timestamp = System.currentTimeMillis();

            String fileName = "crash-" + str + "-" + timestamp + ".log";
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                String path = Environment.getExternalStorageDirectory().getPath()+"/crash/";
                File dir = new File(path);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                FileOutputStream fos = new FileOutputStream(path + fileName);
                fos.write(sb.toString().getBytes());
                System.out.println(sb.toString());
                fos.close();
            }
            return fileName;
        } catch (Exception e) {
            Log.e(TAG, "an error occured while writing file...", e);
        }
        return null;
    }

    /**
     * 初始化
     *
     * @param context
     */
    public void init(Context context) {
        mContext = context;
        //獲取系統默認的UncaughtException處理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //設置該CrashHandler為程序的默認處理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 收集設備參數信息
     *
     * @param ctx
     */
    public void collectDeviceInfo(Context ctx) {
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
            if (pi != null) {
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infos.put("versionName", versionName);
                infos.put("versionCode", versionCode);
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "an error occured when collect package info", e);
        }
        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                infos.put(field.getName(), field.get(null).toString());
                Log.d(TAG, field.getName() + " : " + field.get(null));
            } catch (Exception e) {
                Log.e(TAG, "an error occured when collect crash info", e);
            }
        }
    }

    private MyCrashHandler() {
    }

    //單例
    public static MyCrashHandler instance() {
        if (crashHandler == null) {
            crashHandler = new MyCrashHandler();
        }
        return crashHandler;
    }
}

2? 為ui線程添加默認異常事件Handler

在ui線程中添加異常處理Handler,在應用的Application中添加而不是在Activity中添加。應用啟動時是先走Application,故只需要在Application的onCreate方法里進行CrashHandler的初始化即可。

   public class CrashApplication extends MultiDexApplication  {
        private static final String TAG ="CrashApplication" ;
        private static Context mContext;
        @Override
        public void onCreate() {
            super.onCreate();
            MyCrashHandler crashHandler = MyCrashHandler.instance();
            crashHandler.init(getApplicationContext());
        }
    }

3?方法2

當然也可以集成第三方SDK,如友盟統計分析(http://dev.umeng.com/analytics/android-doc/integration),不僅能自動幫助應用收集crash log,還可以統計應用用戶使用情況,總用戶數、新增用戶數、日活、月活、用戶手手機版本、每個應用版本信息等情況。

具體集成方式也很簡單,只需要在友盟平臺上注冊開發者賬號,申請應用,并在AndroidMainfest.xml里配置權限

<meta-data android:value="YOUR_APP_KEY" android:name="UMENG_APPKEY"/>
<meta-data android:value="Channel ID" android:name="UMENG_CHANNEL"/>

在每個Activity的onResume方法中調用 MobclickAgent.onResume(Context) ,
onPause方法中調用 MobclickAgent.onPause(Context)

   public void onResume() {
        super.onResume();
        MobclickAgent.onResume(this);
    }
    public void onPause() {
        super.onPause();
        MobclickAgent.onPause(this);
    }

當然這可以寫在基類里,省去每個類都去寫。

二、NDK的崩潰:

由于jni代碼不支持backtrace,故不可通過實現UncaughtExceptionHandler來捕獲異常。樓主沒有具體深入研究過,但大致可歸綱以下方法:

1? 通過api sigaction來捕捉對應的異常信號

#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

參考資料:
1)【Android平臺的崩潰捕獲機制及實現】http://www.uml.org.cn/mobiledev/2016011510.asp

  1. stackOverFlow上提出的:http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on

2? 借助第三方SDK:如騰訊 Bugly

騰訊 Bugly不權可以抓取到JAVA的崩潰,同樣也可以抓取到NDK代碼的崩潰。

3? 借助google的google-breakpad

google-breakpad(https://github.com/svn2github/google-breakpad)可以捕獲ndk的異常信息,它會將堆棧信息寫入用戶指定的本地MiniDump文件中。下次啟動應用程序的時候,便可以讀取該MiniDump文件進行相應的操作,比如上傳到后臺服務器。
參考資料:1) [Android崩潰日志獲取與解析](http://openflight.blog.51cto.com/11047400/1728909

  1. [Android端線上NativeCrash收集的兩種方法(上)](http://www.lxweimin.com/p/c65186af3089

三、總結及注意事項

1? 如果代碼被proguad混淆后,輸出的crash log,出錯的行號看不出來(Unknown Source)

需要在代碼混淆的時候,加上在混淆文件里面記得加上這句:
-keepattributes SourceFile,LineNumberTable # keep住源文件以及行號

2?java層crash log收集

  1. 自己收集,可以實現系統的UncaughtExceptionHandler類,并重寫uncaughtException方法,將crash log寫到手機本地,在適時的時候,上傳到我們的服務器,優點是,所需類和方法少,可以自定義log輸出,缺點是沒有第三方平臺強大,只能收集crash log,統計信息等需要自己去做;
  2. 借助第三方平臺,如友盟、騰訊 Bugly等,這些平臺不僅能收集crash log,還添加了豐富的統計信息,可以很方便看出各版本、各平臺等出現的crash,缺點是,導入第三庫,代碼量肯定比自己重寫的類大,無法自定義應用崩潰時彈框;

3?ndk層crash log收集

1)在自己寫的jni方法里,用sigaction來鋪獲,

  1. 采用google的google-breakpad的NativeCrash日志收集方法,優點是:可以很方便google-breakpad是夸平臺開源工具,體量較大,在其基礎上生成的通用SO和dmp日志也都較大,對于sdk大小有嚴格要求的APP,可能不是很方便。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容