在Android開發(fā)中經(jīng)常會使用日志來進(jìn)行調(diào)試、記錄運(yùn)行狀態(tài)、定位問題,但是系統(tǒng)提供的日志組件(android.util.Log)只提供了基本的日志輸出功能,使用上并不方面,因此需要分裝系統(tǒng)日志組件,擴(kuò)展更豐富的功能。
主要功能
根據(jù)Gradle配置設(shè)置統(tǒng)一的日志開關(guān)
在項(xiàng)目開發(fā)中常有需要控制日志能否打印,比如,開發(fā)中要打印日志,上線后要關(guān)閉日志;再如,有些渠道要打印日志,而有些要關(guān)閉日志。
??原來都是把開關(guān)定義為一個(gè)常量
public static final boolean debug = false;
然后根據(jù)不同的情況來回來修改,不僅繁瑣而且容易遺忘。
??其實(shí)Gradle構(gòu)建配置提供了一個(gè)屬性BuildConfig.DEBUG用來判斷當(dāng)前構(gòu)建是否是debug類型,利用這個(gè)屬性就可以解決開發(fā)中和上線的日志開關(guān)的設(shè)置。
ps.只能使用主工程的BuildConfig.DEBUG屬性,Libaray工程的構(gòu)建類型都是release類型,無法區(qū)分。
但是如果要處理不同渠道應(yīng)用的日志開關(guān),就需要新建一個(gè)構(gòu)建屬性,在不同的渠道配置中設(shè)置
productFlavors{
dev{
buildConfigField "boolean", "LOG_SWITCH", "true"
}
rele{
buildConfigField "boolean", "LOG_SWITCH", "false"
}
}
利用新的屬性BuildConfig.LOG_SWITCH來區(qū)分。
支持對象打印
系統(tǒng)日志組件只能打印字符串類型的內(nèi)容,但是常有要打印自定對象內(nèi)容的需要,比如返回的內(nèi)容、傳遞的參數(shù),希望以json格式展示出來。自己實(shí)現(xiàn)了對象轉(zhuǎn)換為json字符串功能。
ps.為什么不用第三方j(luò)son庫?
??1.第三方的json庫比如Gson、Jackson,都有100-200KB,而日志組件組件只需要把對象轉(zhuǎn)換為json字符串這一個(gè)功能,不希望為此增加額外的大小。
??2.自己實(shí)現(xiàn)json轉(zhuǎn)換能夠做一些定制化的展示和處理
實(shí)現(xiàn)對象轉(zhuǎn)換為json字符串主要就是利用java反射,獲取到屬性的名稱和對應(yīng)的值,再按json規(guī)則拼接成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);
}
}
特殊情況:
- 非靜態(tài)內(nèi)部類解析
非靜態(tài)內(nèi)部類持有一個(gè)外部類的引用,這個(gè)外部類屬性是沒有必要打印出來的。編譯后內(nèi)部類所持有的外部類屬性都是命名為"this$0",利用這個(gè)特點(diǎn)判斷屬性名稱做過濾。 - 系統(tǒng)所屬類的解析
如果要解析的類或類其中的屬性涉及到系統(tǒng)所屬的類比如Activity、Application,我們并不希望再去解析這些系統(tǒng)所屬類的屬性,如果解析的話往往會導(dǎo)致屬性過多而一直阻塞,只要直接打印出系統(tǒng)所屬類的名稱就好了。
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類則不是系統(tǒng)類
if (superClass.equals(Object.class)) {
return false;
} else {
return isSystemClass(superClass);
}
}
}
return false;
}
系統(tǒng)所屬類的包名基本都是以“java”、“android”開頭,可以以此來判斷;如果是系統(tǒng)所屬類的子類,需要遞歸判斷父類是否是系統(tǒng)所屬類。
- 類之間互相引用時(shí)的解析
如果兩個(gè)類相互引用,解析這樣類就會無線循環(huán),拋出stackOverFlowError異常,因此增加了解析層級判斷,超過3層就不繼續(xù)解析。
快速定位到打印日志位置
打印日志后,要查看所打印地方的邏輯,還需要查找對應(yīng)類所在方法的具體行數(shù),往往這個(gè)過程就花了不少時(shí)間,而我們平時(shí)如果程序出來拋出異常,是可以直接點(diǎn)擊定位到具體行數(shù)的。

類似于異常打印的實(shí)現(xiàn),獲取方法的調(diào)用棧信息
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();
}
}
遍歷出調(diào)用日志打印的方法信息拼接成字符串"(類名:行數(shù))",如

點(diǎn)擊就可以快速定位到日志具體打印的地方。
無侵入打印方法執(zhí)行時(shí)間
在做應(yīng)用的一些性能測試的時(shí)候,有時(shí)需要打印出方法的執(zhí)行時(shí)間 ,常用的方式就是在方法的前后獲取時(shí)間,計(jì)算時(shí)間差。

每處需要打印時(shí)間的地方都要都要加入這塊邏輯,導(dǎo)致大量重復(fù)代碼,而且與方法本身的業(yè)務(wù)耦合的也很緊密,那么有沒有更好的實(shí)現(xiàn)方式呢?
??使用AOP面向切面的設(shè)計(jì)實(shí)現(xiàn),定義某一類方法為一個(gè)切面,在這個(gè)切面的前后計(jì)算時(shí)間,打印出時(shí)間差,這樣就統(tǒng)一了方法時(shí)間的計(jì)算,對方法本身無任何影響。
??AOP只是一種方法論,具體該如何實(shí)現(xiàn)呢?先介紹幾種方式
- jdk代理
a. jdk靜態(tài)代理
//真實(shí)類
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 + "");
}
}
就是創(chuàng)建了一個(gè)代理類持有真實(shí)類的引用,并封裝了被代理類的方法,可以執(zhí)行時(shí)長計(jì)算。這種方式的好處就是在編譯器就創(chuàng)建好了代理對象,及靜態(tài)代理,雖然把計(jì)算時(shí)長的邏輯與具體方法的業(yè)務(wù)隔離開了,但是但是要為每一個(gè)計(jì)算時(shí)長的類創(chuàng)建代理類,會產(chǎn)生大量的代理類不利于管理,而且也無法解決大量重復(fù)計(jì)算時(shí)長邏輯的問題。
b.jdk動(dòng)態(tài)代理
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 + "");
}
}
//執(zhí)行
public static void main(String args[]){
Real target = new Real();//要進(jìn)行代理的目標(biāo)業(yè)務(wù)類
Proxy handler = new Proxy(target);//用代理類把目標(biāo)業(yè)務(wù)類進(jìn)行編織
//創(chuàng)建代理實(shí)例,它可以看作是要代理的目標(biāo)業(yè)務(wù)類的加多了橫切代碼(方法)的一個(gè)子類
IReal proxy = (IReal )Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(), handler);
proxy.action();
}
在運(yùn)行時(shí)創(chuàng)建了代理類,利用java反射機(jī)制執(zhí)行被代理的方法。好處就是計(jì)算時(shí)長的邏輯與具體方法的業(yè)務(wù)隔離開了,只要定義一個(gè)代理對象Proxy ,就是代理其他類,這樣就只要寫一份計(jì)算時(shí)長的邏輯;但是jdk動(dòng)態(tài)只能針對實(shí)現(xiàn)接口的類,但是我要計(jì)算時(shí)長的類并不能保證一定實(shí)現(xiàn)了接口,而且由于是在運(yùn)行時(shí)通過反射執(zhí)行被代理方法會影響一定的性能。
- CGLib
public class CglibProxy implements MethodInterceptor{
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
//設(shè)置需要?jiǎng)?chuàng)建子類的類
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通過字節(jié)碼技術(shù)動(dòng)態(tài)創(chuàng)建子類實(shí)例
return enhancer.create();
}
//實(shí)現(xiàn)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();
//通過生成子類的方式創(chuàng)建代理類
Real proxyImp = (Real)proxy.getProxy(Real.class);
proxyImp.action();
}
CGLib也是動(dòng)態(tài)代理,在運(yùn)行時(shí)通過asm(字節(jié)碼處理框架),動(dòng)態(tài)構(gòu)建字節(jié)碼文件,創(chuàng)建一個(gè)被代理類的子類,并織入計(jì)算時(shí)長的邏輯,他的好處就是被代理類不需要實(shí)現(xiàn)接口,方法的執(zhí)行并不是利用反射,所以快很多,但是代理類的創(chuàng)建比jdk動(dòng)態(tài)代理慢,開銷較大。
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是在編譯時(shí)使用自身的編譯器ajc(ajc只是在java編譯器上增加了對自己的一些關(guān)鍵字識別和編譯方法),把我們要添加的計(jì)算時(shí)長邏輯織入目標(biāo)類中。
AspectJ的使用非常方便,可以通過java注解的方法來定義。
@Aspect,表示該類處理具體切面定義邏輯
@Pointcut,定義關(guān)注的切面,可以用正則表達(dá)式來過濾
@Around,定義每一個(gè)被關(guān)注的執(zhí)行點(diǎn)的處理邏輯。

??在這個(gè)例子中我們定義了一個(gè)注解,定義關(guān)注的切面是設(shè)置這個(gè)注解的方法,然后在處理邏輯上加上時(shí)長的計(jì)算。我們想要打印哪個(gè)方法的執(zhí)行時(shí)間,就只要添加這個(gè)注解就好了。真正做到了無侵入,而且是在編譯時(shí)就織入了計(jì)算時(shí)長代碼,性能也比較高。
??當(dāng)然他有個(gè)條件就是必須要用自身的編譯器ajc編譯,需要在我們個(gè)構(gòu)建腳本中定義這塊配置
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;
}
}
}
}
配置的代碼非常長,而且每一個(gè)要使用AspectJ功能模塊都要在其build.gradle中定義這塊配置,這又是一大串的重復(fù)代碼,有沒有更友好的方式呢,這里使用了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構(gòu)建配置中引入這個(gè)插件就好了
apply plugin: com.logger.plugin.MLoggerPlugin
這對這一塊我們只想在開發(fā)時(shí)監(jiān)控,因此在插件中配置debug時(shí)依賴aspectJ庫
project.dependencies {
debugCompile project.project(":aoplog")
debugCompile 'org.aspectj:aspectjrt:1.8.6'
compile project.project(":aopannotation")}
這樣對于線上版本不會使用ajc去編譯代碼。