android自定義日志組件

在Android開發(fā)中經常會使用日志來進行調試摧玫、記錄運行狀態(tài)、定位問題衅斩,但是系統(tǒng)提供的日志組件(android.util.Log)只提供了基本的日志輸出功能盆顾,使用上并不方面,因此需要分裝系統(tǒng)日志組件畏梆,擴展更豐富的功能您宪。

主要功能

根據Gradle配置設置統(tǒng)一的日志開關

在項目開發(fā)中常有需要控制日志能否打印,比如奠涌,開發(fā)中要打印日志宪巨,上線后要關閉日志;再如溜畅,有些渠道要打印日志捏卓,而有些要關閉日志。
??原來都是把開關定義為一個常量

public static final boolean debug = false;

然后根據不同的情況來回來修改,不僅繁瑣而且容易遺忘慈格。
??其實Gradle構建配置提供了一個屬性BuildConfig.DEBUG用來判斷當前構建是否是debug類型天吓,利用這個屬性就可以解決開發(fā)中和上線的日志開關的設置贿肩。

ps.只能使用主工程的BuildConfig.DEBUG屬性,Libaray工程的構建類型都是release類型龄寞,無法區(qū)分。

但是如果要處理不同渠道應用的日志開關汤功,就需要新建一個構建屬性物邑,在不同的渠道配置中設置

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

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

利用新的屬性BuildConfig.LOG_SWITCH來區(qū)分。

支持對象打印

系統(tǒng)日志組件只能打印字符串類型的內容滔金,但是常有要打印自定對象內容的需要色解,比如返回的內容、傳遞的參數餐茵,希望以json格式展示出來科阎。自己實現了對象轉換為json字符串功能。

ps.為什么不用第三方json庫忿族?
??1.第三方的json庫比如Gson锣笨、Jackson,都有100-200KB,而日志組件組件只需要把對象轉換為json字符串這一個功能道批,不希望為此增加額外的大小错英。
??2.自己實現json轉換能夠做一些定制化的展示和處理

實現對象轉換為json字符串主要就是利用java反射,獲取到屬性的名稱和對應的值,再按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);
  }
}

特殊情況:

  1. 非靜態(tài)內部類解析
    非靜態(tài)內部類持有一個外部類的引用椭岩,這個外部類屬性是沒有必要打印出來的。編譯后內部類所持有的外部類屬性都是命名為"this$0"璃赡,利用這個特點判斷屬性名稱做過濾判哥。
  2. 系統(tǒng)所屬類的解析
    如果要解析的類或類其中的屬性涉及到系統(tǒng)所屬的類比如Activity、Application碉考,我們并不希望再去解析這些系統(tǒng)所屬類的屬性塌计,如果解析的話往往會導致屬性過多而一直阻塞,只要直接打印出系統(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)所屬類。

  1. 類之間互相引用時的解析
    如果兩個類相互引用嫩痰,解析這樣類就會無線循環(huán),拋出stackOverFlowError異常剿吻,因此增加了解析層級判斷,超過3層就不繼續(xù)解析串纺。

快速定位到打印日志位置

打印日志后丽旅,要查看所打印地方的邏輯椰棘,還需要查找對應類所在方法的具體行數,往往這個過程就花了不少時間榄笙,而我們平時如果程序出來拋出異常邪狞,是可以直接點擊定位到具體行數的。

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

點擊就可以快速定位到日志具體打印的地方。

無侵入打印方法執(zhí)行時間

在做應用的一些性能測試的時候米丘,有時需要打印出方法的執(zhí)行時間 剑令,常用的方式就是在方法的前后獲取時間,計算時間差拄查。

Paste_Image.png

每處需要打印時間的地方都要都要加入這塊邏輯吁津,導致大量重復代碼,而且與方法本身的業(yè)務耦合的也很緊密堕扶,那么有沒有更好的實現方式呢碍脏?
??使用AOP面向切面的設計實現,定義某一類方法為一個切面挣柬,在這個切面的前后計算時間潮酒,打印出時間差,這樣就統(tǒng)一了方法時間的計算邪蛔,對方法本身無任何影響急黎。
??AOP只是一種方法論,具體該如何實現呢侧到?先介紹幾種方式

  1. jdk代理
    a. jdk靜態(tài)代理
//真實類
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)建了一個代理類持有真實類的引用勃教,并封裝了被代理類的方法,可以執(zhí)行時長計算匠抗。這種方式的好處就是在編譯器就創(chuàng)建好了代理對象故源,及靜態(tài)代理,雖然把計算時長的邏輯與具體方法的業(yè)務隔離開了汞贸,但是但是要為每一個計算時長的類創(chuàng)建代理類绳军,會產生大量的代理類不利于管理,而且也無法解決大量重復計算時長邏輯的問題矢腻。

b.jdk動態(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();//要進行代理的目標業(yè)務類
        Proxy  handler = new Proxy(target);//用代理類把目標業(yè)務類進行編織
 //創(chuàng)建代理實例门驾,它可以看作是要代理的目標業(yè)務類的加多了橫切代碼(方法)的一個子類
        IReal proxy = (IReal )Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), handler);

        proxy.action();
   }

在運行時創(chuàng)建了代理類,利用java反射機制執(zhí)行被代理的方法多柑。好處就是計算時長的邏輯與具體方法的業(yè)務隔離開了奶是,只要定義一個代理對象Proxy ,就是代理其他類,這樣就只要寫一份計算時長的邏輯聂沙;但是jdk動態(tài)只能針對實現接口的類秆麸,但是我要計算時長的類并不能保證一定實現了接口,而且由于是在運行時通過反射執(zhí)行被代理方法會影響一定的性能及汉。

  1. CGLib
public class CglibProxy implements MethodInterceptor{
  private Enhancer enhancer = new Enhancer();  
  public Object getProxy(Class clazz){  
//設置需要創(chuàng)建子類的類  
  enhancer.setSuperclass(clazz);  
  enhancer.setCallback(this);  
  //通過字節(jié)碼技術動態(tài)創(chuàng)建子類實例  
  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();  
  //通過生成子類的方式創(chuàng)建代理類  
  Real proxyImp = (Real)proxy.getProxy(Real.class);  
  proxyImp.action();  
 }  

CGLib也是動態(tài)代理沮趣,在運行時通過asm(字節(jié)碼處理框架),動態(tài)構建字節(jié)碼文件豁生,創(chuàng)建一個被代理類的子類兔毒,并織入計算時長的邏輯,他的好處就是被代理類不需要實現接口甸箱,方法的執(zhí)行并不是利用反射,所以快很多迅脐,但是代理類的創(chuàng)建比jdk動態(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是在編譯時使用自身的編譯器ajc(ajc只是在java編譯器上增加了對自己的一些關鍵字識別和編譯方法)豌骏,把我們要添加的計算時長邏輯織入目標類中。
AspectJ的使用非常方便隐锭,可以通過java注解的方法來定義窃躲。
@Aspect,表示該類處理具體切面定義邏輯
@Pointcut钦睡,定義關注的切面蒂窒,可以用正則表達式來過濾
@Around,定義每一個被關注的執(zhí)行點的處理邏輯荞怒。

Paste_Image.png

??在這個例子中我們定義了一個注解洒琢,定義關注的切面是設置這個注解的方法,然后在處理邏輯上加上時長的計算褐桌。我們想要打印哪個方法的執(zhí)行時間衰抑,就只要添加這個注解就好了。真正做到了無侵入荧嵌,而且是在編譯時就織入了計算時長代碼呛踊,性能也比較高。
??當然他有個條件就是必須要用自身的編譯器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

這對這一塊我們只想在開發(fā)時監(jiān)控,因此在插件中配置debug時依賴aspectJ庫

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

這樣對于線上版本不會使用ajc去編譯代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末薄辅,一起剝皮案震驚了整個濱河市要拂,隨后出現的幾起案子,更是在濱河造成了極大的恐慌站楚,老刑警劉巖脱惰,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異窿春,居然都是意外死亡拉一,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門旧乞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔚润,“玉大人,你說我怎么就攤上這事尺栖〉站溃” “怎么了?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵延赌,是天一觀的道長除盏。 經常有香客問我,道長挫以,這世上最難降的妖魔是什么者蠕? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮掐松,結果婚禮上踱侣,老公的妹妹穿的比我還像新娘。我一直安慰自己甩栈,他們只是感情好泻仙,可當我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著量没,像睡著了一般玉转。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上殴蹄,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天究抓,我揣著相機與錄音,去河邊找鬼袭灯。 笑死刺下,一個胖子當著我的面吹牛,可吹牛的內容都是我干的稽荧。 我是一名探鬼主播橘茉,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了畅卓?” 一聲冷哼從身側響起擅腰,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翁潘,沒想到半個月后趁冈,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡拜马,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年渗勘,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俩莽。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡旺坠,死狀恐怖,靈堂內的尸體忽然破棺而出扮超,到底是詐尸還是另有隱情价淌,我是刑警寧澤,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布瞒津,位于F島的核電站,受9級特大地震影響括尸,放射性物質發(fā)生泄漏巷蚪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一濒翻、第九天 我趴在偏房一處隱蔽的房頂上張望屁柏。 院中可真熱鬧,春花似錦有送、人聲如沸淌喻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽裸删。三九已至,卻和暖如春阵赠,著一層夾襖步出監(jiān)牢的瞬間涯塔,已是汗流浹背响驴。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工绍坝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人祟牲。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓枷邪,卻偏偏與公主長得像榛搔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,344評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理践惑,服務發(fā)現腹泌,斷路器,智...
    卡卡羅2017閱讀 134,720評論 18 139
  • What? As we all know童本,在進行項目構建時真屯,追求各模塊高內聚,模塊間低耦合穷娱。然而現實并不總是如此美...
    MasterNeo閱讀 2,085評論 0 17
  • 清明世界绑蔫,城市總是下著雨。這個本就擁擠的城市并沒有因為下雨的原因而變的松散泵额,反而增添了幾分凌亂配深。形形色色的人群走在...
    暮歌有個小愿景O_o閱讀 147評論 0 0
  • 有幸在2014年的時候去過《非誠勿擾》現場,見過孟非真人嫁盲,那個時候他對我來說只是一個有明星光環(huán)的主持人篓叶,只是聽說文...
    巍翎閱讀 526評論 8 9