Android 高级进阶(源码剖析篇) 便于性能分析的日志框架 hugo

作者简介:ASCE1885, 《Android 高级进阶》作者。本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!本文分析的源码版本已经 fork 到我的 Github。

0?wx_fmt=jpeg

在 Android 性能调优中,通常存在需要对方法的执行时间进行统计的需求,这样就可以看出哪些方法耗时多,是系统的瓶颈。最容易想到的方案是在每个方法的开头处获取系统时间,在方法的结尾处再次获取系统时间,前后两个时间戳的差值就是这个方法执行所消耗的总时间。这个方案虽然简单易懂,但实际操作起来要写很多样板代码,同时对原有的代码浸入性太高。那么有没有更好的方案实现方法的性能监控呢?当然是有的,它就是本文的主角:hugo。

hugo 也是 Android 平台著名的日志框架,跟 timber 一样出自 JakeWharton 之手。在《Android 高级进阶》一书的《面向切面编程及其在 Android 中的应用》一节中其实已经介绍过 hugo 相关内容,本文会再做拓展,对 hugo 源码做更详细的剖析。

基本用法

在介绍 hugo 的核心原理前,有必要先了解其基本用法。hugo 以 gradle 插件的形式供开发者集成和使用,分为两步:

  • 在项目全局添加对 hugo 插件的依赖

  • 在需要使用 hugo 的 module 中应用 hugo 插件

 
 

就这么简单,之后这个插件会帮我们下载一些依赖库,分别是:

  • aspectjrt.jar:aspectJ 运行时的依赖库,想要使用 aspectJ 的功能都需要引入这个库

  • hugo-annotations:hugo 的注解库,定义了 DebugLog 这个注解,后面会介绍到

  • hugo-runtime:hugo 的运行时库,是实现 hugo 日志功能的核心库

hugo 的使用很简单,在需要进行日志记录的类名或者方法名处使用 @DebugLog注解标记即可,之后 hugo 就会在编译时织入(weaving)打印日志的代码,从而省去了开发者手动编写日志代码的繁琐。例如下面这个方法使用 @DebugLog 注解:

 
 

在程序运行的时候会打印出下面的日志信息,其中 ⇢ printArgs(args=["The", "Quick", "Brown", "Fox"])  ⇠ printArgs [0ms] 是 Hugo 这个函数库为我们自动添加的日志信息。

 
 

通过查看编译后生成的 .class 文件(位于build/intermediates/classes/类所在包名 中),可以看到 printArgs方法经过 AspectJ 框架的代码织入后,已经面目全非了:

 
 

核心知识点

hugo 这个框架麻雀虽小但五脏俱全,它使用了很多 Android 开发中流行的技术,例如注解,AOP,AspectJ,Gradle 插件等。在进行 hugo 源码解读之前,你需要首先对这些知识点有一定的了解。

注解

注解是 Java 语言的特性之一,它是在源代码中插入的标签,这些标签在后面的编译或者运行过程中起到某种作用,每个注解都必须通过注解接口 @interface 进行声明,接口的方法对应着注解的元素。

元注解,注解的一种类型,顾名思义,就是用来定义和实现注解的注解,总共有如下五种,在 hugo 中会用到 @Target  @Retention 这两个元注解,我们来做个简单的介绍。

  • @Target:这个注解的取值是一个 ElementType 类型的数组,用来指定注解所适用的对象范围,总共有十种不同的类型,根据定义的注解进行灵活的组合,如下所示(加粗的三个元素类型是 hugo 用到的,可重点关注):

元素类型适用于
ANNOTATION_TYPE注解类型声明
CONSTRUCTOR构造函数
FIELD实例变量
LOCAL_VARIABLE局部变量
METHOD方法
PACKAGE
PARAMETER方法参数或者构造函数的参数
TYPE类(包含enum)和接口(包含注解类型)
TYPE_PARAMETER类型参数
TYPE_USE类型的用途
  • @Retention:用来指明注解的访问范围,也就是在什么级别保留注解,有如下三种选择:

    • 源码级注解:在定义注解接口时,使用@Retention(RetentionPolicy.SOURCE) 修饰的注解,该类型的注解信息只会保留在 .java 源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的.class 文件中

    • 编译时注解:在定义注解接口时,使用@Retention(RetentionPolicy.CLASS) 修饰的注解,该类型的注解信息会保留在 .java 源码里和 .class文件里,在执行的时候,会被 Java 虚拟机丢弃,不会加载到虚拟机中,hugo 就是用的这一种

    • 运行时注解:在定义注解接口时,使用@Retention(RetentionPolicy.RUNTIME) 修饰的注解,Java 虚拟机在运行期也保留注解信息,可以通过反射机制读取注解的信息(.java 源码、.class 文件和执行的时候都有注解的信息)

未指定类型时,默认是 CLASS 类型。

更多关于注解的相关知识点,可以参考《Android 高级进阶》中的《注解在 Android 中的应用》一节。

AOP

AOP,全称为 Aspect Oriented Programming,即面向切面编程。AOP 是软件开发中的一个编程范式,通过预编译方式或者运行期动态代理等实现程序功能的统一维护的一种技术,它是 OOP(面向对象编程)的延续,利用 AOP 开发者可以实现对业务逻辑中的不同部分进行隔离,从而进一步降低耦合,提高程序的可复用性,进而提高开发的效率。AOP 能够实现将日志纪录,性能统计,埋点统计,安全控制,异常处理等代码从具体的业务逻辑代码中抽取出来,放到统一的地方进行处理。AOP 涉及到的基本概念有:

  • 横切关注点(Cross-cutting concerns):在面向对象编程中,经常需要在不同的模块代码中添加一些类似的代码,例如在函数入口处打印日志,在 View 的点击处添加点击事件的埋点统计,或者对一个函数进行性能监控,查看它的执行耗时等等,在 AOP 中把软件系统分成两个部分:核心关注点和横切关注点,核心关注点就是业务逻辑处理的主要流程,而横切关注点就是上面所说的经常发生在核心关注点的多个地方,且基本相似的日志纪录,埋点统计等等。

  • 连接点(Joint point):在核心关注点中可能会存在横切关注点的地方,例如方法调用的入口,View 的点击处理等地方,在 AOP 中习惯称为连接点。

  • 增强(Advice):特定连接点处所执行的动作,也就是 AOP 织入的代码,目的是对原有代码进行功能的增强,典型的有:

    • before:在目标方法执行之前的动作

    • around:在目标方法之前前后的动作

    • after:在目标方法执行之后的动作

  • 切入点(Pointcut):连接点的集合,这些连接点可以确定什么时机会触发一个通知。切入点通常使用正则表达式或者通配符语法表示,可以指定执行某个方法,也可以指定多个方法,例如指定标记了某个注解的所有方法。

  • 切面(Aspect):切入点和通知可以组合成一个切面。

  • 织入(Weaving):将通知注入到连接点的过程。

AOP 中代码的织入根据类型的不同,主要可以分为三类:

  • 编译时织入:在 Java 类文件编译的时候进行织入,这需要通过特定的编译器来实现,例如使用 AspectJ 的织入编译器。

  • 类加载时织入:通过自定义类加载器 ClassLoader 的方式在目标类被加载到虚拟机之前进行类的字节代码的增强。

  • 运行时织入:切面在运行的某个时刻被动态织入,基本原理是使用 Java 的动态代理技术。

hugo 使用到的代码织入属于编译时织入,用到了 AspectJ 这样一个面向切面的框架,它扩展了 Java 语言,定义了一套 AOP 语法,实现了一个专门的编译器来在编译期生成遵守 Java 字节码规范的 .class 文件。

AspectJ

AspectJ 框架主要包含三部分内容:

  • aspectjrt:运行时函数库,AOP 所需要用到的

  • aspectjtools:工具库

  • aspectjweaver:实现织入功能的函数库

AspectJ 涉及的知识点比较多,可以独立成书,这里我们只介绍 hugo 使用到的相关知识点,主要包括切点表达式,类型匹配通配符,逻辑运算符,增强类型等。

切点表达式

AspectJ 的切点表达式由关键字和操作参数组成,以切点表达式 execution(* helloWorld(..)) 为例,其中 execution 是关键字,为了便于理解,通常也称为函数,而 * helloWorld(..) 是操作参数,通常也称为函数的入参。切点表达式函数的类型很多,例如方法切点函数,方法入参切点函数,目标类切点函数等,hugo 用到的有两种类型:

  • 方法切点函数之一 execution()

  • 目标类切点函数之一 within()

具体涵义如下表所示:

函数名入参说明
execution()方法匹配模式字符串表示所有目标类中满足某个匹配模式的方法连接点,例如execution(* helloWorld(..)) 表示所有目标类中的 helloWorld 方法,返回值和参数任意
within()类名匹配模式字符串表示满足某个匹配模式的特定域中的类的所有连接点,例如 within(com.asce1885.debug.*) 表示com.asce1885.debug 中的所有类的所有方法

接下来我们来介绍这两个切入点函数入参的语法格式,先来看 execution() 的入参语法格式:

 
 

其中,[] 号中的签名组件是可选的。

对于 execution(* *(..)) 这个切入点而言,

  • 第一个 * 号对应方法的返回值,* 号表示方法返回值是任意的;

  • 第二个 * 号对应方法名,* 表示可以匹配该类中的所有方法;

  • (..) 表示方法的参数是任意的

within() 函数入参语法格式如下:

 
 

可以看出,execution()  within() 两者的主要区别是 within() 所指定的连接点最小范围只能到类,而 execution() 所指定的连接点可以实现包,类,方法,方法入参范围全覆盖。

类型匹配通配符

AspectJ 切点表达式中的操作参数支持通配符,有三种类型的通配符可供选择,具体的涵义如下表所示:

通配符涵义
*匹配任意字符,但只能匹配上下文中的一个元素
..匹配任意字符,可以匹配上下文中多个元素,比如在目标类模式的匹配中,表示匹配任意数量的子包;在方法参数模式的匹配中,表示匹配任意数量的参数
+匹配指定类型的子类型,只能作为后缀放在类名后面

逻辑运算符

切点表达式由切点函数和操作参数组成,它们是可以通过逻辑操作符进行组合的。常见的有三种逻辑运算符,如下表所示:

通配符涵义
&&与操作符,交集运算
||或操作符,并集运算
!非操作符,反集运算

增强类型

AspectJ 的增强类型很多,我们只介绍三种,也是前面 AOP 基本概念中介绍的,它们分别对应 AspectJ 中的一个注解:

  • 前置增强:@Before

  • 环绕增强:@Around

  • 后置增强:@AfterReturning

关于 AOP 和 AspectJ 的更多知识点,读者可以阅读 《精通Spring 4.x 企业应用开发实战》一书的相关章节,当然也可以直接看 AspectJ 的官方文档。

Gradle 插件开发

Gradle 插件的作用是封装可复用的构建逻辑,方便使用者简单快速地在不同项目中集成插件提供的功能。我们可以使用任何最终可以编译为字节码的编程语言来编写 Gradle 插件,常见的有 Groovy,Java 和 Kotlin。一般来说,由于 Java 和 Kotlin 是静态类型语言,因此,使用这两种语言实现的 Gradle 插件性能要比使用 Groovy 实现的插件好。hugo 使用的是 Groovy 语言实现的插件,因此我们下面介绍插件开发的一般流程就以 Groovy 为例。需要说明的是,Gradle 插件开发有三种模式,本文介绍的是常见的独立工程模式。

首先新建一个 Java Library Module,然后手动将工程结构修改为 Groovy 工程结构,同时在 build.gradle 中引入插件开发所需的 gradle sdk 和 groovy sdk 这两个依赖,最后在 main 目录下面增加 resources/META-INF/gradle-plugins 这样的目录结构,如下所示:

0?wx_fmt=png

这样,一个基于 Groovy 的 Gradle 插件工程结构就完成了。为了让 Gradle 能够找到我们实现的插件类,需要在 resources/META-INF/gradle-plugins 目录中新增一个 .properties 文件,这个文件名就是我们插件的唯一 id,其他工程要使用这个插件,也是使用这个 id 来定位到的。比如这个文件名为com.asce1885.nice.properties,那么插件 id 就是com.asce1885.nice,其他工程通过:

 
 

来使用这个插件。文件的内容指向我们实现的插件类,比如我们的插件类是com/asce1885/weaving/NicePlugin.groovy,那么 .properties 文件内容就是:

自定义插件类 NicePlugin 需要实现 org.gradle.api.Plugin 接口,并实现 apply 方法,如下所示:...更多内容请点击阅读原文继续阅读。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值