首页投稿(暂停使用,暂停投稿)

03 | Android 高级进阶(源码剖析篇) 便于性能分析的

2018-03-09  本文已影响314人  asce1885

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

2d1e92443570a341f4383473ffc0bdca8df3jacobpostuma409826jpg.jpg

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

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

基本用法

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

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' // 添加 Hugo 的 Gradle 插件依赖
  }
}

apply plugin: 'com.jakewharton.hugo' // 应用 Hugo 插件

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

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

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    printArgs("The", "Quick", "Brown", "Fox");
}

@DebugLog
private void printArgs(String... args) {
    for (String arg : args) {
        Log.i("Args", arg);
    }
}

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

com.asce1885.hugodemo V/MainActivity: ⇢ printArgs(args=["The", "Quick", "Brown", "Fox"])
com.asce1885.hugodemo I/Args: The
com.asce1885.hugodemo I/Args: Quick
com.asce1885.hugodemo I/Args: Brown
com.asce1885.hugodemo I/Args: Fox
com.asce1885.hugodemo V/MainActivity: ⇠ printArgs [0ms]

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

@DebugLog
private void printArgs(String... args) {
    JoinPoint var7 = Factory.makeJP(ajc$tjp_0, this, this, args);
    Hugo var10000 = Hugo.aspectOf();
    Object[] var8 = new Object[]{this, args, var7};
    var10000.logAndExecute((new HugoActivity$AjcClosure1(var8)).linkClosureAndJoinPoint(69648));
}

核心知识点

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

注解

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

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

元素类型 适用于
ANNOTATION_TYPE 注解类型声明
CONSTRUCTOR 构造函数
FIELD 实例变量
LOCAL_VARIABLE 局部变量
METHOD 方法
PACKAGE
PARAMETER 方法参数或者构造函数的参数
TYPE 类(包含enum)和接口(包含注解类型)
TYPE_PARAMETER 类型参数
TYPE_USE 类型的用途

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

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

AOP

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

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

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

AspectJ

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

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

切点表达式

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

具体涵义如下表所示:

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

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

execution([注解] [修饰符] 返回值类型 方法名(参数列表) [异常列表])

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

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

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

within(类匹配模式)

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

类型匹配通配符

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

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

猜你喜欢

热点阅读