转载

AOP在Android中最佳用法

概述

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. 采用静态织入的方式,引入特定的语句创建“方面”,从而使得编译器可以在编译期间织入有关“方面的代码

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
}

很多情况,饿哦们不仅会匹配一个类,会有注入某个类所有子类,或者实现某个接口的所有类的需求。所以通过 TargetClassImplementedInterface 2个注解及 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 指定的森林结构中的所有叶节点。

如下图所示:

AOP在Android中最佳用法

当我们使用 @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

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

一些已知的缺陷:

  • 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 关键字

使用场景建议

  1. 如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。

  2. 如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。

喜欢 就关注吧,欢迎投稿!

AOP在Android中最佳用法

原文  http://mp.weixin.qq.com/s?__biz=MzA3NzMxODEyMQ==&mid=2666455352&idx=2&sn=d276f2ea815c90ca8c06d5a3e33406e0
正文到此结束
Loading...