博客 Android AOP之字节码插桩
博客 Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
由衷感谢以上博主分享的技术知识!
AOP(面向切面编程)这个概念的提出主要是相对于OOP(面向对象编程)。OOP能够将项目划分为多个模块,但有些功能是各模块都需要的,例如性能监控、日志管理等,AOP便是一刀切入(切开并织入)多个模块,为这些模块提供功能,也为这些功能提供统一的管理。如下图:
Android中AOP的实现方式分两类:
运行时切入
编译时切入
由于本篇想要讨论的是实现原理,因此不讨论如何使用第三方框架实现切入,仅讨论如何在APK文件生成前获取class文件并修改。这种方式局限性小,对程序运行性能几乎没影响。
Google官方推荐使用Gradle构建Android项目,在Android Gradle构建流程中,会将源文件编译为class文件,再将class文件整合到dex文件中。我们修改class文件的时机就在class文件编译完成后,dex文件整合之前,我们需要找到这样一个入口进行代码织入。打包流程如下图:
上图中dex步骤就是我们的入口,在Android Gradle Plugin 1.5.0 之前,我们需要hook dx.jar(将class文件整合到dex文件的过程)来获取织入入口。好在Android Gradle Plugin 1.5.0 以后,Google官方提供了Transform API用作字节码插桩的入口。因此本篇就不再赘述hook dx.jar方面的知识。
Gradle构建项目流程便是执行一个又一个task,包括官方提供的和第三方插件提供的,允许开发者灵活地构建项目。
Transfrom是Gradle 1.5.0 以后提供的一个API,是一个有固定运行时机的task,注册后便会运行在class文件整合到dex文件之前。
每一个task都有input和output,input来自上一个task,output输出给下一个task。
Plugin是插件,一个plugin中含有多个task,在build.gradle文件中这样依赖plugin:
apply plugin : 'package'
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile gradleApi()
compile 'com.android.tools.build:gradle:1.5.0'//大于等于1.5.0就行
}
package com.zyn.plugin
import org.gradle.api.Plugin;
import org.gradle.api.Project
public class MyPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.logger.error "========自定义Plugin========="
}
}
apply plugin: 'com.android.application'
apply plugin: com.zyn.plugin.MyPlugin
Configuration on demand is an incubating feature.
:buildsrc:compileJava UP-TO-DATE
:buildsrc:compileGroovy
:buildsrc:processResources UP-TO-DATE
:buildsrc:classes
:buildsrc:jar
:buildsrc:assemble
:buildsrc:compileTestJava UP-TO-DATE
:buildsrc:compileTestGroovy UP-TO-DATE
:buildsrc:processTestResources UP-TO-DATE
:buildsrc:testClasses UP-TO-DATE
:buildsrc:test UP-TO-DATE
:buildsrc:check UP-TO-DATE
:buildsrc:build
========自定义Plugin=========
...
新建一个groovy类继承com.android.build.api.transform.Transform
package com.zyn.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
public class PreDexTransform extends Transform {
Project project
public PreDexTransform(Project project) {
this.project = project
}
// Transfrom在Task列表中的名字
// TransfromClassesWithPreDexForXXXX
@Override
String getName() {
return "preDex"
}
// 指定input的类型
@Override
Set.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 指定transfrom的作用范围
@Override
Set.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection inputs,
Collection referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
// Transfrom的inputs有两种类型,一种是目录,一种是jar包,分别遍历
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput->
//TODO 这里可以对input的文件做处理,比如代码注入!
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each {JarInput jarInput->
//TODO 这里可以对input的文件做处理,比如代码注入!
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if(jarName.endsWith(".jar")) {
jarName = jarName.substring(0,jarName.length()-4)
}
def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
如此就拿到了代码织入的入口,在上图TODO注释处可以处理input文件并输出到output中去。
最后还需要修改MyPlugin的apply方法,添加注册Transfrom的逻辑:
@Override
public void apply(Project project) {
project.logger.error "========自定义Plugin========="
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PreDexTransform(project))
}
这样就获取了代码织入的入口。
对于字节码的处理,有多个工具可以选择,常用的有ASM,Javassist,BCEL等,各有优劣,开发者可以根据项目需求选择:
- ASM优点是更高效,缺点是较难使用,API非常底层,贴近字节码层面,需要字节码知识及虚拟机相关知识
- Javassist、BCEL等工具可以更简单地操作字节码,但性能方面不如ASM
不同工具库生成同一个类的耗时比较,如下表:
Framework | First time | Later times |
---|---|---|
Javassist | 257 | 5.2 |
BCEL | 473 | 5.5 |
ASM | 62.4 | 1.1 |
此篇作为本人的学习记录,水平有限,如有谬误,欢迎指正。