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层的异常我们都可以去捕获。
到这里我们基本就算搞定了,但是还少点东西,如果把应用的外部信息携带上岂不是更美?
应用版本号、系统类型、手机型号、手机唯一ID、渠道号、时间、包名…
具体的上传要跟据你自己的实际情况来定了,像我是自己搭建的一个日志接受服务器。