Drawable复用及问题

概述

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

复用 State

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

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

解决方案:

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

源码解析

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

drawable加载流程

// 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 加入缓存池中
// 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的抽象内部类。

 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的实际用途。

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并把自身当作参数传递进去实现了状态共享。

// 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 的一整个复用流程的大致逻辑。

posted @ 2023-09-08 21:40  zxzhang  阅读(293)  评论(1编辑  收藏  举报