AGP资源编译过程分析一compile

本篇章里分析的AGP源码都是基于3.4.2版本的,很老的版本,也没办法,因为公司里用的就是3.4.2. 。。

简介

在AGP里面,aapt(Android Asset Packaging Tool)担任着资源编译的角色,aapt前后经历了两个版本,aapt以及aapt2,aapt2在AGP3.0之后就已经是默认的资源编译工具了,因此我们现在接触到的都是aapt2。

  • aapt 1.0
    aapt 1.0的年代,资源编译并不支持增量,这意味着修改一个资源文件,项目里所有资源都得被重新编译打包。随着项目变得庞大,资源越来越多的同时,编译速度也会变得越来越慢了。

  • aapt 2.0
    因为aapt 1.0版本存在着明显的缺陷,对此谷歌对它进行了升级改造,主要是把资源的编译拆解为两个阶段:编译阶段链接阶段。编译阶段会把资源文件编译成文件后缀为.arsc.flat的二进制文件,而链接阶段会把编译好的.arsc.flat文件链接成资源包(在AGP里就是resources-debug.ap_),当有资源被修改了,只需要重新编译修改资源,然后重新链接就可以了。编译时会检查语法,链接时会检查符号,过程是跟c++类似的。

因为aapt2把资源编译过程分解成两个步骤,这也使得资源的增量编译变得可行了,关于aapt2的更详细介绍大家可以看官方给这篇文档AAPT2。下面我将用两篇博客来浅析下AGP资源的编译跟链接过程,大神过路莫笑。

资源compile

之前我的文章介绍过java的编译是由compileDebugJavaXXX提供的,kotlin的编译是由compileDebugKotlin提供的,在惯性思维作用下,期初我在研究资源编译时,也是在找compileDebugResources之类的任务,找半天没找着,最后才发现,资源编译居然是在mergeResource 任务里完成的,以前以为它只是检查资源合并资源作用,没想到合并完资源后顺便的也把编译资源也给做了(library模块不会编译资源,而是copy resource)

在AGP里,资源的编译过程大概可以分解为以下三部分

  • 读取依赖并且解析出resources
  • 进行资源的merge并且保存本地
  • 编译所有资源文件

下面我们来一一的分析每个部分细节。

读取解析resources资源

MergeResources的任务入口函数有doFullTaskActiondoIncrementalTaskAction,顾名思义的,一个是全量编译入口,一个是增量编译入口,但仔细看增量编译方法,其内部的实现跟全量编译方法实现是差不多,里面只是做了些是否支持增量编译之类的检测工作。这里我们只分析全量编译的入口函数。

protected void doFullTaskAction() throws IOException, JAXBException {
    //省略掉部分代码。。。
    //1. 获取所有依赖资源
    List resourceSets = getConfiguredResourceSets(preprocessor);

    //2. 创建资源合并工作类ResourceMerger
    ResourceMerger merger = new ResourceMerger(minSdk.get());

    //3.创建资源编译器,对于library其实仅仅是个文件拷贝器
    try (ResourceCompilationService resourceCompiler =
            getResourceProcessor(
                    getBuilder(),
                    aapt2FromMaven,
                    workerExecutorFacade,
                    flags,
                    processResources)) {

        //4读取本地资源并且添加到ResourceMerger准备进行资源合并.
        for (ResourceSet resourceSet : resourceSets) {
            resourceSet.loadFromFiles(getILogger());
            merger.addDataSet(resourceSet);
        }
    }
    //省略掉部分代码。。。
}

先是从configuration拿到依赖module资源,接着是创建出ResourceMerger用作来做资源合并,getResourceProcessor方法会返回真正编译资源的类对象,它的实现如下:

private static ResourceCompilationService getResourceProcessor(
        @NonNull AndroidBuilder builder,
        @Nullable FileCollection aapt2FromMaven,
        @NonNull WorkerExecutorFacade workerExecutor,
        ImmutableSet flags,
        boolean processResources) {
    
    //对于library模块返回的是个文件拷贝器
    if (!processResources) {
        return CopyToOutputDirectoryResourceCompilationService.INSTANCE;
    }

    Aapt2ServiceKey aapt2ServiceKey =
            Aapt2DaemonManagerService.registerAaptService(
                    aapt2FromMaven, builder.getBuildToolInfo(), builder.getLogger());

    //对于application模块返回的是WorkerExecutorResourceCompilationService对象
    return new WorkerExecutorResourceCompilationService(workerExecutor, aapt2ServiceKey);
}

getResourceProcessor对于不同模块会返回不同的对象,对于library模块来说返回的是个文件拷贝器,作用是把merge后的资源拷贝到application模块下,由application来编译。对于application模块返回的是WorkerExecutorResourceCompilationService对象,作用是资源合并完马上进行编译,这个对象是如何进行资源编译的我们先放一放,先接着往下分析。

创建出资源编译类对象后,开始把所有依赖资源文件解析加载到内存中,ResourceSet继承了DataSetloadFromFiles由后者提供,代码如下:

public void loadFromFiles(ILogger logger) throws MergingException {
    List errors = new ArrayList<>();
    for (File file : mSourceFiles) {
        if (file.isDirectory()) {
            try {
                readSourceFolder(file, logger);
            } catch (MergingException e) {
                errors.addAll(e.getMessages());
            }
        } else if (file.isFile()) {
            // TODO support resource bundle
            loadFile(file, file, logger);
        }
    }
}

这段代码很简单,就是遍历所有资源文件读取文件,我们先分析从目录读取资源的逻辑代码readSourceFolder

protected void readSourceFolder(File sourceFolder, ILogger logger)
throws MergingException {
File[] folders = sourceFolder.listFiles();
if (folders != null) {
for (File folder : folders) {
    if (folder.isDirectory() && !isIgnored(folder)) {
        //1.先返回资源类型.
        FolderData folderData = getFolderData(folder);
        if (folderData != null) {
            try {
                //2.开始解析目录下面的资源
                parseFolder(sourceFolder, folder, folderData, logger);
            } catch (MergingException e) {
                errors.addAll(e.getMessages());
            }
        }
    }
}

先是解析出资源的类型,譬如是drawable资源还是layout资源colors资源等等这些,解析的过程也十分的简单粗暴,就是根据目录名称来判断的,如目录为values,就认定为是values类型,代码这里就不贴了,效果如下:

解析完资源类型后,接着parseFolder方法便开始解析目录下的所有资源文件,首先也是会遍历目录下的所有资源文件,然后根据前面解析出来的不同的资源类型会有不同的处理逻辑。

  • 针对于layout drawable资源,由于这类资源通常情况下一个xml文件仅描述一种资源,所以简单的返回一个ResourceFile对象就可以,这个ResourceFile对象就是当前资源文件的描述。

  • 对于values目录下面的资源,譬如strings.xml colors.xml,由于一个xml文件里面可以存在着多条的资源记录,为了把所有资源item给读取出来,会创建出ValueResourceParser2来解析xml内容,后者会为每一条资源item创建出与之对应的ResourceMergerItem对象用作来描述资源,最终也是返回ResourceFile对象用来描述此xml资源文件,代码如下:

private void parseFolder(File sourceFolder, File folder, FolderData folderData, ILogger logger)
        throws MergingException {
    File[] files = folder.listFiles();
    if (files != null && files.length > 0) {
        for (File file : files) {
            if (!file.isFile() || isIgnored(file)) {
                continue;
            }

            ResourceFile resourceFile = createResourceFile(file, folderData, logger);
            processNewResourceFile(sourceFolder, resourceFile);
        }
    }
}

private ResourceFile createResourceFile(@NonNull File file,
        @NonNull FolderData folderData, @NonNull ILogger logger) throws MergingException {

    if (folderData.type != null) {
        //对于layout drawable这类资源由于一个xml通常仅描述着一个单独资源.
        //因此这里直接返回ResourceFile对象 ResourceMergerItem就是当前资源.
        return new ResourceFile(
                file,
                new ResourceMergerItem(
                        getNameForFile(file),
                        mNamespace,
                        folderData.type,
                        null,
                        mLibraryName),
                folderData.folderConfiguration);
    } else {
        try {
        //对于values目录下的资源 譬如string.xml, colors.xml这类资源由于一个xml
        //里面会有多条资源item, ValueResourceParser2会把所有资源项给解析出来
        //因此这里返回的ResourceFile对象是对应的xml资源文件 而ResourceMergerItem
        //是里面的所有资源项的描述
            ValueResourceParser2 parser =
                    new ValueResourceParser2(file, mNamespace, mLibraryName);
            parser.setTrackSourcePositions(mTrackSourcePositions);
            parser.setCheckDuplicates(mCheckDuplicates);
            List items = parser.parseFile();

            return new ResourceFile(file, items, folderData.folderConfiguration);
        } catch (MergingException e) {
            logger.error(e, "Failed to parse %s", file.getAbsolutePath());
            throw e;
        }
    }
}

ValueResourceParser2会解析xml文件格式,把xml文件里定义的资源内容读取出来,并且里面会检查资源是否有重复。实际上就是一些xml的读取逻辑,这里就不再仔细的分析下去了,有兴趣的读者可以自行分析。

解析出来的每一条资源数据会用ResourceMergerItem对象来保存,里面会记录了资源的一些信息,譬如有以下资源文件



    #ff0092

那么ValueResourceParser2会给这条资源信息生成一个与之对应的ResourceMergerItem对象,这个对象会把资源类型、资源名称、资源内容等等信息保存下来,给下面的资源merge过程使用

在解析完所有资源文件后,最后会调用processNewResourceFile方法把解析出来的结果存到map里面,代码如下:

private void processNewResourceFile(File sourceFolder, ResourceFile resourceFile)
        throws MergingException {
    if (resourceFile != null) {
        if (resourceFile.getType() == DataFile.FileType.GENERATED_FILES
                && mGeneratedSet != null) {
            mGeneratedSet.processNewDataFile(sourceFolder, resourceFile, true);
        } else {
            processNewDataFile(sourceFolder, resourceFile, true /*setTouched*/);
        }
    }
}

protected void processNewDataFile(@NonNull File sourceFolder,
                                    @NonNull F dataFile,
                                    boolean setTouched) throws MergingException {
    //这里的item其实就是前面解析出来的ResourceMergerItem对象
    Collection dataItems = dataFile.getItems();
    addDataFile(sourceFolder, dataFile);
    for (I dataItem : dataItems) {
        //以map结构保存解析出来的内容
        mItems.put(dataItem.getKey(), dataItem);
        if (setTouched) {
            dataItem.setTouched();
        }
    }
}

代码也比较的简单,遍历解析出来的所有资源并且存储到map里面,map的key通过调用ResourceMergerItem::getKey方法获取,map的value就是ResourceMergerItem对象,这里我们需要关心下key的生成规则,后面merge的时候需要用到。

public String getKey() {
    String qualifiers = getQualifiers();

    //省略部分代码。。。
    if (!qualifiers.isEmpty()) {
        return typeName + "-" + qualifiers + "/" + getName();
    }
    return typeName + "/" + getName();
}

如果qualifiers是空的话,key就是类型/资源名 这样的格式,譬如上面我们提到的test_white_color资源,它的类型是color,那么与之对应的key便是color/test_white_color了。

merge资源并保存到本地

回到MergeResources::doFullTaskAction继续往下看,接着是创建出MergedResourceWriter对象,并调用了ResourceMerger的mergeData方法,传入了前面创建出来的MergedResourceWriter对象。mergeData方法比较复杂,我们拆解出来分析。

public void mergeData(@NonNull MergeConsumer consumer, boolean doCleanUp)
        throws MergingException {
    consumer.start(mFactory);

    try {
        // get all the items keys.
        Set dataItemKeys = new HashSet<>();
        //1. 遍历所有资源把key存起来。
        for (S dataSet : mDataSets) {
            // quick check on duplicates in the resource set.
            dataSet.checkItems();
            ListMultimap map = dataSet.getDataMap();
            dataItemKeys.addAll(map.keySet());
        }

        // loop on all the data items.
        for (String dataItemKey : dataItemKeys) {
            //2. 如果是styleable资源的话进行styleable的merge.
            if (requiresMerge(dataItemKey)) {
                // get all the available items, from the lower priority, to the higher
                // priority
                List items = new ArrayList<>(mDataSets.size());
                for (S dataSet : mDataSets) {

                    // look for the resource key in the set
                    ListMultimap itemMap = dataSet.getDataMap();

                    if (itemMap.containsKey(dataItemKey)) {
                        List setItems = itemMap.get(dataItemKey);
                        items.addAll(setItems);
                    }
                }

                mergeItems(dataItemKey, items, consumer);
                continue;
            }
        }
    }
    //省略部分代码...
}

这里的dataSet其实就是前面add进去的ResourceSet对象,首先是调用getDataMap获取ResourceSet里面的所有资源,返回的map结构前面我们已经介绍过了,它的key是:类型/资源名,这种结构,value是ResourceMergerItem对象。先是把所有资源的key值存储起来,接着判断资源是否是styleable资源,如果是的话会通过mergeItems方法来mergestyleable资源,这里我们跳过直接往下分析。mergeData后半部分的代码逻辑比较复杂,我剔除了大部分的逻辑整理出以下代码:

public void mergeData(@NonNull MergeConsumer consumer, boolean doCleanUp)
        throws MergingException {
    consumer.start(mFactory);

    try {
        // loop on all the data items.
        for (String dataItemKey : dataItemKeys) {
            //省略部分代码。。。
            setLoop: for (int i = mDataSets.size() - 1 ; i >= 0 ; i--) {
                S dataSet = mDataSets.get(i);
                // look for the resource key in the set
                ListMultimap itemMap = dataSet.getDataMap();

                if (!itemMap.containsKey(dataItemKey)) {
                    continue;
                }
                List items = itemMap.get(dataItemKey);
                if (items.isEmpty()) {
                    continue;
                }

                for (int ii = items.size() - 1 ; ii >= 0 ; ii--) {
                    I item = items.get(ii);
                    //省略部分代码。。。
                    if (toWrite == null && !item.isRemoved()) {
                        toWrite = item;
                    }

                    if (toWrite != null && previouslyWritten != null) {
                        break setLoop;
                    }
                }
            }
            //省略部分代码。。。。
            if (toWrite == null) {
                // nothing to write? delete only then.
                assert previouslyWritten.isRemoved();

                consumer.removeItem(previouslyWritten, null /*replacedBy*/);

            } else if (previouslyWritten == null || previouslyWritten == toWrite) {
                // easy one: new or updated res
                consumer.addItem(toWrite);
            }
        }
    } finally {
        consumer.end();
    }
}

收集完所有key之后,下面就是遍历所有ResourceSet对象,找到对应的资源并且调addItem方法把资源回调给consumer,consumer其实就是前面创建的MergedResourceWriter对象,我们跟进去看下它的addItem方法,代码如下:

public void addItem(@NonNull final ResourceMergerItem item) throws ConsumerException {
    final ResourceFile.FileType type = item.getSourceType();

    if (type == ResourceFile.FileType.XML_VALUES) {
        //省略部分代码。。。
        mValuesResMap.put(item.getQualifiers(), item);
    } else {
        //省略部分代码。。。
        if (item.isTouched()) {
            mCompileResourceRequests.add(
                    new CompileResourceRequest(file, getRootFolder(), folderName));
        }
    }
}

如果是values目录下面的xml资源会先保存到map里,map的key是getQualifiers方法返回,它对应的值有好几种类型,譬如version类型,如果我们的新建的资源目录时带-v23这种版本号的,那么getQualifiers返回的就是对应的版本号值,又譬如对于string资源,涉及到多语言的话getQualifiers返回的就是对应的语言类型,如en zh zh-rCN 等等。
如果是非values目录下的资源,如layout资源,drawable等等这些会构造出CompileResourceRequest对象把需要编译的资源信息保存起来,对于全量编译来讲,item.isTouched方法恒返回true,这个是在前面介绍的解析资源那part的ResourceSet::processNewResourceFile方法里面把isTouched强行设置为true了。

前面的mergeData处理完资源后,接着就开始把该合并的资源合并后写到本地,然后进行资源的编译。

mergeData处理完资源后接着会调用MergedResourceWriter对象的end方法,这个方法做了两件事情,第一,先把前面合并的资源保存到本地,第二,调用资源编译API编译资源文件。我们先来看第一部分

首先是调用了父类的end方法,后者会调用postWriteAction抽象方法,并且等它执行结束,MergedResourceWriter实现了这个抽象方法

@Override
protected void postWriteAction() throws ConsumerException {

    //本地保存目录,合并后的资源将会保存到这个目录下
    ///Users/nls/Desktop/job/abooster/installer/build/intermediates/incremental/mergeDebugResources/merged.dir
    File tmpDir = new File(mTemporaryDirectory, "merged.dir");
    try {
        FileUtils.cleanOutputDir(tmpDir);
    } catch (IOException e) {
        throw new ConsumerException(e);
    }

    // now write the values files.
    for (String key : mValuesResMap.keySet()) {
            String folderName = key.isEmpty() ?
                    ResourceFolderType.VALUES.getName() :
                    ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key;

            //创建出合并资源文件
            File valuesFolder = new File(tmpDir, folderName);
            File outFile = new File(valuesFolder, folderName + DOT_XML);

            FileUtils.mkdirs(valuesFolder);

            DocumentBuilder builder = mFactory.newDocumentBuilder();
            Document document = builder.newDocument();
            final String publicTag = ResourceType.PUBLIC.getName();
            List publicNodes = null;

            Node rootNode = document.createElement(TAG_RESOURCES);
            document.appendChild(rootNode);

            Collections.sort(items);

            for (ResourceMergerItem item : items) {
                Node nodeValue = item.getValue();
                if (nodeValue != null && publicTag.equals(nodeValue.getNodeName())) {
                    if (publicNodes == null) {
                        publicNodes = Lists.newArrayList();
                    }
                    publicNodes.add(nodeValue);
                    continue;
                }

                // add a carriage return so that the nodes are not all on the same line.
                // also add an indent of 4 spaces.
                rootNode.appendChild(document.createTextNode("\n    "));

                ResourceFile source = item.getSourceFile();

                Node adoptedNode = NodeUtils.adoptNode(document, nodeValue);
                if (source != null) {
                    XmlUtils.attachSourceFile(
                            adoptedNode, new SourceFile(source.getFile()));
                }
                rootNode.appendChild(adoptedNode);
            }
            //省略部分代码。。。。
            CompileResourceRequest request =
                    new CompileResourceRequest(
                            outFile,
                            getRootFolder(),
                            folderName,
                            pseudoLocalesEnabled,
                            crunchPng,
                            blame != null ? blame : ImmutableMap.of());

            //编译合并后的资源文件
            mResourceCompiler.submitCompile(request);
            //后面的代码全被我省略了。。
    }
}

postWriteAction方法也是比较复杂的,让了方便读者理解,我已经把核心代码逻辑抽出来,不相关的其他代码逻辑先剔除掉了。

首先是创建出merged.dir目录,合并后的资源会被保存到这里,接着是创建出资源保存的目录跟保存资源文件名。目录名就是values-qualifier这样的格式,譬如values-v23 values-zh-CN等等这些,文件名跟目录名格式一样,只是会加上.xml文件后缀。

把目录跟文件创建出来后接着是遍历所有ResourceMergerItem对象,把它写进values.xml里面,这个资源文件就是合并所有资源后的资源文件,最后就是编译这个资源文件。

在前面我们绍过,如果是非values资源的话,会构建出CompileResourceRequest对象用来保存要编译的资源信息,回到end方法,在保存并且编译完合并资源后就开始编译其他资源,代码如下:

@Override
public void end() throws ConsumerException {
    // Make sure all PNGs are generated first.
    super.end();
    //省略部分代码。。。
    while (!mCompileResourceRequests.isEmpty()) {
        CompileResourceRequest request = mCompileResourceRequests.poll();
        //省略部分代码。。。
            if (notCompiledOutputDirectory != null) {
                File typeDir =
                        new File(
                                notCompiledOutputDirectory,
                                request.getInputDirectoryName());
                FileUtils.mkdirs(typeDir);
                FileUtils.copyFileToDirectory(fileToCompile, typeDir);
            }
            //开始编译资源
            mResourceCompiler.submitCompile(
                    new CompileResourceRequest(
                            fileToCompile,
                            request.getOutputDirectory(),
                            request.getInputDirectoryName(),
                            pseudoLocalesEnabled,
                            crunchPng,
                            ImmutableMap.of(),
                            request.getInputFile()));

        } 
    //省略部分代码。。。
}

同样的为了方便读者理解,大部分不相关的代码逻辑已被我剔除了。整理后的代码逻辑已经是相当的清晰了,就是遍历队列,把所有等待编译的资源文件统统提交到mResourceCompiler去进行编译。

资源编译过程

关于mResourceCompiler对象,前面我们已经是介绍过了,对于library模块来说返回的是个文件拷贝器,它会把merge后的资源文件拷贝到application模块去进行编译,因为过程比较简单,这里我们就不再做太多的分析了,对于application来说就是mResourceCompiler其实就是个WorkerExecutorResourceCompilationService对象,它的代码如下:

override fun submitCompile(request: CompileResourceRequest) {
    // b/73804575
    requests.add(request)
}
override fun close() {
    //省略部分代码。。
    // b/73804575
    workerExecutor.submit(Aapt2CompileWithBlameRunnable::class.java,
        Aapt2CompileWithBlameRunnable.Params(aapt2ServiceKey, bucketRequests))
    //省略部分代码。。。
}

WorkerExecutorResourceCompilationService的代码显然比它的名字简单得多了,submitCompile也只是简单的保存下编译请求,close的时候会把这些编译请求任务提交给线程池去做。

Aapt2CompileWithBlameRunnable代码如下:

class Aapt2CompileWithBlameRunnable @Inject constructor(
private val params: Params
) : Runnable {

override fun run() {
    val logger = LoggerWrapper(Logging.getLogger(this::class.java))
    useAaptDaemon(params.aapt2ServiceKey) { daemon ->
        params.requests.forEach { request ->
            try {
                daemon.compile(request, logger)
            } catch (e: Aapt2Exception) {
                throw rewriteCompileException(e, request)
            }
        }
    }
}
}

代码也是比较简单,useAaptDaemon方法返回了aapt的包装对象,内部会调用getAaptDaemon方法,后者会通过WorkerActionServiceRegistry拿到前面注册进去的RegisteredAaptService对象以及对象里比较重要的成员变量:aapt守护进程管理器Aapt2DaemonManager

Aapt2DaemonManager内部维护了aapt进程池,当调用leaseDaemon方法时,会首先判断池子里有没有空闲aapt进程,有就直接返回,没有的话会重新创建一个新的aapt守护进程,代码如下:

fun leaseDaemon(): LeasedAaptDaemon {
    val daemon =
            pool.find { !it.busy } ?: newAaptDaemon()
    daemon.busy = true
    return LeasedAaptDaemon(daemon, this::returnProcess)
}

private fun newAaptDaemon(): LeasableAaptDaemon {
    val displayId = latestDisplayId++
    val process = daemonFactory.invoke(displayId)
    val daemon = LeasableAaptDaemon(process, timeSource.read())
    if (pool.isEmpty()) {
        listener.firstDaemonStarted(this)
    }
    pool.add(daemon)
    return daemon
}

daemonFactory其实就是在Aapt2DaemonManagerService的registerAaptService方法里面设置进去的回调,代码如下:

serviceRegistry.registerService(key, {
    val manager = Aapt2DaemonManager(logger = logger,
            daemonFactory = { displayId ->
                Aapt2DaemonImpl(
                        displayId = "#$displayId",
                        aaptExecutable = aaptExecutablePath,
                        daemonTimeouts = daemonTimeouts,
                        logger = logger)
            },
            expiryTime = daemonExpiryTimeSeconds,
            expiryTimeUnit = TimeUnit.SECONDS,
            listener = Aapt2DaemonManagerMaintainer())
    RegisteredAaptService(manager)
})

这里可以清楚看到daemonFactory其实就是kt block,因此newAaptDaemon最终实例化的是Aapt2DaemonImpl对象。实例化出来的Aapt2DaemonImpl对象没有直接返回给外面使用,而是通过LeasedAaptDaemon对象又包了一层,LeasedAaptDaemon只是用作来判断一些状态,本质上最终的编译任务还是由Aapt2DaemonImpl来完成的。

回到Aapt2CompileWithBlameRunnable类,

override fun run() {
    //省略部分代码。。。
    //这里的daemon其实就是LeasedAaptDaemon,LeasedAaptDaemon只是做一些
    //状态的检测,真正的资源编译是由Aapt2DaemonImpl来完成
    daemon.compile(request, logger)
}

现在我们已经知道了这里的daemon其实是LeasedAaptDaemon对象,但是LeasedAaptDaemon并不参与资源的编译,最终资源的编译是由Aapt2DaemonImpl来完成的,我们直接看Aapt2DaemonImpl的代码

Aapt2DaemonImpl继承Aapt2Daemon,并且实现了它的doCompile doLink 以及startProcess stopProcess等抽象方法,前者是资源的编译跟链接相关的,后者是进程相关的。

compile方法代码实现如下:

override fun compile(request: CompileResourceRequest, logger: ILogger) {
    checkStarted()
    //省略部分代码。。。
    doCompile(request, logger)
}

先是调用了checkStarted 检查当前的进程状态,需要开启进程的话会调用startProcess方法来创建新的aapt进程

private fun checkStarted() {
    when (state) {
        State.NEW -> {
            logger.verbose("%1\$s: starting", displayName)
            try {
                startProcess()
            } catch (e: TimeoutException) {
                handleError("Daemon startup timed out", e)
            } catch (e: Exception) {
                handleError("Daemon startup failed", e)
            }
            state = State.RUNNING
        }
        State.RUNNING -> {
            // Already ready
        }
        State.SHUTDOWN -> error("$displayName: Cannot restart a shutdown process")
    }
}

Aapt2DaemonImpl重写了startProcess,代码如下:

override fun startProcess() {
    val waitForReady = WaitForReadyOnStdOut(displayName, logger)
    processOutput.delegate = waitForReady
    val processBuilder = ProcessBuilder(aaptCommand)
    process = processBuilder.start()
    writer = try {
         GrabProcessOutput.grabProcessOutput(
               process,
               GrabProcessOutput.Wait.ASYNC,
               processOutput)
         process.outputStream.bufferedWriter(Charsets.UTF_8)
      } 
    //省略部分代码。。。
}

可以看到,其实最终的aapt进程是通过ProcessBuilder来创建,aaptCommand指向了aapt的可执行文件,我的Mac电脑就是/Users/nls/.gradle/caches/transforms-2/files-2.1/2808f45549b8e37bfeb50699ed4844d4/aapt2-3.4.2-5326820-osx/aapt2

进程创建成功后,接着会通过GrabProcessOutput类来设置进程的输入输出句柄,用作来跟进程打交道的,譬如往进程写入命令,从进程里读取执行结果等等。没了解过Java ProcessBuilder的可以先看下这篇文章 Java ProcessBuilder

进程创建完了接着会调用doCompile开始编译资源,代码如下:

override fun doCompile(request: CompileResourceRequest, logger: ILogger) {
    val waitForTask = WaitForTaskCompletion(displayName, logger)
    try {
        processOutput.delegate = waitForTask
        //往appt进程写入编译命令
        Aapt2DaemonUtil.requestCompile(writer, request)
        //省略部分代码。。。
        
        //等待进程执行完毕并且读取编译结果.
        val result = waitForTask.future.get(daemonTimeouts.compile, daemonTimeouts.compileUnit)
        when (result) {
            is WaitForTaskCompletion.Result.Succeeded -> {}
            is WaitForTaskCompletion.Result.Failed -> {
                val args = makeCompileCommand(request).joinToString(" \\\n        ")
                throw Aapt2Exception.create(
                    logger = logger,
                    description = "Android resource compilation failed",
                    output = result.stdErr,
                    processName = displayName,
                    command = "$aaptPath compile $args"
                )
            }
            is WaitForTaskCompletion.Result.InternalAapt2Error -> {
                throw result.failure
            }
        }
    } finally {
        processOutput.delegate = noOutputExpected
    }
}

Aapt2DaemonUtil 的requestCompile方法负责构造aapt的执行命令跟参数,并且把命令行push到aapt进程去,这里的writer便是前面创建的aapt进程写句柄,代码如下:

public static void requestLink(@NonNull Writer writer, @NonNull AaptPackageConfig command)
        throws IOException {
    ImmutableList args;
    try {
        args = AaptV2CommandBuilder.makeLinkCommand(command);
    } catch (AaptException e) {
        throw new IOException("Unable to make AAPT link command.", e);
    }
    request(writer, "l", args);
}

fun makeCompileCommand(request: CompileResourceRequest): ImmutableList {
    val parameters = ImmutableList.Builder()

    if (request.isPseudoLocalize) {
        parameters.add("--pseudo-localize")
    }

    if (!request.isPngCrunching) {
        // Only pass --no-crunch for png files and not for 9-patch files as that breaks them.
        val lowerName = request.inputFile.path.toLowerCase(Locale.US)
        if (lowerName.endsWith(SdkConstants.DOT_PNG)
                && !lowerName.endsWith(SdkConstants.DOT_9PNG)) {
            parameters.add("--no-crunch")
        }
    }

    if (request.partialRFile != null) {
        parameters.add("--output-text-symbols", request.partialRFile!!.absolutePath)
    }

    parameters.add("--legacy")
    //指定编译后的文件输出路径,譬如我的项目下这个路径是:
    ///Users/nls/Desktop/job/abooster/app/build/intermediates/res/merged/debug
    parameters.add("-o", request.outputDirectory.absolutePath)
    //需要进行编译的文件
    parameters.add(request.inputFile.absolutePath)

    return parameters.build()
}

最后request会把构造好的命令push给aapt进程执行,

private static void request(Writer writer, String command, Iterable args)
        throws IOException {
    writer.write(command);
    writer.write('\n');
    for (String s : args) {
        writer.write(s);
        writer.write('\n');
    }
    // Finish the request
    writer.write('\n');
    writer.write('\n');
    writer.flush();
}

我们到build/intermediates/res/merged/debug目录下面就能看到刚才构建好的资源文件了

结语

实际上MergeResources的过程比我们上面分析的复杂多了,只不过我们只管全量编译过程,很多不相关的条件分支代码都被我删掉忽略了。实际上也不是每次都需要通过configuration来获取依赖moduel资源的,也不是每次都要通过创建ValueResourceParser2对象来解析资源,合并过的资源信息会被保存一份build/intermediates/incremental/mergeDebugResources/merger.xml来,下次只需要解析merger.xml 文件。这些都是在增量编译的时候才会有的逻辑,至于增量编译的流程这里就不再分析了。

你可能感兴趣的:(AGP资源编译过程分析一compile)