在Android中,所有的資源都在res目錄下存放,包括drawable,layout,strings,anim等等,當我們向工程中加入任何一個資源時,會在R類中相應會爲該
資源分配一個id,我們在應用中就是通過這個id來訪問資源的,相信做過Andorid開發的朋友對於這些肯定不會陌生,所以這個也不是我今天想要說的,我今天想和大家一起學習的是android是如何管理資源的,在Android系統中,資源大部分都是通過xml文件定義的(drawable是圖片),如layout,string,anim都是xml文件,而對於layout,anim和strings等xml文件僅僅是解析xml文件,讀取指定的值而已,但是對於layout文件中控件的解析就比較複雜了,例如對於一個Button,需要解析它所有的屬性值,這個是如何實現的呢。
這裏我們首先要考慮一個問題,就是一個控件有哪些屬性是如何定義的?比如TextView具有哪些屬性?爲什麼我設置TextView的樣式只能用style而不能用android:theme?這些信息都是在哪裏定義的,想要弄清楚這個問題,就必須從源碼工程招答案,我使用的是android4.1工程,如果你使用的是其他版本的,那麼可能用些出入。
先看三個文件
1、d:\android4.1\frameworks\base\core\res\res\values\attrs.xml
看到attrs.xml文件,不知道你有沒有想起什麼?當我們在自定義控件的時候,是不是會創建一個attrs.xml文件?使用attrs.xml文件的目的其實就是給我們自定義的控件添加屬性,打開這個目錄後,你會看到定義了一個叫"Theme"的styleable,如下(我只截取部分)
-
<declare-styleable name="Theme">
-
-
-
-
<eat-comment />
-
-
-
<attr name="colorForeground" format="color" />
-
-
<attr name="colorForegroundInverse" format="color" />
-
-
<attr name="colorBackground" format="color" />
在這個文件中,定義了Android中大部分可以使用的屬性,這裏我說的是“定義”而不是“聲明”,同名在語法上面最大的區別就是定義要有format屬性,而聲明沒有format屬性。
2、d:\android4.1\frameworks\base\core\res\res\values\attrs_manifest.xml
這個文件的名字和上面的文件的名字很像,就是多了一個manifest,故名思議就是定義了AndroidManifest.xml文件中的屬性,這裏面有一個很重要的一句話
-
<attr name="theme" format="reference" />
定義了一個theme屬性,這個就是我們平時在Activity上面使用的theme屬性
3、d:\android4.1\frameworks\base\core\res\res\values\themes.xml
這個文件開始定義了一個叫做"Theme" 的sytle,如下(截圖部分)
-
<style name="Theme">
-
-
<item name="colorForeground">@android:color/bright_foreground_dark</item>
-
<item name="colorForegroundInverse">@android:color/bright_foreground_dark_inverse</item>
-
<item name="colorBackground">@android:color/background_dark</item>
-
<item name="colorBackgroundCacheHint">?android:attr/colorBackground</item>
這個就是我們平時在Application或者Activity中使用的Theme,從這裏可以看出,Theme也是一種style,那爲什麼style只能永遠View/ViewGorup,而Theme只能用於Activity或者Application呢?先記住此問題,我們後續會爲你解答
我們再來整合這三個文件的內容吧,首先在attrs.xml文件中,定義了Android中大部分的屬性,也就是說以後所有View/Activity中大部分的屬性就是在這裏定義的,然後在attrs_manifest.xml中定義了一個叫做theme的屬性,它的值就是再themes文件中定義的Theme或者繼承自“Theme”的style。
有了上面的知識後,我們再來分析上面說過的兩個問題:
1、TextView控件(其他控件也一樣)的屬性在哪裏定義的。
2、既然Theme也是style,那爲什麼View只能用style,Activity只能使用theme?
所有View的屬性定義都是在attrs.xml文件中的,所以我們到attrs.xml文件中尋找TextView的styleable吧
-
<declare-styleable name="TextView">
-
<!-- Determines the minimum type that getText() will return.
-
The default is "normal".
-
Note that EditText and LogTextBox always return Editable,
-
even if you specify something less powerful here. -->
-
<attr name="bufferType">
-
<!-- Can return any CharSequence, possibly a
-
Spanned one if the source text was Spanned. -->
-
<enum name="normal" value="0" />
-
-
<enum name="spannable" value="1" />
-
-
<enum name="editable" value="2" />
-
</attr>
-
-
<attr name="text" format="string" localization="suggested" />
-
-
<attr name="hint" format="string" />
-
-
<attr name="textColor" />
上面的屬性我只截取了部分,請注意,這裏所有的屬性都是進行“聲明”,你去搜索這個styleable,會發現在TextView的styleable中不會找到theme這個屬性的聲明,所以你給任何一個view設置theme屬性是沒有效果的。請看下面一段代碼就知道爲什麼了。
定義一個attrs.xml
-
<?xml version="1.0" encoding="utf-8"?>
-
<resources>
-
<declare-styleable name="MyTextView">
-
<attr name="orientation">
-
<enum name="horizontal" value="0" />
-
<enum name="vertical" value="1" />
-
</attr>
-
</declare-styleable>
-
</resources>
定義一個MyTextView
-
public class MyTextView extends TextView {
-
private static final String TAG = "MyTextView";
-
public MyTextView(Context context)
-
{
-
super(context);
-
}
-
public MyTextView(Context context, AttributeSet attrs)
-
{
-
super(context, attrs);
-
-
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
-
String value=ta.getString(R.styleable.MyTextView_orientation);
-
Log.d("yzy", "value1--->"+value);
-
ta.recycle();
-
}
-
}
在attrs.xml我爲MyTextView定義了一個orientation屬性,然後再MyTextView的構造函數中去讀取這個屬性,這裏就涉及到TypeArray這個類,我們發現得到TypeArray需要傳入R.style.MyTextView這個值,這個就是系統爲我們訪問MyTextView這個styleable提供的一個id,當我們需要拿到orientation這個屬性的值時,我們通過R.style.MyTextView_orientation拿到,由於MyTextView中沒有定義或者聲明theme屬性,所以我們找不到R.styleable.MyTextView_theme這個id,所以導致我們無法解析它的theme屬性。同樣回到TextView這個styleable來,由於TextView的styleable中沒有定義theme屬性,所以theme對於TextView是沒有用的。所以即使你在TextView裏面加入theme屬性,即使編譯器不會給你報錯,這個theme也是被忽略了的。
我們再來看看Activity的屬性是如何定義的,由於Activity是在AndroidManigest.xml文件中定義的,所以我們到attrs_manifest.xml中查找。
-
<declare-styleable name="AndroidManifestActivity" parent="AndroidManifestApplication">
-
<!-- Required name of the class implementing the activity, deriving from
-
{@link android.app.Activity}. This is a fully
-
qualified class name (for example, com.mycompany.myapp.MyActivity); as a
-
short-hand if the first character of the class
-
is a period then it is appended to your package name. -->
-
<attr name="name" />
-
<attr name="theme" />
-
<attr name="label" />
-
<attr name="description" />
-
<attr name="icon" />
-
<attr name="logo" />
-
<attr name="launchMode" />
-
<attr name="screenOrientation" />
-
<attr name="configChanges" />
-
<attr name="permission" />
-
<attr name="multiprocess" />
-
<attr name="process" />
-
<attr name="taskAffinity" />
-
<attr name="allowTaskReparenting" />
-
<attr name="finishOnTaskLaunch" />
-
<attr name="finishOnCloseSystemDialogs" />
-
<attr name="clearTaskOnLaunch" />
-
<attr name="noHistory" />
-
<attr name="alwaysRetainTaskState" />
-
<attr name="stateNotNeeded" />
-
<attr name="excludeFromRecents" />
-
<!-- Specify whether the activity is enabled or not (that is, can be instantiated by the system).
-
It can also be specified for an application as a whole, in which case a value of "false"
-
will override any component specific values (a value of "true" will not override the
-
component specific values). -->
-
<attr name="enabled" />
-
<attr name="exported" />
-
<!-- Specify the default soft-input mode for the main window of
-
this activity. A value besides "unspecified" here overrides
-
any value in the theme. -->
-
<attr name="windowSoftInputMode" />
-
<attr name="immersive" />
-
<attr name="hardwareAccelerated" />
-
<attr name="uiOptions" />
-
<attr name="parentActivityName" />
-
</declare-styleable>
很明顯,Activity對於的styleable中是聲明瞭theme的,所以它可以解析theme屬性。
上面兩個問題都已經解答完了,下面來討論另一個話題,就是Resources的獲取過程。
在我的另外一篇文章曾經討論過這個話題更深層次理解Context 這裏我們再來學習一下Resources的獲取過程。
在Android系統中,獲取Resources主要有兩種方法,通過Context獲取和PackageManager獲取
首先,我們看看我們通過Context獲取,下面這張圖是Context相關類的類圖
從圖中可以看出,Context有兩個子類,一個是ContextWrapper,另一個是ContextImpl,而ContextWrapper依賴於ContextImpl。結合源碼,我們會發現,Context是一個抽象類,它的真正實現類就是ContextImpl,而ContextWrapper就像他的名字一樣,僅僅是對Context的一層包裝,它的功能都是通過調用屬性mBase完成,該mBase實質就是指向一個ContextImpl類型的變量。我們獲取Resources時就是調用Context的getResources方法,那麼我們直接看看ContextImpl的getResources方法吧
-
@Override
-
public Resources getResources() {
-
return mResources;
-
}
我們發現這個方法很簡單,就是返回mResources屬性,那麼這個屬性是在哪裏 賦值的呢,通過尋找發現,其實就是在創建ContextImpl,通過調用Init進行賦值的(具體邏輯參照《更深層次理解Context》).這裏我先給出getResource方法的時序圖,然後跟蹤源碼。
先從init方法開始吧
-
final void init(LoadedApk packageInfo,
-
IBinder activityToken, ActivityThread mainThread,
-
Resources container, String basePackageName) {
-
mPackageInfo = packageInfo;
-
mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
-
mResources = mPackageInfo.getResources(mainThread);
-
-
if (mResources != null && container != null
-
&& container.getCompatibilityInfo().applicationScale !=
-
mResources.getCompatibilityInfo().applicationScale) {
-
if (DEBUG) {
-
Log.d(TAG, "loaded context has different scaling. Using container's" +
-
" compatiblity info:" + container.getDisplayMetrics());
-
}
-
mResources = mainThread.getTopLevelResources(
-
mPackageInfo.getResDir(), container.getCompatibilityInfo());
-
}
-
mMainThread = mainThread;
-
mContentResolver = new ApplicationContentResolver(this, mainThread);
-
-
setActivityToken(activityToken);
-
}
我們發現,對mResource進行賦值,是通過調用LoadedApk中的getResource進行的,傳入了ActivityThead類型的參數
-
public Resources getResources(ActivityThread mainThread) {
-
if (mResources == null) {
-
mResources = mainThread.getTopLevelResources(mResDir, this);
-
}
-
return mResources;
-
}
在getResources方法中,其實就是調用了ActivityThrad的getTopLevelResources方法,其中mResDir就是apk文件的路徑(對於用戶安裝的app,此路徑就在/data/app下面的某一個apk),從時序圖中可以知道,getTopLevelResources其實就是調用了一個同名方法,我們直接看它的同名方法吧
-
Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
-
ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
-
Resources r;
-
synchronized (mPackages) {
-
-
if (false) {
-
Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
-
+ compInfo.applicationScale);
-
}
-
WeakReference<Resources> wr = mActiveResources.get(key);
-
r = wr != null ? wr.get() : null;
-
-
if (r != null && r.getAssets().isUpToDate()) {
-
if (false) {
-
Slog.w(TAG, "Returning cached resources " + r + " " + resDir
-
+ ": appScale=" + r.getCompatibilityInfo().applicationScale);
-
}
-
return r;
-
}
-
}<span style="font-family: Arial, Helvetica, sans-serif;">;</span>
-
-
-
-
-
-
-
-
AssetManager assets = new AssetManager();
-
if (assets.addAssetPath(resDir) == 0) {
-
return null;
-
}
-
-
-
DisplayMetrics metrics = getDisplayMetricsLocked(null, false);
-
r = new Resources(assets, metrics, getConfiguration(), compInfo);
-
if (false) {
-
Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
-
+ r.getConfiguration() + " appScale="
-
+ r.getCompatibilityInfo().applicationScale);
-
}
-
-
synchronized (mPackages) {
-
WeakReference<Resources> wr = mActiveResources.get(key);
-
Resources existing = wr != null ? wr.get() : null;
-
if (existing != null && existing.getAssets().isUpToDate()) {
-
-
-
r.getAssets().close();
-
return existing;
-
}
-
-
-
mActiveResources.put(key, new WeakReference<Resources>(r));
-
return r;
-
}
-
}
這段代碼的邏輯不復雜,首先從mActiveResouuces中通過key拿到資源,如果資源不爲null,並且是最新的,那麼直接返回,否則創建一個AssetManager對象,並調用AssetManager的addAssetPath方法,然後使用創建的AssetManager爲參數,創建一個Resources對象,保存並返回。通過上面的時序圖,我們發現在創建AssetManager的時候,在其構造函數中調用init方法,我們看看init方法做了什麼吧
-
private native final void init();
居然是一個本地方法,那麼我們只有看看對應的Jni代碼了
-
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
-
{
-
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->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
-
}
這個裏面調用了本地的AssetManager的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);
-
}
這例的ANDROID_ROOT保存的就是/system路徑,而kSystemAssets是
-
static const char* kSystemAssets = "framework/framework-res.apk";
還記得framework-res.apk是什麼嗎,就是系統所有的資源文件。
到這裏終於明白了,原理就是將系統的資源加載進來。
接下來看看addAssetPath方法吧,進入源碼後,你會發現它也是一個本地方法,也需要看jni代碼
-
static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
-
jstring path)
-
{
-
ScopedUtfChars path8(env, path);
-
if (path8.c_str() == NULL) {
-
return 0;
-
}
-
-
AssetManager* am = assetManagerForJavaObject(env, clazz);
-
if (am == NULL) {
-
return 0;
-
}
-
-
void* cookie;
-
bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
-
-
return (res) ? (jint)cookie : 0;
-
}
這裏調用了本地AssetManager方法的addAssetPath方法。和系統資源一樣,都被加載進來了。
下面看看PackageManager獲取Resource的流程吧
在PackageManager裏面獲取資源調用的是getResourcesForApplication方法,getResourcesForApplication也有一個同名方法,我們看辦正事的那個吧,
-
@Override public Resources getResourcesForApplication(
-
ApplicationInfo app) throws NameNotFoundException {
-
if (app.packageName.equals("system")) {
-
return mContext.mMainThread.getSystemContext().getResources();
-
}
-
Resources r = mContext.mMainThread.getTopLevelResources(
-
app.uid == Process.myUid() ? app.sourceDir
-
: app.publicSourceDir, mContext.mPackageInfo);
-
if (r != null) {
-
return r;
-
}
-
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
-
}
首先判斷包名是否是system,如果不是那麼直接調用ActivityThread的getTopLevelResources方法。不過這裏會根據當前應用的應用的uid和進程Id相等,如果相等則傳入app.sourceDir,否則傳入publicSourceDir,但是根據經驗時期sourceDir和publicSource一般情況下是相同的。後面的邏輯和Context中的是一樣的,這裏就不在說了。