Bitmap ImageView大小的一些祕密
我們平時在使用ImageView,當設定寬高為wrap_content的時候,設定bitmap,有沒有想過一個問題,那就是大小究竟是如何計算的,平時說的那些density又和最終顯示的圖片大小有什麼關係呢。本著嚴謹的態度,我開始了探索原始碼解讀的不歸路上。
過程
本次實驗所用測試機density為420。我們首先來解碼一張bitmap(ic_launcher大小為144 * 144),程式碼如下:
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") 複製程式碼
列印結果是{height: 126---width: 126},那麼這個數值是怎麼來的呢。我們進入decodeResource一看究竟,
public static Bitmap decodeResource(Resources res, int id, Options opts) { validate(opts); Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /*do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is != null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; } 複製程式碼
bitmap是decodeResourceStream產生的,那我們接著往下看,
@Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); } 複製程式碼
可以看到,如果options.inDensity等於0,這裡會對options做賦值操作,inDensity指的是圖片資源所在資原始檔夾的density,即xhdpi這些檔案對應的density,inTargetDensity是指目標的density即手機螢幕dpi,在這個實驗中,資源的原始density是480,目標density是420。賦值操作之後,我們繼續往下看。
@Nullable public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, @Nullable Options opts) { // we don't throw in this case, thus allowing the caller to only check // the cache, and not force the image to be decoded. if (is == null) { return null; } validate(opts); Bitmap bm = null; Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap"); try { if (is instanceof AssetManager.AssetInputStream) { final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset(); bm = nativeDecodeAsset(asset, outPadding, opts); } else { bm = decodeStreamInternal(is, outPadding, opts); } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } setDensityFromOptions(bm, opts); } finally { Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); } return bm; } 複製程式碼
這裡做的是呼叫native方法進行解碼,具體就不往下看。但是我們掐指一算和本著直覺來對大小計算,原始大小是144,解碼大小是126,inDensity是480,inTargetDensity是420,相信看到這裡,聰明的讀者很快就可以算出來了,沒錯,126 = 144 * 420 / 480, 也就是說 targetSize = rawSize * targetDensity / rawDensity,其實也很好理解,就是對圖片進行縮放,縮放的依據就是為了適應當前手機的density。那可以對圖片解碼的大小做修改嗎?當然可以,程式碼獻上:
val options = BitmapFactory.Options() options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") 複製程式碼
列印結果是{height: 144---width: 144},按照上面的公式計算即可得到這個結果,其實我們就是把目標density做了修改,從而影響bitmap的解碼過程。我們接著修改options,這一次如下:
val options = BitmapFactory.Options() options.inDensity = 240 options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") 複製程式碼
心算一下,就知道結果是288。這一次我們是通過修改圖片資源的density影響了bitmap的解碼產生的大小。 那麼ImageView的大小是否和bitmap的一致呢,二話不說上程式碼跑起來:
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") image_view.setImageBitmap(bitmap) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height}---width: ${image_view.width}}") true } 複製程式碼
結果還真的是一樣的,都是126,但是這樣還不夠,改下options引數試一下,inTargetDensity 改為 480,你猜結果怎麼著,bitmap是144,imageview是126,咦這麼神奇。老實看程式碼去吧。從setImageBitmap入手,如下:
public void setImageBitmap(Bitmap bm) { // Hacky fix to force setImageDrawable to do a full setImageDrawable // instead of doing an object reference comparison mDrawable = null; if (mRecycleableBitmapDrawable == null) { mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm); } else { mRecycleableBitmapDrawable.setBitmap(bm); } setImageDrawable(mRecycleableBitmapDrawable); } 複製程式碼
可以看到實際上內部是把bitmap裝進BitmapDrawable,繼續往下看:
public void setImageDrawable(@Nullable Drawable drawable) { if (mDrawable != drawable) { mResource = 0; mUri = null; final int oldWidth = mDrawableWidth; final int oldHeight = mDrawableHeight; updateDrawable(drawable); if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { requestLayout(); } invalidate(); } } 複製程式碼
關鍵程式碼是updateDrawable,除此之外,還會進行新舊寬高的判斷,決定是否重新requestLayout。檢視updateDrawable程式碼,
private void updateDrawable(Drawable d) { if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) { mRecycleableBitmapDrawable.setBitmap(null); } boolean sameDrawable = false; if (mDrawable != null) { sameDrawable = mDrawable == d; mDrawable.setCallback(null); unscheduleDrawable(mDrawable); if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) { mDrawable.setVisible(false, false); } } mDrawable = d; if (d != null) { d.setCallback(this); d.setLayoutDirection(getLayoutDirection()); if (d.isStateful()) { d.setState(getDrawableState()); } if (!sameDrawable || sCompatDrawableVisibilityDispatch) { final boolean visible = sCompatDrawableVisibilityDispatch ? getVisibility() == VISIBLE : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown(); d.setVisible(visible, true); } d.setLevel(mLevel); mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); applyImageTint(); applyColorMod(); configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; } } 複製程式碼
關鍵的有幾處,一處是drawable的賦值,另外一處是
mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); configureBounds(); 複製程式碼
對drawable的寬高進行賦值,然後重新調整bound的大小,configureBounds方法程式碼較多,這裡先摘抄最重要的一部分,
final int dwidth = mDrawableWidth; final int dheight = mDrawableHeight; mDrawable.setBounds(0, 0, dwidth, dheight); 複製程式碼
到這裡就水落石出了,ImageView的寬高由上面d.getIntrinsicWidth(),d.getIntrinsicHeight()決定,所以破案的關鍵就在於這兩個方法,走,看原始碼去,由於這裡drawable的實現類是BitmapDrawable,所以需要檢視BitmapDrawable的實現方法,如下
@Override public int getIntrinsicWidth() { return mBitmapWidth; } @Override public int getIntrinsicHeight() { return mBitmapHeight; } 複製程式碼
好的,離勝利不遠了,檢視mBitmapWidth賦值,
private void computeBitmapSize() { final Bitmap bitmap = mBitmapState.mBitmap; if (bitmap != null) { mBitmapWidth = bitmap.getScaledWidth(mTargetDensity); mBitmapHeight = bitmap.getScaledHeight(mTargetDensity); } else { mBitmapWidth = mBitmapHeight = -1; } } 複製程式碼
保持微笑:blush:,離結果又近了一步,
public int getScaledHeight(int targetDensity) { return scaleFromDensity(getHeight(), mDensity, targetDensity); } /** * @hide */ static public int scaleFromDensity(int size, int sdensity, int tdensity) { if (sdensity == DENSITY_NONE || tdensity == DENSITY_NONE || sdensity == tdensity) { return size; } // Scale by tdensity / sdensity, rounding up. return ((size * tdensity) + (sdensity >> 1)) / sdensity; } 複製程式碼
到這裡就又恍然大悟了,原來繪製到ImageView的bitmapDrawable會對bitmap再進行一次縮放,縮放的比例還是inDensity,targetDensity,只不過這裡的inDensity是bitmap的density,如果options沒有做設定,bitmap的density即為圖片資原始檔夾的density,在這裡是480,那targetDensity又是多少呢,找到BitmapDrawable賦值的地方,程式碼如下:
state.mTargetDensity = Drawable.resolveDensity(r, 0); static int resolveDensity(@Nullable Resources r, int parentDensity) { final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi; return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; } 複製程式碼
這裡很明顯可以得到 targetDensity等於裝置的density,即420。說到這裡,是不是有種柳暗花明又一村的感覺呢,因為這和bitmap的預設縮放配置是一樣的,雖然我們修改了bitmap的縮放配置,但是並沒有影響到bitmapDrawable的配置,所以BitmapDrawable的大小為 144 * 420 / 480 = 126。 看到這裡,聰明的讀者A肯定可以想到,既然不能修改BitmapDrawable的targetDensity, 那麼我通過修改options的inDensity不就可以修改圖片大小了嗎,恭喜你,答對了,
val options = BitmapFactory.Options() options.inDensity = 240 options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") image_view.setImageBitmap(bitmap) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height}---width: ${image_view.width}}") true } 複製程式碼
鐺鐺鐺,小學數學問題,結果是256,因為分母少了二分之一,所以相當於變成兩倍。看到這裡,讀者A肯定覺得自己很聰明,一切都在自己掌握當中, 但是too young too naive,其實可以修改BitmapDrawable的targetDensity,程式碼獻上,
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height}---width: ${bitmap.width}}") val bitmapDrawable = BitmapDrawable(resources, bitmap) bitmapDrawable.setTargetDensity(480) image_view.setImageDrawable(bitmapDrawable) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height}---width: ${image_view.width}}") true } 複製程式碼
什麼,還想要結果,這麼簡單的問題。
好吧,偷偷告訴你,其實結果是144。
總結
- 對於Bitmap,大小等於 rawSize * targetDensity / rawDensity,targetDensity是目標的density, rawDensity是原始資源的density,當然這兩個值都可以通過options進行修改,其實從這裡也可以看出圖片資源放在適合的資源夾的重要性,如果圖片資源放的資料夾density太小,會導致解碼的bitmap放大,從而導致記憶體增加,畢竟解碼之後的面積變大了,單位面積的佔用記憶體又不變。
- 對於ImageView,我們可以知道,即使我們對bitmap進行了縮放,在記憶體的drawable又會重新進行縮放,以用來適應實際大小。縮放比例我們還是可以通過targetDensity,inDensity修改進行控制的。
- 好的,這一次的分享就到此結束了,喜歡的點個讚唄,或者大家討論討論。