在Android开发的过程中,APP UI随着大众对于审美的变化总是在不断的演进,自定义 View 算是 Android 开发中常见的技巧之一,其实现主要包含两个部分:
- 定义
declare-styleable
中的自定义属性,并在构造函数中获得并初始化; - 实现
onMeasure
、onLayout
和onDraw
等方法。
本文我们主要记录一下关于自定义控件中的属性部分。
1、介绍
在开始介绍自定义属性之前,我们需要先搞清楚一件事情,那就是影响控件的属性都有哪些:
- 在布局文件中的某个View节点中,直接指定的;
- 在布局文件中的某个View节点中,通过style属性中设置的;
- 从defStyleAttr和defStyleRes中设置的;
- 在Theme中直接设置的属性。
接下来我们将通过一个简单的Demo来验证,这些属性是如何起作用的?首先来看一下,一般自定义控件的四个构造方法:
public class MView extends View{
public MView(Context context) ;
public MView(Context context, @Nullable AttributeSet attrs);
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr);
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) ;
}
如果你继承一些兼容库的控件比如AppCompatTextView,他是不提供四个参数的构造方法的。
通过查看View类的源码,我们能够发现前三个方法最后都会调用的最后一个四参方法上,示例代码如下:
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
/*此处省略部分代码*/
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;
case com.android.internal.R.styleable.View_padding:
padding = a.getDimensionPixelSize(attr, -1);
mUserPaddingLeftInitial = padding;
mUserPaddingRightInitial = padding;
leftPaddingDefined = true;
rightPaddingDefined = true;
break;
从上述代码中,可以看到它调用了obtainStyledAttributes方法,获取到TypedArray,再从TypedArray中获取相应的属性值,比如background等,并赋值给View的成员变量,接着我们看一下obtainStyledAttributes
方法;
@NonNull
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);
// 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;
mAssets.applyStyle(mTheme, defStyleAttr, defStyleRes, parser, attrs,
array.mDataAddress, array.mIndicesAddress);
array.mTheme = wrapper;
array.mXml = parser;
return array;
}
}
通过一系列的跳转,然后调用到android.content.res.ResourcesImpl.ThemeImpl#obtainStyledAttributes
方法,在这段代码中 obtain 了我们需要的 TypedArray,根据之前说过的规则通过调用 AssetManager 的 applyStyle
方法(本地方法),确定了最后各个 attribute 的值。下面看看 android_util_AssetManager.cpp 中 android_content_AssetManager_applyStyle
函数的源码,里面有我们需要的 native applyStyle
方法(代码很长,只保留了注释):
static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject clazz, jint themeToken, jint defStyleAttr, jint defStyleRes, jint xmlParserToken, jintArray attrs, jintArray outValues, jintArray outIndices)
{
// ...
// Retrieve the style class associated with the current XML tag.
// 检索与当前 XML 标签关联的样式类
// ...
// Now lock down the resource object and start pulling stuff from it.
// 锁定资源对象并开始从其中抽取所需要的内容
// ...
// Retrieve the default style bag, if requested.
// 如有需要取出默认样式
//...
// Retrieve the XML attributes, if requested.
// 如有需要检索 XML 属性
// ...
// Now iterate through all of the attributes that the client has requested,
// filling in each with whatever data we can find.
// 遍历客户端请求的所有属性,填充每个可以找到的数据
// ...
for (// ...) {
// ...
// Try to find a value for this attribute... we prioritize values
// coming from, first XML attributes, then XML style, then default
// style, and finally the theme.
// 尝试找到这个属性的值... 优先级:
// 首先是 XML 中定义的,其次是 XML 中的 style 定义的,然后是默认样式,最后是主题
// ...
}
return JNI_TRUE;
}
从上述代码中,我们知道了一个 attribute 值的确定过程大致如下:
-
xml 中查找,若未找到进入第 2 步;
-
xml 中的 style 查找,若未找到进入第 3 步;
-
若 defStyleAttr 不为 0,由 defStyleAttr 指定的 style 中寻找,若未找到进入第 4 步;
-
若 defStyleAttr 为 0 或 defStyleAttr 指定的 style 中寻找失败,进入 defStyleRes 指定的 style 中寻找,若寻找失败,进入第 5 步查找;
-
查找在当前 Theme 中指定的属性值。
至此,关于自定义控件中属性的解析就已经结束,这儿有一个Demo来验证我们的分析:github地址;
2、其他
2.1、xmlns
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/window_background">
</LinearLayout>
xmlns是 XML 文档中的一个概念:英文叫做 XML namespace,中文翻译为 XML 命名空间。一讲到命名空间,我想很多人会联想到C++
中的namespace
和Java
中的 packagename,而这两者的作用都是为了解决命名上的冲突(例如类名,接口名等)。类似的,XML namespace
也是为了解决 XML 中元素和属性命名冲突,因为 XML 中的标签并不是预定义的,这一点与 HTML 是有区别的,HTML 中的标签是预定义的,所以我们会遇到命名冲突的问题。
XML 命名空间定义语法为xmlns:namespace-prefix="namespaceURI"
,一共分为三个部分:
-
xmlns
:声明命名空间的保留字,其实就是XML中元素的一个属性; -
namespace-prefix
:命名空间的前缀,这个前缀与某个命名空间相关联; -
namespaceURI
:命名空间的唯一标识符,一般就是一个URI引用。
2.1.1、xmlns:android
用于 Android 系统定义的一些属性。在Android xml布局文件头部的 xmlns:android="http://schemas.android.com/apk/res/android"
,即Android API的Namespace。
2.1.2、xmlns:app
用于我们应用自定义的一些属性,在引用Library的第三方View时,我们需要在XML布局文件头部添加
xmlns:app="http://schemas.android.com/apk/res-auto"
或者
xmlns:app="http://schemas.android.com/apk/res/包名"
2.1.3、tools
根据官方定义,tools
命名空间用于在 XML 文档记录一些,当应用打包的时候,会把这部分信息给过滤掉,不会增加应用的 size,说直白点,这些属性是为IDE提供相关信息。
2.2、TypedArray
/**
* Container for an array of values that were retrieved with
* {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)}
* or {@link Resources#obtainAttributes}. Be
* sure to call {@link #recycle} when done with them.
*
* The indices used to retrieve values from this structure correspond to
* the positions of the attributes given to obtainStyledAttributes.
*/
public class TypedArray {}
使用 TypedArray 类可以帮助我们简化获取 attribute 值的流程。类介绍也表明了其作用,需要的是上面用 [] 括起来的一句话:用完之后必须调用 recycle
方法。对,我们通常都会这么做,但是为什么要这么做? 查看这个方法源码:
public void recycle() {
if (mRecycled) {
throw new RuntimeException(toString() + " recycled twice!");
}
mRecycled = true;
// These may have been set by the client.
mXml = null;
mTheme = null;
mAssets = null;
mResources.mTypedArrayPool.release(this);
}
其中主要就是释放了相应的资源,注意看到 mResources.mTypedArrayPool.release(this);
这一行代码,mTypedArrayPool 是 Resource 类中的一个同步对象(存储 TypedArray 对象)池,这里使用了 Pool 来进行优化。
既然是用了 Pool,那就肯定有获取对象的方法,焦点来到 obtain
方法:
static TypedArray obtain(Resources res, int len) {
final TypedArray attrs = res.mTypedArrayPool.acquire();
if (attrs != null) {
// 重置从 Pool 中获取到的对象
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 的缓存和复用,达到优化的目的。
3、参考
1、从 View 构造函数中被忽略的 {int defStyleAttr} 说起
网友评论