TypedArray流程分析

作者: CP9 | 来源:发表于2017-06-02 16:29 被阅读369次
    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

    相关文章

      网友评论

        本文标题:TypedArray流程分析

        本文链接:https://www.haomeiwen.com/subject/nzidtxtx.html