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、渠道號、時間、包名…
具體的上傳要跟據你自己的實際情況來定了,像我是自己搭建的一個日誌接受伺服器。