Android自定义控件--属性篇

2020-02-17  本文已影响0人  minhelloworld

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

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

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",一共分为三个部分:

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);
}

简单总结这两个方法如下:

对于 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

上一篇 下一篇

猜你喜欢

热点阅读