在Android开发的过程中,APP UI随着大众对于审美的变化总是在不断的演进,自定义 View 算是 Android 开发中常见的技巧之一,其实现主要包含两个部分:
- 定义
declare-styleable
中的自定义属性,并在构造函数中获得并初始化; - 实现
onMeasure
、onLayout
和onDraw
等方法。
本文我们主要记录一下关于自定义控件中的属性部分。
1、介绍
在开始介绍自定义属性之前,我们需要先搞清楚一件事情,那就是影响控件的属性都有哪些:
- 在布局文件中的某个View节点中,直接指定的;
- 在布局文件中的某个View节点中,通过style属性中设置的;
- 从defStyleAttr和defStyleRes中设置的;
- 在Theme中直接设置的属性。
接下来我们将通过一个简单的Demo来验证,这些属性是如何起作用的?首先来看一下,一般自定义控件的四个构造方法:
1 | public class MView extends View{ |
如果你继承一些兼容库的控件比如AppCompatTextView,他是不提供四个参数的构造方法的。
通过查看View类的源码,我们能够发现前三个方法最后都会调用的最后一个四参方法上,示例代码如下:
1 | public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
从上述代码中,可以看到它调用了obtainStyledAttributes方法,获取到TypedArray,再从TypedArray中获取相应的属性值,比如background等,并赋值给View的成员变量,接着我们看一下obtainStyledAttributes
方法;
1 |
|
通过一系列的跳转,然后调用到android.content.res.ResourcesImpl.ThemeImpl#obtainStyledAttributes
方法,在这段代码中 obtain 了我们需要的 TypedArray,根据之前说过的规则通过调用 AssetManager 的 applyStyle
方法(本地方法),确定了最后各个 attribute 的值。下面看看 android_util_AssetManager.cpp 中 android_content_AssetManager_applyStyle
函数的源码,里面有我们需要的 native applyStyle
方法(代码很长,只保留了注释):
1 | static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject clazz, jint themeToken, jint defStyleAttr, jint defStyleRes, jint xmlParserToken, jintArray attrs, jintArray outValues, jintArray outIndices) |
从上述代码中,我们知道了一个 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
1 | <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
1 | /** |
使用 TypedArray 类可以帮助我们简化获取 attribute 值的流程。类介绍也表明了其作用,需要的是上面用 [] 括起来的一句话:用完之后必须调用 recycle
方法。对,我们通常都会这么做,但是为什么要这么做? 查看这个方法源码:
1 | public void recycle() { |
其中主要就是释放了相应的资源,注意看到 mResources.mTypedArrayPool.release(this);
这一行代码,mTypedArrayPool 是 Resource 类中的一个同步对象(存储 TypedArray 对象)池,这里使用了 Pool 来进行优化。
既然是用了 Pool,那就肯定有获取对象的方法,焦点来到 obtain
方法:
1 | static TypedArray obtain(Resources res, int 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} 说起