参考
博客 Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
由衷感谢以上博主分享的技术知识!
1.AOP的概念
AOP(面向切面编程)这个概念的提出主要是相对于OOP(面向对象编程)。OOP能够将项目划分为多个模块,但有些功能是各模块都需要的,例如性能监控、日志管理等,AOP便是一刀切入(切开并织入)多个模块,为这些模块提供功能,也为这些功能提供统一的管理。如下图:
2.Android中AOP的实现方式
Android中AOP的实现方式分两类:
运行时切入
- 集成Dexposed,Xposed框架(运行时hook某些关键方法)
- Java API实现动态代理机制(基于反射,性能不佳)
编译时切入
- 集成AspactJ框架(特殊的插件或编译器来生成特殊的class文件)
- 使用ASM,Javassit等字节码工具类来修改字节码(编译打包APK文件前修改class文件)
由于本篇想要讨论的是实现原理,因此不讨论如何使用第三方框架实现切入,仅讨论如何在APK文件生成前获取class文件并修改。这种方式局限性小,对程序运行性能几乎没影响。
3.Android编译流程
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方面的知识。
4.Gradle需知
Task
Gradle构建项目流程便是执行一个又一个task,包括官方提供的和第三方插件提供的,允许开发者灵活地构建项目。
Transfrom
Transfrom是Gradle 1.5.0 以后提供的一个API,是一个有固定运行时机的task,注册后便会运行在class文件整合到dex文件之前。
Input/output
每一个task都有input和output,input来自上一个task,output输出给下一个task。
Plugin
Plugin是插件,一个plugin中含有多个task,在build.gradle文件中这样依赖plugin:
apply plugin : 'package'
5.获取织入入口
新建plugin
- 新建一个library module,名字为BuildSrc,否则apply plugin时会提示找不到
- 删除module下除build.gradle外的所有文件
- 新建以下文件夹 src-main-groovy
- 修改build.gradle并同步:
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile gradleApi()
compile 'com.android.tools.build:gradle:1.5.0'//大于等于1.5.0就行
}
- 在groovy文件夹下新建包,之后的类都放下此包下。包名随意,如com.zyn.plugin
- 新建groovy类(新建file,并且以.groovy作为后缀),继承自org.gradle.api.Plugin:
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========="
}
}
- 在app module下的buiil.gradle中apply插件:
apply plugin: 'com.android.application'
apply plugin: com.zyn.plugin.MyPlugin
- 运行项目后可以在gradle console窗口看到:
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=========
...
自定义Transfrom
新建一个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<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 指定transfrom的作用范围
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> 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))
}
这样就获取了代码织入的入口。
6.字节码处理方案
对于字节码的处理,有多个工具可以选择,常用的有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 |
7.最后
此篇作为本人的学习记录,水平有限,如有谬误,欢迎指正。