美文网首页
Android自定义控件--属性篇

Android自定义控件--属性篇

作者: minhelloworld | 来源:发表于2020-02-17 11:14 被阅读0次

    在Android开发的过程中,APP UI随着大众对于审美的变化总是在不断的演进,自定义 View 算是 Android 开发中常见的技巧之一,其实现主要包含两个部分:

    • 定义 declare-styleable 中的自定义属性,并在构造函数中获得并初始化;
    • 实现 onMeasureonLayoutonDraw 等方法。

    本文我们主要记录一下关于自定义控件中的属性部分。

    1、介绍

    在开始介绍自定义属性之前,我们需要先搞清楚一件事情,那就是影响控件的属性都有哪些:

    1. 在布局文件中的某个View节点中,直接指定的;
    2. 在布局文件中的某个View节点中,通过style属性中设置的;
    3. 从defStyleAttr和defStyleRes中设置的;
    4. 在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.cppandroid_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 值的确定过程大致如下:

    1. xml 中查找,若未找到进入第 2 步;

    2. xml 中的 style 查找,若未找到进入第 3 步;

    3. 若 defStyleAttr 不为 0,由 defStyleAttr 指定的 style 中寻找,若未找到进入第 4 步;

    4. 若 defStyleAttr 为 0 或 defStyleAttr 指定的 style 中寻找失败,进入 defStyleRes 指定的 style 中寻找,若寻找失败,进入第 5 步查找;

    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++中的namespaceJava中的 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} 说起

    2、Android中Attributes、defStyleAttr、defStyleRes关系理解与应用

    3、Android xmlns

    4、如何理解Android中的xmlns

    相关文章

      网友评论

          本文标题:Android自定义控件--属性篇

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