TypedArray流程分析

obtainStyledAttributes_flow.png

Context#obtainStyledAttributes

// 調用Resources.Theme的obtainStyledAttributes方法
return getTheme().obtainStyledAttributes(attrs);

在Context中的getTheme方法是抽象方法,那我們得看他的子類的具體實現,我們一般會在自定義View的時候調用此方法,而自定義View中的Context是傳進來的,一般指的是它顯示的Activity,我們在Activity中搜索getTheme方法,會搜索到它的父類ContextThemeWrapper中,那我們來看看ContextThemeWrapper中getTheme怎么實現的:

ContextThemeWrapper#getTheme

  1. 根據版本選擇默認主題并保存在mThemeResource中
mThemeResource = Resources.selectDefaultTheme(mThemeResource,
        getApplicationInfo().targetSdkVersion);
  1. 初始化主題
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

  1. 調用TypedArray的obtain方法
final TypedArray array = TypedArray.obtain(Resources.this, len);
  1. 調用本地方法給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

  1. 從Resource.mTypedArrayPool(SynchronizedPool<TypedArray>)池中取TypedArray對象
final TypedArray attrs = res.mTypedArrayPool.acquire();
...
  1. 沒取到,則調用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
注意:

  1. 不能重復調用該方法,否則會拋出以下異常:
Caused by: java.lang.RuntimeException: [0, 0, 1, 0, ...] recycled twice!
  1. 不能在調用該方法后,還調用getxxx等TypedArray的方法,否則回拋出以下異常:
Caused by: java.lang.RuntimeException: Cannot make calls to a recycled instance!

TypedArray#getInt

  1. 根據下標index獲取mData數組存儲的Type類型的值,判斷Type是否為TypedValue.TYPE_NULL,true則,返回默認值defValue
...
final int type = data[index+AssetManager.STYLE_TYPE];
if (type == TypedValue.TYPE_NULL) {
            return defValue;
        } 
  1. 根據下標index獲取data、assetCookie、resourceId、changingConfigurations、density等類型的值,并存儲在TypedValue中
getValueAt(index, v)
  1. 通過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

  1. 轉換負數
if ('-' == nm.charAt(0)) {
    sign = -1;
    index++;
}
  1. 轉換十六進制和八進制
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;
    }
}
  1. 轉換顏色數值
else if ('#' == nm.charAt(index))
{
    index++;
    base = 16;
}
  1. 將String轉換成數值
Integer.parseInt(nm.substring(index), base) * sign;

總結

  1. TypedArray是用來檢索項目中各種資源的
  2. 只有當 defStyleAttr 為 0 或者無法在對應的主題下找到資源文件時才起作用,defStyleRes中定義的默認樣式才起作用
  3. TypedArray檢索完資源,必須調用recycle方法來循環使用

參考:
A deep dive into Android View constructors
http://blog.csdn.net/luoshengyang/article/details/8738877

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容