Android 的打包流程

image.png
apk 打包的流程,可以看启用有一步是 .class 文件到 .dex 发文件的转换。而 Gradle Transform 是 Android 官方提供的在这一阶段用来修改 .class 文件的一套标准 API。 这一应用现在主要集中在字节码查找、代码注入等
Transform
Transform 是一个 gradle task
第一步,创建 Android Library module
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation gradleApi()
implementation localGroovy()
compileOnly 'com.android.tools.build:gradle:3.4.1'
}
repositories {
jcenter()
}
// 上传到本地的代码仓
uploadArchives{
repositories.mavenDeployer{
// 本地仓库路径
repository(url: uri('../repo'))
// 设置 groupId
pom.groupId = 'com.sensorsdata.analytics.android'
// 设置 artifactId
pom.artifactId = 'android-gradle-plugin2'
// 设置 插件版本号
pom.version = '1.0.0'
}
}
第二步,新进 groovy 目录,创建 Transform 类
class AnalyticsTransform extends Transform {
@Override
String getName() {
return "AnalyticsAutoTrack"
}
/**
* 需要处理的数据
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* Transform 要操作的内容范围
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
customTransform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getOutputProvider(), transformInvocation.incremental)
}
void customTransform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider,
boolean isIncremental) {
if (!isIncremental) {
outputProvider.deleteAll()
}
// 遍历
inputs.forEach { TransformInput input ->
// 遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInputs(directoryInput)
}
// 遍历 jar
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput)
}
}
}
第三步创建 plugin, 注册 transform
class AnalyticsPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new AnalyticsTransform(extension))
}
}
第四步 创建 properties
在 plugin/src/main 目录下创建新目录 resources/META-INFO/gradle-plguins, 然后创建文件,内容指定为上一步创建的 plugin,命名为com.sensorsdata.analytics.android.properties, 这里的命名主要是要和项目中apply plugin 对应
implementation-class=com.sensorsdata.analytics.android.plugin.AnalyticsPlugin
最后一步publish 之后,在项目中进行引用
根build.gradle中
classpath 'com.sensorsdata.analytics.android:android-gradle-plugin2:1.0.0'
Module build.gradle 中
apply plugin: 'com.sensorsdata.analytics.android'
结合ASM
前面已经将plugin 基本框架写了,但是 handleDirectoryInputs(directoryInput)和handleJarInputs(jarInput)并没有实现
插件build.gradle 添加
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
implementation 'org.ow2.asm:asm-analysis:9.1'
implementation 'org.ow2.asm:asm-util:9.1'
implementation 'org.ow2.asm:asm-tree:9.1'
继续完成以下两个方法:
private void handleDirectoryInputs(Context context, DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
println("== AnalyticsTransform directoryInputs = " + directoryInput.file.listFiles().toArrayString())
File dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
File dir = directoryInput.file
if (dir) {
Map<String, File> modifyMap = new HashMap<>()
// 遍历以某一拓展名结尾的文件
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File classFile ->
if (AnalyticsClassModifier.isShouldModify(classFile.name)) {
println("-----AnalyticsTransform modifyClassFile dir =" + dir.absolutePath
+ "\nclassFile= " + classFile.name + "\ngetTemporaryDir " + context.getTemporaryDir()
+ "\nmAnalyticsExtension.disableAutoTrack=" + mAnalyticsExtension.disableAutoTrack)
File modified = null
modified = AnalyticsClassModifier.modifyClassFile(dir, classFile, context.getTemporaryDir())
if (modified != null) {
String key = classFile.absolutePath.replace(dir.absolutePath, "")
modifyMap.put(key, modified)
}
}
}
// 复制到文件
FileUtils.copyDirectory(directoryInput.file, dest)
modifyMap.entrySet().each { Map.Entry<String, File> en ->
File target = new File(dest.absolutePath + en.getKey())
if (target.exists()) {
target.delete()
}
FileUtils.copyFile(en.getValue(), target)
en.getValue().delete()
}
}
}
private void handleJarInputs(Context context, JarInput jarInput, TransformOutputProvider outputProvider) {
println("\n\n== AnalyticsTransform jarInput = " + jarInput.file.name)
String destName = jarInput.file.name
// 截取文件路径的 md5 值重命名输出路文件
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath.substring(0, 8))
// 获取 jar 的名字
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
// 获取输出文件
File dest = outputProvider.getContentLocation(destName + "_" + hexName,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
def modifiedJar = null
modifiedJar = AnalyticsClassModifier.modifyJar(jarInput.file, context.getTemporaryDir(), true)
if (modifiedJar == null) {
modifiedJar = jarInput.file
}
println("== AnalyticsTransform jarInput = modifiedJar " + modifiedJar.name + "\ndest=" + dest)
FileUtils.copyFile(modifiedJar, dest)
}
AnalyticsClassModifier 在 AnalyticsClassModifier.modifyClassFile 和 AnalyticsClassModifier.modifyJar 方法里面都调用了 modifyClass 方法,在这个方法里面设置 ASM 的 ClassWriter, ClassVisitor 和 ClassReader。
private static byte[] modifyClass(byte[] sourceClass){
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AnalyticsClassVisitor(classWriter)
ClassReader classReader = new ClassReader(sourceClass)
classReader.accept(classVisitor, ClassReader.SKIP_FRAMES)
return classWriter.toByteArray()
}
具体AnalyticsClassVisitor 类可以参考 第一篇文章
Android 使用Plugin 实现自动化埋点,ASM - 简书