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 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.Entryentry:sharedPrefs.entrySet()){
                final StringsharedPrefId=entry.getKey();
                final SharedPreferencesprefs=entry.getValue();
                final MapprefEntries=prefs.getAll();

                //如果sharedpreference为空
                if(prefEntries.isEmpty()){
                    result.append(sharedPrefId).append("=").append("empty\n");
                    continue;
                }
                //遍历添加某个sharedpreference文件中的内容
                for(finalMap.EntryprefEntry: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.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、渠道号、时间、包名…
具体的上传要跟据你自己的实际情况来定了,像我是自己搭建的一个日志接受服务器。

你可能感兴趣的:(Android知识点总结)