美文网首页
ColorDrawable系统缓存的问题

ColorDrawable系统缓存的问题

作者: PeytonWu | 来源:发表于2017-10-27 16:51 被阅读0次

    1:问题及现象

    最近新迭代时,遇到一问题分割线颜色与设置颜色不符,透明度引起的问题。一般来说为了项目中简单的分割线写法都如下,直接在xml中
    <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/black_e"/>
    排除各种可能后,猜测是系统帮我们缓存了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系统缓存的问题

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