ColorDrawable系统缓存的问题

1:问题及现象

最近新迭代时,遇到一问题分割线颜色与设置颜色不符,透明度引起的问题。一般来说为了项目中简单的分割线写法都如下,直接在xml中

排除各种可能后,猜测是系统帮我们缓存了ColorDrawable对象(background属性会被解析成drawable,至于是哪种drawable,取决于你设置的值)引起的。
通过如下尝试,有如下现象:

1.1:代码view.getBackground()去获取两条相同background为black_e的不同分割线的背景后,发现每个view的背景都是不同的ColorDrawable对象,那么问题到底出在哪,系统是否有混存drawable对象?
2.2: 通过代码view.setBackgroundDrawable(getResources().getDrawable(R.color.black_e))

网上有人遇到类似问题,并解释及给出解决方案,详见链接,这个说法及解释是否可信?

接下来开始从系统源码角度开始分析。

2:分析

2.1 :从xml 出发,android:background属性最终会在android.view.View构造时解析成Drawable

 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);
   //...省略
        Drawable background = null;
     
        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;
        }
    //...省略

2.2:由以上关键代码,跟踪到android.content.res.TypedArraygetDrawablee( int index)方法,如下

 @Nullable
    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(value, value.resourceId, mTheme),其中的mResourcesandroid.content.res.Resources,

到这里就解释了1.2的原因,因为殊途同归,这种方法和xml 设置背景的方法,最终都是android.content.res.Resources的loadDrawable方法。

2.3android.content.res.Resources的loadDrawabl方法如下

  @NonNull
    Drawable loadDrawable(@NonNull TypedValue value, int id, @Nullable Theme theme)
           throws NotFoundException {
        return mResourcesImpl.loadDrawable(this, value, id, theme, true);
    }

mResourcesImplandroid.content.res.ResourcesImplloadDrawable方法如下:

 Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
            boolean useCache) throws NotFoundException {
        try {
          //...省略

            final boolean isColorDrawable;
            final DrawableCache caches;
            final long key;
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }

            // First, check whether we have a cached version of this drawable
            // that was inflated against the specified theme. Skip the cache if
            // we're currently preloading or we're not using the cache.
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }

            // Next, check preloaded drawables. Preloaded drawables may contain
            // unresolved theme attributes.
            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);
            }

            // Determine if the drawable has unresolved theme attributes. If it
            // does, we'll need to apply a theme and store it in a theme-specific
            // cache.
            final boolean canApplyTheme = dr != null && dr.canApplyTheme();
            if (canApplyTheme && theme != null) {
                dr = dr.mutate();
                dr.applyTheme(theme);
                dr.clearMutated();
            }

            // If we were able to obtain a drawable, store it in the appropriate
            // cache: preload, not themed, null theme, or theme-specific. Don't
            // pollute the cache with drawables loaded from a foreign density.
            if (dr != null && useCache) {
                dr.setChangingConfigurations(value.changingConfigurations);
                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
            }

            return dr;
        } catch (Exception e) {
           //...省略
        }

2.3.1 我们的drawable为colorDrawable所以isColorDrawable值为true,
2.3.2 mPreloading值仅在startPreloadingfinishPreloading方法中被赋值,而这2个方法说明如下/** * Start preloading of resource data using this Resources object. Only * for use by the zygote process for loading common system resources. * {@hide} */,也就是系统zygote 进程调用,当我们app在调用时mPreloading必定为false
2.3.3 根据以上Resources的调用,传入的useCache值必定为true
所以必定会进入其中的这部分代码

 if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }
caches先不管是啥,从名字上看应该是一个缓存,从这可以看出系统确实有对drawable以id为键进行了缓存。
也就是当我们app非首次获取某一资源时,肯定achedDrawable != null,即返cachedDrawable对象;
如果为首次时才接着往下走创建对应新的drawable对象,在随后的cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);代码中做缓存
如果是这样,那为何1.1中得到的2个同id的drawable会为不同对象?
接着我们先看看cacheDrawable方法的实现
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
            Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
        final Drawable.ConstantState cs = dr.getConstantState();
        if (cs == null) {
            return;
        }

        if (mPreloading) {
            final int changingConfigs = cs.getChangingConfigurations();
            if (isColorDrawable) {
                if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
                    sPreloadedColorDrawables.put(key, cs);
                }
            } else {
                if (verifyPreloadConfig(
                        changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
                    if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
                        // If this resource does not vary based on layout direction,
                        // we can put it in all of the preload maps.
                        sPreloadedDrawables[0].put(key, cs);
                        sPreloadedDrawables[1].put(key, cs);
                    } else {
                        // Otherwise, only in the layout dir we loaded it for.
                        sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
                    }
                }
            }
        } else {
            synchronized (mAccessLock) {
                caches.put(key, theme, cs, usesTheme);
            }
        }
    }

2.3.2已经知道mPreloading为false,故走到caches.put(key, theme, cs, usesTheme);其中的cs为调用drawable的getConstantState()方式所获得的Drawable.ConstantState对象

再查看caches获取时调用的getInstance方法相关实现
2.4 cachesandroid.content.res.DrawableCachegetInstance方法如下

public Drawable getInstance(long key, Resources resources, Resources.Theme theme) {
        final Drawable.ConstantState entry = get(key, theme);
        if (entry != null) {
            return entry.newDrawable(resources, theme);
        }

        return null;
    }
原来系统对于drawable做的缓存并非为Drawable对象,而是其对应的Drawable.ConstantState对象

所以其实当我们非首次获取某一资源时,我们拿到的其实是entry.newDrawable(resources, theme);这行代码得到的drawable对象。在我们的场景下,entry为colorDrawable时对应的getConstantState()方法得到的android.graphics.drawable.ColorDrawable下的ColorState内部类,继承于Drawable.ConstantState对象,它里面存储了ColorDrawable对应的色值。
其中的newDrawable方法如下``

  @Override
        public Drawable newDrawable(Resources res) {
            return new ColorDrawable(this, res);
        }
到这里我们就明白了,1.1的原因了,每次得到的都是一个系统拿了id对应的缓存的色值,去创建的新的ColorDrawable对象。

3:解决方案

3.1 mutate方法是否有效?先看看其源码

  @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            mColorState = new ColorState(mColorState);
            mMutated = true;
        }
        return this;
    }

可以看到其只是新建了一个ColorState对象返回,所以,如果当整个项目中在用到资源时,都先调用mutate()方法,那就不会有问题。如果有地方更改了其资源对应的色值,后面用到该资源的方法即使调用了mutate()也没有效果,因为系统缓存的那份最原始的ColorState已经被更改了。
3.2 拿到ColorDrawable方法调用其setColor方法,将自己真正想要的色值设置进去,当然为了不影响其它的资源,视情况调用mutate方法

4:总结

系统中所有的drawable都进行了对应Drawable.ConstantState的缓存,所以所有的drawable对像都存在该问题的风险

你可能感兴趣的:(ColorDrawable系统缓存的问题)