aapt與aapt2--資源id固定和PUBLIC標記

語言: CN / TW / HK

整片文章是圍繞 tinkerTinkerResourceIdTask 裡的知識點進行擴充套件的。

  1. aaptaapt2 的差異(執行環境和執行結果);
  2. 資源 id 的固定;
  3. 進行 PUBLIC 的標記;

aapt 執行環境為 gradle:2.2.0gradle-wrapper:3.4.1

aapt2 執行環境為 gradle:3.3.2gradle-wrapper:5.6.2

android-aapt-sample 專案是我自己的實驗樣例。有 aaptaapt2 兩個分支,分別對應其實現。

AAPT概述

Android Studio 3.0 開始, google 預設開啟了 aapt2 作為資源編譯的編譯器, aapt2 的出現,為資源的增量編譯提供了支援。當然使用過程中也會遇到一些問題,我們可以通過在 gradle.properties 中配置 android.enableAapt2=false 來關閉 aapt2

資源

Android 天生為相容各種各樣不同的裝置做了相當多的工作,比如螢幕大小、國際化、鍵盤、畫素密度等等,我們能為各種各樣特定的場景下使用特定的資源做相容而不用改動一行程式碼,假設我們為各種各樣不同的場景適配了不同的資源,如何能快速的應用上這些資源呢? Android 為我們提供了 R 這個類,指定了一個資源的索引( id ),然後我們只需要告訴系統在不同的業務場景下,使用對應的資源就好了,至於具體是指定資源裡面的哪一個具體檔案,由系統根據開發者的配置決定。

在這種場景下,假設我們給定的 idx 值,那麼當下業務需要使用這個資源的時候,手機的狀態就是 y 值,有了( x,y ),在一個表裡面就能迅速的定位到資原始檔的具體路徑了。這個表就是 resources.arsc ,它是從 aapt 編譯出來的。

其實二進位制的資源(比如圖片)是不需要編譯的,只不過這個“編譯”的行為,是為了生成 resources.arsc 以及對 xml 檔案進行二進位制化等操作, resources.arsc 是上面說的表, xml 的二進位制化是為了系統讀取上效能更好。 AssetManager 在我們呼叫 R 相關的 id 的時候,就會在這個表裡面找到對應的檔案,讀取出來。

Gradle 在編譯資源的過程中,就是呼叫的這些 aapt2命令 ,傳的引數也在這個文件裡都介紹了,只不過對開發者隱藏起了呼叫細節。

aapt2 主要分兩步,一步叫 compile ,一步叫 link

建立一個空工程:只寫了兩個 xml ,分別是 AndroidManifest.xmlactivity_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 ,外部資源,輸出是隻包含資源的 apkR.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
  • 第二行 -Iimport 外部資源,此處主要是 android 名稱空間下定義的一些屬性,我們平常使用的 @android:xxx 都是放在這個 jar 裡面,其實我們也可以提供自己的資源供別人連結;
  • 第三行是輸入的 flat 檔案,如果有多個,直接在後面拼接即可;
  • 第四行是 R.java 生成的目錄;
  • 第五行是指定 AndroidManifest.xml ;

Link 完成後會生成 out.apkR.javaout.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 對應的 ID0x7f010000

資源共享

android.jar 只是一個編譯用的樁,真正執行的時候, Android OS 提供了一個執行時的庫( framework.jar )。 android.jar 很像一個 apk ,只不過它存在的是 class 檔案,然後存在一個 AndroidManifest.xmlresources.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 :為應用的相關資源提供唯一的資源 idid 是為了獲得 xml 中的物件需要的引數,也就是 Object = findViewById(R.id.id_name); 中的 id_name

這些值可以在程式碼中用 android.R.id 引用到。

若在 ids.xml 中定義了 ID ,則在 layout 中可如下定義 @id/price_edit ,否則 @+id/price_edit

優點

  1. 命名方便,我們可以把一些特定的控制元件先命好名,在使用的時候直接引用 id 即可,省去了一個命名環節。
  2. 優化編譯效率:
    • 新增 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.xmlpublic.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 按照我們需要生成了。

這是為什麼呢?

  1. android gradle 外掛 1.3 以下版本可以直接將 public.xml 放在原始碼 res 目錄參與編譯;

  2. 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}")
        }
    }
}
  1. 第一次編譯,先通過 --emit-ids 在專案的根目錄生成 public.txt ;
  2. 再將 public.txt 裡面對於的 id 改為自己想要固定的 id ;
  3. 再次編譯,通過 --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 的固定》這兩部分,我們知道 aaptaapt2 進行 id 固定的方法是不相同的。

其實如果我們用 aapt2 dump build/intermediates/res/resources-debug.ap_ 命令檢視生成資源的相關資訊。

aapt 通過 public.xml 進行 id 固定的資源資訊有 PUBLIC 標記:

二使用上面 aapt2 進行 id 固定的方式是沒有下圖中的 PUBLIC 標記的。

原因還是 aaptaapt2 的差異造成的, aapt2public.txt 不等於 aaptpublic.xml ,在 aapt2 中如果要新增 PUBLIC 標記,其實還是得另尋其他途徑。

回顧思考

回顧

  1. aapt 進行資源 id 固定和 PUBLIC 標價,是將 public.xml 複製到 ${mergeResourceTask.outputDir} ;
  2. 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.2gradle-wrapper:5.6.2 ,畢竟 tinker 也不支援最新版的 gradle .

參考:

Github:tinker

android public.xml 用法

Android-Gradle筆記

aapt2 適配之資源 id 固定

文章到這裡就全部講述完啦,若有其他需要交流的可以留言哦~!~!

想閱讀作者的更多文章,可以檢視我的公共號: