AGP資源編譯過程分析一compile

本篇章里分析的AGP源碼都是基于3.4.2版本的洼冻,很老的版本崭歧,也沒辦法,因為公司里用的就是3.4.2. 撞牢。率碾。

簡介

在AGP里面,aapt(Android Asset Packaging Tool)擔(dān)任著資源編譯的角色屋彪,aapt前后經(jīng)歷了兩個版本所宰,aapt以及aapt2,aapt2在AGP3.0之后就已經(jīng)是默認的資源編譯工具了畜挥,因此我們現(xiàn)在接觸到的都是aapt2仔粥。

  • aapt 1.0
    aapt 1.0的年代,資源編譯并不支持增量蟹但,這意味著修改一個資源文件躯泰,項目里所有資源都得被重新編譯打包。隨著項目變得龐大华糖,資源越來越多的同時麦向,編譯速度也會變得越來越慢了。

  • aapt 2.0
    因為aapt 1.0版本存在著明顯的缺陷客叉,對此谷歌對它進行了升級改造磕蛇,主要是把資源的編譯拆解為兩個階段:編譯階段景描,鏈接階段。編譯階段會把資源文件編譯成文件后綴為.arsc.flat的二進制文件秀撇,而鏈接階段會把編譯好的.arsc.flat文件鏈接成資源包(在AGP里就是resources-debug.ap_),當(dāng)有資源被修改了向族,只需要重新編譯修改資源呵燕,然后重新鏈接就可以了。編譯時會檢查語法件相,鏈接時會檢查符號再扭,過程是跟c++類似的。

因為aapt2把資源編譯過程分解成兩個步驟夜矗,這也使得資源的增量編譯變得可行了泛范,關(guān)于aapt2的更詳細介紹大家可以看官方給這篇文檔AAPT2。下面我將用兩篇博客來淺析下AGP資源的編譯跟鏈接過程紊撕,大神過路莫笑罢荡。

資源compile

之前我的文章介紹過java的編譯是由compileDebugJavaXXX提供的,kotlin的編譯是由compileDebugKotlin提供的对扶,在慣性思維作用下区赵,期初我在研究資源編譯時,也是在找compileDebugResources之類的任務(wù)浪南,找半天沒找著笼才,最后才發(fā)現(xiàn),資源編譯居然是在mergeResource 任務(wù)里完成的络凿,以前以為它只是檢查資源合并資源作用骡送,沒想到合并完資源后順便的也把編譯資源也給做了(library模塊不會編譯資源,而是copy resource)

在AGP里絮记,資源的編譯過程大概可以分解為以下三部分

  • 讀取依賴并且解析出resources
  • 進行資源的merge并且保存本地
  • 編譯所有資源文件

下面我們來一一的分析每個部分細節(jié)摔踱。

讀取解析resources資源

MergeResources的任務(wù)入口函數(shù)有doFullTaskActiondoIncrementalTaskAction,顧名思義的到千,一個是全量編譯入口昌渤,一個是增量編譯入口,但仔細看增量編譯方法憔四,其內(nèi)部的實現(xiàn)跟全量編譯方法實現(xiàn)是差不多膀息,里面只是做了些是否支持增量編譯之類的檢測工作。這里我們只分析全量編譯的入口函數(shù)了赵。

protected void doFullTaskAction() throws IOException, JAXBException {
    //省略掉部分代碼潜支。。柿汛。
    //1. 獲取所有依賴資源
    List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor);

    //2. 創(chuàng)建資源合并工作類ResourceMerger
    ResourceMerger merger = new ResourceMerger(minSdk.get());

    //3.創(chuàng)建資源編譯器冗酿,對于library其實僅僅是個文件拷貝器
    try (ResourceCompilationService resourceCompiler =
            getResourceProcessor(
                    getBuilder(),
                    aapt2FromMaven,
                    workerExecutorFacade,
                    flags,
                    processResources)) {

        //4讀取本地資源并且添加到ResourceMerger準備進行資源合并.
        for (ResourceSet resourceSet : resourceSets) {
            resourceSet.loadFromFiles(getILogger());
            merger.addDataSet(resourceSet);
        }
    }
    //省略掉部分代碼埠对。。裁替。
}

先是從configuration拿到依賴module資源项玛,接著是創(chuàng)建出ResourceMerger用作來做資源合并,getResourceProcessor方法會返回真正編譯資源的類對象弱判,它的實現(xiàn)如下:

private static ResourceCompilationService getResourceProcessor(
        @NonNull AndroidBuilder builder,
        @Nullable FileCollection aapt2FromMaven,
        @NonNull WorkerExecutorFacade workerExecutor,
        ImmutableSet<Flag> 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對象,作用是資源合并完馬上進行編譯遭商,這個對象是如何進行資源編譯的我們先放一放固灵,先接著往下分析。

創(chuàng)建出資源編譯類對象后劫流,開始把所有依賴資源文件解析加載到內(nèi)存中巫玻,ResourceSet繼承了DataSetloadFromFiles由后者提供困介,代碼如下:

public void loadFromFiles(ILogger logger) throws MergingException {
    List<Message> 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資源等等這些徒扶,解析的過程也十分的簡單粗暴,就是根據(jù)目錄名稱來判斷的根穷,如目錄為values姜骡,就認定為是values類型,代碼這里就不貼了屿良,效果如下:

解析完資源類型后圈澈,接著parseFolder方法便開始解析目錄下的所有資源文件,首先也是會遍歷目錄下的所有資源文件尘惧,然后根據(jù)前面解析出來的不同的資源類型會有不同的處理邏輯康栈。

  • 針對于layout drawable資源,由于這類資源通常情況下一個xml文件僅描述一種資源喷橙,所以簡單的返回一個ResourceFile對象就可以啥么,這個ResourceFile對象就是當(dāng)前資源文件的描述。

  • 對于values目錄下面的資源贰逾,譬如strings.xml colors.xml悬荣,由于一個xml文件里面可以存在著多條的資源記錄,為了把所有資源item給讀取出來疙剑,會創(chuàng)建出ValueResourceParser2來解析xml內(nèi)容氯迂,后者會為每一條資源item創(chuàng)建出與之對應(yīng)的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就是當(dāng)前資源.
        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對象是對應(yīng)的xml資源文件 而ResourceMergerItem
        //是里面的所有資源項的描述
            ValueResourceParser2 parser =
                    new ValueResourceParser2(file, mNamespace, mLibraryName);
            parser.setTrackSourcePositions(mTrackSourcePositions);
            parser.setCheckDuplicates(mCheckDuplicates);
            List<ResourceMergerItem> 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文件里定義的資源內(nèi)容讀取出來禁灼,并且里面會檢查資源是否有重復(fù)。實際上就是一些xml的讀取邏輯轿曙,這里就不再仔細的分析下去了匾二,有興趣的讀者可以自行分析。

解析出來的每一條資源數(shù)據(jù)會用ResourceMergerItem對象來保存拳芙,里面會記錄了資源的一些信息,譬如有以下資源文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="test_white_color">#ff0092</color>
</resources>

那么ValueResourceParser2會給這條資源信息生成一個與之對應(yīng)的ResourceMergerItem對象皮璧,這個對象會把資源類型舟扎、資源名稱、資源內(nèi)容等等信息保存下來悴务,給下面的資源merge過程使用

在解析完所有資源文件后睹限,最后會調(diào)用processNewResourceFile方法把解析出來的結(jié)果存到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<I> dataItems = dataFile.getItems();
    addDataFile(sourceFolder, dataFile);
    for (I dataItem : dataItems) {
        //以map結(jié)構(gòu)保存解析出來的內(nèi)容
        mItems.put(dataItem.getKey(), dataItem);
        if (setTouched) {
            dataItem.setTouched();
        }
    }
}

代碼也比較的簡單讯檐,遍歷解析出來的所有資源并且存儲到map里面羡疗,map的key通過調(diào)用ResourceMergerItem::getKey方法獲取,map的value就是ResourceMergerItem對象别洪,這里我們需要關(guān)心下key的生成規(guī)則叨恨,后面merge的時候需要用到。

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

    //省略部分代碼挖垛。痒钝。。
    if (!qualifiers.isEmpty()) {
        return typeName + "-" + qualifiers + "/" + getName();
    }
    return typeName + "/" + getName();
}

如果qualifiers是空的話痢毒,key就是類型/資源名 這樣的格式送矩,譬如上面我們提到的test_white_color資源,它的類型是color哪替,那么與之對應(yīng)的key便是color/test_white_color了栋荸。

merge資源并保存到本地

回到MergeResources::doFullTaskAction繼續(xù)往下看,接著是創(chuàng)建出MergedResourceWriter對象凭舶,并調(diào)用了ResourceMerger的mergeData方法晌块,傳入了前面創(chuàng)建出來的MergedResourceWriter對象。mergeData方法比較復(fù)雜库快,我們拆解出來分析摸袁。

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

    try {
        // get all the items keys.
        Set<String> dataItemKeys = new HashSet<>();
        //1. 遍歷所有資源把key存起來。
        for (S dataSet : mDataSets) {
            // quick check on duplicates in the resource set.
            dataSet.checkItems();
            ListMultimap<String, I> 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<I> items = new ArrayList<>(mDataSets.size());
                for (S dataSet : mDataSets) {

                    // look for the resource key in the set
                    ListMultimap<String, I> itemMap = dataSet.getDataMap();

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

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

這里的dataSet其實就是前面add進去的ResourceSet對象义屏,首先是調(diào)用getDataMap獲取ResourceSet里面的所有資源靠汁,返回的map結(jié)構(gòu)前面我們已經(jīng)介紹過了蜂大,它的key是:類型/資源名,這種結(jié)構(gòu)蝶怔,value是ResourceMergerItem對象奶浦。先是把所有資源的key值存儲起來,接著判斷資源是否是styleable資源踢星,如果是的話會通過mergeItems方法來mergestyleable資源澳叉,這里我們跳過直接往下分析。mergeData后半部分的代碼邏輯比較復(fù)雜沐悦,我剔除了大部分的邏輯整理出以下代碼:

public void mergeData(@NonNull MergeConsumer<I> 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<String, I> itemMap = dataSet.getDataMap();

                if (!itemMap.containsKey(dataItemKey)) {
                    continue;
                }
                List<I> 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對象本砰,找到對應(yīng)的資源并且調(diào)addItem方法把資源回調(diào)給consumer碴裙,consumer其實就是前面創(chuàng)建的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方法返回,它對應(yīng)的值有好幾種類型珠十,譬如version類型料扰,如果我們的新建的資源目錄時帶-v23這種版本號的,那么getQualifiers返回的就是對應(yīng)的版本號值,又譬如對于string資源,涉及到多語言的話getQualifiers返回的就是對應(yīng)的語言類型计贰,如en zh zh-rCN 等等。
如果是非values目錄下的資源拯钻,如layout資源帖努,drawable等等這些會構(gòu)造出CompileResourceRequest對象把需要編譯的資源信息保存起來,對于全量編譯來講粪般,item.isTouched方法恒返回true拼余,這個是在前面介紹的解析資源那part的ResourceSet::processNewResourceFile方法里面把isTouched強行設(shè)置為true了。

前面的mergeData處理完資源后亩歹,接著就開始把該合并的資源合并后寫到本地匙监,然后進行資源的編譯。

mergeData處理完資源后接著會調(diào)用MergedResourceWriter對象的end方法小作,這個方法做了兩件事情亭姥,第一,先把前面合并的資源保存到本地顾稀,第二致份,調(diào)用資源編譯API編譯資源文件。我們先來看第一部分

首先是調(diào)用了父類的end方法础拨,后者會調(diào)用postWriteAction抽象方法,并且等它執(zhí)行結(jié)束绍载,MergedResourceWriter實現(xiàn)了這個抽象方法

@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;

            //創(chuàng)建出合并資源文件
            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<Node> 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方法也是比較復(fù)雜的,讓了方便讀者理解矫夯,我已經(jīng)把核心代碼邏輯抽出來鸽疾,不相關(guān)的其他代碼邏輯先剔除掉了。

首先是創(chuàng)建出merged.dir目錄训貌,合并后的資源會被保存到這里制肮,接著是創(chuàng)建出資源保存的目錄跟保存資源文件名。目錄名就是values-qualifier這樣的格式递沪,譬如values-v23 values-zh-CN等等這些豺鼻,文件名跟目錄名格式一樣,只是會加上.xml文件后綴款慨。

把目錄跟文件創(chuàng)建出來后接著是遍歷所有ResourceMergerItem對象儒飒,把它寫進values.xml里面,這個資源文件就是合并所有資源后的資源文件檩奠,最后就是編譯這個資源文件桩了。

在前面我們紹過附帽,如果是非values資源的話,會構(gòu)建出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()));

        } 
    //省略部分代碼洁段。应狱。。
}

同樣的為了方便讀者理解祠丝,大部分不相關(guān)的代碼邏輯已被我剔除了疾呻。整理后的代碼邏輯已經(jīng)是相當(dāng)?shù)那逦耍褪潜闅v隊列写半,把所有等待編譯的資源文件統(tǒng)統(tǒng)提交到mResourceCompiler去進行編譯岸蜗。

資源編譯過程

關(guān)于mResourceCompiler對象,前面我們已經(jīng)是介紹過了叠蝇,對于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的時候會把這些編譯請求任務(wù)提交給線程池去做股囊。

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的包裝對象更啄,內(nèi)部會調(diào)用getAaptDaemon方法稚疹,后者會通過WorkerActionServiceRegistry拿到前面注冊進去的RegisteredAaptService對象以及對象里比較重要的成員變量:aapt守護進程管理器Aapt2DaemonManager

Aapt2DaemonManager內(nèi)部維護了aapt進程池,當(dāng)調(diào)用leaseDaemon方法時,會首先判斷池子里有沒有空閑aapt進程内狗,有就直接返回怪嫌,沒有的話會重新創(chuàng)建一個新的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方法里面設(shè)置進去的回調(diào)柳沙,代碼如下:

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只是用作來判斷一些狀態(tài),本質(zhì)上最終的編譯任務(wù)還是由Aapt2DaemonImpl來完成的数初。

回到Aapt2CompileWithBlameRunnable類找爱,

override fun run() {
    //省略部分代碼。泡孩。车摄。
    //這里的daemon其實就是LeasedAaptDaemon,LeasedAaptDaemon只是做一些
    //狀態(tài)的檢測仑鸥,真正的資源編譯是由Aapt2DaemonImpl來完成
    daemon.compile(request, logger)
}

現(xiàn)在我們已經(jīng)知道了這里的daemon其實是LeasedAaptDaemon對象吮播,但是LeasedAaptDaemon并不參與資源的編譯,最終資源的編譯是由Aapt2DaemonImpl來完成的眼俊,我們直接看Aapt2DaemonImpl的代碼

Aapt2DaemonImpl繼承Aapt2Daemon薄料,并且實現(xiàn)了它的doCompile doLink 以及startProcess stopProcess等抽象方法,前者是資源的編譯跟鏈接相關(guān)的泵琳,后者是進程相關(guān)的。

compile方法代碼實現(xiàn)如下:

override fun compile(request: CompileResourceRequest, logger: ILogger) {
    checkStarted()
    //省略部分代碼誊役。获列。。
    doCompile(request, logger)
}

先是調(diào)用了checkStarted 檢查當(dāng)前的進程狀態(tài)蛔垢,需要開啟進程的話會調(diào)用startProcess方法來創(chuàng)建新的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來創(chuàng)建艺玲,aaptCommand指向了aapt的可執(zhí)行文件括蝠,我的Mac電腦就是/Users/nls/.gradle/caches/transforms-2/files-2.1/2808f45549b8e37bfeb50699ed4844d4/aapt2-3.4.2-5326820-osx/aapt2

進程創(chuàng)建成功后,接著會通過GrabProcessOutput類來設(shè)置進程的輸入輸出句柄饭聚,用作來跟進程打交道的忌警,譬如往進程寫入命令,從進程里讀取執(zhí)行結(jié)果等等秒梳。沒了解過Java ProcessBuilder的可以先看下這篇文章 Java ProcessBuilder

進程創(chuàng)建完了接著會調(diào)用doCompile開始編譯資源法绵,代碼如下:

override fun doCompile(request: CompileResourceRequest, logger: ILogger) {
    val waitForTask = WaitForTaskCompletion(displayName, logger)
    try {
        processOutput.delegate = waitForTask
        //往appt進程寫入編譯命令
        Aapt2DaemonUtil.requestCompile(writer, request)
        //省略部分代碼箕速。。朋譬。
        
        //等待進程執(zhí)行完畢并且讀取編譯結(jié)果.
        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方法負責(zé)構(gòu)造aapt的執(zhí)行命令跟參數(shù)盐茎,并且把命令行push到aapt進程去,這里的writer便是前面創(chuàng)建的aapt進程寫句柄徙赢,代碼如下:

public static void requestLink(@NonNull Writer writer, @NonNull AaptPackageConfig command)
        throws IOException {
    ImmutableList<String> 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<String> {
    val parameters = ImmutableList.Builder<String>()

    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會把構(gòu)造好的命令push給aapt進程執(zhí)行字柠,

private static void request(Writer writer, String command, Iterable<String> 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目錄下面就能看到剛才構(gòu)建好的資源文件了

結(jié)語

實際上MergeResources的過程比我們上面分析的復(fù)雜多了,只不過我們只管全量編譯過程犀忱,很多不相關(guān)的條件分支代碼都被我刪掉忽略了募谎。實際上也不是每次都需要通過configuration來獲取依賴moduel資源的,也不是每次都要通過創(chuàng)建ValueResourceParser2對象來解析資源阴汇,合并過的資源信息會被保存一份build/intermediates/incremental/mergeDebugResources/merger.xml來数冬,下次只需要解析merger.xml 文件。這些都是在增量編譯的時候才會有的邏輯搀庶,至于增量編譯的流程這里就不再分析了拐纱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市哥倔,隨后出現(xiàn)的幾起案子秸架,更是在濱河造成了極大的恐慌,老刑警劉巖咆蒿,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件东抹,死亡現(xiàn)場離奇詭異,居然都是意外死亡沃测,警方通過查閱死者的電腦和手機缭黔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒂破,“玉大人馏谨,你說我怎么就攤上這事「矫裕” “怎么了惧互?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喇伯。 經(jīng)常有香客問我喊儡,道長,這世上最難降的妖魔是什么稻据? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任管宵,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘箩朴。我一直安慰自己岗喉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布炸庞。 她就那樣靜靜地躺著钱床,像睡著了一般。 火紅的嫁衣襯著肌膚如雪埠居。 梳的紋絲不亂的頭發(fā)上查牌,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音滥壕,去河邊找鬼纸颜。 笑死,一個胖子當(dāng)著我的面吹牛绎橘,可吹牛的內(nèi)容都是我干的胁孙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼称鳞,長吁一口氣:“原來是場噩夢啊……” “哼涮较!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冈止,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤狂票,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后熙暴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闺属,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年周霉,在試婚紗的時候發(fā)現(xiàn)自己被綠了掂器。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡诗眨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孕讳,到底是詐尸還是另有隱情匠楚,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布厂财,位于F島的核電站芋簿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏璃饱。R本人自食惡果不足惜与斤,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撩穿,春花似錦磷支、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抵皱,卻和暖如春善榛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呻畸。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工移盆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伤为。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓咒循,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钮呀。 傳聞我的和親對象是個殘疾皇子剑鞍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容