Bitmap ImageView大小的一些祕密

前言

咱們平時在使用ImageView,當設置寬高爲wrap_content的時候,設置bitmap,有沒有想過一個問題,那就是大小到底是如何計算的,平時說的那些density又和最終顯示的圖片大小有什麼關係呢。本着嚴謹的態度,我開始了探索源碼解讀的不歸路上。bash

過程

本次實驗所用測試機density爲420。咱們首先來解碼一張bitmap(ic_launcher大小爲144 * 144),代碼以下:app

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一看究竟,ide

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。賦值操做以後,咱們繼續往下看。ui

@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。那能夠對圖片解碼的大小作修改嗎?固然能夠,代碼獻上:this

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,這一次以下:spa

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的一致呢,二話不說上代碼跑起來:code

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入手,以下:server

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;
        }
    }
複製代碼

保持微笑😊,離結果又近了一步,

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修改進行控制的。
  • 好的,這一次的分享就到此結束了,喜歡的點個讚唄,或者你們討論討論。
相關文章
相關標籤/搜索