BitMap內存佔用

BitMap加載後,佔用內存大小:

width * height *每個像素佔的字節數

代碼中獲取bitmap佔用內存大小的計算:

public final int getByteCount() @Bitmap.java{
    return getRowBytes() * getHeight();
}

其中的getRowBytes()調用的是native方法,具體在android源碼中就是SkBitmap.cpp中的實現。

SkBitmap.h    
/** Returns row bytes, the interval from one pixel row to the next. Row bytes
        is at least as large as width() * info().bytesPerPixel().

        Returns zero if colorType() is kUnknown_SkColorType, or if row bytes supplied to
        setInfo() is not large enough to hold a row of pixels.

        @return  byte length of pixel row
    */
    size_t rowBytes() const { return fPixmap.rowBytes(); }

常見的顏色類型kRGB_565_SkColorType,一個像素佔2個字節,

kRGBA_8888_SkColorType 一個像素佔4個字節。

佔用內存跟容器格式也就是文件後綴名沒有關係,但是跟文件放在drawable的那個目錄有關係,前提同一張圖片,同時只在一個drawable目錄存在。

一張477*550的圖片,放在drawable-xhdpi中,

如代碼:

    public void decodeBitmap() {
        Bitmap jpgFile = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.pngfile);
        Log.d(TAG,"jpgfile,width="+jpgFile.getWidth()+",jpgfile,height="+jpgFile.getHeight()
            +",jpgfile,byteCount="+jpgFile.getByteCount()+",,,="+jpgFile.getDensity());

        Bitmap bmpFile = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.bmpfile);
        Log.d(TAG,"pngfile,width="+bmpFile.getWidth()+",pngfile,height="+bmpFile.getHeight()
                +",pngfile,byteCount="+bmpFile.getByteCount());

        Bitmap pngFile = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.jpgfile);
        Log.d(TAG,"pngfile,width="+pngFile.getWidth()+",pngfile,height="+pngFile.getHeight()
                +",pngfile,byteCount="+pngFile.getByteCount()+",,,="+pngFile.getDensity());
    }

/*output*/

2020-01-02 09:18:10.967 9850-9850/? D/com.jlq.mainthreaddemo.DecodeBitmap: jpgfile,width=477,jpgfile,height=550,jpgfile,byteCount=1049400,,,=320
2020-01-02 09:18:10.986 9850-9850/? D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=477,pngfile,height=550,pngfile,byteCount=1049400
2020-01-02 09:18:11.000 9850-9850/? D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=477,pngfile,height=550,pngfile,byteCount=1049400,,,=320

把那三張圖片放在drawable-xxhdpi中,會縮小圖片

/*output*/

2020-01-02 09:31:07.659 10130-10130/? D/com.jlq.mainthreaddemo.DecodeBitmap: jpgfile,width=318,jpgfile,height=367,jpgfile,byteCount=466824,,,=320
2020-01-02 09:31:07.680 10130-10130/? D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=318,pngfile,height=367,pngfile,byteCount=466824
2020-01-02 09:31:07.696 10130-10130/? D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=318,pngfile,height=367,pngfile,byteCount=466824,,,=320

放在drawable-hdpi中,會放大圖片。

/*output*/

2020-01-02 09:35:23.787 10252-10252/com.jlq.mainthreaddemo D/com.jlq.mainthreaddemo.DecodeBitmap: jpgfile,width=636,jpgfile,height=733,jpgfile,byteCount=1864752,,,=320
2020-01-02 09:35:23.821 10252-10252/com.jlq.mainthreaddemo D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=636,pngfile,height=733,pngfile,byteCount=1864752
2020-01-02 09:35:23.850 10252-10252/com.jlq.mainthreaddemo D/com.jlq.mainthreaddemo.DecodeBitmap: pngfile,width=636,pngfile,height=733,pngfile,byteCount=1864752,,,=320

獲取當前設備的屏幕密度:

    public void getDisplayMetrics() {
        DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
        mDensity = dm.density;
        mDensityDpi = dm.densityDpi;
        TypedValue tv = new TypedValue();
        Log.d(TAG,"mDensity="+mDensity+",mDensityDpi="+mDensityDpi);
    }
2020-01-02 09:35:23.747 10252-10252/com.jlq.mainthreaddemo D/com.jlq.mainthreaddemo.DecodeBitmap: mDensity=2.0,mDensityDpi=320

跟當前設備匹配的資源目錄是drawable-xhdpi,對應屏幕密度是320dpi。

設備常用的屏幕密度:

  • ldpi(低): ~120 dpi 3

  • mdpi(中):120~160dpi

  • hdpi(高):160~240dpi

  • xhdpi(超高):240~320dpi

  • xxhdpi(超超高):320~480dpi

  • xxxhdpi(超超超高):480~640dpi 

通過上面的測試,圖片放的位置如果跟當前的屏幕密度不一致,會對圖片做縮放。具體的縮放算法:

實際加載後的圖片尺寸 = (原圖片的尺寸 * 目標設備的屏幕密度) / 圖片放置目錄的dpi等級。

依據這個公式可以算出,在把圖片放置在drawable-xxhdpi中時,實際的圖片寬度 = (477(原圖寬度)* 320(屏幕密度))/480  (xxhdpi的等級) 。結果是318,跟代碼輸入是一致的。

下面分析下具體代碼實現是不是這樣的。

第一步,從decodeResource開始看起,

//僅關心跟圖片縮放相關代碼   
 public static Bitmap decodeResource(Resources res, int id, Options opts) @BitmapFactory.java{
        validate(opts);
        Bitmap bm = null;
        InputStream is = null; 
        
        try {
            final TypedValue value = new TypedValue();
            //從文件讀取圖片,轉成輸入流,這裏會解析圖片放置目錄的dpi等級,並以出參value返回
            is = res.openRawResource(id, value);
            //根據解析出的圖片目錄的dpi等級,及設備密度,做縮放
            bm = decodeResourceStream(res, value, is, null, opts);
        } 
        return bm;
    }

第二步,跟蹤openRawResource的執行,跳過參數透傳,看AssetManager.java中的實現。

//從參數 TypedValue outValue的名字有out前綴,可以看出,這是一個出參,也就是說BitmapFactory中創建
//的TypedValue對象,只是一個空殼,裏面的實際參數值,會在接下來的解析中去填充。
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs)@AssetManager.java {
        synchronized (this) {
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);

            // Convert the changing configurations flags populated by native code.
            outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                    outValue.changingConfigurations);

            if (outValue.type == TypedValue.TYPE_STRING) {
                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
            }
            return true;
        }
    }

調到jni層

//解析資源是通過AssetManager2來執行的
static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
                                   jshort density, jobject typed_value,
                                   jboolean resolve_references) @android_util_AssetManager.cpp{
  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
  Res_value value;
  ResTable_config selected_config;
  uint32_t flags;
  ApkAssetsCookie cookie =
      assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
                                static_cast<uint16_t>(density), &value, &selected_config, &flags);

  }

先用鎖裝飾了AAssetManager,實際完成解析的還是AAssetManager2中的實現,也就是AssetManager2.cpp中的

ApkAssetsCookie AssetManager2::GetResource(uint32_t resid, bool may_be_bag,
                                           uint16_t density_override, Res_value* out_value,
                                           ResTable_config* out_selected_config,
                                           uint32_t* out_flags) const {}
ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
                                         bool /*stop_at_first_match*/,
                                         FindEntryResult* out_entry) const {}

其中的結構體,ResTable_config,對應了資源的類別。

對資源的解析,需要後續在做補充。。。

解析的圖片所在目錄的dpi等級,會作用到BitmapFactory.Options中,

    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) {
//這裏value.density的值,會隨着圖片放置的目錄變化,如log打印
            final int density = value.density;
			Log.e("BitmapFactory", "decodeResourceStream:" + 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);
    }

把圖片分別放在hdpi,xxhdpi時:

2020-01-20 16:59:45.739 3723-3723/com.jlq.mainthreaddemo E/BitmapFactory: decodeResourceStream:240

2020-01-20 17:00:33.697 3806-3806/? E/BitmapFactory: decodeResourceStream:480

第三步,把元數據轉成流後,接下來就是從流解碼成bitmap,並做縮放。

具體從BitmapFactory.java開始

    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {}

轉到native代碼;

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options)@BitmapFactory.cpp {}

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
                        jobject padding, jobject options) @BitmapFactory.cpp{
 // Update with options supplied by the client.
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        // Correct a non-positive sampleSize.  sampleSize defaults to zero within the
        // options object, which is strange.
        if (sampleSize <= 0) {
            sampleSize = 1;
        }

        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
//這裏計算出縮放因子
                scale = (float) targetDensity / density;
            }
        }
    }
    
//對圖片做縮放
    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
}

 

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