Drawingcache解析

jopen 8年前發布 | 9K 次閱讀 Bitmap 安卓開發 Android開發 移動開發

android為了提高滾動等各方面的繪制速度,可以為每一個view建立一個緩存,使用 View.buildDrawingCache為自己的view建立相應的緩存, 這個cache就是一個bitmap對象。利用這個功能可以對整個屏幕視圖進行截屏并生成Bitmap,也可以 獲得指定的view的Bitmap對象。在有的時候還會影響性能,例如如果自己實現一個Gallery效果,可能就會使用到view緩存。animateCache和scrollingCache 用于動畫和滾動的緩存,使用不當也會造成性能下降。

要獲得一個view的bitmap對象涉及到三個方法:setDrawingCacheEnabled、buildDrawingCache和getDrawingCache。所有的View都有這三種方法。 大部分view如果沒有設置setDrawingCacheEnabled(true);來啟用View的DrawingCache功能的話,那默認是不啟用。

啟用DrawingCache的話,使用getDrawingCache方法時,會先自動去調用buildDrawingCache方法建立DrawingCache,再將結果返回; 不啟用DrawingCache的話,使用getDrawingCache方法時,會返回上一次使用buildDrawingCache方法所產生的結果。 如果在此之前都沒有使用過buildDrawingCache來建立DrawingCache的話,那么getDrawingCache就會返回null。 如果一開始沒有啟用DrawingCache,也是可以事先使用buildDrawingCache來建立DrawingCache,避免getDrawingCache返回null。

getDrawingCache源碼如下:

public Bitmap getDrawingCache() {
        return getDrawingCache(false);
    }

public Bitmap getDrawingCache(boolean autoScale) { if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; } if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) { buildDrawingCache(autoScale); } return autoScale ? mDrawingCache : mUnscaledDrawingCache; }</pre>

可以看出getDrawingCache在設置了DrawingCache的情況下自動調用buildDrawingCache。

照剛才所說的,那么要獲得最新的DrawingCache有兩種方式:

方式一:

view.setDrawingCacheEnabled(true);
Bitmap drawingCache = view.getDrawingCache();

方式二:

view.buildDrawingCache();
Bitmap drawingCache = view.getDrawingCache();

在調用setDrawingCacheEnabled(true);以后就不要再調用buildDrawingCache方法了,以下寫法應該避免,會兩次建立DrawingCache:

view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap drawingCache = view.getDrawingCache();

buildDrawingCache建立drawingCache的同時,會將上次的DrawingCache回收掉,在源碼中buildDrawingCache 會調用destroyDrawingCache方法對之前的DrawingCache回收,源碼如下:

/**

 * <p>Frees the resources used by the drawing cache. If you call
 * {@link #buildDrawingCache()} manually without calling
 * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you
 * should cleanup the cache with this method afterwards.</p>
 *
 * @see #setDrawingCacheEnabled(boolean)
 * @see #buildDrawingCache()
 * @see #getDrawingCache()
 */
public void destroyDrawingCache() {
    if (mDrawingCache != null) {
        mDrawingCache.recycle();
        mDrawingCache = null;
    }
    if (mUnscaledDrawingCache != null) {
        mUnscaledDrawingCache.recycle();
        mUnscaledDrawingCache = null;
    }
}</pre> <p>因此不必在buildDrawingCache方法之前,或者DrawingCache啟用狀態下調用getDrawingCache方法之前,自己手動調用destroyDrawingCache。 會導致RumtimeException:java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@4b8eb8</p>

下面的寫法是錯誤寫法:

if(view.getDrawingCache() != null){
        view.getDrawingCache().recycle();;
    }
    view.buildDrawingCache();
    Bitmap drawingCache = view.getDrawingCache();

圖片質量控制

對于Bitmap對象可以有多種格式,如:

Bitmap.Config.ARGB_8888;

Bitmap.Config.ARGB_4444;

Bitmap.Config.ARGB_8888;

Bitmap.Config.ARGB_8888;

Bitmap.Config.RGB_565;

默認的格式是Bitmap.Config.ARGB_8888,但大多數嵌入式設備使用的顯示格式都是Bitmap.Config.RGB_565. RGB_565并沒有alpha值, 所以繪制的時候不需要計算alpha合成,速度快些。其次,RGB_565可以直接使用優化了的memcopy函數,效率相對高出許多。

可以用以下方法查看bitmap格式:

final Bitmap cache = mContent.getDrawingCache();
     if (cache != null) {
            Config cfg = cache.getConfig();
            Log.d(TAG, "----------------------- cache.getConfig() = " + cfg);
      }

隨著Android API越來越高,DrawingCache的質量也越來越,在大部分的情況下都是使用體積最大且運算速度最慢的ARGB_8888, 過去View所提供的setDrawingCacheQuality方法已經沒有實際作用了,不管設定哪種質量,都還是會使用ARGB_8888。

getDrawingCache返回空

一種可能是view沒有初始化完成,onCreate中view還沒有初始化自己的寬高,所以getDrawingCache();返回空。可以參考viewTreeObserver解析 這篇來獲取view寬高。

下面給出兩種方法:

public static Bitmap convertViewToBitmap(View view){
        view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();
        return bitmap;
    }

第二種方法利用ViewTreeObserver

ViewTreeObserver vto = view.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                Bitmap cac = view.getDrawingCache();
                if(cac != null) {
                    Bitmap.Config cfg = cac.getConfig();
                    Log.e("====", "not null"+cfg);
                    mImageView2.setImageBitmap(cac);
                } else {
                    Log.e("====", "null");
                }
            }
        });

如果很確定View已經有過measure和layout且也調用buildDrawingCache(無論自動或者手動)方法了,但是getDrawingCache還是返回null, 那就是因為要繪制的DrawingCache太大了,超過Android系統設定的drawingCacheSize,這時,就只能放棄使用DrawingCache了。

Android系統設定的DrawingCache大小上限,在不同的裝置上有不同的設定,甚至有可能差了好幾倍,如果要查看數值的話可以使用以下方式來取得drawingCacheSize:

ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize();

getDrawingCache的替代方法

如果不用getDrawingCache想自己建立出Bitmap也是可以的,代碼如下:

public Bitmap getMagicDrawingCache(View view) {
        Bitmap bitmap = (Bitmap) view.getTag(R.id.cacheBitmapKey);
        Boolean dirty = (Boolean) view.getTag(R.id.cacheBitmapDirtyKey);
        int viewWidth = view.getWidth();
        int viewHeight = view.getHeight();
        if (bitmap == null || bitmap.getWidth() != viewWidth || bitmap.getHeight() != viewHeight) {
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
            bitmap = Bitmap.createBitmap(viewWidth, viewHeight, bitmap_quality);
            view.setTag(R.id.cacheBitmapKey, bitmap);
            dirty = true;
        }
        if (dirty == true || !quick_cache) {
            bitmap.eraseColor(getResources().getColor(android.R.color.transparent));
            Canvas canvas = new Canvas(bitmap);
            view.draw(canvas);
            view.setTag(R.id.cacheBitmapDirtyKey, false);
        }
        return bitmap;
    }

如果要加入View不在Activity或是Fragment的RootView中的判斷的話,代碼如下:

public Bitmap getMagicDrawingCache2(View view) {
        Bitmap bitmap = (Bitmap) view.getTag(R.id.cacheBitmapKey);
        Boolean dirty = (Boolean) view.getTag(R.id.cacheBitmapDirtyKey);
        if (view.getWidth() + view.getHeight() == 0) {
            view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        }
        int viewWidth = view.getWidth();
        int viewHeight = view.getHeight();
        if (bitmap == null || bitmap.getWidth() != viewWidth || bitmap.getHeight() != viewHeight) {
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
            bitmap = Bitmap.createBitmap(viewWidth, viewHeight, bitmap_quality);
            view.setTag(R.id.cacheBitmapKey, bitmap);
            dirty = true;
        }
        if (dirty == true || !quick_cache) {
            bitmap.eraseColor(getResources().getColor(android.R.color.transparent));
            Canvas canvas = new Canvas(bitmap);
            view.draw(canvas);
            view.setTag(R.id.cacheBitmapDirtyKey, false);
        }
        return bitmap;
    }

其中,cacheBitmapKey和cacheBitmapDirtyKey是不同的整數,分別用來指定View的Tag ID。cacheBitmapKey的位置會存放使用 這個方法建立出來的DrawingCache;cacheBitmapDirtyKey的位置會存放這個View的DrawingCache是否已經是臟數據(dirty)而需要使用 View的draw方法重新繪制。DrawingCache所用的Bitmap只在沒有Bitmap或是Bitmap的大小和View的大小不合的時候才重新建立, 在建立新的Bitmap前會先將先前的Bitmap進行recycle,新的Bitmap的參考會再被存入至View的Tag中。他們的定義如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item type="id" name="cacheBitmapKey"></item>
    <item type="id" name="cacheBitmapDirtyKey"></item>
</resources>

另外需要設置bitmap_quality和quick_cache:

Bitmap.Config bitmap_quality = Bitmap.Config.ARGB_8888 ;
    boolean quick_cache = false ;

quick_cache若設置為false,則不論DrawingCache是否dirty,都進行重繪,只有在View常常變化的時候才需要這樣做。 bitmap_quality可以設置為Bitmap.Config.RGB_565或是Bitmap.Config.ARGB_8888,Bitmap.Config.ARGB_4444已經隨 著Android API升級家而慢慢被禁用了。

scrollingCache和animateCache

scrollingCache是listview這種滾動布局的一個屬性,animateCache是viewgroup的一個屬性。他們的作用都是控制DrawingCache。 他們都可以在xml布局中控制,也可以用代碼調用:

mylayout.setAnimationCacheEnabled(false);

setAnimationCacheEnabled源碼如下:

/**

 * Enables or disables the children's drawing cache during a layout animation.
 * By default, the drawing cache is enabled but this will prevent nested
 * layout animations from working. To nest animations, you must disable the
 * cache.
 *
 * @param enabled true to enable the animation cache, false otherwise
 *
 * @see #isAnimationCacheEnabled()
 * @see View#setDrawingCacheEnabled(boolean)
 */
public void setAnimationCacheEnabled(boolean enabled) {
    setBooleanFlag(FLAG_ANIMATION_CACHE, enabled);
}</pre> <p>方法的注釋說他的功能是在執行一個Layout動畫時開啟或關閉子控件的繪制緩存。默認情況下,繪制緩存是開啟的,但是這將阻止嵌套Layout動畫的正常執行。 對于嵌套動畫,你必須禁用這個緩存。這個屬性如果設置true后,在動畫繪制過程中會為每一個子布局設置cache,這會提高顯示效果, 但是需要消耗更多內存和更長的初始化時間。這個屬性默認是true。</p>

為什么設置了緩存,動畫會更加平滑,是因為避免了在每一幀的重繪。設置了緩存的動畫還可以被硬件加速,因為在硬件層,渲染系統 可以把bitmap交給GPU處理,并對其進行快速的矩陣操作(如改變透明度,平移、旋轉)。而不使用緩存的情況下,則是在每一幀進行 重繪,即調用onDraw()方法。

scrollingCache屬性和animateCache相似,源碼如下:

/**

 * Enables or disables the children's drawing cache during a scroll.
 * By default, the drawing cache is enabled but this will use more memory.
 *
 * When the scrolling cache is enabled, the caches are kept after the
 * first scrolling. You can manually clear the cache by calling
 * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}.
 *
 * @param enabled true to enable the scroll cache, false otherwise
 *
 * @see #isScrollingCacheEnabled()
 * @see View#setDrawingCacheEnabled(boolean)
 */
public void setScrollingCacheEnabled(boolean enabled) {
    if (mScrollingCacheEnabled && !enabled) {
        clearScrollingCache();
    }
    mScrollingCacheEnabled = enabled;
}</pre> <p>對于listview當滾動的時候,實際上是可見的item布局的執行了動畫,使用緩存可以加速動畫。但是他的缺點就是它消耗的內存。 所以可以手動設置關閉,對于流暢性目前并沒有發現有什么影響。</p>

優化后的listview:

<ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="@color/list_background_color"
    android:dividerHeight="0dp"
    android:listSelector="#00000000"
    android:smoothScrollbar="true"
    android:scrollingCache="false"
    android:animationCache="false" />

下面寫一個demo驗證chche對內存的影響 首先關閉硬件加速

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="

<application
    android:allowBackup="true"
    android:icon="@mipmap/icon"
    android:label="@string/app_name"
    android:name=".BaseApplication"
    android:theme="@style/AppTheme" >
    <activity
        android:name=".Test5Activity"
        android:hardwareAccelerated="false"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

</manifest></pre>

在一個LinearLayout中放16個imageview,讓這個LinearLayout執行縮小動畫,imageview執行旋轉動畫

public class Test5Activity extends Activity  {

private ImageView[] mImageViews = new ImageView[16];
private int[] mImageViewIDs = {R.id.img1,R.id.img2,R.id.img3,R.id.img4,R.id.img5,R.id.img6,R
        .id.img7,R.id.img8,R.id.img9,R.id.img10,R.id.img11,R.id.img12,R.id.img13,R.id.img14,R
        .id.img15,R.id.img16} ;
private LinearLayout mylayout ;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test5);
    mylayout = (LinearLayout) findViewById(R.id.mylayout);
    for(int n = 0 ; n < mImageViews.length ; n ++ ){
        mImageViews[n] = (ImageView) findViewById(mImageViewIDs[n]);
    }

    mylayout.setAnimationCacheEnabled(true);
    mylayout.setOnClickListener(new View.OnClickListener() {
        public void onClick(View arg0) {
            doAnimation() ;
        }
    });
}


public void doAnimation() {
    AnimationSet animationSet=new AnimationSet(true);
    ScaleAnimation scaleAnimation=new ScaleAnimation(
            1, 0.1f, 1, 0.1f,
            Animation.RELATIVE_TO_SELF, 0.5f,
            Animation.RELATIVE_TO_SELF, 0.5f);
    scaleAnimation.setDuration(2000);
    animationSet.addAnimation(scaleAnimation);
    mylayout.startAnimation(scaleAnimation) ;

    RotateAnimation rotateAnimation =new RotateAnimation(0f,360, Animation.RELATIVE_TO_SELF,
            0.5f,Animation.RELATIVE_TO_SELF,0.5f);
    rotateAnimation.setDuration(2000);
    animationSet.addAnimation(rotateAnimation);

    for(int n = 0 ; n < mImageViews.length ; n ++ ){
        mImageViews[n].startAnimation(rotateAnimation) ;
    }

}

}</pre>

最終效果如下:

觀察內存使用情況,設置mylayout.setAnimationCacheEnabled(false);時如下:

可以看出不管動畫如何變化,內存沒有變化。

設置mylayout.setAnimationCacheEnabled(true);時如下:

可以看出不管動畫變化是,內存在不斷增加,之后被回收,因為緩存不斷地產生了新的bitmap。對于動畫地流暢性來說幾乎 看出有什么不同。

為了更清楚的觀察設置了緩存后onDraw方法的調用情況,我們用自定義的view代替ImageView.

public class MyImageView extends ImageView {
    static int count  = 0 ;
    public MyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyImageView(Context context) {
        super(context);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (BuildConfig.DEBUG) Log.d("===MyImageView","onMeasure 我被調用了"+System.currentTimeMillis());
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        count++ ;
        if (BuildConfig.DEBUG) Log.d("===MyImageView", "onDraw 我被調用了"+System.currentTimeMillis()
                +"==="+count);
    }
}

使用一個靜態變量count記錄onDraw調用的次數。

設置mylayout.setAnimationCacheEnabled(false);時第一次啟動如下:

這個很容易理解,因為有16個view所以調用了16次onDraw。 第一次點擊開始動畫后效果如下:

從第17次開始,到800次結束,平均每一個view調用的onDraw次數為 (800-16)/16 = 49 次.

第二次點擊開始動畫后效果如下:

從第801次開始,到1552次結束,平均每一個view調用的onDraw次數為 (1552-800)/16 = 47 次.

可以看出在不設置緩存的情況的onDraw調用次數均大于40.

現在把setAnimationCacheEnabled改為true進行測試。 第一次啟動和剛才結果一樣。

第一次點擊開始動畫后效果如下:

可以看出onDraw調用次數大大減少,平均是 (260-16)/16 = 15.25 次.

第二次點擊開始動畫后效果如下:

平均是 (488-260)/16 = 14.25 次.

這說明了設置了緩存后onDraw調用次數會減少,但同時會增加內存。 那么為什么onDraw調用次數會減少呢,在源碼中可以找到答案。

/**

 * This is where the invalidate() work actually happens. A full invalidate()
 * causes the drawing cache to be invalidated, but this function can be
 * called with invalidateCache set to false to skip that invalidation step
 * for cases that do not need it (for example, a component that remains at
 * the same dimensions with the same content).
 *
 * @param invalidateCache Whether the drawing cache for this view should be
 *            invalidated as well. This is usually true for a full
 *            invalidate, but may be set to false if the View's contents or
 *            dimensions have not changed.
 */
void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}</pre> <p>onDraw是通過invalidate()觸發的,從注釋中可以看到如果設置了緩存,同時View的內容和大小沒有變化,那么invalidate可以設置false。 這個標志位的改變,導致后面的onDraw沒有必要執行,因為有了緩存就直接顯示緩存就好了,不用重新執行onDraw。等到了動畫的下一幀如果圖片 的內容、大小還沒變,就繼續使用緩存,直到內容或大小改變,就重新生成緩存。同理如果沒有設置緩存,那么就不能減少onDraw的次數了,因為每一次 不管圖片內容和大小有沒有改變,都要調用onDraw。</p>

個人認為一般情況下在不影響流暢性的前提下,應該盡量減少內存的使用,所以這個scrollingCache和animateCache應該設置false。對于onDraw里需要 開銷比較大的view,則視情況而定。

</div>

來自: http://souly.cn/技術博文/2016/01/05/DrawingCache解析/

</code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code>

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!