aapt與aapt2--資源id固定和PUBLIC標記
整片文章是圍繞 tinker
的 TinkerResourceIdTask
裡的知識點進行擴充套件的。
-
aapt
和aapt2
的差異(執行環境和執行結果); - 資源
id
的固定; - 進行
PUBLIC
的標記;
aapt
執行環境為 gradle:2.2.0
和 gradle-wrapper:3.4.1
aapt2
執行環境為 gradle:3.3.2
和 gradle-wrapper:5.6.2
android-aapt-sample 專案是我自己的實驗樣例。有 aapt
和 aapt2
兩個分支,分別對應其實現。
AAPT概述
從 Android Studio 3.0
開始, google
預設開啟了 aapt2
作為資源編譯的編譯器, aapt2
的出現,為資源的增量編譯提供了支援。當然使用過程中也會遇到一些問題,我們可以通過在 gradle.properties 中配置 android.enableAapt2=false 來關閉 aapt2
。
資源
Android
天生為相容各種各樣不同的裝置做了相當多的工作,比如螢幕大小、國際化、鍵盤、畫素密度等等,我們能為各種各樣特定的場景下使用特定的資源做相容而不用改動一行程式碼,假設我們為各種各樣不同的場景適配了不同的資源,如何能快速的應用上這些資源呢? Android
為我們提供了 R
這個類,指定了一個資源的索引( id
),然後我們只需要告訴系統在不同的業務場景下,使用對應的資源就好了,至於具體是指定資源裡面的哪一個具體檔案,由系統根據開發者的配置決定。
在這種場景下,假設我們給定的 id
是 x
值,那麼當下業務需要使用這個資源的時候,手機的狀態就是 y
值,有了( x,y
),在一個表裡面就能迅速的定位到資原始檔的具體路徑了。這個表就是 resources.arsc
,它是從 aapt
編譯出來的。
其實二進位制的資源(比如圖片)是不需要編譯的,只不過這個“編譯”的行為,是為了生成 resources.arsc
以及對 xml
檔案進行二進位制化等操作, resources.arsc
是上面說的表, xml
的二進位制化是為了系統讀取上效能更好。 AssetManager
在我們呼叫 R
相關的 id
的時候,就會在這個表裡面找到對應的檔案,讀取出來。
Gradle
在編譯資源的過程中,就是呼叫的這些 aapt2命令 ,傳的引數也在這個文件裡都介紹了,只不過對開發者隱藏起了呼叫細節。
aapt2
主要分兩步,一步叫 compile
,一步叫 link
。
建立一個空工程:只寫了兩個 xml
,分別是 AndroidManifest.xml
和 activity_main.xml
。
Compile
mkdir compiled aapt2 compile src/main/res/layout/activity_main.xml -o compiled/
在 compiled
資料夾中,生成了 layout_activity_main.xml.flat
這個檔案,它是 aapt2
特有的, aapt
沒有( aapt
拷貝的是原始檔), aapt2
用它能進行增量編譯。如果我們有很多的檔案的話,需要依次呼叫 compile
才行,其實這裡也可以使用 –dir
引數,只不過這個引數就沒有增量編譯的效果了。也就是說,當傳遞整個目錄時,即使只有一個資源發生了變化, AAPT2
也會重新編譯目錄中的所有檔案。
Link
link
的工作量比 compile
要多一點,此處的輸入是多個 flat
的檔案 和 AndroidManifest.xml
,外部資源,輸出是隻包含資源的 apk
和 R.java
。命令如下:
aapt2 link -o out.apk \ -I $ANDROID_HOME/platforms/android-28/android.jar \ compiled/layout_activity_main.xml.flat \ --java src/main/java \ --manifest src/main/AndroidManifest.xml
- 第二行
-I
是import
外部資源,此處主要是android
名稱空間下定義的一些屬性,我們平常使用的@android:xxx
都是放在這個jar
裡面,其實我們也可以提供自己的資源供別人連結; - 第三行是輸入的
flat
檔案,如果有多個,直接在後面拼接即可; - 第四行是
R.java
生成的目錄; - 第五行是指定
AndroidManifest.xml
;
Link
完成後會生成 out.apk
和 R.java
, out.apk
中包含了一個 resources.arsc
檔案。只帶資原始檔的可以用字尾名 .ap_
。
檢視編譯後的資源
除了是用 Android Studio
去檢視 resources.arsc
,還可以直接使用 aapt2 dump apk
資訊的方式來檢視資源相關的 ID
和狀態:
aapt2 dump out.apk
輸出的結果如下:
Binary APK Package name=com.geminiwen.hello id=7f type layout id=01 entryCount=1 resource 0x7f010000 layout/activity_main () (file) res/layout/activity_main.xml type=XML
可以看到 layout/activity_main
對應的 ID
是 0x7f010000
。
資源共享
android.jar
只是一個編譯用的樁,真正執行的時候, Android OS
提供了一個執行時的庫( framework.jar
)。 android.jar
很像一個 apk
,只不過它存在的是 class
檔案,然後存在一個 AndroidManifest.xml
和 resources.arsc
。這就意味著我們也可以對它用 aapt2 dump
,執行如下命令:
aapt2 dump $ANDROID_HOME/platforms/android-28/android.jar > test.out
得到很多類似如下的輸出:
resource 0x010a0000 anim/fade_in PUBLIC () (file) res/anim/fade_in.xml type=XML resource 0x010a0001 anim/fade_out PUBLIC () (file) res/anim/fade_out.xml type=XML resource 0x010a0002 anim/slide_in_left PUBLIC () (file) res/anim/slide_in_left.xml type=XML resource 0x010a0003 anim/slide_out_right PUBLIC () (file) res/anim/slide_out_right.xml type=XML
它多了一些 PUBLIC
的欄位,一個 apk
檔案裡面的資源,如果被加上這個標記的話,就能被其他 apk
所引用,引用方式是 @包名:型別/名字
,例如: @android:color/red
。
如果我們想要提供我們的資源,那麼首先為我們的資源打上 PUBLIC
的標記,然後在 xml
中引用你的包名,比如: `@com.gemini.app :color/red 就能引用到你定義的
color/red` 了,如果你不指定包名,預設是自己。
至於 AAPT2
如何生成 PUBLIC
,感興趣的可以接著閱讀本文。
ids.xml概述
ids.xml
:為應用的相關資源提供唯一的資源 id
。 id
是為了獲得 xml
中的物件需要的引數,也就是 Object = findViewById(R.id.id_name);
中的 id_name
。
這些值可以在程式碼中用 android.R.id
引用到。
若在 ids.xml
中定義了 ID ,則在 layout
中可如下定義 @id/price_edit
,否則 @+id/price_edit
。
優點
- 命名方便,我們可以把一些特定的控制元件先命好名,在使用的時候直接引用
id
即可,省去了一個命名環節。 - 優化編譯效率:
- 新增
id
後會在R.java
中生成; - 使用
ids.xml
統一管理,一次性編譯即可多次使用.
但使用"@+id/btn_next"
的形式,每次檔案儲存(Ctrl+s
)後R.java
都會重新檢測,如果存在該id
則不生成,如果不存在就需要新增該id
。故編譯效率降低。
- 新增
ids.xml
檔案內容:
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="forecast_list" type="id"/> <!-- <item name="app_name" type="string" />--> </resources>
也許有人很好奇上面有一行被註釋的程式碼,開啟註釋會發現編譯會報一下錯誤:
Execution failed for task ':app:mergeDebugResources'. > [string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/strings.xml [string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/ids.xml: Error: Duplicate resources
因為 app_name
對於的資源已經在 value
中被聲明瞭。
public.xml概述
官方相關的說明 官網:選擇要設為公開的資源 。
原文翻譯:庫中的所有資源在預設情況下均處於公開狀態。如需將所有資源隱式設為私有,您必須至少將一個特定屬性定義為公開。資源包括您專案的 res/
目錄中的所有檔案,例如影象。為了防止庫的使用者訪問僅供內部使用的資源,您應該通過宣告一個或多個公開資源的方式來使用這種自動私有標識機制。或者,您也可以通過新增空的 <public />
標記將所有資源設為私有,此標記不會將任何資源設為公開,而是會將一切(所有資源)都設為私有。
通過將屬性隱式設為私有,您不僅可以防止庫的使用者從內部庫資源獲得程式碼補全建議,還可以重新命名或移除私有資源,而不會破壞庫的客戶端。系統會從程式碼補全中過濾掉私有資源,並且 Lint 會在您嘗試引用私有資源時發出警告。
在構建庫時,Android Gradle 外掛會獲取公開資源定義,並將其提取到 public.txt
檔案中,然後系統會將此檔案打包到 AAR 檔案中。
實測結果也僅僅是不回程式碼自動不全,編譯器報紅。如果進行 lint
檢查,編譯都沒有警告~!
現在大部分的解釋為:檔案 RES/value/public.xml 用於將固定資源 ID
分配給 Android
資源。
stackoverfloew:What is the use of the res/values/public.xml file on Android?
public.xml
檔案內容:
<?xml version="1.0" encoding="utf-8"?> <resources> <public name="forecast_list" id="0x7f040001" type="id" /> <public name="app_name" id="0x7f070002" type="string" /> <public name="string3" id="0x7f070003" type="string" /> </resources>
資源id固定
資源id的固定在熱修復和外掛化中極其重要。在熱修復中,構建 patch
時,需要保持 patch
包的資源 id
和基準包的資源 id
一致;在外掛化中,如果外掛需要引用宿主的資源,則需要將宿主的資源 id
進行固定,因此,資源 id
的固定在這兩種場景下是尤為重要的。
在 Android Gradle Plugin 3.0.0
中,預設開啟了 aapt2
,原先aapt的資源固定方式 public.xml
也將失效,必須尋找一種新的資源固定的方式,而不是簡單的禁用掉 aapt
2,因此本文來探討一下 aapt和aapt2
分別如何進行資源 id
的固定。
aapt
進行 id
的固定
專案環境配置(PS:吐槽一下aapt已經被aapt2代替了,aapt相關資料幾乎沒有,環境搭建太費勁了~!)
com.android.tools.build:gradle:2.2.0
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
compileSdkVersion 24
buildToolsVersion '24.0.0'
先在 value
檔案下按照上面的 ids.xml
和 public.xml
的內容以及檔名,生成對應的檔案。
直接編譯結果
通過直接編譯之後的 R檔案
的內容,可以看到我們想要的設定的資源 id
並沒有按照我們預期的生成。
將 public.xml
檔案拷貝到 build/intermediates/res/merged
對應的目錄
afterEvaluate { for (variant in android.applicationVariants) { def scope = variant.getVariantData().getScope() String mergeTaskName = scope.getMergeResourcesTask().name def mergeTask = tasks.getByName(mergeTaskName) mergeTask.doLast { copy { int i=0 from(android.sourceSets.main.res.srcDirs) { include 'values/public.xml' rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml") } into(mergeTask.outputDir) } } } }
這次我們可以直接看到資源 id
按照我們需要生成了。
這是為什麼呢?
-
android gradle
外掛1.3
以下版本可以直接將public.xml
放在原始碼res
目錄參與編譯; -
android gradle
外掛1.3+
版本在執行mergeResource
任務時忽略了public.xml
,所以merge
完成後的build
目錄下的res
目錄下沒有public.xml
相關的內容。所以需要在編譯時通過指令碼將public.xml
插入到merge
完成後的build
目錄下的res
目錄下。之所以這樣做可行,是因為aapt
本身是支援public.xml
的,只是gradle
外掛在對資源做預處(merge)
時對public.xml
做了過濾。
aapt2
進行 id
的固定
在 aapt2
編譯(將資原始檔編譯為二進位制格式)後,發現 merge
的資源都已經經過了預編譯,產生了 flat
檔案,這時候將 public.xml
檔案拷貝至該目錄就會產生編譯錯誤。
但在 aapt2
的 連結 階段中,我們檢視相關的 連結選項 :
選項 | 說明 |
---|---|
--emit-ids path |
在給定的路徑下生成一個檔案,該檔案包含資源型別的名稱及其 ID 對映的列表。它適合與 --stable-ids 搭配使用。 |
--stable-ids outputfilename.ext |
使用通過 --emit-ids 生成的檔案,該檔案包含資源型別的名稱以及為其分配的 ID 的列表。此選項可以讓已分配的 ID 保持穩定,即使您在連結時刪除了資源或添加了新資源也是如此。 |
發現 --emit-ids
和 --stable-ids
命令搭配可以實現 id
的固定。
android { aaptOptions { File publicTxtFile = project.rootProject.file('public.txt') //public檔案存在,則應用,不存在則生成 if (publicTxtFile.exists()) { project.logger.error "${publicTxtFile} exists, apply it." //aapt2新增--stable-ids引數應用 aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}") } else { project.logger.error "${publicTxtFile} not exists, generate it." //aapt2新增--emit-ids引數生成 aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}") } } }
- 第一次編譯,先通過
--emit-ids
在專案的根目錄生成public.txt
; - 再將
public.txt
裡面對於的id
改為自己想要固定的id
; - 再次編譯,通過
--stable-ids
和根目錄下的public.txt
進行資源id
的固定;
--emit-ids
編譯結果
修改 public.txt
檔案內容再次編譯
R.txt轉public.txt
我們一般正常打包生成的中間產物是 build/intermediates/symbols/debug/R.txt
,需要將其轉化為 public.txt
。
R.txt
格式( int
type
name
id
)或者( int[]
styleable
name
{id,id,xxxx}
)
public.txt
格式( applicationId:type/name = id
)
所以在轉化過程中需要過濾掉 R.txt
檔案中的 styleable
型別。
android { aaptOptions { File rFile = project.rootProject.file('R.txt') List<String> sortedLines = new ArrayList<>() // 一行一行讀取 rFile.eachLine {line -> //rLines.add(line) String[] test = line.split(" ") String type = test[1] String name = test[2] String idValue = test[3] if ("styleable" != type) { sortedLines.add("${applicationId}:${type}/${name} = ${idValue}") } } Collections.sort(sortedLines) File publicTxtFile = project.rootProject.file('public.txt') if (!publicTxtFile.exists()) { publicTxtFile.createNewFile() sortedLines?.each { publicTxtFile.append("${it}\n") } } } }
PUBLIC標記
在 AAPT概述
這部分我們講過如果一個 apk
檔案裡面的資源,如果被加上 PUBLIC
標記的話,就能被其他 apk
所引用,引用方式是 @包名:型別/名字
,例如: @android:color/red
。
閱讀上面《 aapt
進行 id
的固定》到《 aapt2
進行 id
的固定》這兩部分,我們知道 aapt
和 aapt2
進行 id
固定的方法是不相同的。
其實如果我們用 aapt2 dump build/intermediates/res/resources-debug.ap_
命令檢視生成資源的相關資訊。
aapt
通過 public.xml
進行 id
固定的資源資訊有 PUBLIC
標記:
二使用上面 aapt2
進行 id
固定的方式是沒有下圖中的 PUBLIC
標記的。
原因還是 aapt
和 aapt2
的差異造成的, aapt2
的 public.txt
不等於 aapt
的 public.xml
,在 aapt2
中如果要新增 PUBLIC
標記,其實還是得另尋其他途徑。
回顧思考
回顧
-
aapt
進行資源id
固定和PUBLIC
標價,是將public.xml
複製到${mergeResourceTask.outputDir}
; -
aapt2
相比於aapt
,做了增量編譯的優化。AAPT2
會解析該檔案並生成一個副檔名為.flat
的中間二進位制檔案。
思考
能否使用 aapt2
自己將 public.xml
編譯為 public.arsc.flat
,並像 aapt
操作一樣將其複製到 ${mergeResourceTask.outputDir}
;
動手實踐
android { //將public.txt轉化為public.xml,並對public.xml進行aapt2的編譯將結果複製到${ergeResourceTask.outputDir} //下面大部分程式碼是copy自tinker的原始碼 applicationVariants.all { def variant -> def mergeResourceTask = project.tasks.findByName("merge${variant.getName().capitalize()}Resources") if (mergeResourceTask) { mergeResourceTask.doLast { //目標轉換檔案,注意public.xml上級目錄必須帶values目錄,否則aapt2執行時會報非法檔案路徑 File publicXmlFile = new File(project.buildDir, "intermediates/res/public/${variant.getDirName()}/values/public.xml") //轉換public.txt檔案為publicXml檔案,最後一個引數true標識固定資源id convertPublicTxtToPublicXml(project.rootProject.file('public.txt'), publicXmlFile, false) def variantData = variant.getMetaClass().getProperty(variant, 'variantData') def variantScope = variantData.getScope() def globalScope = variantScope.getGlobalScope() def androidBuilder = globalScope.getAndroidBuilder() def targetInfo = androidBuilder.getTargetInfo() def mBuildToolInfo = targetInfo.getBuildTools() Map<BuildToolInfo.PathId, String> mPaths = mBuildToolInfo.getMetaClass().getProperty(mBuildToolInfo, "mPaths") as Map<BuildToolInfo.PathId, String> //通過aapt2 compile命令自己生成public.arsc.flat並輸出到${mergeResourceTask.outputDir} project.exec(new Action<ExecSpec>() { @Override void execute(ExecSpec execSpec) { execSpec.executable "${mPaths.get(BuildToolInfo.PathId.AAPT2)}" execSpec.args("compile") execSpec.args("--legacy") execSpec.args("-o") execSpec.args("${mergeResourceTask.outputDir}") execSpec.args("${publicXmlFile}") } }) } } } }
將 public.txt
檔案轉化為 public.xml
檔案.
-
public.txt
中存在styleable
型別資源,public.xml
中不存在,因此轉換過程中如果遇到styleable
型別,需要忽略; -
vector
向量圖資源如果存在內部資源,也需要忽略,在aapt2
中,它的名字是以$
開頭,然後是主資源名,緊跟著__數字遞增索引,這些資源外部是無法引用到的,只需要固定id
,不需要新增PUBLIC
標記,並且$
符號在public.xml
中是非法的,因此忽略它即可; - 由於
aapt2
有資源id
的固定方式,因此轉換過程中可直接丟掉id
,簡單宣告即可(PS:這裡通過withId
引數控制是否需要固定id
); -
aapt2
編譯的public.xml
檔案的上級目錄必須是values
資料夾,否則編譯過程會報非法路徑;
/** * 轉換publicTxt為publicXml * copy tinker:com.tencent.tinker.build.gradle.task.TinkerResourceIdTask#convertPublicTxtToPublicXml */ @SuppressWarnings("GrMethodMayBeStatic") void convertPublicTxtToPublicXml(File publicTxtFile, File publicXmlFile, boolean withId) { if (publicTxtFile == null || publicXmlFile == null || !publicTxtFile.exists() || !publicTxtFile.isFile()) { throw new GradleException("publicTxtFile ${publicTxtFile} is not exist or not a file") } GFileUtils.deleteQuietly(publicXmlFile) GFileUtils.mkdirs(publicXmlFile.getParentFile()) GFileUtils.touch(publicXmlFile) project.logger.info "convert publicTxtFile ${publicTxtFile} to publicXmlFile ${publicXmlFile}" publicXmlFile.append("<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->") publicXmlFile.append("\n") publicXmlFile.append("<resources>") publicXmlFile.append("\n") Pattern linePattern = Pattern.compile(".*?:(.*?)/(.*?)\\s+=\\s+(.*?)") publicTxtFile.eachLine {def line -> Matcher matcher = linePattern.matcher(line) if (matcher.matches() && matcher.groupCount() == 3) { String resType = matcher.group(1) String resName = matcher.group(2) if (resName.startsWith('$')) { project.logger.info "ignore to public res ${resName} because it's a nested resource" } else if (resType.equalsIgnoreCase("styleable")) { project.logger.info "ignore to public res ${resName} because it's a styleable resource" } else { if (withId) { publicXmlFile.append("\t<public type=\"${resType}\" name=\"${resName}\" id=\"${matcher.group(3)}\" />\n") } else { publicXmlFile.append("\t<public type=\"${resType}\" name=\"${resName}\" />\n") } } } } publicXmlFile.append("</resources>") }
以上思考和動手實踐的過程,我們不僅解決了 aapt2
進行 PUBLIC
標記的問題,還找到了一種新的 aapt2
進行 id
固定的方法。
可能遇到的報錯:
no signature of method com.android.build.gradle.internal.variant.applicationvariantdata.getscope() is applicable for argument types: () values: []
解決方法為修改 gradle
版本為 gradle:3.3.2
和 gradle-wrapper:5.6.2
,畢竟 tinker
也不支援最新版的 gradle
.
參考:
文章到這裡就全部講述完啦,若有其他需要交流的可以留言哦~!~!
想閱讀作者的更多文章,可以檢視我的公共號:
