Context#obtainStyledAttributes
// 調用Resources.Theme的obtainStyledAttributes方法
return getTheme().obtainStyledAttributes(attrs);
在Context中的getTheme方法是抽象方法,那我們得看他的子類的具體實現,我們一般會在自定義View的時候調用此方法,而自定義View中的Context是傳進來的,一般指的是它顯示的Activity,我們在Activity中搜索getTheme方法,會搜索到它的父類ContextThemeWrapper中,那我們來看看ContextThemeWrapper中getTheme怎么實現的:
ContextThemeWrapper#getTheme
- 根據版本選擇默認主題并保存在mThemeResource中
mThemeResource = Resources.selectDefaultTheme(mThemeResource,
getApplicationInfo().targetSdkVersion);
- 初始化主題
initializeTheme();
在initializeTheme方法內部的實現原理:最終調用了AssetManager的native方法applyThemeStyle
Context#obtainStyledAttributes
Context類中有4個obtainStyledAttributes
, 最終調用的都是4個參數的obtainStyledAttributes
方法,而最終調用的是ResourcesImpl.ThemeImpl的obtainStyledAttributes
方法。讓我們看看Context的obtainStyledAttributes
方法的4個參數分別代表著什么:
- AttributeSet set :AttributeSet是在布局中定義的一系列屬性的集合,包括系統定義的屬性。在下列例子中,如
layout_width
,還有自定義的屬性,如MyProgress
。 - @StyleableRes int[] attrs :自定義屬性集合,在下列例子中,如
R.styleable.MyView
。 - @AttrRes int defStyleAttr :在當前主題中有一個引用指向樣式文件,這個樣式文件將 TypedArray 設置默認值。如果此參數為0即表示不進行默認值設置。在下列例子中,如
R.attr.DefaultViewStyleAttr
。 - @StyleRes int defStyleRes :默認的樣式資源文件,只有當 defStyleAttr 為 0 或者無法在對應的主題下找到資源文件時才起作用。如果此參數為0即表示不進行默認值設置。在下列例子中,如
R.style.DefaultViewStyleRes
。
以自定義View為例,現在創建一個MyView:
public MyView(Context context, @Nullable AttributeSet attrs) {
Logger logger = Logger.getLogger("MyView");
for (int i = 0; i < attrs.getAttributeCount(); i++) {
logger.info(attrs.getAttributeName(i) + " : " + attrs.getAttributeValue(i));
}
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView, R.attr.DefaultViewStyleAttr, R.style.DefaultViewStyleRes);
logger.info("-----------------------------------------");
logger.info("MyText1的最終值" + " : " + a.getString(R.styleable.MyView_MyText1));
logger.info("MyText2的最終值" + " : " + a.getString(R.styleable.MyView_MyText2));
logger.info("MyText3的最終值" + " : " + a.getString(R.styleable.MyView_MyText3));
logger.info("MyText4的最終值" + " : " + a.getString(R.styleable.MyView_MyText4));
logger.info("MyText5的最終值" + " : " + a.getString(R.styleable.MyView_MyText5));
}
在attr.xml中自定義屬性:
<declare-styleable name="MyView">
<attr name="MyText1" format="string" />
<attr name="MyText2" format="string" />
<attr name="MyText3" format="string" />
<attr name="MyText4" format="string" />
<attr name="MyText5" format="string" />
</declare-styleable>
在styles.xml中自定義style
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="DefaultViewStyleAttr">@style/MyViewStyleAttr</item>
<item name="MyText1">"defStyleAttr中提供的默認的屬性值,在主題中直接定義"</item>
<item name="MyText2">"defStyleAttr中提供的默認的屬性值,在主題中直接定義"</item>
<item name="MyText3">"defStyleAttr中提供的默認的屬性值,在主題中直接定義"</item>
<item name="MyText4">"defStyleAttr中提供的默認的屬性值,在主題中直接定義"</item>
</style>
<style name="MyViewStyle">
<item name="MyText1">"XML中在style里定義的屬性值"</item>
<item name="MyText2">"XML中在style里定義的屬性值"</item>
</style>
<style name="DefaultViewStyleRes">
<item name="MyText1">"defStyleRes中提供的默認的屬性值"</item>
<item name="MyText2">"defStyleRes中提供的默認的屬性值"</item>
<item name="MyText3">"defStyleRes中提供的默認的屬性值"</item>
<item name="MyText4">"defStyleRes中提供的默認的屬性值"</item>
<item name="MyText5">"defStyleRes中提供的默認的屬性值"</item>
</style>
<attr name="DefaultViewStyleAttr" format="reference" />
<style name="MyViewStyleAttr">
<item name="MyText1">"defStyleAttr中提供的默認的屬性值,在主題中的style里定義"</item>
<item name="MyText2">"defStyleAttr中提供的默認的屬性值,在主題中的style里定義"</item>
<item name="MyText3">"defStyleAttr中提供的默認的屬性值,在主題中的style里定義"</item>
</style>
在布局文件中引用這個MyView:
<com.cn.zero.gesture.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/MyViewStyle"
app:MyText1="XML中直接定義的屬性值" />
運行之后輸出的結果是:
I/MyView: layout_width : -1
I/MyView: layout_height : -2
I/MyView: MyText1 : XML中直接定義的屬性值
I/MyView: style : @style/MyViewStyle
I/MyView: -----------------------------------------
I/MyView: MyText1的最終值 : XML中直接定義的屬性值
I/MyView: MyText2的最終值 : XML中在style里定義的屬性值
I/MyView: MyText3的最終值 : defStyleAttr中提供的默認的屬性值,在主題中的style里定義
I/MyView: MyText4的最終值 : defStyleAttr中提供的默認的屬性值,在主題中直接定義
I/MyView: MyText5的最終值 : null
從上面的結果來看,xml attributes > xml style > theme style defStyleAttr > theme defStyleAttr > defStyleRes
**上面例子的代碼不變,我們將defStyleAttr設為0,如: **
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, R.style.DefaultViewStyleRes);
運行之后輸出的結果是:
I/MyView: layout_width : -1
I/MyView: layout_height : -2
I/MyView: MyText1 : XML中直接定義的屬性值
I/MyView: style : @style/MyViewStyle
I/MyView: -----------------------------------------
I/MyView: MyText1的最終值 : XML中直接定義的屬性值
I/MyView: MyText2的最終值 : XML中在style里定義的屬性值
I/MyView: MyText3的最終值 : defStyleRes中提供的默認的屬性值
I/MyView: MyText4的最終值 : defStyleRes中提供的默認的屬性值
I/MyView: MyText5的最終值 : defStyleRes中提供的默認的屬性值
此時,MyText3、MyText4、MyText5的最終值都變成了在DefaultViewStyleRes
中定義的屬性的值了。可以得知在defStyleAttr
中檢索不到值,才會去取defStyleRes
中設置的值。
一般設置屬性的默認值,都會使用defStyleRes
來設置。
Context#obtainStyledAttributes
return getTheme().obtainStyledAttributes(
set, attrs, defStyleAttr, defStyleRes);
ResourcesImpl.ThemeImpl#obtainStyledAttributes
- 調用TypedArray的obtain方法
final TypedArray array = TypedArray.obtain(Resources.this, len);
- 調用本地方法給array(TypedArray)的mData、mIndices賦值
AssetManager.applyStyle(mTheme, 0, 0, 0, attrs, array.mData, array.mIndices);
查看本地方法applyStyle
的具體實現,是在mData數組中存儲了六種類型的數據,分別為:
- STYLE_TYPE
- STYLE_DATA
- STYLE_ASSET_COOKIE
- STYLE_RESOURCE_ID
- STYLE_CHANGING_CONFIGURATIONS
- STYLE_DENSITY
- STYLE_NUM_ENTRIES
在base/core/jni/android_util_AssetManager.cpp
查看android_content_AssetManager_applyStyle
// Write the final value back to Java.
dest[STYLE_TYPE] = value.dataType;
dest[STYLE_DATA] = value.data;
dest[STYLE_ASSET_COOKIE] = block != kXmlBlock ?
static_cast<jint>(res.getTableCookie(block)) : -1;
dest[STYLE_RESOURCE_ID] = resid;
dest[STYLE_CHANGING_CONFIGURATIONS] = typeSetFlags;
dest[STYLE_DENSITY] = config.density;
TypedArray#obtain
- 從Resource.mTypedArrayPool(SynchronizedPool<TypedArray>)池中取TypedArray對象
final TypedArray attrs = res.mTypedArrayPool.acquire();
...
- 沒取到,則調用TypedArray的構造方法
return new TypedArray(res,
new int[len*AssetManager.STYLE_NUM_ENTRIES],
new int[1+len], len);
在TypedArray中我們會看到很多getxxx()
方法,我們點進去看會發現基本上都會有這么一行代碼:
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
猜測:這段代碼會不會和每次在自定義View中取完自定義屬性之后調用的typedArray.recycle();
有關?
if (mRecycled) {
throw new RuntimeException(toString() + " recycled twice!");
}
mRecycled = true;
// These may have been set by the client.
...
mResources.mTypedArrayPool.release(this);
查看recycle()
方法,可以知道Android要求我們在每次不再使用TypedArray
時,必須手動調用該方法以復用TypedArray
。
注意:
- 不能重復調用該方法,否則會拋出以下異常:
Caused by: java.lang.RuntimeException: [0, 0, 1, 0, ...] recycled twice!
- 不能在調用該方法后,還調用getxxx等TypedArray的方法,否則回拋出以下異常:
Caused by: java.lang.RuntimeException: Cannot make calls to a recycled instance!
TypedArray#getInt
- 根據下標index獲取mData數組存儲的Type類型的值,判斷Type是否為TypedValue.TYPE_NULL,true則,返回默認值defValue
...
final int type = data[index+AssetManager.STYLE_TYPE];
if (type == TypedValue.TYPE_NULL) {
return defValue;
}
- 根據下標index獲取data、assetCookie、resourceId、changingConfigurations、density等類型的值,并存儲在TypedValue中
getValueAt(index, v)
- 通過XmlUtils.convertValueToInt方法將諸如"-12,0xa1,014,#fff"這類字符串轉化為真正的數值
return XmlUtils.convertValueToInt(v.coerceToString(), defValue);
TypedArray$getValueAt
將mData數組數組中的數據存儲在TypedValue中:
...
outValue.type = type;
outValue.data = data[index+AssetManager.STYLE_DATA];
outValue.assetCookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
outValue.resourceId = data[index+AssetManager.STYLE_RESOURCE_ID];
outValue.changingConfigurations = data[index+AssetManager.STYLE_CHANGING_CONFIGURATIONS];
outValue.density = data[index+AssetManager.STYLE_DENSITY];
outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null;
XmlUtils$convertValueToInt
- 轉換負數
if ('-' == nm.charAt(0)) {
sign = -1;
index++;
}
- 轉換十六進制和八進制
if ('0' == nm.charAt(index)) {
// Quick check for a zero by itself
if (index == (len - 1))
return 0;
char c = nm.charAt(index + 1);
if ('x' == c || 'X' == c) {
index += 2;
base = 16;
} else {
index++;
base = 8;
}
}
- 轉換顏色數值
else if ('#' == nm.charAt(index))
{
index++;
base = 16;
}
- 將String轉換成數值
Integer.parseInt(nm.substring(index), base) * sign;
總結
- TypedArray是用來檢索項目中各種資源的
- 只有當 defStyleAttr 為 0 或者無法在對應的主題下找到資源文件時才起作用,defStyleRes中定義的默認樣式才起作用
- TypedArray檢索完資源,必須調用recycle方法來循環使用
參考:
A deep dive into Android View constructors
http://blog.csdn.net/luoshengyang/article/details/8738877