概述
AOP出现的原因是为了解决OOP在处理 侵入性业务
上的不足,那么,什么是 侵入性业务
,类似日志统计、性能分析、埋点等就属于 侵入型业务
。本来的业务代码只是业务相关的逻辑,但是由于要加入 侵入性业务
的逻辑,代码就变成了下面的样子:
long begin = System.currentTimeMillis();
// 原本的业务
doSomething();
long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("waste time :" + step);
从上面的代码看到,性能分析的业务代码和原本的业务代码混在一起了,这就破坏了函数的单一原则。所以, 侵入型业务
必须有一个更好的解决方案,这个方案就是AOP。
通俗的讲,AOP就是将日志记录、性能统计、安全控制、事务处理、异常处理代码从业务逻辑代码中划分出来,通过这些行为的分离,我们希望可以将它们独立到一个类中,进而改变这些行为的时候不影响业务逻辑的代码–解耦
实现AOP的技术,主要分为两大类:
-
采用动态代理技术,利用截取消息的方式,对该信息进行装饰,以取代原有对象行为的执行
-
采用静态织入的方式,引入特定的语句创建“方面”,从而使得编译器可以在编译期间织入有关“方面的代码
Lancet
Lancet是一个轻量级Android AOP框架
使用方法
配置
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
模式中,比如你只想代理掉某一个包名下所有的目标操作。或者你在代理所有的网络请求时,不想代理掉自己发起的求情。使用NameRegex
对TargetClass
,ImplementedInterface
筛选出的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 }
很多情况,饿哦们不仅会匹配一个类,会有注入某个类所有子类,或者实现某个接口的所有类的需求。所以通过 TargetClass
, ImplementedInterface
2个注解及 Scope
进行目标类匹配。
-
TargetClass
通过类查找
-
@TargetClass
的value
是一个类的全称 -
Scope.SELF仅代表匹配
value
指定的目标类 -
Scope.DIRECT代表匹配
value
指定类的直接子类 -
Scope.ALL代表匹配
value
指定类的所有子类 -
Scope.LEAF代表匹配
value
指定类的最终子类。众所周知java是单继承,所以继承关系是树形结构,这里代表了指定类为顶点的继承树的所有叶子节点。
-
@ImplementedInterface
通过接口查找,情况比通过类查找稍微复杂一些
-
@ImplementedInterface
的value
可以填写多个接口的全名。 -
Scope.SELF:代表直接实现所有指定接口的类。
-
Scope.DIRECT:代表直接实现所有指定接口,以及指定接口的子接口的类。
-
Scope.ALL:代表
Scope.DIRECT
指定的所有类及他们的所有子类。 -
Scope.LEAF:代表
Scope.ALL
指定的森林结构中的所有叶节点。
如下图所示:
当我们使用 @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
我们可以通过 Origin
与 This
与目标类进行一些交互
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
-
内部类应该命名为
package.outer_class$inner_class
-
尽管我们支持增量编译。但当我们使用
Scope.LEAF、Scope.ALL
覆盖的类有变动或者修改Hook类时,本次编译将会变成全量编译。 -
如果目标函数为静态方法,则需要在方法上添加
static
关键字
使用场景建议
-
如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。
-
如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。
喜欢 就关注吧,欢迎投稿!
原文
http://mp.weixin.qq.com/s?__biz=MzA3NzMxODEyMQ==&mid=2666455352&idx=2&sn=d276f2ea815c90ca8c06d5a3e33406e0
本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » AOP在Android中最佳用法