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 的一整個複用流程的大致邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章