在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);
}
}
特殊情况:
- 非静态内部类解析
非静态内部类持有一个外部类的引用,这个外部类属性是没有必要打印出来的。编译后内部类所持有的外部类属性都是命名为"this$0",利用这个特点判断属性名称做过滤。 - 系统所属类的解析
如果要解析的类或类其中的属性涉及到系统所属的类比如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”开头,可以以此来判断;如果是系统所属类的子类,需要递归判断父类是否是系统所属类。
- 类之间互相引用时的解析
如果两个类相互引用,解析这样类就会无线循环,抛出stackOverFlowError异常,因此增加了解析层级判断,超过3层就不继续解析。
快速定位到打印日志位置
打印日志后,要查看所打印地方的逻辑,还需要查找对应类所在方法的具体行数,往往这个过程就花了不少时间,而我们平时如果程序出来抛出异常,是可以直接点击定位到具体行数的。
类似于异常打印的实现,获取方法的调用栈信息
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();
}
}
遍历出调用日志打印的方法信息拼接成字符串"(类名:行数)",如
点击就可以快速定位到日志具体打印的地方。
无侵入打印方法执行时间
在做应用的一些性能测试的时候,有时需要打印出方法的执行时间 ,常用的方式就是在方法的前后获取时间,计算时间差。
每处需要打印时间的地方都要都要加入这块逻辑,导致大量重复代码,而且与方法本身的业务耦合的也很紧密,那么有没有更好的实现方式呢?
使用AOP面向切面的设计实现,定义某一类方法为一个切面,在这个切面的前后计算时间,打印出时间差,这样就统一了方法时间的计算,对方法本身无任何影响。
AOP只是一种方法论,具体该如何实现呢?先介绍几种方式
- 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动态只能针对实现接口的类,但是我要计算时长的类并不能保证一定实现了接口,而且由于是在运行时通过反射执行被代理方法会影响一定的性能。
- 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,定义每一个被关注的执行点的处理逻辑。
在这个例子中我们定义了一个注解,定义关注的切面是设置这个注解的方法,然后在处理逻辑上加上时长的计算。我们想要打印哪个方法的执行时间,就只要添加这个注解就好了。真正做到了无侵入,而且是在编译时就织入了计算时长代码,性能也比较高。
当然他有个条件就是必须要用自身的编译器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<Project> {
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去编译代码。