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

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui