Android AAPT詳解

 

 

AAPT是什麼

AAPT - Android Asset Packaging Tool, 看全稱,就可知道AAPT是Android資源打包工具。在講這個之前,是有必要簡單說下Android是如何構建一個APK的。

上圖是Google官方發佈的一張非常經典的Apk打包流程圖。

流程概述:

  1. 工程的資源文件(res文件夾下的文件),通過AAPT打包成R.java類(資源索引表),以及.arsc資源文件
  2. 如果有aidl,通過aidl工具,打包成java接口類
  3. R.java和aidl.java通過java編譯成想要的.class文件。
  4. 源碼class文件和第三方jar或者library通過dx工具打包成dex文件。dx工具的主要作用是將java字節碼轉換成Dalvik字節碼,在此過程中會壓縮常量池,消除一些冗餘信息等。
  5. apkbuilder工具會將所有沒有編譯的資源,.arsc資源,.dex文件打包到一個完成apk文件中中。
  6. 簽名,5中完成apk通過配置的簽名文件(debug和release都有),jarsigner工具會對齊簽名。得到一個簽名後的apk,signed.apk
  7. zipAlign工具對6中的signed.apk進行對齊處理,所謂對齊,主要過程是將APK包中所有的資源文件距離文件起始偏移爲4字節整數倍,這樣通過內存映射訪問apk文件時的速度會更快。對齊的作用主要是爲了減少運行時內存的使用。

總結:

  • 輸入:res文件夾所有的資源(layout\drawable\string\array等),asset下的資源,AndroidManifest.xml,Android.jar文件
  • 工具: aapt 地址(/sdk path/build-tools/build tools version/aapt)
  • 輸出:res下的資源都會被編譯成一個資源索引文件resource.arsc以及一個R.java類。asset下的資源不會編譯,直接壓縮進apk。

 

AAPT命令詳解

按照上面aapt的地址配置好環境變量後,在終端中輸入 aapt v 會得到aapt版本信息,如下:

輸入 aapt 命令會列出所有的aapt命令集合:如下

Android Asset Packaging Tool

Usage:
 aapt l[ist] [-v] [-a] file.{zip,jar,apk}
   List contents of Zip-compatible archive.

 aapt d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...] ]
   strings          Print the contents of the resource table string pool in the
APK.
   badging          Print the label and icon for the app declared in APK.
   permissions      Print the permissions from the APK.
   resources        Print the resource table from the APK.
   configurations   Print the configurations in the APK.
   xmltree          Print the compiled xmls in the given assets.
   xmlstrings       Print the strings of the given compiled xml assets.

 aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \
        [-0 extension [-0 extension ...]] [-g tolerance] [-j jarfile] \
        [--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \
        [--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \
        [--rename-manifest-package PACKAGE] \
        [--rename-instrumentation-target-package PACKAGE] \
        [--utf16] [--auto-add-overlay] \
        [--max-res-version VAL] \
        [-I base-package [-I base-package ...]] \
        [-A asset-source-dir]  [-G class-list-file] [-P public-definitions-file]\
        [-D main-dex-class-list-file] \
        [-S resource-sources [-S resource-sources ...]] \
        [-F apk-file] [-J R-file-dir] \
        [--product product1,product2,...] \
        [-c CONFIGS] [--preferred-density DENSITY] \
        [--split CONFIGS [--split CONFIGS]] \
        [--feature-of package [--feature-after package]] \
        [raw-files-dir [raw-files-dir] ...] \
        [--output-text-symbols DIR]

   Package the android resources.  It will read assets and resources that are supplied with the -M -A -S or raw-files-dir arguments.  The -J -P -F and -R options control which files are output.

 aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]
   Delete specified files from Zip-compatible archive.

 aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]
   Add specified files to Zip-compatible archive.

 aapt c[runch] [-v] -S resource-sources ... -C output-folder ...
   Do PNG preprocessing on one or several resource folders and store the results in the output folder.

 aapt s[ingleCrunch] [-v] -i input-file -o outputfile
   Do PNG preprocessing on a single file.

 aapt v[ersion]
   Print program version.

 Modifiers:
   -a  print Android-specific data (resources, manifest) when listing
       列出時打印Android特定數據(資源、清單)
   -c  specify which configurations to include.  The default is all configurations.  The value of the parameter should be a comma separated list of configuration values.  Locales should be specified as either a language or language-region pair.  Some examples:
	   指定要包含的配置。默認爲所有配置。參數的值應該是配置值的逗號分隔列表。區域設置應指定爲語言或語言區域對。一些例子:
		en
		port,en
		port,land,en_US
   -d  one or more device assets to include, separated by commas
       要包含的一個或多個設備assets,用逗號分隔
   -f  force overwrite of existing files
       強制重寫現有文件
   -g  specify a pixel tolerance to force images to grayscale, default 0
       指定像素公差以強制圖像灰度化,默認值爲0
   -j  specify a jar or zip file containing classes to include
       指定包含要包含的類的jar或zip文件
   -k  junk path of file(s) added
       添加的文件垃圾的路徑
   -m  make package directories under location specified by -J
       在-J指定的位置下生成包目錄
   -u  update existing packages (add new, replace older, remove deleted files)
       更新現有包(添加新的,替換舊的,刪除已刪除的文件)
   -v  verbose output
       詳細輸出
   -x  create extending (non-application) resource IDs
       創建擴展(非應用程序)資源ID
   -z  require localization of resource attributes marked with localization="suggested"
       要求本地化標記爲“建議”的資源屬性
   -A  additional directory in which to find raw asset files
       查找原始資產文件的附加目錄
   -G  A file to output proguard options into.
       將proguard選項輸出到的文件。
   -D  A file to output proguard options for the main dex into.
       將主索引的proguard選項輸出到的文件。
   -F  specify the apk file to output
       指定要輸出的apk文件
   -I  add an existing package to base include set
       將現有包添加到基包含集合
   -J  specify where to output R.java resource constant definitions
       指定輸出R.java資源常量定義的位置
   -M  specify full path to AndroidManifest.xml to include in zip
       指定要包含在zip中的AndroidManifest.xml的完整路徑
   -P  specify where to output public resource definitions
       指定輸出公共資源定義的位置
   -S  directory in which to find resources.  Multiple directories will be scanned and the first match found (left to right) will take precedence.
       查找資源的目錄。將掃描多個目錄,找到的第一個匹配項(從左到右)將優先。
   -0  specifies an additional extension for which such files will not be stored compressed in the .apk.  An empty string means to not compress any files at all.
       指定一個附加擴展名,對於該擴展名,此類文件將不會壓縮存儲在.apk中。空字符串意味着根本不壓縮任何文件。
   --debug-mode
       inserts android:debuggable="true" in to the application node of the manifest, making the application debuggable even on production devices.
       將android:debuggable=“true”插入清單的應用程序節點,使應用程序即使在生產設備上也可以調試。
   --include-meta-data
       when used with "dump badging" also includes meta-data tags.
	   與“轉儲標記”一起使用時,還包括元數據標記。
   --pseudo-localize
       generate resources for pseudo-locales (en-XA and ar-XB).
	   爲僞區域設置(en-XA和ar-XB)生成資源。
   --min-sdk-version
       inserts android:minSdkVersion in to manifest.  If the version is 7 or higher, the default encoding for resources will be in UTF-8.
	   Inserts Android:如果版本爲7或更高,則資源編碼將爲UTF-8。
   --target-sdk-version
       inserts android:targetSdkVersion in to manifest.
       在清單中插入android:targetSdkVersion。
   --max-res-version
       ignores versioned resource directories above the given value.
	   忽略高於給定值的版本化資源目錄。
   --values
       when used with "dump resources" also includes resource values.
	   與“轉儲資源”一起使用時,還包括資源值。
   --version-code
       inserts android:versionCode in to manifest.
   --version-name
       inserts android:versionName in to manifest.
   --replace-version
       If --version-code and/or --version-name are specified, these values will replace any value already in the manifest. By default, nothing is changed if the manifest already defines these attributes.
	   如果指定了--version code和/或--version name,則這些值將替換清單中已存在的任何值。默認情況下,如果清單已定義這些屬性,則不會更改任何內容。
   --custom-package
       generates R.java into a different package.
	   將R.java生成到不同的包中。
   --extra-packages
       generate R.java for libraries. Separate libraries with ':'.
	   爲庫生成R.java。用“:”分隔庫。
   --generate-dependencies
       generate dependency files in the same directories for R.java and resource package
	   在R.java和資源包的相同目錄中生成依賴文件
   --auto-add-overlay
       Automatically add resources that are only in overlays.
	   自動添加僅在覆蓋中的資源。
   --preferred-density
       Specifies a preference for a particular density. Resources that do not match this density and have variants that are a closer match are removed.
	   指定特定密度的首選項。與此密度不匹配且具有更接近匹配的變體的資源將被刪除。
   --split
       Builds a separate split APK for the configurations listed. This can be loaded alongside the base APK at runtime.
	   爲列出的配置構建單獨的拆分APK。這可以在運行時與基本APK一起加載。
   --feature-of
       Builds a split APK that is a feature of the apk specified here. Resources in the base APK can be referenced from the the feature APK.
	   構建一個split APK,它是這裏指定的APK的一個特性。基本APK中的資源可以從特性APK中引用。
   --feature-after
       An app can have multiple Feature Split APKs which must be totally ordered. If --feature-of is specified, this flag specifies which Feature Split APK comes before this one. The first Feature Split APK should not define anything here.
	   一個應用程序可以有多個功能拆分的apk,這些apk必須完全訂購。如果指定了--feature of,則此標誌指定哪個feature Split APK早於此。第一個特性Split APK不應該在這裏定義任何東西。
   --rename-manifest-package
       Rewrite the manifest so that its package name is the package name given here.  Relative class names (for example .Foo) will be changed to absolute names with the old package so that the code does not need to change.
	   重寫清單,使其包名爲此處給定的包名。相對類名(例如.Foo)將使用舊包更改爲絕對名稱,這樣代碼就不需要更改。
   --rename-instrumentation-target-package
       Rewrite the manifest so that all of its instrumentation components target the given package.  Useful when used in conjunction with --rename-manifest-package to fix tests against a package that has been renamed.
	   重寫清單,使其所有檢測組件都以給定的包爲目標。與--rename manifest package一起使用時非常有用,可以針對已重命名的包修復測試。
   --product
       Specifies which variant to choose for strings that have product variants
	   指定爲具有產品變量的字符串選擇哪個變量
   --utf16
       changes default encoding for resources to UTF-16.  Only useful when API level is set to 7 or higher where the default encoding is UTF-8.
	   將資源的默認編碼更改爲UTF-16。僅當API級別設置爲7或更高(默認編碼爲UTF-8)時纔有用。
   --non-constant-id
       Make the resources ID non constant. This is required to make an R java class that does not contain the final value but is used to make reusable compiled libraries that need to access resources.
	   使資源ID爲非常量。這需要生成一個不包含最終值但用於生成需要訪問資源的可重用編譯庫的R java類。
   --shared-lib
       Make a shared library resource package that can be loaded by an application at runtime to access the libraries resources. Implies --non-constant-id.
	   創建一個共享庫資源包,應用程序可以在運行時加載該包以訪問庫資源。意味着——非常數-id。
   --app-as-shared-lib
       Make an app resource package that also can be loaded as shared library at runtime. Implies --non-constant-id.
	   創建一個應用程序資源包,該包也可以在運行時作爲共享庫加載。意味着——非常數-id。
   --error-on-failed-insert
       Forces aapt to return an error if it fails to insert values into the manifest with --debug-mode, --min-sdk-version, --target-sdk-version --version-code and --version-name. Insertion typically fails if the manifest already defines the attribute.
	   如果aapt未能使用--debug mode,-min sdk version,-target sdk version--version code和--version name將值插入清單,則強制aapt返回錯誤。如果清單已經定義了屬性,插入通常會失敗。
   --error-on-missing-config-entry
       Forces aapt to return an error if it fails to find an entry for a configuration.
	   如果aapt找不到配置項,則強制它返回錯誤。
   --output-text-symbols
       Generates a text file containing the resource symbols of the R class in the specified folder.
	   生成一個文本文件,其中包含指定文件夾中R類的資源符號。
   --ignore-assets
       Assets to be ignored. Default pattern is:
	   要忽略的資產。默認模式爲:
       !.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~
   --skip-symbols-without-default-localization
       Prevents symbols from being generated for strings that do not have a default localization
	   防止爲沒有默認本地化的字符串生成符號
   --no-version-vectors
       Do not automatically generate versioned copies of vector XML resources.
	   不要自動生成矢量XML資源的版本副本。
   --no-version-transitions
       Do not automatically generate versioned copies of transition XML resources.
	   不要自動生成轉換XML資源的版本副本。
   --private-symbols
       Java package name to use when generating R.java for private resources.
	   爲私有資源生成R.Java時要使用的Java包名稱。

 

下面我們一條條來使用並且分析其作用:

1.aapt l[ist] [-v] [-a] file.{zip,jar,apk}

作用:列出壓縮文件(zip,jar,apk)中的目錄內容。

aapt l app-release.apk

結果如下:

可以看出來不加任何參數,aapt只是簡單的羅列壓縮文件中每一項的內容。

aapt l -v app-release.apk

從圖中可以看出,加上-v後,輸出的內容很詳細,並且以列表的形式標識出很多參數,其中表目有:

  • Length:原始文件的長度

  • Date:日期

  • Time:時間

  • Name:名稱

  • Method:壓縮方法,Deflate及Stored兩種,即該Zip目錄採用的算法是壓縮模式還是存儲模式;可以看出resources.arsc、*.png採用壓縮模式,而其它採用壓縮模式。

  • Ratio:壓縮率

  • Size:這個是壓縮省掉的大小,即如果壓縮率是xx%。那Size是原始長度*(1-xx%)

  • CRC-32:循環冗餘校驗。這個計算是有特定的算法的。

  • offset:zipfile中偏移量的意思

aapt l -a app-release.apk

-a表示會詳細輸出壓縮文件中所有目錄的內容,詳細到什麼程度的,可以看上圖,上圖截取的只是很小的一部分,這部分是manifest.xml文件的所有數據,可以看出來基本上所有的manifest信息都列了出來。

 

2.aapt d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]]

作用:通過參數配置可以dump apk中各種詳細信息。

strings 官方解釋:Print the contents of the resource table string pool in the APK

aapt dump strings app-release.apk

猜測應該是序列化的string字段。

  • bading 官方解釋:Print the label and icon for the app declared in APK.
    aapt dump badging app-release.apk

    查看APK中的配置信息,包括包名,versionCode,versionName,platformBuildVersionName(編譯的時候系統添加的字段,相當targetSdkVersionCode的描述)等。同事該命令還列出了manifest.xml的部分信息,包括啓動界面,manifest裏配置的label,icon等信息。還有分辨率,時區,uses-feature等信息。

  • permissions 官方解釋:Print the permissions from the APK

較簡單,輸出APK中使用到的權限信息。

  • resources 官方解釋:

Print the resource table from the APK

輸出了apk中所有的資源信息,從這裏也可以看出來aapt打包時也包含了android系統很多資源。並且這裏也發現,系統通過標準的aapt構建出來的資源絕大部分的資源id都是0x7f開頭的,這個也是和我們在R.java文件中看到的資源編號是對應起來的。

  • configurations 官方解釋:Print the configurations in the APK

命令輸出了apk所有的資源目錄,僅僅是目錄,裏面有語言,分辨率,夜間模式相關的數據。

 

  • xmltree 官方解釋:

Print the compiled xmls in the given assets

該命令直接反編譯除了apk中某一個xml佈局文件的組織結構。命令需要兩個參數 第一是apk的地址 第二後面是apk中某個編譯好的xml的相對路徑地址

  • xmlstrings 官方解釋:

Print the strings of the given compiled xml assets

從字面解釋,輸出xml文件中所有的string信息。看結果,實際上並沒看出來什麼特殊的,也並不是簡單的string信息,猜測可能是索引吧。

 

3.aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml]


aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \
        [-0 extension [-0 extension ...]] [-g tolerance] [-j jarfile] \
        [--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \
        [--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \
        [--rename-manifest-package PACKAGE] \
        [--rename-instrumentation-target-package PACKAGE] \
        [--utf16] [--auto-add-overlay] \
        [--max-res-version VAL] \
        [-I base-package [-I base-package ...]] \
        [-A asset-source-dir]  [-G class-list-file] [-P public-definitions-file] \
        [-D main-dex-class-list-file] \
        [-S resource-sources [-S resource-sources ...]] \
        [-F apk-file] [-J R-file-dir] \
        [--product product1,product2,...] \
        [-c CONFIGS] [--preferred-density DENSITY] \
        [--split CONFIGS [--split CONFIGS]] \
        [--feature-of package [--feature-after package]] \
        [raw-files-dir [raw-files-dir] ...] \
        [--output-text-symbols DIR]

官方解釋:

Package the android resources. It will read assets and resources that are supplied with the -M -A -S or raw-files-dir arguments. The -J -P -F and -R options control which files are output.

android 編譯資源打包資源文件的命令。

  • -d:包括一個或多個設備資源,由逗號分隔;
  • -f:覆蓋現有的文件命令,加上後編譯生成直接覆蓋目前已經存在的R.java;
  • -m:使生成的包的目錄放在-J參數指定的目錄;
  • -u:更新現有的包 u = update;
  • -v:詳細輸出,加上此命令會在控制檯輸出每一個資源文件信息,R.java生成後還有註釋。
  • -x:創建擴展資源ID;
  • -z:需要本地化的資源屬性標記定位。
  • -M:AndroidManifest.xml的路徑
  • -0:指定一個額外的擴展. apk文件將不會存儲壓縮
  • -g:制定像素迫使圖形的灰度
  • -j:指定包含一個jar或zip文件包,這個命令很特別
  • –debug-mode:指定的是調試模式下的編譯資源;
  • –min-sdk-versopm VAL:最小SDK版本 如是7以上 則默認編譯資源的格式是 utf-8
  • –target-sdk-version VAL:在androidMainfest中的目標編譯SDK版本
  • –app-version VAL:應用程序版本號
  • –app-version-name TEXT:應該程序版本名字;
  • –custom-package VAL:生成R.java到一個不同的包
  • –rename-mainifest-package PACKAGE:修改APK包名的選項;
  • –rename-instrumentation-target-package PACKAGE:重寫指定包名的選項;
  • –utf16:資源編碼修改爲更改默認utf – 16編碼;
  • –auto-add-overlay:自動添加資源覆蓋
  • –max-res-version:最大資源版本
  • -I:指定的SDK版本中android.jar的路徑
  • -A:assert文件夾的路徑
  • -G:一個文件輸出混淆器選項,後面加文件逗號隔開.
  • -P:指定的輸出公共資源,可以指定一個文件 讓資源ID輸出到那上面;
  • -S:指定資源目錄 一般是 res
  • -F:指定把資源輸出到 apk文件中
  • -J:指定R.java輸出的路徑
  • raw-file-dir:附加打包進APK的文件

該命令也是aapt最核心、最複雜的命令。這邊我只嘗試了一下簡單的實踐,講工程的資源編譯到一個包裏。下面是命令


aapt package -f -S AAPTDemo/app/src/main/res -I sdk/platforms/android-25/android.jar -A AAPTDemo/app/src/main/assets -M AAPTDemo/app/src/main/AndroidManifest.xml -F AAPTDemo/app/out.apk

輸出了一個apk文件,解壓以後文件格式如下

這個apk文件除了沒有代碼dex,資源都在的。這個是aapt打包的關鍵步驟之一,還有一個步驟就是把資源文件編譯成R.java,供程序調用。命令如下:

aapt package -m -J <R.java目錄> -S <res目錄> -I <android.jar目錄>  -M <AndroidManifest.xml目錄>

 

4.aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]

官方解釋:

Delete specified files from Zip-compatible archive

就是從一個zip archive文件中刪除一個文件。較簡單,不做實例了。

5.aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]

官方解釋:

Add specified files to Zip-compatible archive.

即在一個zip包中添加一個一個指定的文件。

6.aapt c[runch] [-v] -S resource-sources ... -C output-folder ...

官方解釋:

Do PNG preprocessing on one or several resource folders and store the results in the output folder.

對多個或者單個資源文件夾進行處理,並且將結果保存在輸出文件夾中

6.aapt s[ingleCrunch] [-v] -i input-file -o outputfile

官方解釋:

Do PNG preprocessing on a single file

預處理一個文件

 

 

AAPT源碼解析

首先下載Android源碼

Android Source

我這邊下載的是Android 6.0的源碼

AAPT代碼地址:***/frameworks/tools/aapt/目錄下。

我們這裏以一個命令來跟蹤源碼的流程,即用aapt是如何構建一個R.java的,命令格式如下:


aapt package –m –J <R.java目錄> -S <res目錄> -I <android.jar目錄> -M <AndroidManifest.xml目錄>

 


aapt package -m -J  AAPTDemo/app/ -S AAPTDemo/app/src/main/res/ -I sdk/platforms/android-25/android.jar -M AAPTDemo/app/src/main/AndroidManifest.xml

運行該命令後,在配置的R.java目錄下 會生成一個自己app包名的目錄,裏面會生成R.java文件,如下:

 

R.java裏會生成這樣的索引ID類,都是以0x7f開頭

public final class R {
    public static final class attr {
    }
    public static final class color {
        public static final int colorAccent=0x7f040002;
        public static final int colorPrimary=0x7f040000;
        public static final int colorPrimaryDark=0x7f040001;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
    public static final class mipmap {
        public static final int ic_launcher=0x7f020000;
        public static final int ic_launcher_round=0x7f020001;
    }
    public static final class string {
        public static final int app_name=0x7f050000;
    }
}

好,既然我們知道了輸入以及輸出,那讓我們來分析這塊的代碼。

PS:由於某些函數較長,不會貼出所有的源碼

入口 /frameworks/base/tools/aapt/Main.cpp

int main(int argc, char* const argv[]) {
    ***
    else if (argv[1][0] == 'p')
        bundle.setCommand(kCommandPackage);
    ***
    while (argc && argv[0][0] == '-') {
        //通過case比較,去除命令中所有的參數,並且放進bundle中
        /* flag(s) found */
        const char* cp = argv[0] +1;
        while (*cp != '\0') {
            ***
            switch (*cp) {
            case 'M':
                argc--;
                argv++;
                if (!argc) {
                    fprintf(stderr, "ERROR: No argument supplied for '-M' option\n");
                    wantUsage = true;
                    goto bail;
                }
                //這個僅僅是把傳進來的地址坐下系統路徑分割線的轉換
                convertPath(argv[0]);
                bundle.setAndroidManifestFile(argv[0]);
                break;
            }
            ***
        }
    }
    ***
    result = handleCommand(&bundle);
    ***
}

當通過所有的匹配規則後,該函數實際調用是 handleCommand(&bundle)。 至於執行什麼命令說白了也是命令指定的,-p 設置的command參數是kCommandPackage。

分發指令 /frameworks/base/tools/aapt/Main.cpp

 int handleCommand(Bundle* bundle){
    switch (bundle->getCommand()) {
        case kCommandVersion:      return doVersion(bundle);
        case kCommandList:         return doList(bundle);
        case kCommandDump:         return doDump(bundle);
        case kCommandAdd:          return doAdd(bundle);
        case kCommandRemove:       return doRemove(bundle);
        case kCommandPackage:      return doPackage(bundle);
        case kCommandCrunch:       return doCrunch(bundle);
        case kCommandSingleCrunch: return doSingleCrunch(bundle);
        case kCommandDaemon:       return runInDaemonMode(bundle);
        default:
            fprintf(stderr, "%s: requested command not yet supported\n", gProgName);
            return 1;
    }
}

處理package指令 /frameworks/base/tools/aapt/Command.cpp

int doPackage(Bundle* bundle) {
    const char* outputAPKFile;
    int retVal = 1;
    status_t err;
    sp<AaptAssets> assets;
    int N;
    FILE* fp;
    String8 dependencyFile;
    sp<ApkBuilder> builder;

    sp<WeakResourceFilter> configFilter = new WeakResourceFilter();
    //見註釋3-1
    err = configFilter->parse(bundle->getConfigurations());
    if (err != NO_ERROR) {
        goto bail;
    }

    //資源本地化相關的配置,具體什麼含義也沒有理解清楚
    if (configFilter->containsPseudo()) {
        bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED);
    }
    if (configFilter->containsPseudoBidi()) {
        bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI);
    }

    //校驗命令中是否傳入正確的參數
    N = bundle->getFileSpecCount();
    if (N < 1 && bundle->getResourceSourceDirs().size() == 0 && bundle->getJarFiles().size() == 0
            && bundle->getAndroidManifestFile() == NULL && bundle->getAssetSourceDirs().size() == 0) {
        fprintf(stderr, "ERROR: no input files\n");
        goto bail;
    }

    outputAPKFile = bundle->getOutputAPKFile();

    // 如果輸出文件存在,但是是不合格的,則直接報錯結束,如果不存在,則新建空文件
    if (outputAPKFile) {
        FileType type;
        type = getFileType(outputAPKFile);
        if (type != kFileTypeNonexistent && type != kFileTypeRegular) {
            fprintf(stderr,
                "ERROR: output file '%s' exists but is not regular file\n",
                outputAPKFile);
            goto bail;
        }
    }

    // Load the assets.
    assets = new AaptAssets();
    
    // 設置res和asset的成員,僅僅是外層new一個對象賦值給AaptAssets
    if (bundle->getGenDependencies()) {
        sp<FilePathStore> resPathStore = new FilePathStore;
        assets->setFullResPaths(resPathStore);
        sp<FilePathStore> assetPathStore = new FilePathStore;
        assets->setFullAssetPaths(assetPathStore);
    }
    //調用AaptAssets類的成員函數slurpFromArgs將AndroidManifest.xml文件,目錄assets和res下的資源目錄和資源文件收錄起來保存到AaptAssets中的
    //成員變量中
    err = assets->slurpFromArgs(bundle);
    if (err < 0) {
        goto bail;
    }
    //如果命令中指定需要詳細日誌輸出,這裏會打印所有的資源信息
    if (bundle->getVerbose()) {
        assets->print(String8());
    }

    // Create the ApkBuilder, which will collect the compiled files
    // to write to the final APK (or sets of APKs if we are building
    // a Split APK.
    //new一個ApkBuilder對象,如果需要生成多個apk,則需要將上層的配置寫入改對象中
    builder = new ApkBuilder(configFilter);
    // If we are generating a Split APK, find out which configurations to split on.
    if (bundle->getSplitConfigurations().size() > 0) {
        const Vector<String8>& splitStrs = bundle->getSplitConfigurations();
        const size_t numSplits = splitStrs.size();
        for (size_t i = 0; i < numSplits; i++) {
            std::set<ConfigDescription> configs;
            if (!AaptConfig::parseCommaSeparatedList(splitStrs[i], &configs)) {
                fprintf(stderr, "ERROR: failed to parse split configuration '%s'\n", splitStrs[i].string());
                goto bail;
            }

            err = builder->createSplitForConfigs(configs);
            if (err != NO_ERROR) {
                goto bail;
            }
        }
    }

    // If they asked for any fileAs that need to be compiled, do so.
    //這是最核心的一步,編譯資源(res和asset)。這個成功後,下面的步驟就僅僅是寫輸出文件了
    if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
        err = buildResources(bundle, assets, builder);
        if (err != 0) {
            goto bail;
        }
    }

    // At this point we've read everything and processed everything.  From here
    // on out it's just writing output files.
    if (SourcePos::hasErrors()) {
        goto bail;
    }

    // Update symbols with information about which ones are needed as Java symbols.
    assets->applyJavaSymbols();
    if (SourcePos::hasErrors()) {
        goto bail;
    }

    // If we've been asked to generate a dependency file, do that here
    if (bundle->getGenDependencies()) {
        // If this is the packaging step, generate the dependency file next to
        // the output apk (e.g. bin/resources.ap_.d)
        if (outputAPKFile) {
            dependencyFile = String8(outputAPKFile);
            // Add the .d extension to the dependency file.
            dependencyFile.append(".d");
        } else {
            // Else if this is the R.java dependency generation step,
            // generate the dependency file in the R.java package subdirectory
            // e.g. gen/com/foo/app/R.java.d
            dependencyFile = String8(bundle->getRClassDir());
            dependencyFile.appendPath("R.java.d");
        }
        // Make sure we have a clean dependency file to start with
        fp = fopen(dependencyFile, "w");
        fclose(fp);
    }

    // Write out R.java constants
    if (!assets->havePrivateSymbols()) {
        if (bundle->getCustomPackage() == NULL) {
            // Write the R.java file into the appropriate class directory
            // e.g. gen/com/foo/app/R.java
            err = writeResourceSymbols(bundle, assets, assets->getPackage(), true,
                    bundle->getBuildSharedLibrary());
        } else {
            const String8 customPkg(bundle->getCustomPackage());
            err = writeResourceSymbols(bundle, assets, customPkg, true,
                    bundle->getBuildSharedLibrary());
        }
        if (err < 0) {
            goto bail;
        }
        // If we have library files, we're going to write our R.java file into
        // the appropriate class directory for those libraries as well.
        // e.g. gen/com/foo/app/lib/R.java
        if (bundle->getExtraPackages() != NULL) {
            // Split on colon
            String8 libs(bundle->getExtraPackages());
            char* packageString = strtok(libs.lockBuffer(libs.length()), ":");
            while (packageString != NULL) {
                // Write the R.java file out with the correct package name
                err = writeResourceSymbols(bundle, assets, String8(packageString), true,
                        bundle->getBuildSharedLibrary());
                if (err < 0) {
                    goto bail;
                }
                packageString = strtok(NULL, ":");
            }
            libs.unlockBuffer();
        }
    } else {
        err = writeResourceSymbols(bundle, assets, assets->getPackage(), false, false);
        if (err < 0) {
            goto bail;
        }
        err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true, false);
        if (err < 0) {
            goto bail;
        }
    }

    // Write out the ProGuard file
    err = writeProguardFile(bundle, assets);
    if (err < 0) {
        goto bail;
    }

    // Write the apk
    if (outputAPKFile) {
        // Gather all resources and add them to the APK Builder. The builder will then
        // figure out which Split they belong in.
        err = addResourcesToBuilder(assets, builder);
        if (err != NO_ERROR) {
            goto bail;
        }

        const Vector<sp<ApkSplit> >& splits = builder->getSplits();
        const size_t numSplits = splits.size();
        for (size_t i = 0; i < numSplits; i++) {
            const sp<ApkSplit>& split = splits[i];
            String8 outputPath = buildApkName(String8(outputAPKFile), split);
            err = writeAPK(bundle, outputPath, split);
            if (err != NO_ERROR) {
                fprintf(stderr, "ERROR: packaging of '%s' failed\n", outputPath.string());
                goto bail;
            }
        }
    }

    // If we've been asked to generate a dependency file, we need to finish up here.
    // the writeResourceSymbols and writeAPK functions have already written the target
    // half of the dependency file, now we need to write the prerequisites. (files that
    // the R.java file or .ap_ file depend on)
    if (bundle->getGenDependencies()) {
        // Now that writeResourceSymbols or writeAPK has taken care of writing
        // the targets to our dependency file, we'll write the prereqs
        fp = fopen(dependencyFile, "a+");
        fprintf(fp, " : ");
        bool includeRaw = (outputAPKFile != NULL);
        err = writeDependencyPreReqs(bundle, assets, fp, includeRaw);
        // Also manually add the AndroidManifeset since it's not under res/ or assets/
        // and therefore was not added to our pathstores during slurping
        fprintf(fp, "%s \\\n", bundle->getAndroidManifestFile());
        fclose(fp);
    }

    retVal = 0;
bail:
    if (SourcePos::hasErrors()) {
        SourcePos::printErrors(stderr);
    }
    return retVal;
}

 

註釋:

編譯res和xml資源 /frameworks/base/tools/aapt/Resource.cpp

ps:改函數較長,截取部分代碼分步解析

status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
    // First, look for a package file to parse.  This is required to
    // be able to generate the resource information.
    sp<AaptGroup> androidManifestFile =
            assets->getFiles().valueFor(String8("AndroidManifest.xml"));
    if (androidManifestFile == NULL) {
        fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n");
        return UNKNOWN_ERROR;
    }

    status_t err = parsePackage(bundle, assets, androidManifestFile);
    if (err != NO_ERROR) {
        return err;
    }

    ......
}

首先解析manifest文件,調用的是parsePackage函數,解析之前,manifest被封裝成一個AaptGroup對象

static status_t parsePackage(Bundle* bundle, const sp<AaptAssets>& assets,
    const sp<AaptGroup>& grp)
{
    if (grp->getFiles().size() != 1) {
        fprintf(stderr, "warning: Multiple AndroidManifest.xml files found, using %s\n",
                grp->getFiles().valueAt(0)->getPrintableSource().string());
    }

    sp<AaptFile> file = grp->getFiles().valueAt(0);

    ResXMLTree block;
    status_t err = parseXMLResource(file, &block);
    if (err != NO_ERROR) {
        return err;
    }
    ......省略代碼
    return NO_ERROR;
}

沒有具體細看裏面的代碼,說下具體思路,通過傳進來的形參AaptGroup拿到具體的AaptFile對象。在調用公共類的parseXmlResource解析xml文件得到具體數據後,存放在對象ResXmlTree中。parseXMLResource函數在類 frameworks/base/tools/aapt/XMLNode.cpp 中。有興趣的可以自己去讀下,這裏就不貼了。解析玩manifest.xml後,我們繼續buildResources的分析。

ResourceTable::PackageType packageType = ResourceTable::App;
    ......省略的代碼
    if (bundle->getBuildSharedLibrary()) {
        packageType = ResourceTable::SharedLibrary;
    } else if (bundle->getExtending()) {
        packageType = ResourceTable::System;
    } else if (!bundle->getFeatureOfPackage().isEmpty()) {
        packageType = ResourceTable::AppFeature;
    }

    ResourceTable table(bundle, String16(assets->getPackage()), packageType);
    err = table.addIncludedResources(bundle, assets);
    if (err != NO_ERROR) {
        return err;
    }
    ...省略的代碼

這段代碼的目的主要是收集當前編譯的資源需要依賴的的資源並且存放在ResourceTable這個數據結構中。這邊簡單介紹一下ResourceTable這個數據結構,首先我們得知道R.java裏面的資源標識id的構成,比方說 0x7f040002 其中0x7f表示是packageID,也就是上面的packageType,它是一個命名空間,限定資源的來源,7f表明是當前應用程序的資源,系統的資源是以0x01開頭。04 表示TypeID。資源的類型animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。最後四位是EntryID,指的是每一個資源在起對應的TypID中出現的順序。

 

而ResouceTable裏面存儲的最核心的元素就是這個id的區分。

收集完成當前應用依賴的資源以後,就要編譯當前應用自己的資源。這裏由於代碼太過於複雜,本人也沒有完全看懂,就不貼了,邏輯基本上就是通過命令的輸出,一個個的編譯資源文件和png。然後存儲在一個xml文件中,爲後面生成R.java文件中做準備。實際上前面也有提到,所有的資源都會存在ResouceTable這個數據結構中,做完編譯工作以後,只需要去遍歷這個向量表,然後對裏面的packageID,typeID,EvtryID進行拼接,就可以得到我們所熟悉的 0x7f040002這種資源ID。ResouceTable的構造函數也可以看出來裏面的過程:

ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
    : mAssetsPackage(assetsPackage)
    , mPackageType(type)
    , mTypeIdOffset(0)
    , mNumLocal(0)
    , mBundle(bundle) {
    ssize_t packageId = -1;
    switch (mPackageType) {
        case App:
        case AppFeature:
            packageId = 0x7f;
            break;

        case System:
            packageId = 0x01;
            break;

        case SharedLibrary:
            packageId = 0x00;
            break;

        default:
            assert(0);
            break;
    }
    sp<Package> package = new Package(mAssetsPackage, packageId);
    mPackages.add(assetsPackage, package);
    mOrderedPackages.add(package);

    // Every resource table always has one first entry, the bag attributes.
    const SourcePos unknown(String8("????"), 0);
    getType(mAssetsPackage, String16("attr"), unknown);
}

完成上述的編譯資源的工作以後,細心的讀者就會發現,對於manifest.xml一直都是讀取裏面的配置信息,並沒有編譯,所以最後一步就是把manifest.xml編譯成二進制文件。這個就不貼出源碼了。

最後一步,將上述的編譯結果輸出到R.java和Apk中。其中還會輸出混淆文件,java符號表等。

...省略代碼
// Write out R.java constants
    if (!assets->havePrivateSymbols()) {
        if (bundle->getCustomPackage() == NULL) {
            // Write the R.java file into the appropriate class directory
            // e.g. gen/com/foo/app/R.java
            err = writeResourceSymbols(bundle, assets, assets->getPackage(), true,
                    bundle->getBuildSharedLibrary());
        } else {
            const String8 customPkg(bundle->getCustomPackage());
            err = writeResourceSymbols(bundle, assets, customPkg, true,
                    bundle->getBuildSharedLibrary());
        }
        if (err < 0) {
            goto bail;
        }
        // If we have library files, we're going to write our R.java file into
        // the appropriate class directory for those libraries as well.
        // e.g. gen/com/foo/app/lib/R.java
        if (bundle->getExtraPackages() != NULL) {
            // Split on colon
            String8 libs(bundle->getExtraPackages());
            char* packageString = strtok(libs.lockBuffer(libs.length()), ":");
            while (packageString != NULL) {
                // Write the R.java file out with the correct package name
                err = writeResourceSymbols(bundle, assets, String8(packageString), true,
                        bundle->getBuildSharedLibrary());
                if (err < 0) {
                    goto bail;
                }
                packageString = strtok(NULL, ":");
            }
            libs.unlockBuffer();
        }
    } else {
        err = writeResourceSymbols(bundle, assets, assets->getPackage(), false, false);
        if (err < 0) {
            goto bail;
        }
        err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true, false);
        if (err < 0) {
            goto bail;
        }
    }
...省略代碼

綜上所述,分析完成了了apk資源編譯的過程,由於本人c++功底不佳,有的東西也只是靠猜測來完成,基本上能夠理清楚大體的邏輯。如果想更詳細的內容,可以自己參考源碼。網上有篇博客,有點亂,但是很細,可以看下 http://www.cnblogs.com/dyllove98/p/3144950.html

AAPT命令修改,完成修改資源ID

在第三節我們講AAPT是如何編譯資源並且生成R.java文件的,也提到R.java中資源ID的含義,在組件化框架中,由於組件和宿主分開編譯,爲了防止組件的資源ID和宿主的資源ID衝突,所以就需要修改AAPT源碼。基本思路就是每個組件分配一個不一樣的ID,宿主的ID是以0x7f開頭,組件的ID是0x**開頭,這樣就避免衝突。



可以看出如果不修改AAPT源碼重新構建,就會導致組件之間或者組件與宿主之間的ID衝突。所以就會有如下模型:

 

既然分析AAPT的編譯過程,那思路就很清晰了,在命令中添加一個自定義的ID,然後在代碼中拿到這個ID,拼接的時候替換上即可。當然這只是最簡單的,而實際情況呢,比方說宿主裏有一部分資源是其他組件公用的,如何保證這部分資源和id和組件本身的id不會發生衝突呢?又如何在我們工程裏自動化這套自定義的aapt從而代替系統標準的aapt呢?


 

 

Android運行時資源替換-Runtime Resource Overlay

 

 

發佈了8 篇原創文章 · 獲贊 3 · 訪問量 1411
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章