Drawable复用及问题

概述

每个 Drawable 都有一个对应的ConstantState,这个 state 保存了 Drawable 所有的关键信息。由于 Drawable 的广泛使用,系统为了优化性能(节省内存占用),相同资源的 Drawable 都共享同一个ConstantState。这个的含义用较为白话的方式解释为:假使有一个View,内部逻辑加载了一个 Drawable,即使多次创建这个 View 的实例,但每个 View 实例获取的 Drawable 都是同一个。

复用 State

这种优化也会导致一些问题,当我们修改了 Drawable 的属性,比如透明度,那么会影响到其他 View 实例中 Drawable 的透明度值,因为他们的状态是共享的。 这个问题常见于修改了某些 View 背景的透明度。如 View 背景初始为白色,当更改了其透明度后,其他背景同样为白色的 View 也会受到影响。又或者对于同一个资源在多个地方使用了,在A地方进行透明度修改也会影响到其余使用的地方。

Java 复制代码
// 导致其他用到 Drawable 也受影响的代码
view.getBackground().setAlpha(0);

解决方案:

Java 复制代码
drawable.mutate().setAlpha(0)  // 通过 mutate() 方法,复制一份 ConstantState 进行修改避免影响到其他地方

源码解析

以下基于 API 33 源码进行分析

drawable加载流程

Java 复制代码
// Resources#getDrawable
ppublic Drawable getDrawable(@DrawableRes int id) throws NotFoundException {  
    final Drawable d = getDrawable(id, null);  
    if (d != null && d.canApplyTheme()) {  
        Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "  
                + "attributes! Consider using Resources.getDrawable(int, Theme) or "  
                + "Context.getDrawable(int).", new RuntimeException());  
    }  
    return d;  
}

// Resources#getDrawable
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)  
        throws NotFoundException {  
    return getDrawableForDensity(id, 0, theme);  
}

// Resources#getDrawableForDensity
@Nullable  
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {  
    final TypedValue value = obtainTempTypedValue();  
    try {  
        final ResourcesImpl impl = mResourcesImpl;  
        impl.getValueForDensity(id, density, value, true);  
        return loadDrawable(value, id, density, theme);  
    } finally {  
        releaseTempTypedValue(value);  
    }  
}

// Resources#loadDrawable
@NonNull  
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)  
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)  
        throws NotFoundException {  
    return mResourcesImpl.loadDrawable(this, value, id, density, theme);  
}

`` 实际调用入口为ResourcesImpl#loadDrawable,其大致做了以下事情:

  • 判断是否能够使用缓存
  • 能够使用并命中缓存的话,取出对应的 ConstantState 并创建一个 Drawable
  • 不能使用或没有命中缓存的,走 Drawable 创建流程。创建完成后,对于能够使用缓存的,将创建的 Drawable 对应的 ConstantState 加入缓存池中
Java 复制代码
// ResourcesImpl#loadDrawable
@Nullable  
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,  
        int density, @Nullable Resources.Theme theme)  
        throws NotFoundException {  
    // If the drawable's XML lives in our current density qualifier,  
    // it's okay to use a scaled version from the cache. Otherwise, we   
    // need to actually load the drawable from XML.    
    // 判断是否能够使用缓存,通常我们使用的 Resouces#getDrawable 方法 density 为0,因此 useCache 为true
    final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;  
  
    // Pretend the requested density is actually the display density. If  
    // the drawable returned is not the requested density, then force it    
    // to be scaled later by dividing its density by the ratio of    
    // requested density to actual device density. Drawables that have    
    // undefined density or no density don't need to be handled here.    
    if (density > 0 && value.density > 0 && value.density != TypedValue.DENSITY_NONE) {  
        if (value.density == density) {  
            value.density = mMetrics.densityDpi;  
        } else {  
            value.density = (value.density * mMetrics.densityDpi) / density;  
        }  
    }  
    try {  
        if (TRACE_FOR_PRELOAD) {  
            // Log only framework resources  
            if ((id >>> 24) == 0x1) {  
                final String name = getResourceName(id);  
                if (name != null) {  
                    Log.d("PreloadDrawable", name);  
                }  
            }        
        }  
        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) {  
                cachedDrawable.setChangingConfigurations(value.changingConfigurations);  
                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);  
        }  

		// 判断预加载的 ConstantState 是否存在,不存在创建 Drawable
        Drawable dr;  
        boolean needsNewDrawableAfterCache = false;  
        if (cs != null) {  
            dr = cs.newDrawable(wrapper);  
        } else if (isColorDrawable) {  
            dr = new ColorDrawable(value.data);  
        } else {  
            dr = loadDrawableForCookie(wrapper, value, id, density);  
        }  
        // DrawableContainer' constant state has drawables instances. In order to leave the  
        // constant state intact in the cache, we need to create a new DrawableContainer after        
        // added to cache.        
        if (dr instanceof DrawableContainer)  {  
            needsNewDrawableAfterCache = true;  
        }  
  
        // 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) {  
            dr.setChangingConfigurations(value.changingConfigurations);  
            // 使用缓存,调用cacheDrawable进行缓存,实际缓存的是drawable的ConstantState对象
            if (useCache) {  
                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);  
                if (needsNewDrawableAfterCache) {  
                    Drawable.ConstantState state = dr.getConstantState();  
                    if (state != null) {  
                        dr = state.newDrawable(wrapper);  
                    }  
                }            }        }  
        return dr;  
    } catch (Exception e) {  
        String name;  
        try {  
            name = getResourceName(id);  
        } catch (NotFoundException e2) {  
            name = "(missing name)";  
        }  
  
        // The target drawable might fail to load for any number of  
        // reasons, but we always want to include the resource name.        // Since the client already expects this method to throw a        // NotFoundException, just throw one of those.        final NotFoundException nfe = new NotFoundException("Drawable " + name  
                + " with resource ID #0x" + Integer.toHexString(id), e);  
        nfe.setStackTrace(new StackTraceElement[0]);  
        throw nfe;  
    }  
}  

// ResourcesImpl#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, ActivityInfo.CONFIG_LAYOUT_DIRECTION, value.resourceId, "drawable")) {  
                if ((changingConfigs & ActivityInfo.CONFIG_LAYOUT_DIRECTION) == 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);  
        }  
    }
}

对于ConstantState,其是Drawable的抽象内部类。

Java 复制代码
 public static abstract class ConstantState {  

    public abstract @NonNull Drawable newDrawable();  
     
    public @NonNull Drawable newDrawable(@Nullable Resources res) {  
        return newDrawable();  
    }  

    public @NonNull Drawable newDrawable(@Nullable Resources res,  
            @Nullable @SuppressWarnings("unused") Theme theme) {  
        return newDrawable(res);  
    }  
  
    public abstract @Config int getChangingConfigurations();  
  
    public boolean canApplyTheme() {  
        return false;  
    }  
}

现在通过较为常见的BitmapDrawable相关联的BitmapState来了解一下ConstantState的实际用途。

Java 复制代码
final static class BitmapState extends ConstantState {  
    final Paint mPaint;  
  
    // Values loaded during inflation.  
    int[] mThemeAttrs = null;  
    Bitmap mBitmap = null;  
    ColorStateList mTint = null;  
    BlendMode mBlendMode = DEFAULT_BLEND_MODE;  
  
    int mGravity = Gravity.FILL;  
    float mBaseAlpha = 1.0f;  
    Shader.TileMode mTileModeX = null;  
    Shader.TileMode mTileModeY = null;  
  
    // The density to use when looking up the bitmap in Resources. A value of 0 means use  
    // the system's density.    int mSrcDensityOverride = 0;  
  
    // The density at which to render the bitmap.  
    int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;  
  
    boolean mAutoMirrored = false;  
  
    @Config int mChangingConfigurations;  
    boolean mRebuildShader;  
  
    BitmapState(Bitmap bitmap) {  
        mBitmap = bitmap;  
        mPaint = new Paint(DEFAULT_PAINT_FLAGS);  
    }  
  
    BitmapState(BitmapState bitmapState) {  
        mBitmap = bitmapState.mBitmap;  
        mTint = bitmapState.mTint;  
        mBlendMode = bitmapState.mBlendMode;  
        mThemeAttrs = bitmapState.mThemeAttrs;  
        mChangingConfigurations = bitmapState.mChangingConfigurations;  
        mGravity = bitmapState.mGravity;  
        mTileModeX = bitmapState.mTileModeX;  
        mTileModeY = bitmapState.mTileModeY;  
        mSrcDensityOverride = bitmapState.mSrcDensityOverride;  
        mTargetDensity = bitmapState.mTargetDensity;  
        mBaseAlpha = bitmapState.mBaseAlpha;  
        mPaint = new Paint(bitmapState.mPaint);  
        mRebuildShader = bitmapState.mRebuildShader;  
        mAutoMirrored = bitmapState.mAutoMirrored;  
    }  
  
    @Override  
    public boolean canApplyTheme() {  
        return mThemeAttrs != null || mTint != null && mTint.canApplyTheme();  
    }  
  
    @Override  
    public Drawable newDrawable() {  
        return new BitmapDrawable(this, null);  
    }  
  
    @Override  
    public Drawable newDrawable(Resources res) {  
        return new BitmapDrawable(this, res);  
    }  
  
    @Override  
    public @Config int getChangingConfigurations() {  
        return mChangingConfigurations  
                | (mTint != null ? mTint.getChangingConfigurations() : 0);  
    }  
}

ResourcesImpl#loadDrawable方法我们得知,当能够使用并命中缓存时,会调用ConstantState#newDrawable方法得到一个 Drawable 对象。而这个方法对应到BitmapStatenewDrawable,其实现方式就是创建了一个BitmapDrawable并把自身当作参数传递进去实现了状态共享。

Java 复制代码
// BitmapDrawable
private BitmapDrawable(BitmapState state, Resources res) {  
    init(state, res);  
}  

// BitmapDrawable#init
private void init(BitmapState state, Resources res) {  
    mBitmapState = state;  
    updateLocalState(res);  
  
    if (mBitmapState != null && res != null) {  
        mBitmapState.mTargetDensity = mTargetDensity;  
    }  
}

综上源码分析,我们知道了 Drawable 的一整个复用流程的大致逻辑。

相关推荐
子非衣4 分钟前
MySQL修改JSON格式数据示例
android·mysql·json
openinstall全渠道统计3 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫4 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫4 小时前
一句话说透Android里面的查找服务
android
双鱼大猫4 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫4 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫4 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫5 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫5 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标5 小时前
android 快速定位当前页面
android