TypedArray流程分析

澳门新葡亰3522平台游戏 1

在自定义控件的时候,如果我们想额外的添加一些属性,就会用到TypedArray这个类,那么这个类是怎么得到的,以及怎么使用的,这篇讲会详细讲解,下面是我以前自定义控件的一段代码

澳门新葡亰3522平台游戏 1

TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.myaccount_item_style);

澳门新葡亰3522平台游戏,obtainStyledAttributes_flow.png

我们看到TypedArray是通过Context的方法得到的,但要记住完成之后一定要调用recycle()方法进行回收,我们点进去找到最终实现

Context#obtainStyledAttributes

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

在Context中的getTheme方法是抽象方法,那我们得看他的子类的具体实现,我们一般会在自定义View的时候调用此方法,而自定义View中的Context是传进来的,一般指的是它显示的Activity,我们在Activity中搜索getTheme方法,会搜索到它的父类ContextThemeWrapper中,那我们来看看ContextThemeWrapper中getTheme怎么实现的:

public TypedArray obtainStyledAttributes(AttributeSet set,  
        int[] attrs, int defStyleAttr, int defStyleRes) {  
    final int len = attrs.length;  
    final TypedArray array = TypedArray.obtain(Resources.this, len);  

    // XXX note that for now we only work with compiled XML files.  
    // To support generic XML files we will need to manually parse  
    // out the attributes from the XML file (applying type information  
    // contained in the resources and such).  
    final XmlBlock.Parser parser = (XmlBlock.Parser)set;  
    AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,  
            parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);  

    array.mTheme = this;  
    array.mXml = parser;  

……………  
    return array;  
}

ContextThemeWrapper#getTheme

  1. 根据版本选择默认主题并保存在mThemeResource中

mThemeResource = Resources.selectDefaultTheme(mThemeResource,
        getApplicationInfo().targetSdkVersion);
  1. 初始化主题

initializeTheme();

在initializeTheme方法内部的实现原理:最终调用了AssetManager的native方法applyThemeStyle

我们先看下面AssetManager的applyStyle方法是native方法,也就是用C++实现的,他会提取自定义控件属性的的值保存TypedArray中的mData数组中,这个数组的大小是由你定义控件属性的个数决定的,是他的6倍,上面的attrs其实就是你自定义属性的个数,我们来看一下

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来设置。

static TypedArray obtain(Resources res, int len) {  
    final TypedArray attrs = res.mTypedArrayPool.acquire();  
    if (attrs != null) {  
        attrs.mLength = len;  
        attrs.mRecycled = false;  

        final int fullLen = len * AssetManager.STYLE_NUM_ENTRIES;  
        if (attrs.mData.length >= fullLen) {  
            return attrs;  
        }  

        attrs.mData = new int[fullLen];  
        attrs.mIndices = new int[1 + len];  
        return attrs;  
    }  

    return new TypedArray(res,  
            new int[len*AssetManager.STYLE_NUM_ENTRIES],  
            new int[1+len], len);  
}

Context#obtainStyledAttributes

return getTheme().obtainStyledAttributes(
    set, attrs, defStyleAttr, defStyleRes);

他首先会从TypedArray池中获取,如果有就取出,mDate的大小不能小于属性个数的6倍,因为STYLE_NUM_ENTRIES的值为6,如果没有就new一个然后返回,把属性的值提取出来之后我们就可以来操作了,我们先来看一下View类初始化中的一段代码

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;
final int N = a.getIndexCount();  
for (int i = 0; i < N; i++) {  
    int attr = a.getIndex(i);  
    switch (attr) {  
        case com.android.internal.R.styleable.View_background:  
            background = a.getDrawable(attr);  
            break;  
…………………  
    }  
}

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中的数据提取出来对View的属性赋值,我们来看一下TypedArray类的构造方法

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);
/*package*/ TypedArray(Resources resources, int[] data, int[] indices, int len) {  
    mResources = resources;  
    mMetrics = mResources.mMetrics;  
    mAssets = mResources.mAssets;  
    mData = data;  
    mIndices = indices;  
    mLength = len;  
}

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;

代码很简单,其中mData就是就是从xml文件中提取到的数据,mData的大小是自定义属性个数的6倍,所以这里是每6个作为一组,我们可以看一下上面的obtain方法中data数组的大小是乘以6(STYLE_NUM_ENTRIES)的,这6种类型如下,定义在AssetManager类中,下面的第一个表示每组6个

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;
/*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;  
/*package*/ static final int STYLE_CHANGING_CONFIGURATIONS = 4;  
/*package*/ static final int STYLE_DENSITY = 5;

总结

  1. TypedArray是用来检索项目中各种资源的
  2. 只有当 defStyleAttr 为 0
    或者无法在对应的主题下找到资源文件时才起作用,defStyleRes中定义的默认样式才起作用
  3. TypedArray检索完资源,必须调用recycle方法来循环使用

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

对应着TypedValue类中的这7中类型,其中string是根据type得到的

/** The type held by this value, as defined by the constants here. 
 *  This tells you how to interpret the other fields in the object. */  
public int type;  

/** If the value holds a string, this is it. */  
public CharSequence string;  

/** Basic data in the value, interpreted according to {@link #type} */  
public int data;  

/** Additional information about where the value came from; only 
 *  set for strings. */  
public int assetCookie;  

/** If Value came from a resource, this holds the corresponding resource id. */  
public int resourceId;  

/** If Value came from a resource, these are the configurations for which 
 *  its contents can change. */  
public int changingConfigurations = -1;  

/** 
 * If the Value came from a resource, this holds the corresponding pixel density. 
 * */  
public int density;

如果我们认真看的时候就会发现obtain方法中对mIndices数组初始化的时候是加1的,因为mIndices数组的第一个保存的是我们所使用属性的个数,记住是使用不是定义,我们来看一下其中的一些代码

/** 
 * Return the number of values in this array. 
 */  
public int length() {  
    if (mRecycled) {  
        throw new RuntimeException("Cannot make calls to a recycled instance!");  
    }  

    return mLength;  
}  

/** 
 * Return the number of indices in the array that actually have data. 
 */  
public int getIndexCount() {  
    if (mRecycled) {  
        throw new RuntimeException("Cannot make calls to a recycled instance!");  
    }  

    return mIndices[0];  
}