1. 程式人生 > >Android 日誌收集原理與實踐

Android 日誌收集原理與實踐

導讀

Android應用在開發和測試的過程中,如果出現crash,我們一般通過logcat日誌資訊就可以定位到crash的原因,從而排除BUG。但是如果我們的應用已經發布到了市場上,到時候再發生crash的話,我們想拿到crash的日誌資訊就很麻煩了,因為我們不可能去跟每一個出現crash的使用者來索要crash日誌。那怎麼辦呢?這個時候就需要我們的日誌資訊收集系統出手了。
最後我會把程式碼放的Github上面,並生成依賴包供大家使用。如果有特殊需求,也可以自己修改程式碼去實現。

必要性

應用的日誌資訊收集系統基本上是每一個應用的標配,它對使用者的留存、口碑等都有很大的存在意義。但是當前的大環境導致我們開發人員很少去自己開發這個功能,原因很簡單,因為有很多的第三方SDK供我們選擇,而且功能齊全。但是做為一名合格的程式設計師,如果只是伸手而不去創造,那我們遲早也會被淘汰。

開發流程

要想實現這個功能,我們要做的工作基本上就是三個事情:
1)Crash日誌的捕獲
2)Crash堆疊資訊的獲取
3)獲取到的資訊上報

基本原理

本身我們的Android應用程式都是基於Java開發的,所以異常的處理也是沿用的Java異常處理機制。在Java中異常被分為兩種:CheckedException 和 UnCheckedException。CheckedException是編譯異常,這個一般在我們寫程式碼的時候就已經處理了,所以在這裡我們不需要過多關注。UnCheckedException是執行時異常,這個就是我們今天關注的重點。
好的,現在我們知道了要幹什麼,那要怎麼幹呢?也就是說要怎麼去捕獲UnCheckedException的異常呢?好在JavaAPI提供了一個全域性捕獲異常的處理器,Thread.UncaughtExceptionHandler介面就是我們需要的這個處理器,只要我們實現這個介面並重寫其中的uncaughtException方法豈不就可以獲取到我們的堆疊資訊了麼?

開始實現

1.首先我們new moudel ,這樣以後不管在哪個專案中使用都可以。這樣就很方便了,就算以後升級了,只要對這一個單獨的去升級就OK了。
2.new 一個MyUnCheckedExceptionHandler類並實現Thread.UncaughtExceptionHandler介面,然後重寫uncaughtException方法。然後在方法內去獲取Crash的堆疊資訊。

Public class MyUnCheckedExceptionHandler implements Thread.UncaughtExceptionHandler{
        @Override
        Public
void uncaughtException(Threadt,Throwablee){ Final Writerresult = new StringWriter(); Final PrintWriterprintWriter = new PrintWriter(result); Throwable cause=e; while(null!=cause){ cause.printStackTrace(printWriter); cause=cause.getCause(); } Final StringstacktraceAsString=result.toString(); printWriter.close(); } }

3.依賴我們的lib,然後再Application中引入。

Public class MyApplication extends Application{
        @Override
        publicvoidonCreate(){
            super.onCreate();

            addCrashSystem();
        }
        /*啟用日誌收集系統*/
        privatevoidaddCrashSystem(){
            Thread.setDefaultUncaughtExceptionHandler(new MyUnCheckedExceptionHandler());
        }
    }

至此,我們的第一步就算完成了,Crash日誌已經捕獲到了。但是僅僅獲取堆疊資訊對我們的問題定位和解決還是有點不足,所以我們再加點東西進去。
獲取執行緒資訊
執行緒的基本資訊有ID、Name、優先順序、所線上程組等,可以根據我們的需要去獲取。

/*執行緒資訊收集*/
    public class ThreadCollector{
        Public static Stringcollector(Threadthread){
            StringBuffer result=new StringBuffer();
            if(null!=thread){
                result.append("id=").append(thread.getId()).append("\n");
                result.append("name=").append(thread.getName()).append("\n");
                result.append("priority=").append(thread.getPriority()).append("\n");
                if(null!=thread.getThreadGroup()){
                    result.append("groupName=").append(thread.getThreadGroup().getName()).append("\n");
                }
            }
        returnresult.toString();
        }
    }

3.SharedOreference 資訊
除了執行緒的資訊,有的時候我們的Crash會依賴SharedPreference中的某些資訊項。這就需要我們的Crash資訊攜帶這個有效的資訊了。

/*收集sharedpreference資訊*/
    public class SharedPreferenceCollector{
        private final Context mContext;
        private String[] mSharedPrefIds;
        public SharedPreferenceCollector(Contextcontext,String[]sharedPrefIds){
            mContext=context;
            mSharedPrefIds=sharedPrefIds;
        }
        public String collect(){
            final StringBuilder result=new StringBuilder();
            //收集預設的資訊
            final Map<String,SharedPreferences> sharedPrefs=new TreeMap<>();
            sharedPrefs.put("default",PreferenceManager.getDefaultSharedPreferences(mContext));
            //收集自定義的資訊
            if(null!=mSharedPrefIds){
                for(final StringsharedPrefId:mSharedPrefIds){
                    sharedPrefs.put(sharedPrefId,mContext.getSharedPreferences(sharedPrefId,Context.MODE_PRIVATE));
                }
            }
            //遍歷所有的sharepreference檔案
            for(Map.Entry<String,SharedPreferences>entry:sharedPrefs.entrySet()){
                final StringsharedPrefId=entry.getKey();
                final SharedPreferencesprefs=entry.getValue();
                final Map<String,?>prefEntries=prefs.getAll();

                //如果sharedpreference為空
                if(prefEntries.isEmpty()){
                    result.append(sharedPrefId).append("=").append("empty\n");
                    continue;
                }
                //遍歷新增某個sharedpreference檔案中的內容
                for(finalMap.Entry<String,?>prefEntry:prefEntries.entrySet()){
                    final ObjectprefValus=prefEntry.getValue();
                    result.append(sharedPrefId).append(",").append(prefEntry.getKey()).append("=");
                    result.append(prefValus==null?"null":prefValus.toString()).append("\n");
                }
                result.append("\n");
            }
            return result.toString();
        }
    }

4.系統設定
有時候我們需要獲取藍芽、WiFi、當前語言、螢幕亮度等資訊,這些資訊都存放在了資料庫表中對應的URI分別為:
藍芽:content://settings/system
WiFi:content://setttings/secure
首選語言及螢幕亮度:content://settings/global
獲取system的資訊程式碼如下:

public String collectSystemSettings(){
        final StringBuilderresult=new StringBuilder();
        final Field[]keys=Settings.System.class.getFields();
        for(final Field key:keys){
            if(!key.isAnnotationPresent(Deprecated.class)&&key.getType()==String.class){
                try{
                    Final Objectvalue=Settings.System.getString(mContext.getContentResolver(),(String)key.get(null));
                    if(value!=null){
                        result.append(key.getName()).append("=").append(value).append("\n");
                    }
                }catch(IllegalArgumentExceptione){
                    Log.w(LOG_TAG,"Error:",e);
                }catch(IllegalAccessExceptione){
                    Log.w(LOG_TAG,"Error:",e);
                }
            }
        }
        returnresult.toString();
    }

獲取secure的資訊程式碼如下:

public String collectSystemSettings(){
        final StringBuilderresult=new StringBuilder();
        final Field[]keys=Settings.Secure.class.getFields();
        for(final Field key:keys){
            if(!key.isAnnotationPresent(Deprecated.class)&&key.getType()==String.class&&isAuthorized(key)){
                try{
                    Final Objectvalue=Settings.Secure.getString(mContext.getContentResolver(),(String)key.get(null));
                    if(value!=null){
                        result.append(key.getName()).append("=").append(value).append("\n");
                    }
                }catch(IllegalArgumentException e){
                    Log.w(LOG_TAG,"Error:",e);
                }catch(IllegalAccessException e){
                    Log.w(LOG_TAG,"Error:",e);
                }
            }
        }
        returnresult.toString();
    }

獲取global資訊程式碼如下:

public StringcollectGlobalSettings(){
        if(Build.VERSION.SDK_INT<Build.VERSION_CODES.JELLY_BEAN_MR1){
            return"";
        }
        final StringBuilder result=newStringBuilder();
        try{
            final Class<?> globalClass=Class.forName("android.provider.Settings$Global");
            final Field[] keys=globalClass.getFields();
            final MethodgetString=globalClass.getMethod("getString",ContentProvider.class,String.class);
            for(finalFieldkey:keys){
                if(!key.isAnnotationPresent(Deprecated.class)&&key.getType()==String.class&&isAuthorized(key)){
                    final Objectvalue=getString.invoke(null,mContext.getContentResolver(),key.get(null));
                    if(null!=value){
                        result.append(key.getName()).append("=").append(value).append("\n");
                    }
                }
            }
        }catch(ClassNotFoundException e){
            Log.w(LOG_TAG,"Error:",e);
        }catch(NoSuchMethodException e){
            Log.w(LOG_TAG,"Error:",e);
        }catch(IllegalAccessException e){
            Log.w(LOG_TAG,"Error:",e);
        }catch(InvocationTargetException e){
            Log.w(LOG_TAG,"Error:",e);
        }
        returnresult.toString();
    }

一般情況下這些就夠我們使用了,如果你覺得不夠,還可以有記憶體的使用情況,甚至Native層的異常我們都可以去捕獲。

Crash上報

到這裡我們基本就算搞定了,但是還少點東西,如果把應用的外部資訊攜帶上豈不是更美?
應用版本號、系統型別、手機型號、手機唯一ID、渠道號、時間、包名…
具體的上傳要跟據你自己的實際情況來定了,像我是自己搭建的一個日誌接受伺服器。