一、定義資源
Aandroid 中的資源從類型的角度來看包括:drawable、layout、字符串、顏色值、menu、animation 等。
資源的定義可以分為兩類:
- 屬性的定義
- 值的定義
二、儲存資源
Android 和資源有關的“屬性”包括:
- attr
- styleable
- style
- theme
2.1、styleable 和 attr
attr 表示屬性,風格樣式的最小單元。
styleable 一般和 attr 聯合使用,用于定義一些屬性,理論上可以不使用 styleable 而只是使用 attr,示例代碼如下:
<element
attr name="flower_name" format="string"
attr name="flower_color" format="color|reference"
attr name="flower_types" format="string"
...
</element>
對于以上定義,當解析 XML 時,會返回一個 AtrributeSet 對象,該對象包括了 element 中包含的所有屬性和值。通過使用 R.id.flower_name 作為參數可以得到相應的取值,但是這種方式并不在能體現以上幾個屬性聯合在一起的意義,于是引入了 styleable 的概念,示例代碼如下:
<declare-styleable name="Flower">
<attr name="flower_name" format="string"/>
<attr name="flower_color" format="color|reference"/>
<attr name="flower_types" format="string">
<enum name="rose" value="Rose"/>
<enum name="tulips" value="Tulips"/>
</attr>
</declare-styleable>
這樣從 AtrributeSet 中獲取這三個屬性值時,就可以把 R.styleale.Flower 作為參數,該參數實際會被 aapt 編譯成為一個 int[] 數組,數組的內容就是所包含的 attr 的 id。
2.2、Style
Style 直譯為“風格”,它是一系列 Attr 的集合用來定義一個 View 的樣式,比如 height、width、padding 等,可以理解為是 Attr 屬性集合的具體組合實現,示例代碼如下:
<style name="Netherlands">
<item name="flower_name">yahoo</item>
<item name="flower_color">@color/Amber</item>
<item name="flower_types">Rose</item>
</style>
以上 XML 文件就實現了 Flower 中定義的屬性,并將這些具體的屬性這定義為一個 Style。
2.3、theme
Theme 直譯為“主題”,而主題本身是一個抽象化的概念,比如 Window 系統中的主題包括了桌面背景、系統圖標、字體大小以及按鈕風格等,在 Android 中,從代碼角度來看,主題包括三個方面,其出處和用法如下:
-
在 frameworks/base/core/res/res/values/attrs.xml 中定義了一個名稱為 Theme 的 styleable:
<declare-styleable name="Theme">
其中包括了幾十項屬性,包括窗口樣式、字體顏色、大小、按鈕樣式、進度條等。這些屬性大部分只能用在 AndroidManifest 文件中的 Application 和 Activity 中,而不能用于普通的 View/ViewGroup 中。
原因很簡單,attrs 已經為 View/ViewGroup 定義了可用的屬性,而 styleable 只是為了方便訪問一些特定的 attr 集合,而把這些 attr 放在一個 styleable 中,而 theme 只是把適用于 Application 和 Activity 的屬性單獨放到了一起。
-
在 frameworks/base/core/res/res/values/attrs_manifest.xml 中定義了一個名稱為 theme 的屬性:
<attr name="theme" format="reference"/>
該屬性只有定義到 AndroidManifest 中才有意義。進一步講,只有定義到 Application 和 Activity 元素內部才是有意義的,因為只有在這兩個類中才能讀取 theme 屬性的值。theme 的屬性賦值是一個 reference,其引用類型是 style,即 theme 應該賦值到某個 style,而 該 style 中所包含的屬性應該是 Application 和 Activity 中可以識別的屬性值,也就是名稱為 theme 的 styleable 中定義的屬性。
-
在 frameworks/base/core/res/res/values/themes.xml 中定義了一系列名稱為 theme 的 style:
<style name="Theme"> <item name="isLightTheme">false</item> <item name="colorForeground">@color/bright_foreground_dark</item> <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item> <item name="colorBackground">@color/background_dark</item> ... </Style>
這些 style 將作為 Application 和 Activity 中 theme 中的值,而這些 theme 中所包含的屬性屬性名稱全部取自與 styleable name = “Theme” 處。
三、AttributeSet 與 TypedArray
3.1、AttributeSet
AttributeSet 類的位置在 android.util 包中,從該類的位置看,該類與 Framework 內核沒有任何直接的聯系,而純粹是一個輔助類。該類僅僅是為了解析 XML 文件用的。AttributeSet 類的代碼如下(只列出部分方法):
public interface AttributeSet {
public int getAttributeCount();
public String getAttributeName(int index);
public String getAttributeValue(int index);
...
public String getClassAttribute();
public int getIdAttributeResourceValue(int defaultValue);
public int getStyleAttribute();
}
一般 XML 文件有以下形式:
<ElementName
attr_name1="value1"
attr_name2="value2"
attr_name3="value3"
...
</ElementName>
如果使用一般的 XML 解析工具,則可以通過類似 getElementById() 等方法獲得屬性的名稱和屬性值,然而卻沒有在屬性名稱和 attrs.xml 定義的屬性名稱建立任何聯系。而 AttributeSet 類正是在這之間建立了某中聯系,并提供了一些新的 API 接口,從而可以方便根據 attrs.xml 已有的名稱獲得相應的屬性值。
在開發中,通常并不關心 AttributeSet 接口如何實現,只需要通過該該對象獲取相應的屬性值即可,比如在 TextView 的構造函數中:
public TextView(Context context, AttributeSet attrs) {
super((Context)null, (AttributeSet)null, 0, 0);
throw new RuntimeException("Stub!");
}
AttributeSet 中的 API 可按功能劃分為以下幾類,假定該 XML 的格式為:
<View class="android.widget.TextView"
android:layout_width="@dimem/general_width"
android:layout_height="wrap_content"
android:text="@string/title"
android:id="@+id/output"
style="@style/Test"
/>
-
第一類,操作特定屬性,包括以下幾類:
- public String getIdAttribute(): 獲取 id 屬性對應的字符串,返回值為“@+id/output”。
- public String getClassAttribute(): 獲取 class 對應的字符串,返回值為“android.widget.TextView”。
- public String getStyleAttribute(): 獲取 style 對應的字符串,返回值“@style/Test”。
- public int getIdAttributeResourceValue(int defaultValue): 返回 id 屬性對應的 int 值,此處應該是 R.id.output 的值。
-
第二類,操作通用屬性,包括以下幾類:
public int getAttributeCount(): 獲取屬性的數目。
-
public String getAttributeName(int index): 根據屬性所在的位置返回相應的屬性名稱。本例中,相應的屬性位置如下:
class=0 layout_width=1 layout_height=2 text=3 id=4 style=5
如果 index 為 1,則返回 android:layout_width。
public String getAttributeValue(int index): 根據位置返回屬性值。本例中,如果 index 為 1,則返回“@dimem/general_width”。
public String getAttributeValue(String nameSpase, String name):返回值定命名空間、指定名稱的屬性值,該函數說明 AttributeSet 允許給一個 XML Element 的屬性中添加多個命名空間的屬性值。
public int getAttributeNameResource(int index):返回指定位置的屬性 id 值。本例中,如果 index 為 1,則返回 R.id.layout_width,系統為每一個屬性(attr)分配了惟一的 id 值。
-
第三類,獲取特定類型的值。
-
public XXXType getAttributeXXXTypeValue(int index,XXXType defaultValue);
其中 XXXType 包括 int、unsigned int、boolean、以及 float 類型,使用該方法時,必須明確知道某個位置(index)對應的數據類型。而且該方法僅適用于特定的類型,如果某個屬性的值為 style 類型,或是一個 layout 類型,那么返回值將無效。
-
3.2、TypedArray
TypedArray 是對 AttributeSet 的某種抽象。
在上面的例子中,對于 android:layout_width="@dimem/general_width",如果使用 AttributeSet 只能獲取“@dimem/general_width”字符串,而實際上該字符串對應了一個 dimem 類型的數據,因此還要去解析 id 為 general_width 對應的具體的 dimen 的值。TypedArray 正是免去了這個過程,可以將 AttributeSet 作為參數來構造 TypedArray,TypedArray 提供更加方便的方法來直接獲取該 dimen 的值。
從一個 AttributeSet 構造 TypedArray 對象方法代碼如下:
TypedArray a = context.obtainStyleAttribute(attrs, com.android.internal.R.styleable.XXX, defStyle, 0);
函數 obtainStyleAttribute 的第一個參數為一個 AttributeSet 對象,它包含了一個 XML 元素中所定義的所有屬性。第二個參數就是前面定義的 styleable,
aapt 會把 TypedArray 編譯為一個 int[] 數組,該數組所包含的內容正是通過遍歷 AttributeSet 中的每一個屬性,然后把值和屬性經過重定位,返回一個 TypedArray 對象。
下面分析 TypedArray 類的內部接口和重要成員變量。
該類的重要成員變量包括:
int[] mData;
-
/package/ TypedValue mValue = new TypedValue(): TypedValue 是一個數據類,其意義是為了保存一個屬性值,比如 layout_width、textSize、textColor 等,該類中有四個重要成員變量:
- int type:類型包括 int、boolean、float、String、reference 等;
- int data:如果 type 是一個 int、boolean、float、類型,則 data 包含了具體的數據;
- int referenceId:如果 type 是一個 reference 類型,那么該值為對應的 reference id;
- CharSequence string:如果 type 是一個 String 類型,則該值為具體的 String。
mValue 起到了一個內部緩存的作用。mData 則包含了指定 styleable 中的所有屬性值,mData 的長度為 styleable 中屬性的的個數 × AssetManager.STYLE_NUM_ENTRIES
(該值為 6)。也就是說需要 6 個 int 來表示一個屬性值,以下為 AssetManager 中這 6 個值的定義:
/*package*/ static final int STYLE_NUM_ENTRIES = 6;
/*package*/ static final int STYLE_TYPE = 0;
/*package*/ static final int STYLE_DATA = 1;
/*package*/ static final int STYLE_ASSET_COOKIE = 2;
/*package*/ static final int STYLE_RESOURCE_ID = 3;
/* Offset within typed data array for native changingConfigurations. */
static final int STYLE_CHANGING_CONFIGURATIONS = 4;
/*package*/ static final int STYLE_DENSITY = 5;
在以上常用值中,常用的包含了三個:
- STYLE_TYPE(0):包含了值得類型;
- STYLE_DATA(1):包含了特定的值;
- STYLE_RESOURCE_ID(3):如果 STYLE_TYPE 為一個 reference 類型,該值對應了相應的 resource id;
下面來看 TypedArray 如何使用以上幾個成員變量。當在 XML 中引用某個資源時,比如:
android:background=“@drawable/bkg”
該引用對應的元素一般是某個 View/ViewGroup,View/ViewGroup 的構造函數中一般會通過函數 obtainStyleAttributes() 方法返回一個 TypedArray 對象,然后再調用該對象中的相應 getDrawable() 方法。
下面以 getString() 和 getDrawable() 為例說明 TypedArray 內部接口的工作原理。getString() 代碼如下:
@Nullable
public String getString(@StyleableRes int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
index *= AssetManager.STYLE_NUM_ENTRIES;
final int[] data = mData;
final int type = data[index+AssetManager.STYLE_TYPE];
if (type == TypedValue.TYPE_NULL) {
return null;
} else if (type == TypedValue.TYPE_STRING) {
return loadStringValueAt(index).toString();
}
final TypedValue v = mValue;
if (getValueAt(index, v)) {
final CharSequence cs = v.coerceToString();
return cs != null ? cs.toString() : null;
}
// We already checked for TYPE_NULL. This should never happen.
throw new RuntimeException("getString of bad type: 0x" + Integer.toHexString(type));
}
首先傳遞進來的 index 必須先乘以 6,因為每一個屬性值都站用連續的 6 個 int 值。每一個 styleable 都將被 aapt 編譯為一個 int[] 數組,數組中的內容為 styleable 所包含的每一個屬性(attr)對應的 id 的值,在調用 getString() 時,其參數 index 是該屬性在 styleable 中的位置。當定義了一個 styleable 時, aapt 同時生成了 attr 在 styleable 中的位置,比如 TextView 是一個 styleable,其中包含的 attr 有 textSize,aapt 會自動生成一個 TextView_textSize 常量。該常量的名稱格式是固定的,其形式為“styleable 名稱 _attr 名稱”。
接著從 mData 中取出值得類型,即 index+AssetManager.STYLE_TYPE 處,然后判斷類型,如果為 STYLE_NULL,說明無該屬性值,返回 null,如果類型為 STYLE_STRING,則調用 loadStringValueAt() 方法找到 String 并返回。
接著看 loadStringValueAt(),代碼如下:
private CharSequence loadStringValueAt(int index) {
final int[] data = mData;
final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
if (cookie < 0) {
if (mXml != null) {
return mXml.getPooledString(
data[index+AssetManager.STYLE_DATA]);
}
return null;
}
return mAssets.getPooledStringForCookie(cookie, data[index+AssetManager.STYLE_DATA]);
}
該方法先從 mData 中取出 cookie,如果 cookie 小于 0 并且 mXml 存在,則會從 mXml(用于解析 XML 文件) 中得到 String 的值。mXml 內部有一個 String 池,通過 mData 的 STYLE_DATA 為索引可以得到哦相應的 String 值。如果 cookie 大于 0,那么 cookie 將作為 mResource.mAssets的內部方法 getPooledString() 的參數。cookie 將作為 mAssets 內部 mXml 的索引,而 Data 的 STYLE_DATA 將作為字符串索引。
下面看 getDrawable() 的流程:
@Nullable
public Drawable getDrawable(@StyleableRes int index) {
return getDrawableForDensity(index, 0);
}
/**
* Version of {@link #getDrawable(int)} that accepts an override density.
* @hide
*/
@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
if (density > 0) {
// If the density is overridden, the value in the TypedArray will not reflect this.
// Do a separate lookup of the resourceId with the density override.
mResources.getValueForDensity(value.resourceId, density, value, true);
}
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
四、獲取 Resources 的過程
獲取 Resources 有兩種方式,一是通過 Context,而是通過 PackageManager。
4.1、通過 Context 獲取
在開發中,通常是通過 getResources().getXXX() 方法來獲取 XML 中的指定資源,比如 getDrawable()、getString()、getBoolean() 等。
首先看 getResources() 方法,該方法是 Context 的成員函數,一般在 Activity 或是 Service 中調用,因為 Activity 和 Service 本質上是一個 Context,而真正實現 Context 接口的是 ContextImpl 類。
ContextImpl 是在 ActivityThread 中創建的,它的 getResources() 方法就是返回其內部的 mResources 變量,對該變量的賦值是在創建 ContextImpl 對象時進行初始化的,代碼如下(API 27):
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
...
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
...
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
if (container != null) {
// This is a nested Context, so it can't be a base Activity context.
// Just create a regular Resources object associated with the Activity.
resources = mResourcesManager.getResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
} else {
// This is not a nested Context, so it must be the root Activity context.
// All other nested Contexts will inherit the configuration set here.
resources = mResourcesManager.createBaseActivityResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
}
}
}
mResources = resources;
...
可以看出 mResources 是調用 mPackageInfo 的 getResources() 方法進行賦值的。一個用應用中的多個 ContextImpl 對象實際上共享了同一個 PackageInfo 對象,這就意味著,多個 ContextImpl 對象中的 mResources 變量實際上是同一個。
packageInfo(類型為:LoadedApk)的 getResources() 方法如下:
public Resources getResources(ActivityThread mainThread) {
if(this.mResources == null) {
this.mResources = mainThread.getTopLevelResources(this.mResDir, this.mSplitResDirs, this.mOverlayDirs, this.mApplicationInfo.sharedLibraryFiles, 0, (Configuration)null, this);
}
return this.mResources;
}
ActivityThread 的 getTopLevelResources 的邏輯是得到本應用的對應的資源對象。代碼如下:
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, LoadedApk pkgInfo) {
return this.mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), (IBinder)null);
// ResourcesManager#getTopLevelResources
public Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
float scale = compatInfo.applicationScale;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
synchronized(this) {
WeakReference<Resources> wr = (WeakReference)this.mActiveResources.get(key);
r = wr != null?(Resources)wr.get():null;
if(r != null && r.getAssets().isUpToDate()) {
return r;
}
}
AssetManager assets = new AssetManager();
if(resDir != null && assets.addAssetPath(resDir) == 0) {
return null;
} else {
int len$;
int i$;
String libDir;
String[] arr$;
if(splitResDirs != null) {
arr$ = splitResDirs;
len$ = splitResDirs.length;
for(i$ = 0; i$ < len$; ++i$) {
libDir = arr$[i$];
if(assets.addAssetPath(libDir) == 0) {
return null;
}
}
}
if(overlayDirs != null) {
arr$ = overlayDirs;
len$ = overlayDirs.length;
for(i$ = 0; i$ < len$; ++i$) {
libDir = arr$[i$];
assets.addOverlayPath(libDir);
}
}
if(libDirs != null) {
arr$ = libDirs;
len$ = libDirs.length;
for(i$ = 0; i$ < len$; ++i$) {
libDir = arr$[i$];
if(assets.addAssetPath(libDir) == 0) {
Slog.w("ResourcesManager", "Asset path '" + libDir + "' does not exist or contains no resources.");
}
}
}
DisplayMetrics dm = this.getDisplayMetricsLocked(displayId);
boolean isDefaultDisplay = displayId == 0;
boolean hasOverrideConfig = key.hasOverrideConfiguration();
Configuration config;
if(isDefaultDisplay && !hasOverrideConfig) {
config = this.getConfiguration();
} else {
config = new Configuration(this.getConfiguration());
if(!isDefaultDisplay) {
this.applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
}
if(hasOverrideConfig) {
config.updateFrom(key.mOverrideConfiguration);
}
}
// 重點,使用構造方法創建 Resources
r = new Resources(assets, dm, config, compatInfo, token);
synchronized(this) {
WeakReference<Resources> wr = (WeakReference)this.mActiveResources.get(key);
Resources existing = wr != null?(Resources)wr.get():null;
if(existing != null && existing.getAssets().isUpToDate()) {
r.getAssets().close();
return existing;
} else {
this.mActiveResources.put(key, new WeakReference(r));
return r;
}
}
}
}
// Resources
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
// ResourcesImpl
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
變量 mActiveResources 對象內部保存了該應用所使用到的所有 Resources 對象,其類型為:
ArrayMap<ResourcesKey, WeakReference<Resources>>
參數 ResourcesKey 是一個數據類,其構造方式如下:
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
重點看第一個參數,resDir 變量為源文件路徑,實際就是 APK 程序所在路徑,比如可以是:/data/app/com.serah.android.Kaiyan.apk,該 APK 會對應 /data/dalvik-cache 目錄下的:data@app@com.serah.android.Kaiyan.apk@classes.dex 文件。
所以,如果一個應用沒有訪問該程序外的其他資源,那么 mActiveResources 中只含有一個 Resources 對象。如果 mActiveResources 中沒有包含所要的 Resources 那么,就重新建立一個 Resources 并添加到 mActiveResources 中。
可以發現 Resources 構造函數需要一個 AssetManager 對象,AssetManager 負責訪問 res 下的所有資源(不只是 res/assets 目錄下資源),AssetManager 中幾個關鍵函數都是 native 的。以上代碼 assets.addAssetPath(resDir) 函數非常關鍵,它為所創建的 AssetManager 對象添加資源路徑,剩下的事就 AssetManager 內部完成,內部會從指定的路徑下加載資源文件,AssetManager 構造函數如下:
public AssetManager() {
synchronized(this) {
this.init(false);
ensureSystemAssets();
}
}
構造方法中的來兩個關鍵函數 init() 和 ensureSystemAssets() 都是 native 實現的。
init() 用于初始化 AssetManager 內部的環境變量,初始化過程的一個關鍵任務就是把 Framework 中的資源路徑添加到 AssetManager 中,該 native 代碼如下(android_content_AssetManager_init.cpp):
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
if (isSystem) {
verifySystemIdmaps();
}
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
am->addDefaultAssets();
ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}
以上代碼首先創建一個 AssetManager 類,這是一個 C++ 類,然后調用 am->addDefaultAssets() 將 Framework 的資源文件添加到這個 AssetManager 對象的路徑中。最后調用 SetLongField() 方法將 C++ 創建的 AssetManager 對象引用保存到 java 端的 mObject 變量中,該變量可以在 java 端的 AssetManager 類中找到, 其類型為 int。
addDefaultAssets() 代碼如下:
bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
String8 path(root);
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
}
該函數首先獲取 Android 根目錄, getenv() 是一個 Linux 系統調用,用戶同樣可以使用以下終端命令來獲取:
keyid:tools keyid$ ./adb shell
root@android:/ # echo $ANDROID_ROOT
/system
root@android:/ #
獲取根目錄后,在與 kSystemAssets 路徑進行組合,該變量定義如下:
static const char* kSystemAssets = "framework/framework-res.apk";
所以最中獲得的文件路徑為:/system/framework/framework-res.apk, 這正是 Framework 對應的資源文件。
分析完 init() 后,接著看 ensureSystemAssets() 方法,該方法實際在 Framework 啟動時調用,因為 mSystem 是一個 static 變量,該變量在 Zygote 啟動時已經被賦值。
private static void ensureSystemAssets() {
Object var0 = sSync;
synchronized(sSync) {
if(sSystem == null) {
AssetManager system = new AssetManager(true);
system.makeStringBlocks((StringBlock[])null);
sSystem = system;
}
}
}
因為應用程序中 Resources 對象內部的 AssetManager 對象除了包含應用程序本身的資源文件路徑外,還包含了 Framework 的資源路徑,這就是為什么僅使用本地 Resources 就能訪問系統的資源的原因。
在 AssetManager.cpp 文件中,當使用 getXXX(int id) 訪問資源時,如果 id 小于 0x1000 0000 時,AssetManager 會認為是訪問系統資源。因為 aapt 在對系統資源進行編譯時,所有資源 id 都被編譯為小于該值的一個 int 值, 而當訪問應用程序資源時,id 值都會大于 0x7000 0000。
創建好 Resources 對象后,就把該對象緩存到 mActiveResources 中,方便以后繼續使用。以上就是訪問 Resources 的整個流程。
4.2、通過 PackageManager 獲取
該方法用于訪問其他程序中的資源,使用 PackageManager 獲取資源的代碼如下:
try {
PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.serah.android.Kaiyan");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
和其他 Manager 一樣,PackageManager 負責和遠程 PackageManagerService 進行通信,獲取PackageManager 代碼如下(ContextImpl中實現):
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null;
}
PackageManager 本身是一個抽象類,其實現類是 ApplicationPackageManager,該類的構造函數包含了遠程服務的一個引用,即 IPackageManager,該對象是通過 getPackageManager() 靜態方法的到的,這種獲取遠程服務的方法和大多數獲取遠程服務的方法類似,代碼如下(ActivityThread#getPackageManager()):
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}
即得到一個本地代理,獲得代理后調用 getResourcesForApplication() 方法,該方法代碼在 ApplicationPackageManager 中實現,代碼如下:
@Override
public Resources getResourcesForApplication(@NonNull ApplicationInfo app)
throws NameNotFoundException {
if (app.packageName.equals("system")) {
return mContext.mMainThread.getSystemUiContext().getResources();
}
final boolean sameUid = (app.uid == Process.myUid());
final Resources r = mContext.mMainThread.getTopLevelResources(
sameUid ? app.sourceDir : app.publicSourceDir,
sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
mContext.mPackageInfo);
if (r != null) {
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}
以上代碼調用了 mMainThread.getTopLevelResources,這和從 Contex 中獲取 Resource 過程一致。
需要注意的是這里的參數,其含義是:如果目標資源程序和當前程序是同一個 uid 那么就使用目標程序的 sourceDir 作為路徑,否則就使用目標程序的publicSourceDir 目錄,該目錄可以在 AndroidManifest.xml 中指定。在多數情況下,目標程序和當前程序都不屬于一個 uid,因此,多為 publicSourceDir,而該值在默認情況下和 sourceDir 的值相同。
當進入 mMainThread.getTopLevelResources() 方法后,全局 ActivityThread 對象就會在 mActiveResources 中保存一個新的 Resources 對象,其鍵值對應目標應用程序的包名。
五、Framework 資源
了解了 Resources 的獲取流程后,本節將介紹系統資源的加載、讀取、添加過程,至于系統資源是如何被編譯的,將在后續文章中進行分析(aapt 編譯
framework/base/core/res/res 目錄下資源,并生成 framework-res.apk)。
5.1、加載和讀取
系統資源是在 Zygote 進程啟動時被加載的,并且只有當加載了系統資源之后才開始啟動其他應用進程,從而實現其他應用進程共享系統資的目標。該過程源碼在 com/android/internal/os/ZygoteInit.java 的 main() 函數,核心代碼如下:
public static void main(String argv[]) {
ZygoteHooks.startZygoteNoThreadCreation();
try {
...
// 加載系統資源
preload();
...
// 啟動系統進程
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
runSelectLoop(abiList);
closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (Throwable ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
}
}
以上函數內部過程可以分為三步:
- 加載系統資源;
- 調用 startSystemServer() 啟動系統進程;
- 調用 runSelectLoop() 開始監聽 Socket,并啟動指定的應用進程。
本文主要分析第一步,即加載系統資源,該過程具體是通過 preloadResources() 實現的,該函數代碼如下:
private static void preloadResources() {
final VMRuntime runtime = VMRuntime.getRuntime();
try {
mResources = Resources.getSystem();
mResources.startPreloading();
if (PRELOAD_RESOURCES) {
Log.i(TAG, "Preloading resources...");
long startTime = SystemClock.uptimeMillis();
TypedArray ar = mResources.obtainTypedArray(
com.android.internal.R.array.preloaded_drawables);
int N = preloadDrawables(ar);
ar.recycle();
Log.i(TAG, "...preloaded " + N + " resources in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(
com.android.internal.R.array.preloaded_color_state_lists);
N = preloadColorStateLists(ar);
ar.recycle();
Log.i(TAG, "...preloaded " + N + " resources in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
if (mResources.getBoolean(
com.android.internal.R.bool.config_freeformWindowManagement)) {
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(
com.android.internal.R.array.preloaded_freeform_multi_window_drawables);
N = preloadDrawables(ar);
ar.recycle();
Log.i(TAG, "...preloaded " + N + " resource in "
+ (SystemClock.uptimeMillis() - startTime) + "ms.");
}
}
mResources.finishPreloading();
} catch (RuntimeException e) {
Log.w(TAG, "Failure preloading resources", e);
}
}
以上代碼有兩個關鍵點:
- 第一點,創建 Resources 對象 mResources 是通過 Resources.getSystem() 函數。該函數返回的 Resources 對象只能訪問 Framework 中定義的系統資源。
getSystem() 函數內部會調用一個 private 類型的 Resources 構造函數,該函數內部調用 AssetManager 類的靜態方法 AssetManager.getSystem() 為變量 mAssets 賦值,從而保證了 Resources 類內部 mSystem 變量對應為系統資源,mSystem 是 static 類型的。
從這里可以看出,zygote 中創建 Resources 對象和普通應用程序的不同,前者使用靜態的 getSystem()方法,而后者使用帶有參數的 Resources 構造函數創建,參數間接包含了應用程序資源文件的路徑信息。
- 第二點,有了包含系統資源的 Resources 后,接下來調用兩個重要函數:preloadDrawables 和 preloadColorStateLists,裝載需要的“預裝載”資源。
首先看 preloadDrawables():
private static int preloadDrawables(TypedArray ar) {
int N = ar.length();
for (int i=0; i<N; i++) {
int id = ar.getResourceId(i, 0);
if (false) {
Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
}
if (id != 0) {
if (mResources.getDrawable(id, null) == null) {
throw new IllegalArgumentException(
"Unable to find preloaded drawable resource #0x"
+ Integer.toHexString(id)
+ " (" + ar.getString(i) + ")");
}
}
}
return N;
}
該函數的參數是一個 TypedArray 對象,其來源是 res/values/arrays.xml 中定義的一個 array 數組資源,名稱為 preloaded_drawable,以下是該資源的代碼片段:
<array name="preloaded_drawables">
<item>@drawable/ab_share_pack_material</item>
<item>@drawable/ab_solid_shadow_material</item>
<item>@drawable/action_bar_item_background_material</item>
<item>@drawable/activated_background_material</item>
......
</array>
因此,需要想要讓所有的應用進程共享預裝的資源,則需要在該文件中生命資源的名稱。
接下來看 preloadColorStateLists:
private static int preloadColorStateLists(TypedArray ar) {
int N = ar.length();
for (int i=0; i<N; i++) {
int id = ar.getResourceId(i, 0);
if (false) {
Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
}
if (id != 0) {
if (mResources.getColorStateList(id, null) == null) {
throw new IllegalArgumentException(
"Unable to find preloaded color resource #0x"
+ Integer.toHexString(id)
+ " (" + ar.getString(i) + ")");
}
}
}
return N;
}
該函數的參數同樣是一個 TypedArray, 來源同樣是來自 res/values/arrays.xml 中定義的一個數組資源,名稱為 preloaded_color_state_lists,該資源代碼片段如下所示:
<array name="preloaded_color_state_lists">
<item>@color/primary_text_dark</item>
<item>@color/primary_text_dark_disable_only</item>
<item>@color/primary_text_dark_nodisable</item>
.....
</array>
以上便是加載資源的來源,接著,在 Resources 類中相關資源讀取函數中則需要將讀到的資源緩存起來,為了這個目地,Resources 中定義了四個靜態變量,
如下所示:
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
= new LongSparseArray<>();
private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>>
sPreloadedComplexColors = new LongSparseArray<>();
private static boolean sPreloaded;
前三個都是類表變量,并且是 static 的,正是由于變量類型為 static 所以導致 Resources 類在被應用進程創建新的對象時,保存了 zygote 進程中所預裝載的資源。
由于 zygote 進程在從 framework-res.apk 中裝載資源的實現方式和普通進程基本相同,可以通過 sPreloaded 變量區分是從 zygote 中還是普通進程調用,該變量在 startPreloading() 中被設置為 true,在 finishPreloading() 中被設置為 false,該過程參見 ZygoteInit 的 preloadResources() 函數。
5.2、添加
以上加載的資源僅僅是加載 res/values/arrays.xml 中的資源,而 Framework 中的資源遠不止于此,以設計者角度來看,對于那些非“預裝載”的系統資源不會被添加到靜態列表中,在這種情況下,多個應用進程如果需要一個非預裝載的資源,則會在各自的進程中保持一個資源的緩沖。
至于是否是“預裝載”的,僅僅取決于該資源始否在 res/values/arrays.xml中。
系統資源按照被公開的方式分為公開的和私有的,總的來說只要是放在 res 目錄下的資源都會被 aapt 編譯,而所謂的私有資源是指僅能在 Framework 內部訪問的資源,公開資源是指使用 SDK 的應用程序也能訪問的系統資源。
假設要添加一個字符串資源:
<string name="cus_str">Custom string</string>
那么,可以直接把以上代碼添加到 res/values/arrays.xml 中,然后重新編譯 Framework,編譯完畢后,在 com.android.internal.R.java 文件中,就會包含該字符串的聲明,然后在 Framework 的源碼中就可以直接引用該資源。
需要注意的是,cus_str 被存放到了 com.android.internal.R 文件中,而不是 android.R 文件中,這正是公開資源和私有資源的區別所在,使用 SDK 開發普通應用時,當要引用系統資源時,只能引用 android.R 文件,該文件的內容可以被認為是 com.android.internal.R 的一個子集。
如前所述,res 目錄下的資源總會被 aapt 編譯到一個 R.java 文件中,這個文件就是 com.android.internal.R 文件,而 android.R 文件則是來源于
res/values/public.xml 中定義的資源。
res/values/public.xml 為所有需要公開到 SDK 中的資源進行 id 的預先定義,這些 id 值在不同的 Android 版本中保持一致,從而保證了 Android 版本的資源兼容性。
想要將前面的 cus_str 公開到 SDK 中,需要在 public.xml 文件中聲明該字符串,為新資源指定 id 時必須要考慮來兩個個問題:
- 不能與已有的 id 沖突;
- 盡量避免與未來的 id 沖突。
本例中,就可以給 public.xml 文件添加以下代碼:
<public type="string" name="cus_str" id="0x0104f000" />
id 的含義是,01 代表這是一個 Framework 資源,04 代表著是一個 string 類型的資源,f000 是該資源的編號,之所以從 f000 開始,是因為 Framework 內部的資源是從 0 開始的,防止以后遞增時與我們自己定義的資源值沖突。
六、android 生成資源 id
先看一下 apk 的打包流程,Android Developer 官方流程,下面對官方流程做了系統的總結。
下圖的是官網對于 Android 編譯打包流程的介紹:
虛線方框是打包 APK 的操作,現在開發 Android 都是使用的 Android Studio 基于 gradle 來構建項目,所有打包操作都是執行 gradle 腳本來完成,gradle 編譯腳本具有強大的功能,可以在里面完成多渠道,多版本,不同版本使用不同代碼,不同的資源,編譯后的文件重命名,混淆簽名驗證等等配置,雖然都是基于AndroidSdk 的 platform-tools 的文件夾下面的工具來完成的,但是有了 gradle 這個配置文件,這樣就便捷了。
以下是一張 APK 打包詳細步驟流程圖:
具體步驟:
- 打包資源文件,生成 R.java 文件;
- 處理 aidl 文件,生成相應的 .java 文件;
- 編譯工程源碼,生成相應的 class 文件;
- 轉換所有的 class 文件,生成 classes.dex 文件;
- 打包生成 apk;
- 對 apk 文件進行簽名;
- 對簽名后的 apk 進行對齊處理。
本文只分析 aapt 對資源文件的編譯過程。
6.1、aapt
資源 id 的生成過程主要是調用 aapt 源碼目錄下的 Resouce.cpp 的 buildResources() 函數,該函數首先檢查 AndroidManifest.xml 的合法性,然后對 res 目錄下的資源目錄進行處理,處理函數為 makeFileResource(),處理的內容包括資源文件名的合法性檢查,向資源表 table 添加條目等。處理完后調用 compileResourceFile() 函數編譯 res 與 asserts 目錄下的資源并生 resource.arsc 文件,compileResourceFile() 函數位于 appt 源碼目錄的 ResourceTable.cpp 文件中,該函數最后會調用 parseAndAddEntry() 函數生成 R.java 文件,完成資源編譯后,接下來調用 compileXmlfile() 函數對 res 目錄的子目錄下的 xml 文件進行編譯,這樣處理過的 xml 文件就簡單的被"加密"了,最后將所有資源與編譯生成的 resource.arsc 文件以及"加密"過的 AndroidManifest.xml 打包壓縮成 resources.ap_ 文件。
上面涉及的源碼代碼位置在:
- buildResources() 函數在:Resouce.cpp 1144 行;
- makeFileResource() 函數在:Resouce.cpp 297 行;
- compileResourceFile() 函數在:ResourceTable.cpp 782 行;
- parseAndAddEntry() 函數在:ResourceTable.cpp 690 行;
- compileXmlfile() 函數在:ResourceTable.cpp 42/57/73 行;
aapt 編譯后的輸出文件包括:
- resources.ap_ 文件
- R.java 文件
打包資源的工具 aapt,大部分文本格式的 XML 資源文件會被編譯成二進制格式的 XML 資源文件,除了 assets 和 res/raw 資源被原封不動地打包進 APK 之外,其他資源都會被編譯或者處理。
注意,除了 assets 和 res/raw 資源被原封不動地打包進 APK 之外,其它的資源都會被編譯或者處理。除了 assets 資源之外,其他的資源都會被賦予一個資源 id。
resources.arsc 是清單文件,但是 resources.arsc 跟 R.java 區別還是非常大的,R.java 里面的只是 id 列表,并且里面的 id 值不重復。但是 drawable-xdpi 或者 drawable-xxdpi 這些不同分辨率的文件夾存放的圖片和名稱和 id 是一樣的,在運行的時候就需要 resources.arsc 這個文件了,resources.arsc 里面會對所有的資源 id 進行組裝,在 apk 運行是會根據設備的情況來采用不同的資源。resource.arsc 文件的作用就是通過一樣的 id,根據不同的配置索引到最佳的資源現在 UI 中。
可以這樣理解:R.java 是我們在寫代碼時候引用的 res 資源的 id 表,resources.arsc 是程序在運行時候用到的資源表。R.java 是給程序員讀的,resources.arsc 是給機器讀的。
大體情況如下:
6.2、資源 id 生成規則
資源 id 是一個 32bit的數字,格式是 PPTTNNNN,其中 PP 代表資源所屬的包(package),TT 代表資源的類型(type),NNNN 代表這個類型下面的資源的名稱。
對于應用程序的資源來說,PP 的取值是 0×7f。TT 和 NNNN 的取值是由 aapt 工具隨意指定的–基本上每一種新的資源類型的數字都是從上一個數字累加的(從1開始);而每一個新的資源條目也是從數字 1 開始向上累加的。
所以如果我們的這幾個資源文件按照下面的順序排列,aapt 會依次處理:
<code>layout/main.xml </code>
<code>drawable/icon.xml </code>
<code>layout/listitem.xml</code>
按照順序,第一個資源的類型是”layout” 所以指定 TT==1,這個類型下面的第一個資源是”main”,所以指定 NNNN==1 ,最后這個資源就是 0x7f010001。
第二個資源類型是”drawable”,所以指定 TT==2,這個類型下的”icon” 指定 NNNN==1,所以最終的資源 ID 是 0x7f020001。