AOP在Android中最佳用法

AOP

AOP(Aspect Oriented Programming)是面向切面编程,AOP和我们平时接触的OOP编程是不同的编程思想,OOP是面向对象编程,提倡的是将功能模块化,对象化。而AOP的思想则是提倡针对同一类问题统一处理,当然,我们在实际编程过程中,不可能单纯的AOP或者OOP的思想来编程,很多时候,可能会混合多种编程思想。
代码注入是AOP中的重要部分:AOP可用于日志埋点、性能监控、动态权限控制、甚至是代码调试等等

AOP出现的原因是为了解决OOP在处理侵入性业务上的不足,那么,什么是侵入性业务,类似日志统计、性能分析、埋点等就属于侵入型业务。本来的业务代码只是业务相关的逻辑,但是由于要加入侵入性业务的逻辑,代码就变成了下面的样子:

long begin = System.currentTimeMillis(); 

// 原本的业务
doSomething();

long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("waste time :" + step);

从上面的代码看到,性能分析的业务代码和原本的业务代码混在一起了,这就破坏了函数的单一原则。所以,侵入型业务必须有一个更好的解决方案,这个方案就是AOP。
通俗的讲,AOP就是将日志记录、性能统计、安全控制、事务处理、异常处理代码从业务逻辑代码中划分出来,通过这些行为的分离,我们希望可以将它们独立到一个类中,进而改变这些行为的时候不影响业务逻辑的代码--解耦

实现AOP的技术,主要分为两大类:

  1. 采用动态代理技术,利用截取消息的方式,对该信息进行装饰,以取代原有对象行为的执行
  2. 采用静态织入的方式,引入特定的语句创建“方面”,从而使得编译器可以在编译期间织入有关“方面的代码

有一些AOP术语需要我们理解:

  • Cross-cutting concerns(横切关注点):监管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们喜欢在数据访问层中的类添加日志,同时也希望当UI层中一个县城进入或者退出调用一个方法时添加日志。监管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。
  • Advice(通知):注入到class文件中的代码。典型的Advice类型有before、after和around,分别表示在目标方法执行之前、执行后和完全代替目标方法执行的代码。除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
  • **Joint Point(连接点):程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
  • Pointcut(切入点):告诉代码注入工具,在任何注入一段特定代码的表达式。例如,在哪些joint points应用一个特定的Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebugLog的自定义注解的所有方法。
  • Aspect(切面):Pointcut和Advice的组合看做切面。例如,我们在应用中通过定义一个pointcut和给定恰当的advice,添加一个日志切面。
  • Weaving(织入):注入代码(advices)到目标位置(joint points)的过程

AOP实践

AOP编程思想的实践现在流行的有两种

  1. ASpectJ
  2. Lancet

AspectJ

目前找到实现AspectJ有两个一个是国内大神实现的库,一直在更新,博文介绍

另一个是Jake大神实现的Hugo库,有一篇博文对Hugo进行了介绍,博文介绍

Lancet

Lancet是一个轻量级Android AOP框架

  • 编译速度快,并支持增量编译
  • 简介的API,几行Java代码完成注入需求
  • 没有任何多余代码插入apk
  • 支持用于SDK,可以在SDK编写注入代码来修改依赖SDK的App

使用方法

配置

在根目录的build.gradle添加

dependencies{
    classpath 'me.ele:lancet-plugin:1.0.2'
}

在app目录的build.gradle添加

apply plugin: 'me.ele.lancet'
dependencies{
    provided 'me.lel:lancet-base:1.0.2'
}

实例

Lancet使用注解来指定代码织入的规则与位置

@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag,String msg){
    msg = msg+ "lancet";
    return (int)Origin.call();
}

这里有几个关键点:

  • @TargetClass指定了将要被织入代码的目标类android.util.Log
  • @Proxy指定了将要被织入代码目标方法i
  • 织入方式为Proxy
  • Origin.call()代表了Log.i()这个目标方法
  • 如果被织入的代码是静态方法,这里也需要添加static关键字,否则不会生效

所以这个示例Hook方法的作用就是将代码中所有Log.i(tag,msg)替换为Log.i(tag,msg+"lancet"),将生成的apk反编译后,查看代码,所有调用Log.i的地方都会变为

 _lancet.com_xxx_xxx_xxx(类名)_i(方法名)("tag", "msg");

代码织入方式

  • @Proxy
public @interface Proxy{
    String value();
}

@Proxy将使用新的方法替换代码里存在的原有的目标方法。
比如代码里有10个地方调用了Dog.bark(),代码这个方法后,所有的10个地方的代码会变味_Lancet.xxx.bark()。而在这个新方法中会执行你在Hook方法中所写的代码。
@Proxy通常用与对系统API的劫持。因为虽然我们不能注入代码到系统提供的库之中,但我们可以劫持掉所有调用系统API的地方。

  • @NameRegex
    @NameRegex用来限制范围操作的作用域。仅用于Proxy模式中,比如你只想代理掉某一个包名下所有的目标操作。或者你在代理所有的网络请求时,不想代理掉自己发起的求情。使用NameRegexTargetClassImplementedInterface筛选出的class在进行一次匹配。

  • @Insert

public @interface Insert {
    String value();
    boolean mayCreateSuper() default false;
}

@Insert将新代码插入到目标方法原有代码前后
@Insert常用于操作App与library的类,并且可以通过This操作目标类的私有属性与方法
@Insert当目标方法不存在时,还可以使用mayCreateSuper参数来创建目标方法。
比如下面将代码注入每一个Activity的onStop生命周期

@TargetClass(value="android.support.v7.app.AppCompatActivity",scope=Scope.LEAF)
@Insert(value="onStop",mayCreateSuper = true)
protected void onStop(){
    System.out.println("hello world");
    Origin.callVoid();
}

Scope将在后文介绍,这里的意思为目标是AppCompatActivity的所有最终子类。
如果一个类MyActivity extends AppcompatActivity没有重写onStop会自动创建onStop方法,而Origin在这里就代表了super.onStop(),最后就是这样的效果:

protected void onStop(){
    System.out.println("hello world");
    super.onStop();
}

Note:public/protected/private修饰符会完全照搬Hook方法的修饰符。

匹配目标类

public @interface TargetClass {
    String value();
    Scope scope() default Scope.SELF;
}

public @interface ImplementedInterface {
    String[] value();
    Scope scope() default Scope.SELF;
}

public enum Scope {
    SELF,
    DIRECT,
    ALL,
    LEAF
}

很多情况,饿哦们不仅会匹配一个类,会有注入某个类所有子类,或者实现某个接口的所有类的需求。所以通过TargetClassImplementedInterface2个注解及Scope进行目标类匹配。

  • TargetClass
    通过类查找
  1. @TargetClassvalue是一个类的全称
  2. Scope.SELF仅代表匹配value指定的目标类
  3. Scope.DIRECT代表匹配value指定类的直接子类
  4. Scope.ALL代表匹配value指定类的所有子类
  5. Scope.LEAF代表匹配value指定类的最终子类。众所周知java是单继承,所以继承关系是树形结构,这里代表了指定类为顶点的继承树的所有叶子节点。
  • @ImplementedInterface
    通过接口查找,情况比通过类查找稍微复杂一些
  1. @ImplementedInterfacevalue可以填写多个接口的全名。
  2. Scope.SELF:代表直接实现所有指定接口的类。
  3. Scope.DIRECT:代表直接实现所有指定接口,以及指定接口的子接口的类。
  4. Scope.ALL:代表Scope.DIRECT指定的所有类及他们的所有子类。
  5. Scope.LEAF:代表Scope.ALL指定的森林结构中的所有叶节点。

如下图所示:

image

当我们使用@ImplementedInterface(value="I",scope=...)时,目标类如下:

  • Scope.SELF -> A
  • Scope.DIRECT -> A C
  • Scope.ALL -> A B C D
  • Scope.LEAF -> B D

匹配目标方法

虽然在Proxy,Insert中我们指定了方法名,但识别方法必须要更细致的信息。我们会直接用Hook方法的修饰符,参数类型来匹配方法。
所以一定要保持Hook方法的public/protected/private static信息与目标方法一致,参数类型,返回类型与目标方法一致。
返回类型可以用Object代替。
方法名不限,异常声明也不限。
但有时候我们并没有权限声明目标类。这时候怎么办?

  • @ClassOf
    可以使用ClassOf注解来替代对类的直接import
    比如下面这个例子:
public class A {
    protected int execute(B b) {
        return b.call();
    }
    
    private class B {
        int call(){
            return 0;
        }
    }
}

@TargetClass("com.dieyidezui.demo.A")
@Insert("execute")
public int hookExcute(@ClassOf("com.dieyidezui.demo.A$B" Object o){
    System.out.println(o);
    return (int)Origin.call();
}

ClassOf的value一定按照(package_name.)(outer_classname$)inner_class_nmae([]...)的模板,比如:

  • java.lang.Object
  • java.lang.Integer[][]
  • A[]
  • A$B

API

我们可以通过OriginThis与目标类进行一些交互

Origin

Origin用来调用原目标方法,可以被多次调用
Origin.call()用来调用有返回值的方法。
Origin.callVoid()用来调用没有返回值的方法。
另外,如果你又捕捉异常的需求,可以使用
Origin.call/callThrowOne/callThrowTwo/callThrowThree()
Origin.callVoid/callVoidThrowOne/callVoidThrowTwo/callVoidThrowThree()

for example:

@TargetClass("java.io.InputStream")
@Proxy("read")
public int read(byte[] bytes) throws IOException {
    try {
        return (int)Origin.<IOException>callThrowOne();
    }catch (IOException e){
        e.printStackTrace();
        throw e;
    }
}

This

仅用于Insert方式的非静态方法的Hook中。

get()

返回目标方法被调用的实例化对象

  • putField & getField

你可以直接存取目标类的所有属性,无论是protectedOrprivate。另外,如果这个属性不存在,我们还会自动创建这个属性。自动装箱拆箱肯定也支持了。

一些已知的缺陷:

  • Proxy不能使用This
  • 你不能存取你父类的属性。当你尝试存取父类属性时,我们还是会创建新的属性。
package me.ele;
public class Main {
    private int a = 1;
    public void nothing(){
        
    }
    
    public int getA(){
        return a;
    }
}

@TargetClass("me.ele.Main")
@Insert("nothing")
public void testThis(){
    Log.e("debug",this.get().getClass().getName());
    This.putField(3,"a");
    Origin.callVoid();
}

Tips

  1. 内部类应该命名为package.outer_class$inner_class
  2. SDK开发者不需要apply插件,只需要provided me.ele:lanet-base:x.y.z
  3. 尽管我们支持增量编译。但当我们使用Scope.LEAF、Scope.ALL覆盖的类有变动或者修改Hook类时,本次编译将会变成全量编译。
  4. 如果目标函数为静态方法,则需要在方法上添加static关键字

两种框架优缺点

Lancet相对于AspectJ的优点

  1. Lancet轻量级的框架,编译速度快,支持增量编译
  2. Lancet语法简单,易于上手。AspectJ需要学习的语法比较多。

Lancet相对于AspectJ的缺点

  1. Lancet仅支持hook具体的方法,不能像AspectJ一样根据自定义的注解来Hook一个类或者任意的方法。

使用场景建议

  1. 如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。
  2. 如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容

  • What? As we all know,在进行项目构建时,追求各模块高内聚,模块间低耦合。然而现实并不总是如此美...
    MasterNeo阅读 1,922评论 0 17
  • Android 中的 AOP 编程 原文链接 : Aspect Oriented Programming in A...
    mao眼阅读 18,382评论 19 81
  • 凤凰美璿 缘起 我是一名常年坚守在小学高年级语文一线的教师。多年毕业班的工作让自己形成了一种工作的惯性。工作超负荷...
    fc0b2f104377阅读 507评论 1 3
  • 原创/苹儿(茵草芳菲) 伴随着辞旧迎新的烟花,红火的春节,将离我们渐行渐远……立春虽已转身而去,雨水这两个湿漉漉的...
    茵草芳菲阅读 899评论 5 9
  • 发现了一个可怕的事情,到了这个年纪以后(30以后),身边大多数人进入了追求人生赢家的超级模式,努力卖货,努力挣钱,...
    三爷_8bd4阅读 186评论 0 1