常见的编程架构思想

  • 面向对象(Object Oriented Programming)
  • 面向过程(Procedure Oriented Programming)
  • 面向切面(Aspect Oriented Programming)

以上三种编程架构思想中,面向对象和面向过程是我们大多数开发人员接触比较多的。

其实,对于大部分开发者,前两者编程思想并不陌生。接触更少的应该是面向切面的编程,本文重点和大家浅析一下切面编程以及切面编程在客户端的应用。

面向切面编程(AOP)

AOP是Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP是OOP的延续,是软件开发中的一个热点,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP其实是OOP的补充,OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。用一句更加形象的话讲,AOP就像一把刀,它能将整个过程按照你想要的切点进行切片,让整个过程清晰可见。

image-20190321165445019

AOP的分类以及常见概念

AOP技术可以简单的分为两类,一类是运行期的AOP,例如Java的动态代理;另一类可以归结为编译期的AOP,例如经常听说的ASM,AspecJ等框架。

对于客户端而言,对性能的影响度是一个重要的考量目标,因为客户端本身运行的环境是有限的,不过随着手机技术的不断更新迭代,性能不断的飙升,这个问题也逐渐慢慢退化。然而,和大型服务器的处理能力相比,手机的处理能力还是略显逊色的。因此,在客户端应用我们重点考虑的仍然是编译期的AOP方案,以提高运行期的效率。

AOP常见概念一般包含: 连接点(Joinpoint) 切点(*Pointcut*) 增强(*Advice*) 目标对象 (Target) 引介(Introduction) 织入(Weaving) 代理(Proxy) 切面(Aspect) 等,不太了解的可以自行搜索一下。

客户端开发中的应用

目前来说AOP在后端应用的场景已经非常完善了,比如说Spring框架自身对就AOP编程有一个很好的支持。对于起步稍晚的客户端而言,AOP却显得没有那么多的应用场景。如何能将AOP的思想,融入到客户端的开发,从而提升客户端的开发效率,是我们一直在思考与实践的一个问题。

了解了以上内容之后,下边着重介绍一下AOP编程思想在网易新闻客户端的应用与实践:

Hot Fix (热修复)

两年之前 Hot Fix技术炒的非常热,各大友商也是花样百出,各自献计,一时间出现了百家齐鸣的现象。当然网易其实也不甘落后,公司各个部门也研发了自己的热更新热补丁方案。网易新闻也是不断的探索未知,先后实践过一些方案, 最终还是基于AOP的方案,制定了一套相同成熟稳定的热修复技术。

  • 原理

基于AOP技术,编译期修改Java字节码,对每一个方法进行插装操作,以便于hook每一个方法,做到方法级别的热修复。

  • AOP框架选型

编译期的AOP框架有很多,例如 ASM, AspectJ,Javassist等等。如何选择框架其实是我们研发初期面临的一个问题。我们最终选的是使用AspectJ作为研发框架,原因有以下几点:

  1. 易用性/门槛低:API设计非常简单,无需你深入的了解.class文件的结构,就能轻易的驾驭它。
  2. 对Java语言的完美支持:支持各种表达式,满足你对切点的自定义。

当然,它也有缺点,这个后面我们会介绍。

  • Android热修复的实现

img_android_patch

如上图,实现过程主要有以下几个关键点:

  1. 方法级别的hook

如图所示,gradle进行构建的时候,在Java源码编译完成之后,生成dex文件之前,我们调用AspectJ的编译器进行插桩。插桩的目的是给每一个方法注入一段寻找patch方法的逻辑。
简单来说就是每个方法执行时先去根据自己方法的签名寻找是否有自己对应的patch方法,如果有执行patch方法,没有执行自己原有的逻辑。大致代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
try {
String e = joinPoint.toLongString(); //获取当前方法的签名
PatchDebug.printMethodDesByFile(e);
if(PatchUtils.hasMethodPatch(e)) { //查询是否有自己方法的patch方法
Object target = joinPoint.getTarget(); //获取当前的运行对象,方便设置给patch方法
Object[] params = joinPoint.getArgs(); //获取运行参数
................................. //省略一些处理逻辑
return joinPoint.proceed();//执行原有逻辑
}
} catch (Exception var8) {
var8.printStackTrace();
}
return joinPoint.proceed();
}
  1. 何时进行插桩操作以及如何进行插庄

插桩的时机:最基本的原则是:java源码编译完成之后,Dex文件生成之前。

对于gradle构建来说,我们可以有两种选择:

a. 直接hook java Compiler的Task,在java源码编译完成之后执行AspectJ的编译器,进行字节码插桩操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
project.android.applicationVariants.all { variant ->
if (variant.buildType.name != "release") {
log.debug("Skipping non-release build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
//执行aspectJ编译操作
String[] args = ["-showWeaveInfo",
"-1.5",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
}
}
}
}

b. gradle Transform API方案(推荐使用)

Transform API是Android gradle plugin 1.5之后新API, 作用就是在生成dex之前,给开发者一个机会能够统一进行修改字节码。

对于上述a方案存在一些问题,对于一些jar包或者三方sdk的类,不太方便处理。因此采用android官方推荐的Transform API的方案是一个比较理想的方案,可以根据需求选择性定制处理哪些文件。
用Transform API实现一个gradle 插件,对字节码处理。能够动态可配置的选择处理那些jar或者那些文件。这就大大的方便了对自己的代码和第三方的代码进行字节码的处理。

  • 补丁包如何生成

出现bug时将需要进行替换的方法放到指定类,然后生成一个只含有此java类字节码的apk包,进行下发。

如何将patch包做到最小? 我们的方案是:使用发版是的mapping文件,正常进行apk的编译,在生成dex之前,我们强制性的移除掉现有的类,只保留新增的含有patch方法的class文件;按照同样的思想移除掉多余的资源文件,最终生一个只含有补丁方法的类的apk或者jar。

image-20190321180340590

  • 补丁包加载与运行

运行时通过DexFile.loadDex()将apk加载。然后按照方法签名将补丁方法做缓存处理,每一个方法运行时会去缓存里查找是否有自己对应的patch方法,如果有则执行patch方法。

  • 优缺点
优点 缺点
无兼容行问题 apk原始包方法数增大,体积增大
实时生效 打release包时间变长
patch包体积小 运行时性能有一定影响

对性能的影响:

我们通过对线上埋点测试发现,对方法的影响是毫秒级的,但是从整体来讲,例如从整个启动到主页面加载完成这个过程性能影响在500ms左右。

  • 优化方案

a. 使用更轻量级别的AOP框架,减少包体积和性能影响;

b. 使用类级别的热修复方案。

自动化工具

  • 自动化监测方法耗时

如何自动化的检测和监控主线程方法耗时,降低ANR产生的风险,是一个客户端开发非常关注的问题。为了能快速针对方法耗时做一个监控,我们利用AspectJ框架,hook每一个方法,获取方法执行的耗时时间,如果是主线程方法并且超过了我们设置的耗时阀值,则自动输出警告日志,便于开发人员检测此方法是否存在耗时逻辑。

如下图所示:打印了主线程方法的耗时:

methodtime

  • 自动化日志记录

用户行为操作日志,无论是客户端排查问题,还是对用户行为的分析都是一个非常重要的埋点日志。因此为了能够自动化的记录用户的操作日志,我们通用使用AOP的思想,自动化hook Activity生命周期和Fragment声明周期的方法,从而自动化的做到用户行为的埋点上报和日志记录。

  • 三方SDK的检测与修改

a. 检测三方sdk方法级别耗时,以便于对SDK有一个整体的评估。

b. 上线前临时修改或者hook三方SDK的方法,做到快速容灾上线。

c. 自动化抓取三方SDK网络和行为日志。

  • 统一处理点击抖动

客户端点击抖动是一个比较尴尬的问题,一方面此问题是客观存在的,确实对用户的体验有一定的影响;另一方面从开发角度讲,case by case的处理是一个繁琐的过程。所以整体来看是一个收益不高但是代价不小的问题。

为了能够以自小的代价,解决这个全局性的问题,我们使用了AOP的思想,编译阶段统一hook android.view.View.OnClickListener#onClick(View v)方法; 做一个快速点击无效的防抖动效果,统一解决客户端快速点击多次响应的问题。

这种方案的优点在于,统一处理,全局生效,开发阶段代码无切入性,开发人员完全无感知,同时代价又很低。

APM(应用性能监控)

应用程序性能的监控,是一个商业化应用发展到一定阶段不得不关注的问题。如果纯粹依靠开发人员手动埋点,去监控客户端性能,则存在一些弊端,比如说:维护成本高,和业务耦合性强,无法快速迁移到其他产品中。

为了解决这一问题,我们公司内部也是依靠AOP思想,开发了一套完整的应用性能监控平台。尽量做到开发人员无感知,无侵入的实现,应用网路、UI交互的性能监控。

网络数据监控:网络层通过对网络库的方法hook和拦截器的自动化注入,实现网络过程的全过程监控,包括:握手时长,首包时间,DNS耗时,网络耗时等阶段信息的获取。通过对整个网络过程的数据表现分析,找到网络层面性能的问题点,做出一些列的优化措施。

通过APM对网络性能的监控,新闻客户端不断的采取了HttpDNS,错误数据同步CND优化CND调度链等一些列措施,将网络错误率降最小化。

AOP与APT区别

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。

简单来说APT就是在编译期,通过注解生成.java文件,然后.java文件仍然需要进一步编译生成.class文件。而AOP是在编译完成后直接通过修改.class文件,添加或者修改代码逻辑。