谈谈Android AOP技术方案

2024-06-01 10:04
文章标签 android 技术 方案 aop 谈谈

本文主要是介绍谈谈Android AOP技术方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

先统一一下基本名词,以便表述。

  • 切面:对一类行为的抽象,是切点的集合,比如在用户访问所有模块前做的权限认证。

  • 切点:描述切面的具体的一个业务场景。

  • 通知(Advice)类型:通常分为切点前、切点后和切点内,比如在方法前织入代码是指切点前。

AOP是一种面向切面编程的技术的统称,AOP框架最终都会围绕class字节码的操作展开,无论是对字节码的操作增删改,为方便描述,我们统称为
代码的织入 。

虽然AOP翻译过来叫面向 切面 编程,但在实际使用过程中,切面可能退化成了一个 
,比如我们想统计app的冷启动时间,这就非常具体了。如果我们用AOP的技术实现统计所有函数的耗时时间,自然能统计到类似启动这个阶段的时间。

从狭义来看实现AOP技术的框架必须是能将切面编程抽象成上层可以直接使用的工具或API,但当我们将 切面 降维后,最终面向的就是 切点
而已。换句话说,只要能将代码织入到某个点那这种技术就一定可以实现AOP,这样AOP技术所涵盖的领域就得以拓展,因为从狭义的角度看目前只有AspectJ符合这个标准。

从广义上来讲,AOP技术可以是任何能实现代码织入的技术或框架,对代码的改动最终都会体现在字节码上,而这类技术也可以叫做 字节码增强
,通用名词理解即可。

下面我们将介绍一些常用的AOP技术。

首先 ,从织入的时机的角度看,可以分为源码阶段、class阶段、dex阶段、运行时织入。

对于前三项 源码阶段、class阶段、dex织入 ,由于他们都发生在class加载到虚拟机前,我们统称为静态织入,
而在运行阶段发生的改动,我们统称为动态织入。

常见的技术框架如下表:

首先 ,从织入的时机的角度看,可以分为源码阶段、class阶段、dex阶段、运行时织入。

对于前三项 源码阶段、class阶段、dex织入 ,由于他们都发生在class加载到虚拟机前,我们统称为静态织入,
而在运行阶段发生的改动,我们统称为动态织入。

常见的技术框架如下表:

织入时机技术框架
静态织入APT,AspectJ、ASM、Javassit
动态织入java动态代理,cglib、Javassit

静态织入发生在编译器,因此几乎不会对运行时的效率产生影响;动态织入发生在运行期,可直接将字节码写入内存,并通过反射完成类的加载,所以效率相对较低,但更灵活。

动态织入的前提是类还未被加载,你不能将一个已经加载的类经过修改再次加载,这是ClassLoader的限制。但是可以通过另一个ClassLoader进行加载,虚拟机允许两个相同类名的class被不同的ClassLoader加载,在运行时也会被认为是两个不同的类,因此需要注意不能相互赋值,
不然会抛出ClassCastException。

java动态代理、cglib只会创建新的代理类而不是对原有类的字节码直接修改,Javassit可修改原有字节码。

>
其实利用反射或者hook技术同样可以实现代码行为的改变,但由于这类技术并没有真正的改变原有的字节码,所以暂不在谈论范围内,比如xposed,dexposed。


其次 ,我们需要关注这些框架具备哪切面编程的能力,这有助于帮助我做技术选型,由于AspectJ、ASM
、Javassit是相对比较完善的AOP框架,因此只对三者进行比较。

能力AspectJASMJavassit
切面抽象
切点抽象
通知类型抽象

其中:

  • 切面抽象:具备筛选过滤class的能力,比如我们想为Activity的所有生命周期织入代码,那你是不是首先需要具备过滤Activity及其子类的能力。

  • 切点抽象:具体到某个class,是否具备方法、字段、注解访问的能力。

  • 通知类型抽象:是否直接支持在方法前、后、中直接织入代码。

当然不具备能力不代表不能做AOP编程,可以通过其他方法解决,只是易用性的问题。

下面我们将开始对上述框架逐一介绍,Let' go~~~

APT

APT (Annotation Processing Tool)即注解处理器,在Gradle
版本>=2.2后被annotationProcessor取代。

它用来在编译时扫描和处理注解,扫描过程可使用 auto-service 来简化寻找注解的配置,在处理过程中可生成java文件(创建java文件通常依赖 javapoet 这个库)。常用于生成一些模板代码或运行时依赖的类文件,比如常见的ButterKnife、Dagger、ARouter,它的优点是简单方便。

以ButterKnife为例:

 1public class MainActivity extends AppCompatActivity {23    @BindView(R.id.toolbar)4    Toolbar toolbar;56    @Override7    protected void onCreate(Bundle savedInstanceState) {8        super.onCreate(savedInstanceState);9        setContentView(R.layout.activity_main);
10        ButterKnife.bind(this);
11    }
12
13}

一句简单的ButterKnife.bind(this)是如何实现控件的赋值的?

事实上**@Bind注解 *在编译期会生成一个MainActivity_ViewBinding类,而*
ButterKnife.bind(this)**这次调用最终会通过反射创建出MainActivity_ViewBinding对象,并把activity的引用传递给它。

 1# ButterKnife2public static Unbinder bind(@NonNull Object target, @NonNull View source) {3    Class<?> targetClass = target.getClass();4    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);5    ...6    //创建xxx_binding对象并把activity传入7    return constructor.newInstance(target, source);8}9
10private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
11    ...
12    try {
13      //运行时通过反射加载在编译阶段生成的类
14      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
15      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
16    } 
17    ...
18    return bindingCtor;
19}

这样最终在MainActivity_ViewBinding的构造函数中完成控件的赋值。

1public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
2  protected T target;
3  public MainActivity_ViewBinding(final T target, Finder finder, Object source) {
4    ...
5    //为控件赋值 其中优化了控件的查找
6    target.toolbar = finder.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
7    ...
8  }
9}

为了在此类中能访问到MainActivity中声明的属性,为此ButterKnife框架要求,使用@Bind注解声明的属性不能是private的。

可以看到ButterKnife中仍然用到了反射,这是为了统一API使用ButterKnife.bind(…)作出的牺牲,而Dagger则会通过Component,Module的名字通过动态生成不同的方法名,因此使用之前需要对工程进行build。

之所以会这样,是因为APT技术的不足,通常只是用来创建新的类,而不能对原有类进行改动,在不能改动的情况下,只能通过反射实现动态化。

AspectJ

AspectJ是一种严格意义上的AOP技术,因为它提供了完整的面向切面编程的注解,这样让使用者可以在不关心字节码原理的情况下完成代码的织入,因为编写的切面代码就是要织入的实际代码。

AspectJ实现代码织入有两种方式,一是自行编写.ajc文件,二是使用AspectJ提供的@Aspect、@Pointcut等注解,二者最终都是通过ajc编译器完成代码的织入。

举个简单的例子,假设我们想统计所有view的点击事件,使用AspectJ只需要写一个类即可。

 1@Aspect2public class MethodAspect {3    private static final String TAG = "MethodAspect5";45    //切面表达式,声明需要过滤的类和方法 6    @Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")7    public void callMethod() {8    }9
10    //before表示在方法调用前织入
11    @before("callMethod()")
12    public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
13        //编写业务代码
14    }
15}

注解简明直观,上手难度近乎为0。

常用的函数耗时统计工具Hugo,就是AspectJ的一个实际应用,Android平台Hujiang开源的AspectJX插件灵感也来自于Hugo,详情见旧文Android 函数耗时统计工具之Hugo。

AspectJ虽然好用,但也存在一些严重的问题。

  • 重复织入、不织入

  • 不支持Java8

AspectJ切面表达式支持继承语法,虽然方便了开发,但存在致命的问题,就是在继承树上的类可能都会织入代码,这在多数业务场景下是不适用的,比如无埋点。

另外使用java8语法编写的代码,不会被进入切面范围,也就无法织入代码。

更多详情参见旧文 Android AspectJ详解 。

ASM

ASM是非常底层的面向字节码编程的AOP框架,理论上可以实现任何关于字节码的修改,非常硬核。许多字节码生成API底层都是用ASM实现,常见比如Groovy、cglib,因此在Android平台下使用ASM无需添加额外的依赖。完整的学习ASM必须了解字节码和JVM相关知识。

比如要织入一句简单的日志输出

1Log.d("tag", " onCreate");

使用ASM编写是下面这个样子,没错因为JVM是基于栈的,函数的调用需要参数先入栈,然后执行函数入栈,最后出栈,总共四条JVM指令。

1mv.visitLdcInsn("tag");
2mv.visitLdcInsn("onCreate");
3mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
4mv.visitInsn(POP);

可以看出ASM与AspectJ有很大的不同,AspectJ织入的代码就是实际编写的代码,但ASM必须使用其提供的API编写指令。一行java代码可能对应多行ASM
API代码,因为一行java代码背后可能隐藏这多个JVM指令。

你不必担心不会编写ASM代码,官方提供了ASM Bytecode Outline插件可以直接将java代码生成ASM代码。

ASM的实际使用场景非常广泛,我们以Matrix为例。

Matrix是微信开源的一个APM框架,其中TraceCanary子模块用于监测帧率低、卡顿、ANR等场景,具备函数耗时统计的功能。

为了实现函数的耗时统计,通常的做法都是在函数执行开始和结束为止进行插桩,最后以两个插桩点的时间差为函数的执行时间。

 1# -> MethodTracer.TraceMethodAdapter2@Override3protected void onMethodEnter() {4    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);5    if (traceMethod != null) {6        traceMethodCount.incrementAndGet();7        mv.visitLdcInsn(traceMethod.id);8        //入口插桩9        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
10    }
11}
12
13@Override
14protected void onMethodExit(int opcode) {
15    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
16    ...
17    traceMethodCount.incrementAndGet();
18    mv.visitLdcInsn(traceMethod.id);
19    //出口插桩
20    mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
21}

总体上就是每个方法的开头和结尾处各添加一行代码,然后交由TraceMethod进行统计和计算。

详情见旧文Matrix系列文章(一) 卡顿分析工具之Trace Canary。

接下来,我们分析一下ASM的不足。

  • 切面代码需要硬编码,通常是手动写过滤条件,不够灵活,试想一下如何用ASM实现统计所有Activity的生命周期方法。

  • 很难实现在方法调用前后织入新的代码,而在AspectJ中一个call关键字就解决了。

更多详情参见旧 Android ASM框架详解 :https://juejin.im/post/5de86cbee51d4558381e7365。

javassit

javassit是一个开源的字节码创建、编辑类库,现属于Jboss
web容器的一个子模块,特点是简单、快速,与AspectJ一样,使用它不需要了解字节码和虚拟机指令,这里官方文档:https://www.javassist.org/tutorial/tutorial.html。

javassit核心的类库包含ClassPool,CtClass ,CtMethod和CtField。

  • ClassPool:一个基于HashMap实现的CtClass对象容器。

  • CtClass:表示一个类,可从ClassPool中通过完整类名获取。

  • CtMethods:表示类中的方法。

  • CtFields :表示类中的字段。

javassit API简洁直观,比如我们想动态创建一个类,并添加一个helloWorld方法。

 1ClassPool pool = ClassPool.getDefault();2//通过makeClass创建类3CtClass ct = pool.makeClass("test.helloworld.Test");//创建类4//为ct添加一个方法5CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);6ct.addMethod(helloMethod);7//写入文件8ct.writeFile();9//加载进内存
10// ct.toClass();

然后,我们想在helloWorld方法前后织入代码。

 1ClassPool pool = ClassPool.getDefault();2//获取class3CtClass ct = pool.getCtClass("test.helloworld.Test");4//获取helloWorld方法5CtMethod m = ct.getDeclaredMethod("helloWorld");6//在方法开头织入7m.insertBefore("{ System.out.print(\"before insert\");");8//在方法末尾织入 可使用this关键字9m.insertAfter("{System.out.println(this.x); }");
10//写入文件
11ct.writeFile();

javassit的语法直观简洁的特点,使得在很多开源项目中都有它的身影。

比如QQ
zone的热修复方案,当时遇到的问题是补丁包加载做odex优化时,由于差分的patch包并不依赖其他dex,导致补丁包中的类被打上is_preverfied标签(这有助于运行时提升性能),但在补丁运行时实际会去引用其他dex中的类,就会抛出错误
java.lang.IllegalAccessError:Class ref pre-verified class resovled to unexpected implement 。

当时qq空间团队的解决方案是在编译阶段为对所有类的构造方法进行插桩,引用一个事先定义好的AnalyseLoad类,然后干预分包过程,让这个类处于一个独立的dex中,这样就避免了上述问题。

这里用的AOP方案就是javassit,详情见 QQ空间补丁方案解析 。

还有最近开源的插件化框架 shadow
,shadow框架中的一个需求是,插件包具备独立运行的能力,当运行插件工程时,插件中Activity的父类ShadowActivity继承Activity,当插件作为子模块加载到插件中时ShadowActivity不必继承系统Activity,只是作为一个代理类就够了。此时shadow团队封装了JavassistTransform,在编译期动态修改Activity的父类。

详 调试研究Shadow对字节码编辑的正确姿势 :https://juejin.im/post/5d1f03dce51d455d877e0d91。

动态代理

动态代理是代理模式的一种实现,用于在运行时动态增强原始类的行为,实现方式是运行时直接生成class字节码并将其加载进虚拟机。

JDK本身就提供一个Proxy类用于实现动态代理。我们通常使用下面的API创建代理类。

1# java.lang.reflect.Proxy
2public static Object newProxyInstance(ClassLoader loader,
3    Class<?>[] interfaces, 
4    InvocationHandler h)

其中在InvocationHandler实现类中定义核心切点代码。

 1public class InvocationHandlerImpl implements InvocationHandler {23    /** 被代理的实例 */4    private Object mObj = null;56    public InvocationHandlerImpl(Object obj){7        this.mObj = obj;8    }9
10    @Override
11    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
12        //前切入点
13        Object result = method.invoke(this.mObj, args);
14        //后切入点
15        return result;
16    }
17}

这样在前后切入点的位置可以编写要织入的代码。

在我们常用的Retrofit框架中就用到了动态代理。Retrofit提供了一套易于开发网络请求的注解,而在注解中声明的参数正是通过代理包装之后发出的网络请求。

 1# Retrofit.create2public <T> T create(final Class<T> service) {3    ...4    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },5       new InvocationHandler() {6         private final Platform platform = Platform.get();7         private final Object[] emptyArgs = new Object[0];89         @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
10             throws Throwable {
11           // If the method is a method from Object then defer to normal invocation.
12           if (method.getDeclaringClass() == Object.class) {
13             return method.invoke(this, args);
14           }
15           if (platform.isDefaultMethod(method)) {
16             return platform.invokeDefaultMethod(method, service, proxy, args);
17           }
18           //代理
19           return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
20         }
21    });
22}

java动态代理最大的问题是只能代理接口,而不能代理普通类或者抽象类,这是因为默认创建的代理类继承Porxy,而java又不支持多继承,这一点极大的限制了动态代理的使用场景,cglib可代理普通类。

更多详情参见 设计模式之代理模式 。

总结

最后我们总结一下 上述AOP框架的特点及优劣势,你可以根据自身需求进行技术选型。

技术框架特点开发难度优势不足
APT常用于通过注解减少模板代码,对类的创建于增强需要依赖其他框架。★★开发注解简化上层编码。使用注解对原工程具有侵入性。
AspectJ提供完整的面向切面编程的注解。★★真正意义的AOP,支持通配、继承结构的AOP,无需硬编码切面。重复织入、不织入问题,不支持java8
ASM面向字节码指令编程,功能强大。★★★高效,ASM5开始支持java8。切面能力不足,部分场景需硬编码。
JavassitAPI简洁易懂,快速开发。上手快,新人友好,具备运行时加载class能力。切点代码编写需注意classpath加载问题。
java动态代理运行时扩展代理接口功能。运行时动态增强。仅支持代理接口,扩展性差,使用反射性能差。

这篇关于谈谈Android AOP技术方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1020694

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

金融业开源技术 术语

金融业开源技术  术语 1  范围 本文件界定了金融业开源技术的常用术语。 本文件适用于金融业中涉及开源技术的相关标准及规范性文件制定和信息沟通等活动。

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

JavaFX应用更新检测功能(在线自动更新方案)

JavaFX开发的桌面应用属于C端,一般来说需要版本检测和自动更新功能,这里记录一下一种版本检测和自动更新的方法。 1. 整体方案 JavaFX.应用版本检测、自动更新主要涉及一下步骤: 读取本地应用版本拉取远程版本并比较两个版本如果需要升级,那么拉取更新历史弹出升级控制窗口用户选择升级时,拉取升级包解压,重启应用用户选择忽略时,本地版本标志为忽略版本用户选择取消时,隐藏升级控制窗口 2.

如何选择SDR无线图传方案

在开源软件定义无线电(SDR)领域,有几个项目提供了无线图传的解决方案。以下是一些开源SDR无线图传方案: 1. **OpenHD**:这是一个远程高清数字图像传输的开源解决方案,它使用SDR技术来实现高清视频的无线传输。OpenHD项目提供了一个完整的工具链,包括发射器和接收器的硬件设计以及相应的软件。 2. **USRP(Universal Software Radio Periphera

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动