天天看點

Android建構工具--AAPT2源碼解析(一)

AAPT2是Android資源編譯打包工具,它把Android資源編譯分為編譯和連結兩部分。通過學習AAPT2的源碼,可以增加Android開發對APK編譯流程的了解,幫助解決日常開發中遇到的因為資源導緻的編譯失敗的問題。

在Android開發過程中,我們通過Gradle指令,啟動一個建構任務,最終會生成建構産物“APK”檔案。正常APK的建構流程如下:

Android建構工具--AAPT2源碼解析(一)

(引用自Google官方文檔)

編譯所有的資源檔案,生成資源表和R檔案;

編譯Java檔案并把class檔案打包為dex檔案;

打包資源和dex檔案,生成未簽名的APK檔案;

簽名APK生成正式包。

老版本的Android預設使用AAPT編譯器進行資源編譯,從Android Studio 3.0開始,AS預設開啟了 AAPT2 作為資源編譯的編譯器,目前看來,AAPT2也是Android發展的主流趨勢,學習AAPT2的工作原理可以幫助Android開發更好的掌握APK建構流程,進而幫助解決實際開發中遇到的問題。

AAPT2 的可執行檔案随 Android SDK 的 Build Tools 一起釋出,在Android Studio的build-tools檔案夾中就包含AAPT2工具,目錄為(SDK目錄/build-tools/version/aapt2)。

Android建構工具--AAPT2源碼解析(一)

在看Android編譯流程的時候,我忍不住會想一個問題:

Java檔案需要編譯才能生class檔案,這個我能明白,但資源檔案編譯到底是幹什麼的?為什麼要對資源做編譯?

帶着這個問題,讓我們深入的學習一下AAPT2。和AAPT不同,AAPT2把資源編譯打包過程拆分為兩部分,即編譯和連結:

編譯:将資源檔案編譯為二進制檔案(flat)。

連結:将編譯後的檔案合并,打包成單獨檔案。

通過把資源編譯拆分為兩個部分,AAPT2能夠很好的提升資源編譯的性能。例如,之前一個資源檔案發生變動,AAPT需要做一全量編譯,AAPT2隻需要重新編譯改變的檔案,然後和其他未發生改變的檔案進行連結即可。

如上文描述,Complie指令用于編譯資源,AAPT2提供多個選項與Compile指令搭配使用。

Android建構工具--AAPT2源碼解析(一)

Complie的一般用法如下:

執行指令後,AAPT2會把資源檔案編譯為.flat格式的檔案,檔案對比如下。

Android建構工具--AAPT2源碼解析(一)
Compile指令會對資源檔案的路徑做校驗,輸入檔案的路徑必須符合以下結構:path/resource-type[-config]/file。 例如,把資源檔案儲存在“aapt2”檔案夾下,使用Compile指令編譯,則會報錯“error: invalid file path '.../aapt2/ic_launcher.png'”。把aapt改成“drawable-hdpi”,編譯正常。

在Android Studio中,可以在app/build/intermediates/res/merged/ 目錄下找到編譯生成的.flat檔案。當然Compile也支援編譯多個檔案;

編譯整個目錄,需要制定資料檔案,編譯産物是一個壓縮檔案,包含目錄下所有的資源,通過檔案名把資源目錄結構扁平化。

Android建構工具--AAPT2源碼解析(一)

可以看到經過編譯後,資源檔案(png,xml ... )會被編譯成一個FLAT格式的檔案,直接把FLAT檔案拖拽到as中打開,是亂碼的。那麼這個FLAT檔案到底是什麼?

FLAT檔案是AAPT2編譯的産物檔案,也叫做AAPT2容器,檔案由檔案頭和資源項兩大部分組成:

檔案頭

Android建構工具--AAPT2源碼解析(一)

資源項

Android建構工具--AAPT2源碼解析(一)

資源項中,按照 entry_type 值分為兩種類型:

當entry_type 的值等于 0x00000000時,為RES_TABLE類型。

當entry_type的值等于 0x00000001時,為RES_FILE類型。

RES_TABLE包含的是protobuf格式的 ResourceTable 結構。資料結構如下:

資源表(ResourceTable)中包含:

StringPool:字元串池,字元串常量池是為了把資源檔案中的string複用起來,進而減少體積,資源檔案中對應的字元串會被替換為字元串池中的索引。

Package:包含資源id的相關資訊

資源id的指令方式遵循0xPPTTEEEE的規則,其中PP對應PackageId,一般應用使用的資源為7f,TT對應的是資源檔案夾的名成,最後4位為資源的id,從0開始。

RES_FILE類型格式如下:

Android建構工具--AAPT2源碼解析(一)

RES_FILE類型的FLAT檔案結構可以參考下圖;

Android建構工具--AAPT2源碼解析(一)

從上圖展示的檔案格式中可以看出,一個FLAT中可以包含多個資源項,在資源項中,Header字段中儲存的是protobuf格式序列化的 CompiledFile 内容。在這個結構中,儲存了檔案名、檔案路徑、檔案配置和檔案類型等資訊。data字段中儲存資源檔案的内容。通過這種方式,一個檔案中既儲存了檔案的外部相關資訊,又包含檔案的原始内容。

上文,我們學習了編譯指令Compile的用法和編譯産物FLAT檔案的檔案格式,接下來,我們通過檢視代碼,從源碼層面來學習AAPT2的編譯流程,本文源碼位址。

根據常識,一般函數的入口都是和main有關,打開Main.cpp,可以找到main函數入口;

在MainImpl中,首先從輸入中擷取參數部分,然後建立一個MainCommand來執行指令。

MainCommand繼承自Command,在MainCommand初始化方法中會添加多個二級指令,通過類名,可以容易的推測出,這些Command和終端通過指令檢視的二級指令一一對應。

AddOptionalSubcommand方法定義在基類Command中,内容比較簡單,把傳入的subCommand儲存在數組中。

接下來,再來分析main_command.Execute的内容,從方法名可以推測這個方法裡面有指令執行的相關代碼。在MainCommand中并沒有Execute方法的實作,那應該是在父類中實作了,再到Command類中搜尋,果然在這裡。

在Execute方法中,會先對參數作判斷,如果參數第一位命中二級指令(Compile,link,.....),則調用二級指令的Execute方法。參考上文編譯指令的示例可知,一般情況下,在這裡就會命中二級指令的判斷,進而調用二級指令的Execute方法。

在Command.cpp的同級目錄下,可以找到Compile.cpp,其Execute繼承自父類。但是由于參數已經經過移位,是以最終會執行Action方法。在Compile.cpp中可以找到Action方法,同樣在其他二級指令的實作類中(Link.cpp,Dump.cpp...),其核心處理的處理也都有Action方法中。整體調用的示意圖如下:

Android建構工具--AAPT2源碼解析(一)

在開始看Action代碼之前,我們先看一下Compile.cpp的頭檔案Compile.h的内容,在CompileCommand初始化時,會把必須參數和可選參數都初始化定義好。

官網中列出的編譯選項并不全,使用compile -h列印資訊後就會發現列印的資訊和代碼中的設定是一緻的。

在Action方法的執行流程可以總結為:

1)會根據傳入參數判斷資源類型,并建立對應的檔案加載器(file_collection)。 2)根據傳入的輸出路徑判斷輸出檔案的類型,并建立對應的歸檔器(archive_writer),archive_writer在後續的調用鍊中一直向下傳遞,最終通過archive_writer把編譯後的檔案寫到輸出目錄下。 3)調用Compile方法執行編譯。

過程1,2中涉及的檔案讀寫對象如下表。

Android建構工具--AAPT2源碼解析(一)

簡化的主流程代碼如下:

Compile方法中會編譯輸入的資源檔案名,每個資源檔案的處理方式如下:

解析輸入的資源路徑擷取資源名,擴充名等資訊;

根據path判斷檔案類型,然後給compile_func設定不同的編譯函數;

生成輸出的檔案名。輸出的就是FLAT檔案名,會對全路徑拼接,最終生成上文案例中類似的檔案名—“drawable-hdpi_ic_launcher.png.flat”;

傳入各項參數,調用compile_func方法執行編譯。

ResourcePathData中包含了資源路徑,資源名,資源擴充名等資訊,AAPT2會從中擷取資源的類型。

不同的資源類型會有四種編譯函數:

CompileFile

CompileTable

CompileXml

CompilePng

raw目錄下的XML檔案不會執行CompileXml,猜測是因為raw下的資源是直接複制到APK中,不會做XML優化編譯。values目錄下資源除了執行CompileTable編譯之外,還會修改資源檔案的擴充名,可以認為除了CompileFile,其他編譯方法多多少少會對原始資源做處理後,在寫編譯生成的FLAT檔案中。這部分的流程如下圖所示:

Android建構工具--AAPT2源碼解析(一)

編譯指令執行的主流程到這裡就結束了,通過源碼分析,我們可以知道AAPT2把輸入檔案編譯為FLAT檔案。下面,我們在進一步分析4個編譯方法。

函數中先構造ResourceFile對象和原始檔案資料,然後調用 WriteHeaderAndDataToWriter 把資料寫到輸出檔案(flat)中。

ResourceFile的内容相對簡單,完成檔案相關資訊的指派後就會調用通過WriteHeaderAndDataToWriter方法。

在WriteHeaderAndDataToWriter這個方法中,對之前建立的archive_writer(可在本文搜尋,這個歸檔寫建立完成後,會一直傳下來)做一次包裝,經過包裝的ContainerWriter則具備普通檔案寫和protobuf格式序列化寫的能力。

pb提供了ZeroCopyStream 接口使用者資料讀寫和序列化/反序列化操作。

WriteHeaderAndDataToWriter的流程可以簡單歸納為:

IArchiveWriter.StartEntry,打開檔案,做好寫入準備;

ContainerWriter.AddResFileEntry,寫入資料;

IArchiveWriter.FinishEntry,關閉檔案,釋放記憶體。

我們再分别來看這三個方法,首先是StartEntry和FinishEntry,這個方法在Archive.cpp中,ZipFileWriter和DirectoryWriter實作有些差別,但邏輯上是一緻的,這裡隻分析DirectoryWriter的實作。

StartEntry,調用fopen打開檔案。

FinishEntry,調用reset釋放記憶體。

ContainerWriter類定義在Container.cpp這個類檔案中。在ContainerWriter類的構造方法中,可以找到檔案頭的寫入代碼,其格式和上文“FLAT格式”一節中介紹的一緻。

調用ContainerWriter的AddResFileEntry方法,寫入資源項内容。

這樣,FLAT檔案就完成寫入了,并且産物檔案除了包含資源内容,還包含了檔案名,路徑,配置等資訊。

該方法和CompileFile流程上是類似的,差別在于會先對PNG圖檔做處理(png優化和9圖處理),處理完成後在寫入FLAT檔案。

AAPT2 對于 PNG 圖檔的壓縮可以分為三個方面:

RGB 是否可以轉化成灰階;

透明通道是否可以删除;

是不是最多隻有 256 色(Indexed_color 優化)。

PNG優化,有興趣的同學可以看看

在完成PNG處理後,同樣會調用WriteHeaderAndDataToWriter來寫資料,這部分内容可閱讀上文分析,不再贅述。

該方法先會解析XML,然後建立XmlResource,其中包含了資源名,配置,類型等資訊。通過FlattenXmlToOutStream函數寫入輸出檔案。

在編譯XML方法中,并沒有像前面兩個方法那樣建立ResourceFile,而是建立了XmlResource,用于儲存XML資源的相關資訊,其結構包含如下内容:

Android建構工具--AAPT2源碼解析(一)

在執行Inflate方法後,XmlResource 中會包含資源資訊和XML的dom樹資訊。InlineXmlFormatParser是用于解析出内聯屬性aapt:attr。

使用 AAPT 的内嵌資源格式,可以在同一 XML 檔案中定義所有多種資源,如果不需要資源複用的話,這種方式更加緊湊。XML 标記告訴 AAPT,該标記的子标記應被視為資源并提取到其自己的資源檔案中。屬性名稱中的值用于指定在父标記内使用内嵌資源的位置。AAPT 會為所有内嵌資源生成資源檔案和名稱。使用此内嵌格式建構的應用可與所有版本的 Android 相容。——官方文檔

解析後的FlattenXmlToOutStream 中首先會調用SerializeCompiledFileToPb方法,把資源檔案的相關資訊轉化成protobuf格式,然後在調用SerializeXmlToPb把之前解析的Element 節點資訊轉換成XmlNode(protobuf結構,同樣定義在 Resources),然後再把生成XmlNode轉換成字元串。最後,再通過上文的AddResFileEntry方法添加到FLAT檔案的資源項中。這裡可以看出,通過XML生成的FLAT檔案檔案,存在一個FLAT檔案中可包含多個資源項。

protobuf格式處理的方法(SerializeXmlToPb)在ProtoSerialize.cpp中,其通過周遊和遞歸的方式實作節點結構的複制,有興趣的讀者的可以檢視源碼。

CompileTable函數用于處理values下的資源,從上文中可知,values下的資源在編譯時會被修改擴充為arsc。最終輸出的檔案名為*.arsc.flat,效果如下圖:

Android建構工具--AAPT2源碼解析(一)

在函數開始,會讀取資源檔案,完成xml解析并儲存為ResourceTable結構,然後在通過SerializeTableToPb将其轉換成protobuf格式的pb::ResourceTable,然後調用SerializeWithCachedSizes把protobuf格式的table序列化到輸出檔案。

通過上文的學習,我們知道AAPT2是Android資源打包的建構工具,它把資源編譯分為編譯和連結兩個部分。其中,編譯是把不同的資源檔案,統一編譯生成針對 Android 平台進行過優化的二進制格式(flat)。FLAT檔案除了包含原始資源檔案的内容,還有該資源來源,類型等資訊,這樣一個檔案中包含資源所需的所有資訊,于其它依賴接耦。

在本文的開頭,我們有如下的問題:

Java檔案需要編譯才能生.class檔案,這個我能明白,但資源檔案編譯到底是幹什麼的?為什麼要對資源做編譯?

那麼,本文的答案是:AAPT2的編譯時把資源檔案編譯為FLAT檔案,而且從資源項的檔案結構可以知道,FLAT檔案中部分資料是原始的資源内容,一部分是檔案的相關資訊。通過編譯,生成的中間檔案包含的資訊比較全面,可用于增量編譯。另外,網上的一些資料還表示,二進制的資源體積更小,且加載更快。

AAPT2通過編譯,實作把資源檔案編譯成FLAT檔案,接下來則通過連結,來生成R檔案和資源表。由于篇幅問題,連結的過程将在下篇文章中分析。

1.https://juejin.cn

2.https://github.com

3.https://booster.johnsonlee.io

作者:vivo網際網路前端團隊-Shi Xiang

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。