Android:hook很“危险”,使用需谨慎

前言

上篇文章《Android安卓进阶技术分享之AGP工作原理》和大家分析了 AGP(Android Gradle Plugin) 做了哪些事,了解到 AGP 就是为打包这个过程服务的。

那么,本篇文章就和大家聊一聊其中的 Transform,解决一下为什么在 AGP 3.x.x 的版本可以通过反射获取的 transformClassesWithDexBuilderForXXX Task 在 4.0.0 的版本就不灵了?

源码走起!

Transform的流程

读本篇文章以前,相信同学们已经具备 Transform 的使用基础。

相信很多人都看过这张图:

Transform过程

正如上图中展示的,我们可以看到:

• 在一个项目中,我们可能既会有自定义的 Transform,也会有系统的 Transform。

• 在处理过程中,每一个 Transform 的接受流都是接收到上一个 Transform 的输出流,原始的文件流会经过很多 Transform 的处理。

Transform源码分析

既然我们已经了解了整体的流程,再来看一下其中的细节吧。

第一步 Transform的起点

我们都知道,使用 Transform 的目的,是为了修改其中的字节码,那么,这些 Class 文件是哪里来的呢?

直接打开 AGP 的源码,直接跳到创建编译 Task 的时候,这个方法发生在 AGP 创建跟 Variant 相关的 Task 的时候,在 AbstractAppTaskManager 里:

private void createCompileTask(@NonNull VariantPropertiesImpl variantProperties) {  
    ApkCreationConfig apkCreationConfig = (ApkCreationConfig) variantProperties;  
    // 执行javac  
    TaskProvider<? extends JavaCompile> javacTask = createJavacTask(variantProperties);  
    // 添加Class输入流  
    addJavacClassesStream(variantProperties);  
    setJavaCompilerTask(javacTask, variantProperties);  
    // 执行transform和dex相关的任务  
    createPostCompilationTasks(apkCreationConfig);  
}  

虽然只有几个方法,但是每个方法的作用还挺大,先看 javac。

第二步 执行javac

大家对 javac 的命令肯定很熟悉,它可以将 .java 文件转化成 .class 文件。这个方法确实也是这样:

public TaskProvider<? extends JavaCompile> createJavacTask(  
        @NonNull ComponentPropertiesImpl componentProperties) {  
    // Java预编译任务,看了一下,主要是处理Java注解  
    taskFactory.register(new JavaPreCompileTask.CreationAction(componentProperties));  
    // Java编译任务  
    final TaskProvider<? extends JavaCompile> javacTask =  
            taskFactory.register(new JavaCompileCreationAction(componentProperties));  
    postJavacCreation(componentProperties);  
    return javacTask;  
}

它的方法注释:

❝Creates the task for creating *.class files using javac. These tasks are created regardless of whether Jack is used or not, but assemble will not depend on them if it is. They are always used when running unit tests.❞

很明显,就是为了创建 .class 文件。

这一步中,最重要的一步就是注册了一个名叫 JavaCompile 的任务,也就是将 Java 文件和 Java 注解转变成 .class 的 Task。

JavaCompile 的 Task 的代码比较绕,直接跟大家说结果了,最终是调用 JDK 下面的 JavaCompiler 类,动态将 .java 转化成 .class 文件。

当然,不仅仅只有 .class 文件,还有其他的诸如 .kt 和 .jar 等,都需要特定的 Task,才能转化成我们需要的输入源。

第三步 建立原始的输入流

回到第一步,进入 addJavacClassesStream 方法:

protected void addJavacClassesStream(@NonNull ComponentPropertiesImpl componentProperties) {  
    // create separate streams for the output of JAVAC and for the pre/post javac  
    // bytecode hooks  
    TransformManager transformManager = componentProperties.getTransformManager();  
    boolean needsJavaResStreams =  
            componentProperties.getVariantScope().getNeedsJavaResStreams();  
    transformManager.addStream(  
            OriginalStream.builder(project, "javac-output")  
                    // Need both classes and resources because some annotation  
                    // processors generate resources  
                    .addContentTypes(  
                            needsJavaResStreams  
                                    ? TransformManager.CONTENT_JARS  
                                    : ImmutableSet.of(DefaultContentType.CLASSES))  
                    .addScope(Scope.PROJECT)  
                    .setFileCollection(project.getLayout().files(javaOutputs))  
                    .build());  
    BaseVariantData variantData = componentProperties.getVariantData();  
    transformManager.addStream(  
            OriginalStream.builder(project, "pre-javac-generated-bytecode")  
                    .addContentTypes(  
                            needsJavaResStreams  
                                    ? TransformManager.CONTENT_JARS  
                                    : ImmutableSet.of(DefaultContentType.CLASSES))  
                    .addScope(Scope.PROJECT)  
                    .setFileCollection(variantData.getAllPreJavacGeneratedBytecode())  
                    .build());  
    transformManager.addStream(  
            OriginalStream.builder(project, "post-javac-generated-bytecode")  
                    .addContentTypes(  
                            needsJavaResStreams  
                                    ? TransformManager.CONTENT_JARS  
                                    : ImmutableSet.of(DefaultContentType.CLASSES))  
                    .addScope(Scope.PROJECT)  
                    .setFileCollection(variantData.getAllPostJavacGeneratedBytecode())  
                    .build());  
}  

这个 transformManager 就是处理 Transform 的,它在建立第一个 Transform 的原始数据流。

细心的同学可能发现了,第一个数据流的 contentType 至少也是 DefaultContentType.CLASSES,scope 是 Scope.PROJECT,自定义过 Transform 的同学肯定知道,这样设置我们自定义的 Transform 能够接收到原始数据流。

第四步 创建编译后的任务

回到第一步中的 createPostCompilationTasks 方法,它用来创建编译后的任务:

public void createPostCompilationTasks(@NonNull ApkCreationConfig creationConfig) {  
    //...  
    TransformManager transformManager = componentProperties.getTransformManager();  
    // ...  
    // java8脱糖  
    maybeCreateDesugarTask(  
            componentProperties,  
            componentProperties.getMinSdkVersion(),  
            transformManager,  
            isTestCoverageEnabled);  
    BaseExtension extension = componentProperties.getGlobalScope().getExtension();  
    // Merge Java Resources.  
    createMergeJavaResTask(componentProperties);  
    // ----- External Transforms -----  
    // apply all the external transforms.  
    List<Transform> customTransforms = extension.getTransforms();  
    List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();  
    boolean registeredExternalTransform = false;  
    for (int i = 0, count = customTransforms.size(); i < count; i++) {  
        Transform transform = customTransforms.get(i);  
  
        List<Object> deps = customTransformsDependencies.get(i);  
        registeredExternalTransform |=  
                transformManager  
                        .addTransform(  
                                taskFactory,  
                                componentProperties,  
                                transform,  
                                null,  
                                task -> {  
                                    if (!deps.isEmpty()) {  
                                        task.dependsOn(deps);  
                                    }  
                                },  
                                taskProvider -> {  
                                    // if the task is a no-op then we make assemble task depend on it.  
                                    if (transform.getScopes().isEmpty()) {  
                                        TaskFactoryUtils.dependsOn(  
                                                componentProperties  
                                                        .getTaskContainer()  
                                                        .getAssembleTask(),  
                                                taskProvider);  
                                    }  
                                })  
                        .isPresent();  
    }  
  
    // Add a task to create merged runtime classes if this is a dynamic-feature,  
    // or a base module consuming feature jars. Merged runtime classes are needed if code  
    // minification is enabled in a project with features or dynamic-features.  
    if (componentProperties.getVariantType().isDynamicFeature()  
            || variantScope.consumesFeatureJars()) {  
        taskFactory.register(new MergeClassesTask.CreationAction(componentProperties));  
    }  
    // ----- Minify next -----  
    // 混淆  
    // ----- Multi-Dex支持...  
    // 创建 dex  
    createDexTasks(  
            creationConfig, componentProperties, dexingType, registeredExternalTransform);  
    // ... 资源压缩等  
}  

在进行 Transform 之前,它还会进行一些 java8 的脱糖以及合并 java 资源的 Task,这些都是会被添加到原始的数据流中。

第五步 为Transfrom创建Task

首先,我们得明白,每一种 Transform 其实有两种类型:

1. 消费型:需要将数据源输出给下一个 Transform。

2. 引用型:只需要读取,不需要输出。

接下来就到了我们关心的处理 Transform 的逻辑了。

从上面的方法我们可以看出,系统会为我们找到所有已经在BaseExtension 注册的 Transform 并遍历,使用 transformManager 通过 addTransform 做处理:

public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(  
        @NonNull TaskFactory taskFactory,  
        @NonNull ComponentPropertiesImpl componentProperties,  
        @NonNull T transform,  
        @Nullable PreConfigAction preConfigAction,  
        @Nullable TaskConfigAction<TransformTask> configAction,  
        @Nullable TaskProviderCallback<TransformTask> providerCallback) {  
    //... 省略  
    List<TransformStream> inputStreams = Lists.newArrayList();  
    String taskName = componentProperties.computeTaskName(getTaskNamePrefix(transform));  
    // get referenced-only streams  
    List<TransformStream> referencedStreams = grabReferencedStreams(transform);  
    // find input streams, and compute output streams for the transform.  
    IntermediateStream outputStream =  
            findTransformStreams(  
                    transform,  
                    componentProperties,  
                    inputStreams,  
                    taskName,  
                    componentProperties.getGlobalScope().getBuildDir());  
    // ... 检测工作  
    transforms.add(transform);  
    TaskConfigAction<TransformTask> wrappedConfigAction =  
            t -> {  
                t.getEnableGradleWorkers()  
                        .set(  
                                componentProperties  
                                        .getGlobalScope()  
                                        .getProjectOptions()  
                                        .get(BooleanOption.ENABLE_GRADLE_WORKERS));  
                if (configAction != null) {  
                    configAction.configure(t);  
                }  
            };  
    // create the task...  
    return Optional.of(  
            taskFactory.register(  
                    new TransformTask.CreationAction<>(  
                            componentProperties.getName(),  
                            taskName,  
                            transform,  
                            inputStreams,  
                            referencedStreams,  
                            outputStream,  
                            recorder),  
                    preConfigAction,  
                    wrappedConfigAction,  
                    providerCallback));  
}  

这里呢,先定义了一个 taskName,规则是:

transform${inputType}With${transformName}For${BuildType}  

关于 taskName 规则先放这儿,后面我们会用到!

上面代码中的 referencedStreams 用来处理引用型的 Transform,所以我们着重看 outputStream,outputStream 是通过方法 findTransformStreams 方法生成的,关于数据流向的问题这个方法里面讲的特别明白:

private final List<TransformStream> streams = Lists.newArrayList();  
private IntermediateStream findTransformStreams(  
        @NonNull Transform transform,  
        @NonNull ComponentPropertiesImpl componentProperties,  
        @NonNull List<TransformStream> inputStreams,  
        @NonNull String taskName,  
        @NonNull File buildDir) {  
    //...  
    // 消费数据流,inputStreams添加需要消费的数据流  
    // 1. inputStreams会消费掉streams可以消费的数据流  
    consumeStreams(requestedScopes, requestedTypes, inputStreams);  
  
    Set<ContentType> outputTypes = transform.getOutputTypes();  
    File outRootFolder =  
            FileUtils.join(  
                    buildDir,  
                    StringHelper.toStrings(  
                            AndroidProject.FD_INTERMEDIATES,  
                            FD_TRANSFORMS,  
                            transform.getName(),  
                            componentProperties.getVariantDslInfo().getDirectorySegments()));  
    // 创建输出流  
    IntermediateStream outputStream =  
            IntermediateStream.builder(  
                    project,  
                    transform.getName() + "-" + componentProperties.getName(),  
                    taskName)  
                    .addContentTypes(outputTypes)  
                    .addScopes(requestedScopes)  
                    .setRootLocation(outRootFolder)  
                    .build();  
    // 2. 为下一个Transform添加生成的数据流  
    streams.add(outputStream);  
    return outputStream;  
}

流程如图:

数据消费

意思就是每一个 Transform 都要走一遍图中的流程,对于大部分 Transform 来说,每一个的输入源就是上一个Transform 的输出源。

所以对于开发者来说,如果我们定义 Transform 却不将生成的文件添加到输出目录,这就会导致后面的 Transform 找不到输入源,编译器就只能报错了。

这个错误我最近才犯过。

回到这一步的开始,taskFactory 最终为我们注册了一个 TransformTask。

第六步 TransformTask做了什么

进入 TransformTask 这个类,里面有一个方法 transform 添加了 @TaskAction 注解,所以,一旦该 Task 执行了,这个方法就会被调用。

@TaskAction  
void transform(final IncrementalTaskInputs incrementalTaskInputs)  
        throws IOException, TransformException, InterruptedException {  
  
    // 设置增量编译  
    isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());  
    // ...  
    recorder.record(  
            ExecutionType.TASK_TRANSFORM_PREPARATION,  
            preExecutionInfo,  
            getProjectPath().get(),  
            getVariantName(),  
            new Recorder.Block<Void>() {  
                @Override  
                public Void call() throws Exception {  
                    // ... 针对增量编译对文件处理  
                    return null;  
                }  
            });  
    GradleTransformExecution executionInfo =  
            preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();  
    recorder.record(  
            ExecutionType.TASK_TRANSFORM,  
            executionInfo,  
            getProjectPath().get(),  
            getVariantName(),  
            new Recorder.Block<Void>() {  
                @Override  
                public Void call() throws Exception {  
                    // ...  
                    transform.transform(  
                            new TransformInvocationBuilder(context)  
                                    .addInputs(consumedInputs.getValue())  
                                    .addReferencedInputs(referencedInputs.getValue())  
                                    .addSecondaryInputs(changedSecondaryInputs.getValue())  
                                    .addOutputProvider(  
                                            outputStream != null  
                                                    ? outputStream.asOutput()  
                                                    : null)  
                                    .setIncrementalMode(isIncremental.getValue())  
                                    .build());  
  
                    if (outputStream != null) {  
                        outputStream.save();  
                    }  
                    return null;  
                }  
            });  
}

recorder 不用管,它只是一个执行器,最终会执行 Block 中的代码。

如果是增量编译的 Task,它会处理文件,告诉我们哪些文件变化了。

之后,就执行 Transform 的 transform 方法,整个 Transform 就结束了。

第七步 DexBuild

回到第四步,AGP 会我们先后注册了混淆和多 Dex 支持的 Task,之后就到了创建 Dex 的 Task:

private void createDexTasks(  
        @NonNull ApkCreationConfig apkCreationConfig,  
        @NonNull ComponentPropertiesImpl componentProperties,  
        @NonNull DexingType dexingType,  
        boolean registeredExternalTransform) {  
    // ...  
    taskFactory.register(  
            new DexArchiveBuilderTask.CreationAction(  
                    dexOptions,  
                    enableDexingArtifactTransform,  
                    componentProperties));  
    //...  
}  

DexArchiveBuilderTask 就是名为 dexBuilder 的任务,它的注释:

❝Task that converts CLASS files to dex archives❞

它就是创建 dex 文件的 Task。

如果想要对 Dex 有进一步的了解,可以阅读:

❝《浅谈 Android Dex 文件》https://tech.youzan.com/qian-tan-android-dexwen-jian/

到了这一步,我们的源码分析就结束了。

解决问题

之前我一直说 AGP 3.x.x 的时候可以 hook 到 transformClassesWithDexBuilderForXXX 的 task,到了 AGP 4.x.x 就不行了。

Transform

仔细看一下我上面提到 taskName 命名规则,就会发现,在 3.x.x 之前,transformClassesWithDexBuilderForXXX 其实是一个 Transform,我记得对应的类 DexTransform,它会帮助 AGP 生成 .dex 文件。

而在 4.1.1 的代码中,这个任务交给了 DexArchiveBuilderTask,已经不是一个 Transform 了。

所以啊,经常看到安卓开发者骂骂咧咧的说:卧槽,AGP版本升级了,我的这个方法不能用了!

因此,得出结论,在 AGP 上,最好还是不要去 hook 源码,建议使用官方推荐的接口去处理。

总结

本篇文章的内容其实是对上面 Transform 流程的验证,相信大家已经对 Transform 流程有了整体的把握!

如有什么争议的内容,欢迎评论区留言,如果觉得本文不错,「点赞」是对本文最大的肯定!

原文地址:https://cloud.tencent.com/developer/article/1987724

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具