android自定义日志组件

在Android开发中经常会使用日志来进行调试、记录运行状态、定位问题,但是系统提供的日志组件(android.util.Log)只提供了基本的日志输出功能,使用上并不方面,因此需要分装系统日志组件,扩展更丰富的功能。

主要功能

根据Gradle配置设置统一的日志开关

在项目开发中常有需要控制日志能否打印,比如,开发中要打印日志,上线后要关闭日志;再如,有些渠道要打印日志,而有些要关闭日志。
  原来都是把开关定义为一个常量

public static final boolean debug = false;

然后根据不同的情况来回来修改,不仅繁琐而且容易遗忘。
  其实Gradle构建配置提供了一个属性BuildConfig.DEBUG用来判断当前构建是否是debug类型,利用这个属性就可以解决开发中和上线的日志开关的设置。

ps.只能使用主工程的BuildConfig.DEBUG属性,Libaray工程的构建类型都是release类型,无法区分。

但是如果要处理不同渠道应用的日志开关,就需要新建一个构建属性,在不同的渠道配置中设置

productFlavors{
    dev{
      buildConfigField "boolean", "LOG_SWITCH", "true"
    }

    rele{
      buildConfigField "boolean", "LOG_SWITCH", "false"        
    }
} 

利用新的属性BuildConfig.LOG_SWITCH来区分。

支持对象打印

系统日志组件只能打印字符串类型的内容,但是常有要打印自定对象内容的需要,比如返回的内容、传递的参数,希望以json格式展示出来。自己实现了对象转换为json字符串功能。

ps.为什么不用第三方json库?
  1.第三方的json库比如Gson、Jackson,都有100-200KB,而日志组件组件只需要把对象转换为json字符串这一个功能,不希望为此增加额外的大小。
  2.自己实现json转换能够做一些定制化的展示和处理

实现对象转换为json字符串主要就是利用java反射,获取到属性的名称和对应的值,再按json规则拼接成json字符串。

public String parse(Object obj){
  Class clazz = obj.getClass();
  Field[] fields = clazz.getDeclaredFields();
  for (int i = 0, size = fields.length; i < size; i++) {
      Field field = fields[i];
      field.setAccessible(true);
      Object suObj = field.get(obj);
      String fieldName = field.getName();
      String value = parse(suObj);
  }
}

特殊情况:

  1. 非静态内部类解析
    非静态内部类持有一个外部类的引用,这个外部类属性是没有必要打印出来的。编译后内部类所持有的外部类属性都是命名为"this$0",利用这个特点判断属性名称做过滤。
  2. 系统所属类的解析
    如果要解析的类或类其中的属性涉及到系统所属的类比如Activity、Application,我们并不希望再去解析这些系统所属类的属性,如果解析的话往往会导致属性过多而一直阻塞,只要直接打印出系统所属类的名称就好了。
private boolean isSystemClass(Class clazz) {
    String name = clazz.getName();
    if (name.startsWith("java") || name.startsWith("android")) {
        return true;
    } else {
        Class superClass = clazz.getSuperclass();
        if (superClass != null) {
            //如果父类是Object类则不是系统类
            if (superClass.equals(Object.class)) {
                return false;
            } else {
                return isSystemClass(superClass);
            }
        }
    }
    return false;
}

系统所属类的包名基本都是以“java”、“android”开头,可以以此来判断;如果是系统所属类的子类,需要递归判断父类是否是系统所属类。

  1. 类之间互相引用时的解析
    如果两个类相互引用,解析这样类就会无线循环,抛出stackOverFlowError异常,因此增加了解析层级判断,超过3层就不继续解析。

快速定位到打印日志位置

打印日志后,要查看所打印地方的逻辑,还需要查找对应类所在方法的具体行数,往往这个过程就花了不少时间,而我们平时如果程序出来抛出异常,是可以直接点击定位到具体行数的。

android自定义日志组件_第1张图片
Paste_Image.png

类似于异常打印的实现,获取方法的调用栈信息

StackTraceElement[] elements = Thread.currentThread().getStackTrace();
for (int i = 0, size = elements.length; i < size; i++) {
    element = elements[i];
    if (element.getClassName().equals(MLogger.class.getName())) {
        isFindTag = true;
        continue;
    }
    if (isFindTag) {
        String methodName = filterMethodName(element.getMethodName());
        sb.append(element.getFileName());
        sb.append(".");
        sb.append(methodName); 
       String eleStr = element.toString();
        int start = eleStr.indexOf("(");
        sb.append(eleStr.substring(start));
        sb.append("\n");
        return sb.toString();
    }
}

遍历出调用日志打印的方法信息拼接成字符串"(类名:行数)",如

Paste_Image.png

点击就可以快速定位到日志具体打印的地方。

无侵入打印方法执行时间

在做应用的一些性能测试的时候,有时需要打印出方法的执行时间 ,常用的方式就是在方法的前后获取时间,计算时间差。

android自定义日志组件_第2张图片
Paste_Image.png

每处需要打印时间的地方都要都要加入这块逻辑,导致大量重复代码,而且与方法本身的业务耦合的也很紧密,那么有没有更好的实现方式呢?
  使用AOP面向切面的设计实现,定义某一类方法为一个切面,在这个切面的前后计算时间,打印出时间差,这样就统一了方法时间的计算,对方法本身无任何影响。
  AOP只是一种方法论,具体该如何实现呢?先介绍几种方式

  1. jdk代理
    a. jdk静态代理
//真实类
class Real{
    public void action(){
    }
}
//代理类
class ProxyReal{
    private Real real;
    public ProxyReal(Real real){
        this.real = real;
    }
    public void action(){ 
       long startTime = System.nanoTime(); 
       real.action(); 
       long totalTime = TimeUnit.NANOSECONDS.toMillis(endTime - startTime); 
       Log.d(TAG, totalTime + "");
    }
}

就是创建了一个代理类持有真实类的引用,并封装了被代理类的方法,可以执行时长计算。这种方式的好处就是在编译器就创建好了代理对象,及静态代理,虽然把计算时长的逻辑与具体方法的业务隔离开了,但是但是要为每一个计算时长的类创建代理类,会产生大量的代理类不利于管理,而且也无法解决大量重复计算时长逻辑的问题。

b.jdk动态代理

public class Proxy  implements InvocationHandler {
private  Object object;
public proxy(Object obj){     
  this.object=obj;  
}
  
@Override  
public Object invoke(Object proxy, Method method, Object[] args)  throws Throwable {
       long startTime = System.nanoTime(); 
       Object resultObject= method.invoke(object, args);
       long totalTime = TimeUnit.NANOSECONDS.toMillis(endTime - startTime); 
       Log.d(TAG, totalTime + "");

  }      
}

//执行
 public static void main(String args[]){
        Real target = new Real();//要进行代理的目标业务类
        Proxy  handler = new Proxy(target);//用代理类把目标业务类进行编织
 //创建代理实例,它可以看作是要代理的目标业务类的加多了横切代码(方法)的一个子类
        IReal proxy = (IReal )Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), handler);

        proxy.action();
   }

在运行时创建了代理类,利用java反射机制执行被代理的方法。好处就是计算时长的逻辑与具体方法的业务隔离开了,只要定义一个代理对象Proxy ,就是代理其他类,这样就只要写一份计算时长的逻辑;但是jdk动态只能针对实现接口的类,但是我要计算时长的类并不能保证一定实现了接口,而且由于是在运行时通过反射执行被代理方法会影响一定的性能。

  1. CGLib
public class CglibProxy implements MethodInterceptor{
  private Enhancer enhancer = new Enhancer();  
  public Object getProxy(Class clazz){  
//设置需要创建子类的类  
  enhancer.setSuperclass(clazz);  
  enhancer.setCallback(this);  
  //通过字节码技术动态创建子类实例  
  return enhancer.create();  
 } 
//实现MethodInterceptor接口方法  
 public Object intercept(Object obj, Method method, Object[] args,  
   MethodProxy proxy) throws Throwable {  
       long startTime = System.nanoTime(); 
       Object result = proxy.invokeSuper(obj, args);  
       long totalTime = TimeUnit.NANOSECONDS.toMillis(endTime - startTime); 
       Log.d(TAG, totalTime + "");
    return result;  
 }  
}  
public static void main(String[] args) {  
  CglibProxy proxy = new CglibProxy();  
  //通过生成子类的方式创建代理类  
  Real proxyImp = (Real)proxy.getProxy(Real.class);  
  proxyImp.action();  
 }  

CGLib也是动态代理,在运行时通过asm(字节码处理框架),动态构建字节码文件,创建一个被代理类的子类,并织入计算时长的逻辑,他的好处就是被代理类不需要实现接口,方法的执行并不是利用反射,所以快很多,但是代理类的创建比jdk动态代理慢,开销较大。
3、AspectJ

@Aspect
public class TraceAspect {
    private static final String POINTCUT_METHOD =
            "execution(@aop.annotation.DebugTimeTrace * *(..))";
    private static final String POINTCUT_CONSTRUCTOR =
            "execution(@aop.annotation.DebugTimeTrace *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodAnnotatedWithDebugTrace() {    }
    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructorAnnotatedDebugTrace() {    }

    @Around("methodAnnotatedWithDebugTrace()||constructorAnnotatedDebugTrace()")
    public Object traceTimeJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginNanos = System.nanoTime();
        Object result = joinPoint.proceed();
        long endNanos = System.nanoTime();
        long totalMillis = TimeUnit.NANOSECONDS.toMillis(endNanos - beginNanos);
        Log.d("TraceAspect", createLogMsg(joinPoint, totalMillis));
        return result;
    }
}
@DebugTimeTrace
public void action(){
}

AspectJ是在编译时使用自身的编译器ajc(ajc只是在java编译器上增加了对自己的一些关键字识别和编译方法),把我们要添加的计算时长逻辑织入目标类中。
AspectJ的使用非常方便,可以通过java注解的方法来定义。
@Aspect,表示该类处理具体切面定义逻辑
@Pointcut,定义关注的切面,可以用正则表达式来过滤
@Around,定义每一个被关注的执行点的处理逻辑。

android自定义日志组件_第3张图片
Paste_Image.png

  在这个例子中我们定义了一个注解,定义关注的切面是设置这个注解的方法,然后在处理逻辑上加上时长的计算。我们想要打印哪个方法的执行时间,就只要添加这个注解就好了。真正做到了无侵入,而且是在编译时就织入了计算时长代码,性能也比较高。
  当然他有个条件就是必须要用自身的编译器ajc编译,需要在我们个构建脚本中定义这块配置

buildscript {
    repositories {
        jcenter(){
            url 'http://jcenter.bintray.com'
        }
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.6'
    }
}
dependencies {
    compile 'org.aspectj:aspectjrt:1.8.6'
    compile project(':aopannotation')}repositories {
    jcenter(){
        url 'http://jcenter.bintray.com'
    }
}
android {
    compileSdkVersion 23
    buildToolsVersion "25.0.1"
    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner
 "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        } 
   }
}
android.libraryVariants.all {variant ->
    def log = project.logger
    LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = [
                "-showWeaveInfo", 
               "-1.5", 
               "-inpath", javaCompile.destinationDir.toString(),
                "-aspectpath", javaCompile.classpath.asPath,
                "-d", javaCompile.destinationDir.toString(),
                "-classpath", javaCompile.classpath.asPath,
                "-bootclasspath", android.bootClasspath.join(File.pathSeparator)
        ] 
       MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL: 
                   log.error message.message, message.thrown
                    break; 
               case IMessage.WARNING: 
                   log.warn message.message, message.thrown 
                   break;
                case IMessage.INFO:
                    log.info message.message, message.thrown 
                   break; 
               case IMessage.DEBUG:
                    log.debug message.message, message.thrown 
                   break; 
           } 
       }
    }
}

配置的代码非常长,而且每一个要使用AspectJ功能模块都要在其build.gradle中定义这块配置,这又是一大串的重复代码,有没有更友好的方式呢,这里使用了gradle自定义插件

class MLoggerPlugin implements Plugin {
    void apply(Project project) {
        def hasApp = project.plugins.withType(AppPlugin)
        def hasLib = project.plugins.withType(LibraryPlugin) 
       final def variants
        final def log = project.logger
        if (hasApp) { 
           variants = project.android.applicationVariants
        } else {
            variants = project.android.libraryVariants
        } 
       project.dependencies { 
           debugCompile project.project(":aoplog")
           debugCompile 'org.aspectj:aspectjrt:1.8.6' 
           compile project.project(":aopannotation")
        } 
       variants.all { variant ->
            if (!variant.buildType.isDebuggable()) {
                println "Skipping non-debuggable build type '${variant.buildType.name}'." 
               return;
            } 
           JavaCompile javaCompile = variant.javaCompile
            javaCompile.doLast {
                String[] args = [ 
                       "-showWeaveInfo",
                        "-1.5",
                        "-inpath", javaCompile.destinationDir.toString(), 
                       "-aspectpath", javaCompile.classpath.asPath,
                        "-d", javaCompile.destinationDir.toString(), 
                       "-classpath", javaCompile.classpath.asPath, 
                       "-bootclasspath", 
project.android.bootClasspath.join(File.pathSeparator)
                ]
                MessageHandler handler = new MessageHandler(true);  
              new Main().run(args, handler);
                for (IMessage message : handler.getMessages(null, true)) {  
                  switch (message.getKind()) {
                        case IMessage.ABORT: 
                       case IMessage.ERROR:
                        case IMessage.FAIL:
                            log.error message.message, message.thrown
                            break;
                        case IMessage.WARNING: 
                           log.warn message.message, message.thrown
                            break;
                        case IMessage.INFO: 
                           log.info message.message, message.thrown
                            break;
                        case IMessage.DEBUG:
                            log.debug message.message, message.thrown 
                           break; 
                   } 
               }
            }
        } 
   }
}

在需要使用AspectJ功能的module构建配置中引入这个插件就好了

apply plugin: com.logger.plugin.MLoggerPlugin

这对这一块我们只想在开发时监控,因此在插件中配置debug时依赖aspectJ库

project.dependencies {
    debugCompile project.project(":aoplog")
    debugCompile 'org.aspectj:aspectjrt:1.8.6'
    compile project.project(":aopannotation")}

这样对于线上版本不会使用ajc去编译代码。

你可能感兴趣的:(android自定义日志组件)