前言
一个好的 APP 总是不断演进,版本迭代的同时跟随着产品形态的变化,自定义 View 算是 Android 开发中常用满足产品需求的技巧之一。
定义 declare-styleable
中的自定义属性;
构造函数,初始化自定义属性;
实现 onMeasure
、onLayout
和 onDraw
等方法。
使用上面这几个步骤,根据自己的具体逻辑,一个自定义 View 就可以简单使用了。现在要关注的是一个不起眼的家伙,构造函数中的 defStyleAttr
参数。 ## 探寻
首先看看 Button 源码中的几个构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Button (Context context) {
this (context, null );
}
public Button (Context context, AttributeSet attrs) {
this (context, attrs, com.android.internal.R.attr.buttonStyle);
}
public Button (Context context, AttributeSet attrs, int defStyleAttr) {
this (context, attrs, defStyleAttr, 0 );
}
public Button (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super (context, attrs, defStyleAttr, defStyleRes);
}
可以观察到第一个构造方法调用的是第二个构造方法,第二个调用的是第三个构造方法,最终前三个构造方法调用都是第四个构造方法。这里有几种情况:
使用 new Button(context)
直接实例化一个 Button 会调用第一个方法;
在 .xml 文件中使用 Button 调用第二个构造方法。
这里重点关注第二个构造方法。方法的第一参数是 Context,第二个参数是我们自定义属性的集合,那么第三个参数是什么?进入源码继续看,最后来到 View 的代码,看到对于该参数的解释:
An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
大意是:当前主题中一个包含 style 资源引用(Style 中有该 View 默认属性值集合)的值,这个引用对应的资源属性/值会填充 attrs 中没有声明的属性。如果是 0 则不会寻找默认属性值填充。
对上面的解释进行验证,分别使用第二个和第三个构造方法:
1
2
3
4
Button button1 = new Button(this , null );
Button button2 = new Button(this , null , 0 );
button1.setText("button1" );
button2.setText("button2" );
效果:
运行可以发现 button1 有 Button 预置的一些基础属性(如背景、点击效果等),而 button2 没有。其中 button1 的预置属性从 com.android.internal.R.attr.buttonStyle
中获得。
更近一步,我们知道了这个参数是为一个 View 提供基础的属性,下面尝试实现这样的功能:
1
2
3
4
5
<resources >
<declare-styleable name ="AppTheme" >
<attr name ="myButtonStyle" format ="reference" />
</declare-styleable >
</resources >
在我们当前的 Theme 中,为上面定义的 attribute 添加一个 style
1
2
3
4
5
6
7
8
<resources >
<style name ="AppTheme" parent ="Theme.AppCompat.Light.DarkActionBar" >
<item name ="myButtonStyle" > @style/MyButtonStyle</item >
</style >
<style name ="MyButtonStyle" parent ="@style/Widget.AppCompat.Button" >
<item name ="android:textColor" > @android:color/holo_red_dark</item >
</style >
</resources >
其中 style 继承自 Button style,但修改了 Button 文字颜色为红色。
在自定义 View 中使用自定义 attribute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyButton extends Button {
public MyButton (Context context) {
this (context, null );
}
public MyButton (Context context, AttributeSet attrs) {
this (context, attrs, R.attr.myButtonStyle);
}
public MyButton (Context context, AttributeSet attrs, int defStyleAttr) {
this (context, attrs, defStyleAttr, 0 );
}
public MyButton (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super (context, attrs, defStyleAttr, defStyleRes);
}
}
最后在 .xml 中使用 MyButton,由于默认填充使用了 Button style(文字颜色被修改为红色),所以样式如下:
看完 defStyleAttr
,第四个构造方法中的 defStyleRes
参数又引起了我们的注意,进入 View 的源码,同样可以看到对于该参数的解释:
A resource identifier of a style resource that supplies default values for the view, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
大意是:为 View 提供默认值的一个样式资源标识符(不局限于当前 Theme 中),仅在 defStyleAttr
为 0 或 defStyleAttr
指定的 style 中无法找到默认值。如果设置为 0 无效。
继续看:
1
2
3
4
Button button3 = new Button(this , null , 0 , 0 );
Button button4 = new Button(this , null , 0 , android.R.style.Widget_Button_Small);
button3.setText("button3" );
button4.setText("button4" );
效果(连同第一、二种情况对比):
这里设置 defStyleRes 为 android.R.style.Widget_Button_Small
style,相比默认的 Button style 有区别。
分析
attribute 值的确定过程分析
利用 Context 的 obtainStyledAttributes
方法,可以将属性值取回到一个 TypedArray 中(为什么使用 TypedArray )。
一个 attribute 值的确定过程大致如下:
xml 中查找,若未找到进入第 2 步;
xml 中的 style 查找,若未找到进入第 3 步;
若 defStyleAttr 不为 0,由 defStyleAttr 指定的 style 中寻找,若未找到进入第 4 步;
若 defStyleAttr 为 0 或 defStyleAttr 指定的 style 中寻找失败,进入 defStyleRes 指定的 style 中寻找,若寻找失败,进入第 5 步查找;
查找在当前 Theme 中指定的属性值。
进入 TextView 的源码,一路找寻 obtainStyledAttributes
的调用链,如下(tl;dr):
TextView
1
2
3
4
public TextView (Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super (context, attrs, defStyleAttr, defStyleRes);
}
View
1
2
3
4
5
public View (Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
}
Context
1
2
3
public final TypedArray obtainStyledAttributes (AttributeSet set, @StyleableRes int [] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return getTheme().obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}
Resource.Theme
1
2
3
public TypedArray obtainStyledAttributes (AttributeSet set, @StyleableRes int [] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this , set, attrs, defStyleAttr, defStyleRes);
}
来到 Resource 类,该类的作用就是帮助我们获取 Application 的资源,其中内部类 Theme 持有当前主题中所有定义的属性值(也就是上面说到的确定 attribute 值的第 5 步)。方法中调用了 mThemeImpl
的 obtainStyledAttributes
方法,ThemeImpl 类就是 Theme 类的"实现",进入到 ThemeImpl 类 obtainStyledAttributes
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TypedArray obtainStyledAttributes (@NonNull Resources.Theme wrapper, AttributeSet set, @StyleableRes int [] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
synchronized (mKey) {
final int len = attrs.length;
final TypedArray array = TypedArray.obtain(wrapper.getResources(), len);
final XmlBlock.Parser parser = (XmlBlock.Parser) set;
AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes, parser != null ? parser.mParseState : 0 , attrs, array.mData, array.mIndices);
array.mTheme = wrapper;
array.mXml = parser;
return array;
}
}
这里 obtain 了我们需要的 TypedArray,根据之前说过的规则通过调用 AssetManager 的 applyStyle
方法(本地方法),确定了最后各个 attribute 的值。
下面看看 android_util_AssetManager.cpp 中 android_content_AssetManager_applyStyle
函数的源码,里面有我们需要的 native applyStyle
方法(代码很长,只保留了注释):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static jboolean android_content_AssetManager_applyStyle (JNIEnv* env, jobject clazz, jint themeToken, jint defStyleAttr, jint defStyleRes, jint xmlParserToken, jintArray attrs, jintArray outValues, jintArray outIndices)
{
for (
}
return JNI_TRUE;
}
到此,attribute 值的查找过程结束。attribute 值的确定是按照一系列规则来最终确定的。
看看 TypedArray 这个类
使用 TypedArray 类可以帮助我们简化获取 attribute 值的流程。类介绍也表明了其作用:
ontainer for an array of values that were retrieved with Resources.Theme#obtainStyledAttributes
or Resources#obtainAttributes
. [Be sure to call recycle
when done with them.]
The indices used to retrieve values from this structure correspond to the positions of the attributes given to obtainStyledAttributes.
注意上面用 [] 括起来的一句话:用完之后必须调用 recycle
方法。对,我们通常都会这么做,但是为什么要这么做? 查看这个方法源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void recycle () {
if (mRecycled) {
throw new RuntimeException(toString() + " recycled twice!" );
}
mRecycled = true ;
mXml = null ;
mTheme = null ;
mAssets = null ;
mResources.mTypedArrayPool.release(this );
}
其中主要就是释放了相应的资源,注意看到 mResources.mTypedArrayPool.release(this);
这一行代码,mTypedArrayPool 是 Resource 类中的一个同步对象(存储 TypedArray 对象)池,这里使用了 Pool 来进行优化。
既然是用了 Pool,那就肯定有获取对象的方法,焦点来到 obtain
方法:
1
2
3
4
5
6
7
8
9
static TypedArray obtain (Resources res, int len) {
final TypedArray attrs = res.mTypedArrayPool.acquire();
if (attrs != null ) {
return attrs;
}
return new TypedArray(res, new int [len*AssetManager.STYLE_NUM_ENTRIES], new int [1 +len], len);
}
简单总结这两个方法如下:
recycle
方法就相当于 Pool 中的 release,用于归还对象到 Pool 中;
obtain
方法就相当于 Pool 中的 acquire,用于从 Pool 中请求对象。
对于 mTypedArrayPool 的大小 Android 默认是 5。对象池不能太大也不能太小,太大可能造成内存占用,太小可能造成无效对象或有无对象池无明显效果等问题。具体大小的设置,是需要根据具体的场景结合数据分析得到。
Android 应用程序就是由大量 View 构成,因此 View 成了最经常使用的对象。一个 View 创建过程中有大量的 attributes 需要设置,Android 使用了 TypedArray 来简化流程,当频繁的创建和销毁对象(对象的创建成本还比较大)时,会有一定的成本及比较差的体验(如内存抖动导致掉帧)。通过使用 Pool 来实现对 TypedArray 的缓存和复用,达到优化的目的。
TypedArray 中还有很多类似 getDrawable
的方法用于从 TypedArray 中根据索引获取值,下面就看看 getDrawable
方法,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Drawable getDrawable (@StyleableRes int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!" );
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException("Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null ;
}
首先是进行一系列判断,最后调用 mResources.loadDrawable
方法获取 drawable,这里调用的是 Resource 类的方法,Resource.loadDrawable
又调用 ResourceImpl.loadDrawable
方法,所以看到 ResourceImpl.loadDrawable
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Drawable loadDrawable (Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws NotFoundException {
try {
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null ) {
return cachedDrawable;
}
}
final Drawable.ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null ) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, null );
}
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null ) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
if (dr != null && useCache) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
} catch (Exception e) {
final NotFoundException nfe = new NotFoundException("Drawable " + name
+ " with resource ID #0x" + Integer.toHexString(id), e);
nfe.setStackTrace(new StackTraceElement[0 ]);
throw nfe;
}
}
总结下来就是以下几步:
检查指定主题下是否缓存有这个版本的 drawable,如果正在预加载或者不使用缓存,进入下一步,否则返回这个 缓存的drawable;
检查预加载过的 drawables,预加载的 drawables 可能包含没有解析的主题属性;
确定是否 drawable 有未解析的主题属性。如果有则应用该主题到 drawable 并存储到特定的主题缓存中;
如果拿到 drawable,将它存储到适当的缓存中:比如 reload, not themed, null theme, or theme-specific,最后返回。
以上就是通过 attribute 值代表的引用取得 drawable 的过程,获取其它资源也大同小异。
示例源码
https://github.com/whilu/IgnoredDefStyleAttr
参考