DrawableCache遇坑小记

一.问题背景描述

测试妹子:我发现我一打开X界面,Y界面的分隔线消失了!!!

我:这么神奇???

二.初找原因

经过一顿操作后发现,这两个界面用到同一个颜色值,而X界面对用了这个颜色值的当背景的view改变了透明度,所以再打开Y界面的时候,用同一个颜色的分割线就消失了。

三.临时解决

考虑到项目进度,我先用这个颜色写了一个shape来当背景,五分钟解决问题。

四.寻找真相

对于真相的好奇,打算看看究竟咋回事,之前对于资源加载这块也没好好了解过,趁这次正好看一下。

猜测

一定是哪里对这颜色做了缓存,如果这个猜测是对的,那么打印这两个drawable应该是同一个对象

I bbbb    : ====background=android.graphics.drawable.ColorDrawable@f74fed8
I bbbb    : ====getDrawable=android.graphics.drawable.ColorDrawable@d868231

凉凉,不是同一个对象,猜想错误

分析

那么接下来只能看源码了。通过setbackground不断的深入,最后来到ResourcesImpl.loadDrawable。
仔细看一下这个drawable的加载流程。

Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
            int density, @Nullable Resources.Theme theme)
            throws NotFoundException {
                ...
    }

1. 先判断是不是ColorDrawable,然后去取相应类型的缓存。

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

ColorDrawable和其他drawable分别是两个缓存

    private final DrawableCache mDrawableCache = new DrawableCache();
    private final DrawableCache mColorDrawableCache = new DrawableCache();

至于其他drawable都有些啥,这就有点多了,如下图,

1.jpg

接下来其他Drawable都以BitmapDrawable为代表。

2. 缓存里是否能取到Drawable,如果有就直接返回。这里代码请先忽略!mPreloading && useCache这个条件

if (!mPreloading && useCache) {
    //注意这里getInstance方法,等一下会分析到
    final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
    if (cachedDrawable != null) {
        return cachedDrawable;
    }
}

3. 如果缓存里没有,就加载新的drawable。(中间略过了跟预加载相关的部分)

Drawable dr;
if (cs != null) {
    //这里是对预加载来的判断,暂时先跳过
    dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
    dr = new ColorDrawable(value.data);
} else {
    dr = loadDrawableForCookie(wrapper, value, id, null);
}

4. 缓存这次生成的drawable,并返回,然后看一下缓存的代码

if (dr != null && useCache) {
    dr.setChangingConfigurations(value.changingConfigurations);
    cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;

然后看一下缓存的代码,就是把当前的ConstantState缓存起来,ConstantState是从Drawable获取的,具体ConstantState是啥,接着往下。

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;
        }
        ...
            synchronized (mAccessLock) {
                caches.put(key, theme, cs, usesTheme);
            }
        }
    }

目前的结论,加载drawable这个过程,针对ColorDrawable和非ColorDrawable分别加了两个缓存mColorDrawableCache和mDrawableCache,这两个cache都是DrawableCache,都继承于ThemedResourceCache。里面都存放着Drawable.ConstantState。

class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
    ...
    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;
    }
    ...
}

其中ThemedResourceCache有mThemedEntries,mUnthemedEntries,mNullThemedEntries存放着各种资源,主要是对不同主题的支持。这里的存储数据结构是ArrayMap和LongSparseArray。

abstract class ThemedResourceCache<T> {
    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
    private LongSparseArray<WeakReference<T>> mNullThemedEntries;
    ...
}

mColorDrawableCache和ColorDrawable具体缓存的东西不一样,mColorDrawableCache是存放ColorDrawable.ColorState,mDrawableCache是存放BItmapDrawable.BItmapState。ColorDrawable.ColorState和BItmapDrawable.BItmapState都继承于Drawable.ConstantState。

5. 接着分析,从刚才的第2步里面看到,如果缓存里面取到Drawable.ConstantState,就会调用getInstance,然后返回Drawable。

那么这个getInstance是怎么得到相应的Drawable的呢?
在刚才的DrawableCache的代码看到是通过Drawable.ConstantState.newDrawable得到的。再贴一下这段代码

class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
    ...
    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.ConstantState.newDrawable,这里我们选取ColorDrawable.ColorState作为代表,比较简单,易于理解。

public class ColorDrawable extends Drawable {

    private ColorState mColorState;
    private boolean mMutated;
    public ColorDrawable() {
        mColorState = new ColorState();
    }

    public ColorDrawable(@ColorInt int color) {
        mColorState = new ColorState();
        setColor(color);
    }

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

    @ColorInt
    public int getColor() {
        return mColorState.mUseColor;
    }

    @Override
    public ConstantState getConstantState() {
        return mColorState;
    }

    final static class ColorState extends ConstantState {
        int[] mThemeAttrs;
        int mBaseColor; // base color, independent of setAlpha()
        @ViewDebug.ExportedProperty
        int mUseColor;  // basecolor modulated by setAlpha()
        @Config int mChangingConfigurations;
        ColorStateList mTint = null;
        Mode mTintMode = DEFAULT_TINT_MODE;

        ColorState() {
            // Empty constructor.
        }

        ColorState(ColorState state) {
            mThemeAttrs = state.mThemeAttrs;
            mBaseColor = state.mBaseColor;
            mUseColor = state.mUseColor;
            mChangingConfigurations = state.mChangingConfigurations;
            mTint = state.mTint;
            mTintMode = state.mTintMode;
        }

        @Override
        public Drawable newDrawable() {
            return new ColorDrawable(this, null);
        }

        @Override
        public Drawable newDrawable(Resources res) {
            return new ColorDrawable(this, res);
        }

    }

    private ColorDrawable(ColorState state, Resources res) {
        mColorState = state;
        updateLocalState(res);
    }
}

这里我删除了很多于这里无关的代码。从newDrawable的方法看出,即使刚才我们从缓存里面取到了缓存的ColorState,但还是会new ColorDrawable,所以就出现了,刚开始打脸的一幕,Drawable并不是同一个对象。但是,new drawable这个过程中,会把ColorState也传到新的Drawable中,所以ColorState中的属性都会传到新的Drawable。

那么现在可以大胆的猜想:

1. 刚开始的猜想,再进一步获取ConstantState,这两个的ConstantState肯定就是同一个对象了。验证如下

I bbbb    : ====background cs=android.graphics.drawable.ColorDrawable$ColorState@dfe7bd1
I bbbb    : ====getDrawable cs=android.graphics.drawable.ColorDrawable$ColorState@dfe7bd1

2. ConstantState里面一定存储着改变后的Drawable,要不然也不会获取到,透明度改变后的颜色了。
这个可以从ColorState里面的属性可以容易的发现mUseColor,这个就是改变后的颜色。

同理BitmapDrawable等一些其他的Drawable也是类似的,只是相应的ConstantState存储东西不一样而已。

至此我们,大概缕清楚了这个缓存的过程。

五.正经的解决办法

那么这个getDrawable互相影响的问题,到底怎么解决呢?难道只能想我刚开始用的方法一样,新建另外一个资源吗。那可能有点低估了Google爸爸了。

在刚才ColorDrawable里面的代码中有个mutate方法注意到没,这个方法里面有new ColorState(mColorState),会把原来的ColorState传进去,再new一个ColorState。

这样我们只要在设置独有的属性前,调用mutate,就不会影响到其他的Drawable了。

六.总结

问题: drawable在不同的地方,修改属性会相互影响,
原因: android缓存了调用过的drawable的ConstantState,ConstantState里面保存了drawable的一些属性
解决方法: 设置独有的属性前,调用mutate,就不会影响到其他的Drawable了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容

  • 1、Drawable 简介 Drawable——可简单理解为可绘制物,表示一些可以绘制在 Canvas 上的对象。...
    牧秦丶阅读 14,800评论 0 15
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,401评论 0 17
  • Drawable在android当中非常常见,每天都会和它打交道,但很多人却很少知道Drawable有哪些常用的方...
    想飞的小小小鱼阅读 1,959评论 2 7
  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,509评论 0 13
  • 今天喝了一杯星巴克,在喝的时候就拍不拍照这件事作了一个简单的思考。 刚上大学那会儿,有个高中同学在qq空间发了张星...
    柯卡拉阅读 240评论 0 0