Drawingcache解析
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>
</code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code>