Android开发——自动化【Transform】

作者: 谁动了我的代码 | 来源:发表于2023-02-05 16:19 被阅读0次

    Transform的使用及原理

    什么是Transform

    自从1.5.0-beta1版本开始, android gradle插件就包含了一个Transform API, 它允许第三方插件在编译后的类文件转换为dex文件之前做处理操作. 而使用Transform API, 我们完全可以不用去关注相关task的生成与执行流程, 它让我们可以只聚焦在如何对输入的类文件进行处理

    Transform的使用

    Transform的注册和使用非常易懂, 在我们自定义的plugin内, 我们可以通过android.registerTransform(theTransform)或者android.registerTransform(theTransform, dependencies).就可以进行注册.

    class DemoPlugin: Plugin<Project> {
        override fun apply(target: Project) {
            val android = target.extensions.findByType(BaseExtension::class.java)
            android?.registerTransform(DemoTransform())
        }
    }
    

    而我们自定义的Transform继承于com.android.build.api.transform.Transform, 具体我们可以看javaDoc, 以下代码是比较常见的transform处理模板

    class DemoTransform: Transform() {
        /**
         * transform 名字
         */
        override fun getName(): String = "DemoTransform"
    
        /**
         * 输入文件的类型
         * 可供我们去处理的有两种类型, 分别是编译后的java代码, 以及资源文件(非res下文件, 而是assests内的资源)
         */
        override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
    
        /**
         * 是否支持增量
         * 如果支持增量执行, 则变化输入内容可能包含 修改/删除/添加 文件的列表
         */
        override fun isIncremental(): Boolean = false
    
        /**
         * 指定作用范围
         */
        override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
    
        /**
         * transform的执行主函数
         */
        override fun transform(transformInvocation: TransformInvocation?) {
          transformInvocation?.inputs?.forEach {
              // 输入源为文件夹类型
              it.directoryInputs.forEach {directoryInput->
                  with(directoryInput){
                      // TODO 针对文件夹进行字节码操作
                      val dest = transformInvocation.outputProvider.getContentLocation(
                          name,
                          contentTypes,
                          scopes,
                          Format.DIRECTORY
                      )
                      file.copyTo(dest)
                  }
              }
    
              // 输入源为jar包类型
              it.jarInputs.forEach { jarInput->
                  with(jarInput){
                      // TODO 针对Jar文件进行相关处理
                      val dest = transformInvocation.outputProvider.getContentLocation(
                          name,
                          contentTypes,
                          scopes,
                          Format.JAR
                      )
                      file.copyTo(dest)
                  }
              }
          }
        }
    }
    

    每一个Transform都声明它的作用域, 作用对象以及具体的操作以及操作后输出的内容.

    作用域 通过Transform#getScopes方法我们可以声明自定义的transform的作用域, 指定作用域包括如下几种

    image

    作用对象 通过Transform#getInputTypes我们可以声明其的作用对象, 我们可以指定的作用对象只包括两种

    image

    TransformManager整合了部分常用的Scope以及Content集合, 如果是application注册的transform, 通常情况下, 我们一般指定TransformManager.SCOPE_FULL_PROJECT;如果是library注册的transform, 我们只能指定TransformManager.PROJECT_ONLY , 我们可以在LibraryTaskManager#createTasksForVariantScope中看到相关的限制报错代码

                Sets.SetView<? super Scope> difference =
                        Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
                if (!difference.isEmpty()) {
                    String scopes = difference.toString();
                    globalScope
                            .getAndroidBuilder()
                            .getIssueReporter()
                            .reportError(
                                    Type.GENERIC,
                                    new EvalIssueException(
                                            String.format(
                                                    "Transforms with scopes '%s' cannot be applied to library projects.",
                                                    scopes)));
                }
    

    而作用对象我们主要常用到的是TransformManager.CONTENT_CLASS TransformInvocation 我们通过实现Transform#transform方法来处理我们的中间转换过程, 而中间相关信息都是通过TransformInvocation对象来传递

    public interface TransformInvocation {
    
        /**
         * transform的上下文
         */
        @NonNull
        Context getContext();
    
        /**
         * 返回transform的输入源
         */
        @NonNull
        Collection<TransformInput> getInputs();
    
        /**
         * 返回引用型输入源
         */
        @NonNull Collection<TransformInput> getReferencedInputs();
        /**
         * 额外输入源
         */
        @NonNull Collection<SecondaryInput> getSecondaryInputs();
    
        /**
         * 输出源
         */
        @Nullable
        TransformOutputProvider getOutputProvider();
    
    
        /**
         * 是否增量
         */
        boolean isIncremental();
    }
    

    关于输入源, 我们可以大致分为消费型和引用型和额外的输入源

    1. 消费型就是我们需要进行transform操作的, 这类对象在处理后我们必须指定输出传给下一级, 我们主要通过getInputs()获取进行消费的输入源, 而在进行变换后, 我们也必须通过设置getInputTypes()和getScopes()来指定输出源传输给下个transform.
    2. 引用型输入源是指我们不进行transform操作, 但可能存在查看时候使用, 所以这类我们也不需要输出给下一级, 在通过覆写getReferencedScopes()指定我们的引用型输入源的作用域后, 我们可以通过TransformInvocation#getReferencedInputs()获取引用型输入源
    3. 另外我们还可以额外定义另外的输入源供下一级使用, 正常开发中我们很少用到, 不过像是ProGuardTransform中, 就会指定创建mapping.txt传给下一级; 同样像是DexMergerTransform, 如果打开了multiDex功能, 则会将maindexlist.txt文件传给下一级

    Transform的原理

    Transform的执行链

    我们已经大致了解它是如何使用的, 现在看下他的原理(本篇源码基于gradle插件3.3.2版本)在去年AppPlugin源码解析中, 我们粗略了解了android的com.android.application以及com.android.library两个插件都继承于BasePlugin, 而他们的主要执行顺序可以分为三个步骤

    1. project的配置
    2. extension的配置
    3. task的创建

    在BaseExtension内部维护了一个transforms集合对象, android.registerTransform(theTransform)实际上就是将我们自定义的transform实例新增到这个列表对象中. 在3.3.2的源码中, 也可以这样理解. 在BasePlugin#createAndroidTasks中, 我们通过VariantManager#createAndroidTasks创建各个变体的相关编译任务, 最终通过TaskManager#createTasksForVariantScope(application插件最终实现方法在TaskManager#createPostCompilationTasks中, 而library插件最终实现方法在LibraryTaskManager#createTasksForVariantScope中)方法中获取BaseExtension中维护的transforms对象, 通过TransformManager#addTransform将对应的transform对象转换为task, 注册在TaskFactory中.这里关于一系列Transform Task的执行流程, 我们可以选择看下application内的相关transform流程, 由于篇幅原因, 可以自行去看相关源码, 这里的transform task流程分别是从Desugar->MergeJavaRes->自定义的transform->MergeClasses->Shrinker(包括ResourcesShrinker和DexSplitter和Proguard)->MultiDex->BundleMultiDex->Dex->ResourcesShrinker->DexSplitter, 由此调用链, 我们也可以看出在处理类文件的时候, 是不需要去考虑混淆的处理的.

    TransformManager

    TransformManager管理了项目对应变体的所有Transform对象, 它的内部维护了一个TransformStream集合对象streams, 每当新增一个transform, 对应的transform会消费掉对应的流, 而后将处理后的流添加会streams内

    public class TransformManager extends FilterableStreamCollection{
        private final List<TransformStream> streams = Lists.newArrayList();
    }
    

    我们可以看下它的核心方法addTransform

    @NonNull
        public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
                @NonNull TaskFactory taskFactory,
                @NonNull TransformVariantScope scope,
                @NonNull T transform,
                @Nullable PreConfigAction preConfigAction,
                @Nullable TaskConfigAction<TransformTask> configAction,
                @Nullable TaskProviderCallback<TransformTask> providerCallback) {
    
            ...
    
            List<TransformStream> inputStreams = Lists.newArrayList();
            // transform task的命名规则定义
            String taskName = scope.getTaskName(getTaskNamePrefix(transform));
    
            // 获取引用型流
            List<TransformStream> referencedStreams = grabReferencedStreams(transform);
    
            // 找到输入流, 并计算通过transform的输出流
            IntermediateStream outputStream = findTransformStreams(
                    transform,
                    scope,
                    inputStreams,
                    taskName,
                    scope.getGlobalScope().getBuildDir());
    
            // 省略代码是用来校验输入流和引用流是否为空, 理论上不可能为空, 如果为空, 则说明中间有个transform的转换处理有问题
            ...
    
            transforms.add(transform);
    
            // transform task的创建
            return Optional.of(
                    taskFactory.register(
                            new TransformTask.CreationAction<>(
                                    scope.getFullVariantName(),
                                    taskName,
                                    transform,
                                    inputStreams,
                                    referencedStreams,
                                    outputStream,
                                    recorder),
                            preConfigAction,
                            configAction,
                            providerCallback));
        }
    

    在TransformManager中添加一个Transform管理, 流程可分为以下几步

    1. 定义transform task名
    static String getTaskNamePrefix(@NonNull Transform transform) {
            StringBuilder sb = new StringBuilder(100);
            sb.append("transform");
    
            sb.append(
                    transform
                            .getInputTypes()
                            .stream()
                            .map(
                                    inputType ->
                                            CaseFormat.UPPER_UNDERSCORE.to(
                                                    CaseFormat.UPPER_CAMEL, inputType.name()))
                            .sorted() // Keep the order stable.
                            .collect(Collectors.joining("And")));
            sb.append("With");
            StringHelper.appendCapitalized(sb, transform.getName());
            sb.append("For");
    
            return sb.toString();
        }
    

    从上面代码, 我们可以看到新建的transform task的命名规则可以

    image

    , 对应的我们也可以通过已生成的transform task来验证

    image
    1. 通过transform内部定义的引用型输入的作用域(SCOPE)和作用类型(InputTypes), 通过求取与streams作用域和作用类型的交集来获取对应的流, 将其定义为我们需要的引用型流
    private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
            Set<? super Scope> requestedScopes = transform.getReferencedScopes();
            ...
    
            List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());
    
            Set<ContentType> requestedTypes = transform.getInputTypes();
            for (TransformStream stream : streams) {
                Set<ContentType> availableTypes = stream.getContentTypes();
                Set<? super Scope> availableScopes = stream.getScopes();
    
                Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
                        availableTypes);
                Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);
    
                if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
                    streamMatches.add(stream);
                }
            }
    
            return streamMatches;
        }
    
    1. 根据transform内定义的SCOPE和INPUT_TYPE, 获取对应的消费型输入流, 在streams内移除掉这一部分消费性的输入流, 保留无法匹配SCOPE和INPUT_TYPE的流; 构建新的输出流, 并加到streams中做管理
    private IntermediateStream findTransformStreams(
                @NonNull Transform transform,
                @NonNull TransformVariantScope scope,
                @NonNull List<TransformStream> inputStreams,
                @NonNull String taskName,
                @NonNull File buildDir) {
    
            Set<? super Scope> requestedScopes = transform.getScopes();
            ...
    
            Set<ContentType> requestedTypes = transform.getInputTypes();
            // 获取消费型输入流
            // 并将streams中移除对应的消费型输入流
            consumeStreams(requestedScopes, requestedTypes, inputStreams);
    
            // 创建输出流
            Set<ContentType> outputTypes = transform.getOutputTypes();
            // 创建输出流转换的文件相关路径
            File outRootFolder =
                    FileUtils.join(
                            buildDir,
                            StringHelper.toStrings(
                                    AndroidProject.FD_INTERMEDIATES,
                                    FD_TRANSFORMS,
                                    transform.getName(),
                                    scope.getDirectorySegments()));
    
            // 输出流的创建
            IntermediateStream outputStream =
                    IntermediateStream.builder(
                                    project,
                                    transform.getName() + "-" + scope.getFullVariantName(),
                                    taskName)
                            .addContentTypes(outputTypes)
                            .addScopes(requestedScopes)
                            .setRootLocation(outRootFolder)
                            .build();
            streams.add(outputStream);
    
            return outputStream;
        }
    
    1. 最后, 创建TransformTask, 注册到TaskManager中

    TransformTask

    如何触发到我们实现的Transform#transform方法, 就在TransformTask对应的TaskAction中执行

    void transform(final IncrementalTaskInputs incrementalTaskInputs)
                throws IOException, TransformException, InterruptedException {
    
            final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
            final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
            final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
            final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
                    ReferenceHolder.empty();
    
            isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());
    
            GradleTransformExecution preExecutionInfo =
                    GradleTransformExecution.newBuilder()
                            .setType(AnalyticsUtil.getTransformType(transform.getClass()).getNumber())
                            .setIsIncremental(isIncremental.getValue())
                            .build();
    
            // 一些增量模式下的处理, 包括在增量模式下, 判断输入流(引用型和消费型)的变化
            ...
    
            GradleTransformExecution executionInfo =
                    preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();
    
            ...
            transform.transform(
                                    new TransformInvocationBuilder(TransformTask.this)
                                            .addInputs(consumedInputs.getValue())
                                            .addReferencedInputs(referencedInputs.getValue())
                                            .addSecondaryInputs(changedSecondaryInputs.getValue())
                                            .addOutputProvider(
                                                    outputStream != null
                                                            ? outputStream.asOutput(
                                                                    isIncremental.getValue())
                                                            : null)
                                            .setIncrementalMode(isIncremental.getValue())
                                            .build());
    
                            if (outputStream != null) {
                                outputStream.save();
                            }
        }
    

    通过上文的介绍, 我们现在应该知道了自定义的Transform执行的时序, 位置, 以及相关原理.

    写个Transform

    一个合格的Transform插件是需要增量编译的功能的,我拿以前增编的文章的数据给大家做个比较好了。

    全量编译的情况下

    image

    二次增量编译情况下

    image

    我们抛开别的Task,同样一个Transform全量编译的耗时是2784ms,而代码变更增量编译的情况下只有68ms。其中的差距之大也值得各位去把增量编译给写出来了。

    当项目后期代码持续不断的增加之后,不可避免的Transform变多了,只要有任意的一个Transform不是增量的,就会导致整个编译Transform过程都变成全量。这个以前我也介绍过,其实有很多系统的Transform任务,比如Shrink和Dex合并等等。

    你们想一想哦,一个人优化了1分半的编译时间的话,那么如果团队人员一多,那么岂不就是Kpi美滋滋。

    如何去实现一个增编

    首先我将Transform流程进行了一次抽象,主要是因为我比较懒,同样代码和功能如果要让我复制黏贴好两遍其实我都不乐意。所以我先对这部分代码进行了梳理,整理出来两个部分,第一就是文件的复制拷贝,第二就是文件的ASM操作。其中我觉第一部分的代码是可以进行整合的,然后就是下面的逻辑了。

    image

    先从Transform开始吧,简单的说Transform就是一个输入文件集合Collection<TransformInput>一个输出文件集合TransformOutputProvider的过程。我们先读取原始的class jar,然后我们自己对其进行加工之后生成好另外一部分class jar,最后把这个Transform的输出产物当作下一个Transform的输入产物。当class+jar输入的时候,我会先把整个流数据进行一次copy操作,然后对这个temp文件进行加工,如果asm操作完了,我们就将文件进行覆盖操作。

    而在增量编译的情况下,输入流就会发生轻微的变更,TransformInput会告诉我们其中变更的类是什么,其中变更被定义为三种,无论是Jar还是Class都是一样的。

    1. NOTCHANGED 当前文件不需要处理跳过就行了。
    2. ADDED、CHANGED 因为我们都是先用temp然后覆盖当前文件,所以采用同样的处理方式。
    3. REMOVED 删除当前文件夹下的该历史文件。

    所以当增编被调用的情况下,我们只是对于上述这四种不同的操作符号进行不同的处理就好了,只要几个if else就能搞定了。

    而一般我们在使用asm的时候,我们都只会操作Class文件,然后根据class的文件名+路径对其进行一次简单的判断,当前类是不是我们需要做插桩或者扫描操作的,然后我们会读取这个文件byte数组,之后在完成asm操作之后返回一个byte数组,之后覆盖掉原始文件。那么其实我在这里就对其进行了第一次的抽象,asm操作被我定义成了一个接口。

    package com.kronos.plugin.base
     
    interface TransformCallBack {
        fun process(className: String, classBytes: ByteArray?): ByteArray?
    }
    

    这个接口只负责接受一个文件名和一个byte数组,然后方法结束返回一个byte数组就行了。如果byte数组非空的情况下,代表当前类被进行了字节码修改操作,然后我们只要把这个文件进行一次覆盖操作就可以了。进行了这个抽象,我们就可以把上面的文件操作和ASM操作进行一次整合,sdk使用者只需要对这个接口负责就好了。

    那么剩下来我们需要做的就是对这部分文件的写入进行封装了。我是怎么做的呢?我参考了另外一个大佬的多线程优化transform的思路,大佬的项目地址Leaking / Hunter

    1. 所有的输入文件先进行第一次文件拷贝操作
    2. forecah 遍历将每一个文件操作压入线程池中执行
    3. 获取文件名以及byte数组 调用我们定义的抽象接口
    4. 根据interface 返回的byte生成temp文件,然后进行文件覆盖操作
    5. 线程池等待所有任务执行完成之后结束transform

    DoubleTap的编译速度优化

    原来的DoubleTap plugin是整个项目的代码进行扫描的,虽然完成了增量编译功能,同时我也过滤了很多无效扫描的逻辑,但是其实还是会拖慢整个编译速度的。一直到我前一阵子学习了另外一个大佬的一个StringFog项目的时候,发现大佬的常量加密的Transform,可以直接对Module生效。

    
    public class DoubleTapPlugin implements Plugin<Project> {
     
     
    private static final String EXT_NAME = "doubleTab";
     
    @Override
    public void apply(Project project) {
            boolean isApp = project.getPlugins().hasPlugin(AppPlugin.class);
            project.getExtensions().create(EXT_NAME, DoubleTabConfig.class);
            project.afterEvaluate(project1 -> {
                DoubleTabConfig config = (DoubleTabConfig) project1.getExtensions().findByName(EXT_NAME);
                if (config == null) {
                    config = new DoubleTabConfig();
                }
                config.transform();
            });
            if (isApp) {
                AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
                appExtension.registerTransform(new DoubleTapAppTransform());
     
                return;
            }
            if (project.getPlugins().hasPlugin("com.android.library")) {
                LibraryExtension libraryExtension = project.getExtensions().getByType(LibraryExtension.class);
                libraryExtension.registerTransform(new DoubleTapLibraryTransform());
            }
        }
    }
     
    

    以前我在写的时候一般只会给AppExtension注册一个Transform,而在LibraryExtension同样也可以注册一个Transform。在LibraryExtension上注册的会让这部分字节码操作被使用在使用了这个Plugin的Module上。

    小贴士: 这个Transform同样会对Aar生效哦,不仅仅是本地产物。

    而这个Transform的代码上最大的差别就是,其中的输入产物和类型有差别以外,其实别的代码全是一样的。

    
    class DoubleTapLibraryTransform : DoubleTapTransform() {
     
        override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
            return ImmutableSet.of(
                QualifiedContent.Scope.PROJECT
            )
        }
     
        override fun getInputTypes(): Set<QualifiedContent.ContentType>? {
            return TransformManager.CONTENT_CLASS
        }
    
    

    这边有个Scope作用域,InputTypes

    我个人的一个小看法哦,如果只是一个需要针对模块内修改的话,那么你完全不需要写一个全局操作的Transform,只需要对每个Module进行操作就好了。这样有几个好处,扫描速度会变得更快,因为我们不需要操作无关的Jar。另外如果Module没有变更的情况下就不会参与编译,可以变得更快。

    自动化埋点的参数传递

    我在写自动化埋点Demo的时候,一直没有特别好的解决关于参数的问题。以前留了个小坑,只能使用匿名内部类内定义的属性,而如果是外部类的话,因为asm中的ClassVisitor写起来,其实我感觉很不舒服,其原理都是基于事件的。当一个方法被触发之后你要记录下相关值,然后在另外一个函数内进行插入操作。

    之前在做ThreadPoolHook的时候了解到滴滴的Booster内的asm用的都是ClassNode,这里我先简单的说下ClassNode好了。

    ClassNode简介

    如果你仔细读了关于字节码的文章后,你应该会知道Java中当一个方法被调用时会产生一个栈帧(Stack Frame),可以理解为那个方法被包含在了这个栈帧里,栈帧包括3个部分,局部变量区,操作数栈区和帧数据区.接下来我们主要要用到的是局部变量区和操作数栈区.

    一般一句简单的java代码,被翻译成字节码的情况下复杂度都会翻好几倍,其中特别是Java字节码的栈帧。给一个方法传递参数,就是压栈的操作,所以当用ClassVisitor直接操作的时候,我想要修改一行代码,其实难度都非常大。

    ClassNode是ClassVisitor的一个实现类,相比较于ClassVisitor,ClassNode已经存储记录了所有的ClassVisitor信息,构建好了语法树,包括方法内的代码以及行号,还有当前的类属性,类信息等等。其核心就是牺牲了内存,但是由于记录了所有类信息,所以对于复杂的多类联动的操作,会更加方便实用。

    不过TreeAPI比CoreAPI慢30%左右,内存占用也高。

    修改Class,我们只需使用ClassTransformer,然后在transform方法中修改对应的ClassNode即可。使用TreeAPI比CoreAPI更耗时,内存占用也多,但是对于某些复杂的修改也相对简单。treeAPI被设计用于那些使用coreAPI一遍解析无法完成,需要解析多次的场景。

    这部分如果大家有兴趣详细了解的话可以看下这篇文章啊,Java字节码(Bytecode)与ASM简单说明。

    ClassNode传入参数

    好了 show me the code 吧

    
    class AutoTrackHelper : AsmHelper {
     
        private val classNodeMap = hashMapOf<String, ClassNode>()
     
        @Throws(IOException::class)
        override fun modifyClass(srcClass: ByteArray): ByteArray {
            val classNode = ClassNode(ASM5)
            val classReader = ClassReader(srcClass)
            //1 将读入的字节转为classNode
            classReader.accept(classNode, 0)
            classNodeMap[classNode.name] = classNode
            // 判断当前类是否实现了OnClickListener接口
            classNode.interfaces?.forEach {
                if (it == "android/view/View$OnClickListener") {
                    val field = classNode.getField()
                    classNode.methods?.forEach { method ->
                        // 找到onClick 方法
                        insertTrack(classNode, method, field)
                    }
                }
            }
            //调用Fragment的onHiddenChange方法
            visitFragment(classNode)
            val classWriter = ClassWriter(0)
            //3  将classNode转为字节数组
            classNode.accept(classWriter)
            return classWriter.toByteArray()
        }
     
     
        private fun insertTrack(node: ClassNode, method: MethodNode, field: FieldNode?) {
            // 判断方法名和方法描述
            if (method.name == "onClick" && method.desc == "(Landroid/view/View;)V") {
                val className = node.outerClass
                val parentNode = classNodeMap[className]
                // 根据outClassName 获取到外部类的Node
                val parentField = field ?: parentNode?.getField()
                val instructions = method.instructions
                instructions?.iterator()?.forEach {
                    // 判断是不是代码的截止点
                    if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
                        instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 1))
                        instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 1))
                        // 获取到数据参数 
                        if (parentField != null) {
                            parentField.apply {
                                instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
                                instructions.insertBefore(
                                        it, FieldInsnNode(Opcodes.GETFIELD, node.name, parentField.name, parentField.desc)
                                )
                            }
                        } else {
                            instructions.insertBefore(it, LdcInsnNode("1234"))
                        }
                        instructions.insertBefore(
                                it, MethodInsnNode(
                                Opcodes.INVOKESTATIC,
                                "com/wallstreetcn/sample/ToastHelper",
                                "toast",
                                "(Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V",
                                false
                        )
                        )
                    }
                }
            }
        }
        // 判断Field是否包含注解
        private fun ClassNode.getField(): FieldNode? {
            return fields?.firstOrNull { field ->
                var hasAnnotation = false
                field?.visibleAnnotations?.forEach { annotation ->
                    if (annotation.desc == "Lcom/wallstreetcn/sample/adapter/Test;") {
                        hasAnnotation = true
                    }
                }
                hasAnnotation
            }
        }
    
    
    image

    这次我顺便给大家画了一个这部分逻辑的流程图,方便大家可以搞懂这部分代码。这里顺便给大家展开下我之前用ClassVisitor的痛苦吧,这个地方可能是我的操作方式有问题哦。asm操作的是.class文件,每一个内部类其实都是.class文件,这部分扫描都是单独的,如果你要用内部类去访问一些外部类的Field,我是完全没办法的。因为两个类的实例都不同,然后我整个人都感觉有点裂开了。

    这次使用ClassNode,我用HashMap保存了大部分类的ClassNode,然后通过outClassName,去获取到ClassNode实例,然后就可以对其进行修改了。

    image

    上面基本上就是我所有的插桩的代码了,更多Android进阶技术可以参考《Android核心技术手册》里面包含许多技术板块;上千个技术知识带你学习进步。

    其实基本上都是字符串匹配之类的,只是因为bytecode上的和java的不一样,而且bytecode的可读性比较差了点,之前也安利过大家asm bytecode viewer。还是很香的。

    文末

    Transform API在应用工程方面的摸索使用

    组件通信中的作用

    Transform API在组件化工程中有很多应用方向, 目前我们项目中在自开发的路由框架中, 通过其去做了模块的自动化静态注册, 同时考虑到路由通过协议文档维护的不确定性(页面路由地址的维护不及时导致对应开发无法及时更新对应代码), 我们做了路由的常量管理, 首先通过扫描整个工程项目代码收集路由信息, 建立符合一定规则的路由原始基础信息文件, 通过variant#registerJavaGeneratingTask注册 通过对应原始信息文件生成对应常量Java文件下沉在基础通用组件中的task, 这样上层依赖于这个基础组件的项目都可以通过直接调用常量来使用路由.在各组件代码隔离的情况下, 可以通过由组件aar传递原始信息文件, 仍然走上面的步骤生成对应的常量表, 而存在的类重复的问题, 通过自定义Transform处理合并

    业务监控中的作用

    在应用工程中, 我们通常有关于网络监控,应用性能检测(包括页面加载时间, 甚至包括各个方法调用所耗时间, 可能存在超过阈值需要警告)的需求, 这些需求我们都不可能嵌入在业务代码中, 都是可以基于Transform API进行处理. 而针对于埋点, 我们也可以通过Transform实现自动化埋点的功能, 通过ASM Core和ASM Tree将尽可能多的字段信息形成记录传递, 这里有些我们项目中已经实现了, 有一些则是我们需要去优化或者去实现的.

    相关文章

      网友评论

        本文标题:Android开发——自动化【Transform】

        本文链接:https://www.haomeiwen.com/subject/bafzhdtx.html