Android gradle打包涉及task源码解析(五)

文章序号

此篇文章将分析如下3个task。

:app:transformClassesWithDexBuilderForDebug
:app:transformDexArchiveWithExternalLibsDexMergerForDebug
:app:transformDexArchiveWithDexMergerForDebug

transform vs task

本篇文章主要分析transform相关的任务,分析transform任务之前跟大家大致的聊下transform和task的关联。
在本篇之前的文章分析的task基本都是Task的子类。 transform相关的任务均是Transform的子类。那task和transform有什么关联呢?

先看下Transform:

public abstract class Transform {
    
    public abstract String getName();

    public abstract Set<ContentType> getInputTypes();

    public Set<ContentType> getOutputTypes() {
        return getInputTypes();
    }

    public abstract Set<? super Scope> getScopes();

    public Set<? super Scope> getReferencedScopes() {
        return ImmutableSet.of();
    }

    public Collection<File> getSecondaryFileInputs() {
        return ImmutableList.of();
    }

    public Collection<SecondaryFile> getSecondaryFiles() {
        return ImmutableList.of();
    }

    public Collection<File> getSecondaryFileOutputs() {
        return ImmutableList.of();
    }

    public Collection<File> getSecondaryDirectoryOutputs() {
        return ImmutableList.of();
    }

    public Map<String, Object> getParameterInputs() {
        return ImmutableMap.of();
    }

    public abstract boolean isIncremental();

    public void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental) throws IOException, TransformException, InterruptedException {
    }

    public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        // Just delegate to old method, for code that uses the old API.
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

    public boolean isCacheable() {
        return false;
    }
}

Transform实际就是一个抽象类,提供了一些抽象方法,仔细看会发现很多方法定义的和Task中的方法定义的很类似。
接下来我们下TransformManager.java里面的addTransform()方法:

public <T extends Transform> Optional<AndroidTask<TransformTask>> addTransform(
            @NonNull TaskFactory taskFactory,
            @NonNull TransformVariantScope scope,
            @NonNull T transform,
            @Nullable TransformTask.ConfigActionCallback<T> callback) {
        
        ...

        transforms.add(transform);

        // create the task...
        AndroidTask<TransformTask> task =
                taskRegistry.create(
                        taskFactory,
                        new TransformTask.ConfigAction<>(
                                scope.getFullVariantName(),
                                taskName,
                                transform,
                                inputStreams,
                                referencedStreams,
                                outputStream,
                                recorder,
                                callback));

        return Optional.ofNullable(task);
    }

addTransform 方法在执行过程中,会将 Transform 包装成一个 AndroidTask 对象,所以transfrom最终会被转换成一个task。了解了transform,我们接下来继续分析这几个task。

transformClassesWithDexBuilderForDebug

输入命令:./gradlew transformClassesWithDexBuilderForDebug

  • inputs&outputs
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/appcompat-v7-26.1.0.aar/6b443e96f1af9aa241aaa70576c67a57/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.3.aar/f44da5c361a1f52801511229596f72e7/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/butterknife-8.5.1.aar/9d5de52440cb778daab09db33955642f/jars/classes.jar
...
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/classes/debug/android/support/constraint/R$id.class
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/classes/debug/android/support/constraint/R$styleable.class
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/classes/debug/android/support/constraint/R$attr.class
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/classes/debug/android/arch/lifecycle/R.class
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/classes/debug/butterknife/R.class
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug

输入文件比较多,中间省略了一部分,但是还是比较明显的看出输入文件分为两种类型:

1、依赖库的jar文件;

2、intermediates/classes/debug/目下的class文件(即本项目产生的class文件)。

输出文件:

1、编号0-16的jar包;

2、本项目的class文件生成的dex文件。

  • 源码

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/DexArchiveBuilderTransform.java

  • 主要代码逻辑

DexArchiveBuilderTransform.java类中的transform()方法:

public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, IOException, InterruptedException {
        
        ...

        try {
            // 1、遍历输入
            for (TransformInput input : transformInvocation.getInputs()) {
                // 2、输入的类型是Directory,调用convertToDexArchive()方法。
                for (DirectoryInput dirInput : input.getDirectoryInputs()) {
                    logger.verbose("Dir input %s", dirInput.getFile().toString());
                    convertToDexArchive(
                            transformInvocation.getContext(),
                            dirInput,
                            outputProvider,
                            transformInvocation.isIncremental());
                }
                // 3、输入类型是Jar,则调用processJarInput方法。
                for (JarInput jarInput : input.getJarInputs()) {
                    logger.verbose("Jar input %s", jarInput.getFile().toString());
                    List<File> dexArchives =
                            processJarInput(
                                    transformInvocation.getContext(),
                                    transformInvocation.isIncremental(),
                                    jarInput,
                                    outputProvider);
                    cacheableItems.putAll(jarInput, dexArchives);
                }
            }
        ...
    }

通过代码分析,知道transform是遍历所有的输入文件,分为两种类型来处理:

1、输入的类型是Directory,调用convertToDexArchive()方法,接着调用了launchProcessing()方法,最终调用DxDexArchiveBuilder的dex()方法,代码如下:

    // 1、通过方法应该就能知道,这个方法就是将class转变成dex的。
    public void dex(String relativePath, ByteArray classBytes, DexArchive output)
            throws IOException {

        // Copied from dx, from com.android.dx.command.dexer.Main
        DirectClassFile cf = new DirectClassFile(classBytes, relativePath, true);
        cf.setAttributeFactory(StdAttributeFactory.THE_ONE);
        cf.getMagic(); // triggers the actual parsing

        // 2、DexFile obj
        // starts the actual translation and writes the content to the dex file
        // specified
        DexFile dexFile = new DexFile(config.getDexOptions());

        // Copied from dx, from com.android.dx.command.dexer.Main
        ClassDefItem classDefItem =
                CfTranslator.translate(
                        config.getDxContext(),
                        cf,
                        null,
                        config.getCfOptions(),
                        config.getDexOptions(),
                        dexFile);
        dexFile.add(classDefItem);

        if (outStorage != null) {
            ByteArrayAnnotatedOutput byteArrayAnnotatedOutput = dexFile.writeTo(outStorage);
            output.addFile(
                    ClassFileEntry.withDexExtension(relativePath),
                    byteArrayAnnotatedOutput.getArray(),
                    0,
                    byteArrayAnnotatedOutput.getCursor());
        } else {
            // 3、dexFile to dex
            byte[] bytes = dexFile.toDex(null, false);
            output.addFile(ClassFileEntry.withDexExtension(relativePath), bytes, 0, bytes.length);
        }
    }

通过方法的注视,可以看出此方法就是将输入文件生成dex文件。所以如果输入类型为目录的话,transform()方法会将该目录下的所有class文件转成dex文件。

现在再次回到transform()方法中来,另一个分支输入类型是Jar,则调用processJarInput方法,该方法代码如下:

    private List<File> processJarInput(
            @NonNull Context context,
            boolean isIncremental,
            @NonNull JarInput jarInput,
            TransformOutputProvider transformOutputProvider)
            throws Exception {
        // 1、非增量编译
        if (!isIncremental) {
            ...
            // 2、调用convertJarToDexArchive方法
            return convertJarToDexArchive(context, jarInput, transformOutputProvider);
        } else if (jarInput.getStatus() != Status.NOTCHANGED) {
            // 3、增量编译处理逻辑
            ...
        }
        return ImmutableList.of();
    }

通过注视1、2可知,在非增量编译的情况下,会调用convertJarToDexArchive()方法。(增量编译的逻辑再分析完所有的任务后,会单独写文章来分析gradle tool 3+版本如何实现增量编译的。)继续分析convertJarToDexArchive()方法,代码入下:

    private List<File> convertJarToDexArchive(
            @NonNull Context context,
            @NonNull JarInput toConvert,
            @NonNull TransformOutputProvider transformOutputProvider)
            throws Exception {
        // 1、获取JarInput的缓存版本。
        File cachedVersion = cacheHandler.getCachedVersionIfPresent(toConvert);
        // 2、如果不存在缓存版本,则调用convertToDexArchive方法,将其转变成Dex文件,convertToDexArchive方法前面已经分析。
        if (cachedVersion == null) {
            return convertToDexArchive(context, toConvert, transformOutputProvider, false);
        } else {
            // 3、如果存在缓存版本,则直接copy缓存文件。
            File outputFile = getPreDexJar(transformOutputProvider, toConvert, null);
            Files.copy(
                    cachedVersion.toPath(),
                    outputFile.toPath(),
                    StandardCopyOption.REPLACE_EXISTING);
            // no need to try to cache an already cached version.
            return ImmutableList.of();
        }
    }

通过注视1、2、3可以知道,此方法有两种方式得到Dex文件,

1、在没有缓存版本的时候,则调用convertToDexArchive方法,将其生成Dex文件。(实际平时在执行的时候,基本都是复用的缓存,如果有兴趣的朋友,可以先执行./gradlew cleanBuildCache命令,清除build cache,看看执行输出,会发现此任务的task输出是不一样的)

2、在有缓存版本的时候,直接复用缓存版本。

现在我们的transform分析完了,但是大家有没有一个疑问,通过我们的分析实际上不管输入文件类型是Dir还是Jar,最终都是转变成了Dex文件,但是我们发现输入文件是Jar类型时,最后的输出是.jar结尾的Jar文件。大家可以把生成的jar文件后缀改成zip,然后解压看下,实际上里面的文件都是dex文件,这个jar只是dex文件的一个压缩集合。

所以通过以上分析可以很清楚的知道transformClassesWithDexBuilderForDebug任务就是将项目依赖的Jar包,以及项目本身的Class文件全部transform为Dex文件。

transformDexArchiveWithExternalLibsDexMergerForDebug

执行命令:./gradlew transformDexArchiveWithExternalLibsDexMergerForDebug

  • inputs&outputs
...
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/android/support/graphics/drawable/animated/R$attr.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/android/arch/lifecycle/R.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/butterknife/R.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/5.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/7.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/6.jar
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/externalLibsDexMerger/debug

输入文件进行了部分删减,此任务的输入即transformClassesWithDexBuilderForDebug任务的输出。

输出文件是一个classes.dex文件,以及相应内容的json文件,json文件内容如下:

[{
    "name": "main",
    "index": 0,
    "scopes": ["EXTERNAL_LIBRARIES"],
    "types": ["DEX_ARCHIVE"],
    "format": "DIRECTORY",
    "present": true
}]

json文件的scopes的value是EXTERNAL_LIBRARIES,所以大胆猜测,这个classes.dex是所有依赖的jar的dex集合。

  • 源码

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/ExternalLibsMergerTransform.kt

  • 主要代码逻辑

ExternalLibsMergerTransform类是用kotlin实现,代码如下:

    override fun transform(transformInvocation: TransformInvocation) {

        // we need to re-merge all jars except the removed ones.
        val jarInputList = flattenInputs
                .filter { it.status != Status.REMOVED }
                .map {it.file.toPath()}
                .toList()
        ...

        outputHandler.createOutput().use { processOutputHandler ->
            val callable = callableFactory.create(dexingType,
                    processOutputHandler,
                    outputDir,
                    jarInputList,
                    null,
                    forkJoinPool,
                    dexMergerTool,
                    minSdkVersion,
                    isDebuggable)
            // since we are merging into a single DEX_ARCHIVE (possibly containing 1 to many DEX
            // merged DEX files, no need to use a separate thread.
            callable.call()
        }
    }

代码很简单,jarInputList是dex jar的集合,最终调用了callable.call()方法,该方法代码如下:

    public Void call() throws Exception {
        DexArchiveMerger merger;
        switch (dexMerger) {
            case DX:
                DxContext dxContext =
                        new DxContext(
                                processOutput.getStandardOutput(), processOutput.getErrorOutput());
                merger = DexArchiveMerger.createDxDexMerger(dxContext, forkJoinPool);
                break;
            ...
        }

        merger.mergeDexArchives(dexArchives, dexOutputDir.toPath(), mainDexList, dexingType);
        return null;
    }

call方法中DexArchiveMerger 对象,然后调用mergeDexArchives()方法。所以我们前面的猜测是完全正确的,此task就是将前一个task生成的依赖库的dex jar 执行merge操作,生成一个classes.dex文件。

transformDexArchiveWithDexMergerForDebug

命令输入:./gradlew transformDexArchiveWithDexMergerForDebug

  • inputs&outputs
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/8.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/9.jar
...
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/android/support/constraint/R$styleable.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/android/arch/lifecycle/R.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/17/butterknife/R.dex
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/5.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/7.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexBuilder/debug/6.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/externalLibsDexMerger/debug/0/classes.dex
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexMerger/debug

输入文件进行了部分删减,此任务的输入是前面两个任务的输出。

输出文件是一个classes.dex文件,以及相应内容的json文件,json文件内容如下:

[{
    "name": "main",
    "index": 0,
    "scopes": ["PROJECT", "SUB_PROJECTS", "EXTERNAL_LIBRARIES"],
    "types": ["DEX"],
    "format": "DIRECTORY",
    "present": true
}]

json文件的scopes的value是PROJECTSUB_PROJECTSEXTERNAL_LIBRARIES,所以大胆猜测,这个classes.dex是本项目、子模块和依赖的库的dex集合(也就是所有的dex集合)。

  • 源码

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/DexMergerTransform.java

  • 主要代码逻辑
    public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, IOException, InterruptedException {
        ...
        mergeTasks = handleLegacyAndMonoDex(
                        transformInvocation.getInputs(), output, outputProvider);

            // now wait for all merge tasks completion
            mergeTasks.forEach(ForkJoinTask::join);
        ...
    }

transform()方法里面调用了handleLegacyAndMonoDex(),该方法又调用了submitForMerging(),该方法代码如下:

    private ForkJoinTask<Void> submitForMerging(
            @NonNull ProcessOutput output,
            @NonNull File dexOutputDir,
            @NonNull Iterable<Path> dexArchives,
            @Nullable Path mainDexList) {
        DexMergerTransformCallable callable =
                new DexMergerTransformCallable(
                        dexingType,
                        output,
                        dexOutputDir,
                        dexArchives,
                        mainDexList,
                        forkJoinPool,
                        dexMerger,
                        minSdkVersion,
                        isDebuggable);
        return forkJoinPool.submit(callable);
    }

submitForMerging()方法又调用了DexMergerTransformCallable 这个类,又回到了上个task所介绍的。

所以这个task正如我们前面分析的猜测的一样,将本项目、子模块和依赖的库的dex merge到一个classes.dex中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容