Android 使用Plugin 实现自动化埋点 Gradle Transform

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 - 简书

引用

神策插件化开发

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容