Bitmap的内存大小、创建、回收

先简要学习下dpi的计算和dp转px的计算

image.png

dpi为单位像素的密度,就是每英寸所占的密度
= 开根号(1080 * 1080 + 1920 * 1920) /5.2

1dp = dpi/160*1dp(px)

Bitmap占用内存大小

计算一张图片占用内存的大小?

  • 公式:内存大小 = 宽 * 高 * 单个像素点所占字节数
    我们用代码打印信息出来看一下:
    @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.earth);
          Log.e("TAG","图片的宽=520,图片的高=618");
          Log.e("TAG","bitmap width = "+bitmap.getWidth());
          Log.e("TAG","bitmap height = "+bitmap.getHeight());
          Log.e("TAG","bitmap size = "+bitmap.getByteCount());
      }
    

打印:

图片的宽=520,图片的高=618
bitmap width = 247
bitmap height = 208
bitmap size = 205504
247 * 208 * 4 =  205504
520 * 618 * 4 = 1285440

从打印的结果可以看出,占用内存的实际大小,并不是按照图片的宽高来算,而是按照Bitmap的宽高来算。那么Bitmap.width.height是如何计算的呢?
看看BitmapFactory.decodeResource(getResources(), R.drawable.earth, options)源码:
java层的代码:

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
          bm = decodeResourceStream(res, value, is, null, opts);
      } 
      ...
      return bm;
  }

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();
      }

      //opts的赋值
      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) {
          //拿到手机的dpi赋值
          opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
      }
      //给opts赋值后,调用这个方法
      return decodeStream(is, pad, opts);
  }


public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
          @Nullable Options opts) {
      if (is == null) {
          return null;
      }
      Bitmap bm = null;

      try {
          if (is instanceof AssetManager.AssetInputStream) {
              final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
              //调用这个方法创建bm
              bm = nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts),
                  Options.nativeColorSpace(opts));
          } else {
              //否则调用这个方法创建bm
              bm = decodeStreamInternal(is, outPadding, opts);
          }
       ...
      return bm;
  }

private static Bitmap decodeStreamInternal(@NonNull InputStream is,
          @Nullable Rect outPadding, @Nullable Options opts) {
      // ASSERT(is != null);
      byte [] tempStorage = null;
      if (opts != null) tempStorage = opts.inTempStorage;
      if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
      //最后调用这个方法创建bm
      return nativeDecodeStream(is, tempStorage, outPadding, opts,
              Options.nativeInBitmap(opts),
              Options.nativeColorSpace(opts));
  }

//调用native层代码
@UnsupportedAppUsage
  private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
          Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);
  @UnsupportedAppUsage
  private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts,
          long inBitmapHandle, long colorSpaceHandle);

在java层的代码中,创建bm只是创建了Options对象并赋值了dpi和density。具体的创建都放在了JNI中用c++代码写了,并且是调用了JNI层的nativeDecodeStream()方法创建。那只能看看JNI的源码了。

JNI层代码:

//8.0的代码
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
      jobject padding, jobject options) {

  jobject bitmap = NULL;
  std::unique_ptr stream(CreateJavaInputStreamAdaptor(env, is, storage));

  if (stream.get()) {
      std::unique_ptr bufferedStream(
              SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
      SkASSERT(bufferedStream.get() != NULL);
      //调用doDecode()方法创建bitmap
      bitmap = doDecode(env, bufferedStream.release(), padding, options);
  }
  return bitmap;
}


doDecode()的代码实在是太多了,我只大概放跟width和height计算有关的。

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// scale = dpi/480
scale = (float) targetDensity / density;
SkISize size = codec->getSampledDimensions(sampleSize);
int scaledWidth = size.width(); //原图的宽
int scaledHeight = size.height(); //原图的高

//如果需要缩放,则宽高/缩放值
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
      willScale = true;
      scaledWidth = codec->getInfo().width() / sampleSize;
      scaledHeight = codec->getInfo().height() / sampleSize;
  }

if (scale != 1.0f) {
      willScale = true;
      scaledWidth = static_cast(scaledWidth * scale + 0.5f);
      scaledHeight = static_cast(scaledHeight * scale + 0.5f);
  }
}

从上面代码可以看见,会先拿到原图的宽和高,然后判断是否需要对图片进行缩放,默认是不缩放的,如果你设置了options.inSampleSize = value就会对原图的宽高/value。然后是scale是否为1,scale的计算方法是dpi/ density,然后是宽高 * scale + 0.5f。后面的0.5可以理解为四舍五入。得出:Bitmap的宽高跟你是否设置了缩放有关,跟手机的dpi有关,即跟手机的像素密度有关。(补:还跟项目中存放的文件夹有关)

写个代码验证一下,我们理解的bitmap的宽高是否正确:

@Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      BitmapFactory.Options options = new BitmapFactory.Options();
      Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.earth, options);
      Log.e("TAG","图片的宽=520,图片的高=618");
      Log.e("TAG", "bitmap width = " + bitmap.getWidth());
      Log.e("TAG", "bitmap height = " + bitmap.getHeight());
      Log.e("TAG", "bitmap size = " + bitmap.getByteCount());
      
     //验证scale
      float scale = (float)options.inTargetDensity/options.inDensity;
      Log.e("TAG","scale = "+ scale);
      int width = (int) ((520 * scale)+ 0.5f);
      int height = (int) ((618 * scale )+ 0.5f);
      Log.e("TAG","验证 width = "+width+" height= "+height);
  }

打印:

图片的宽=618,图片的高=520
bitmap width = 247
bitmap height = 208
bitmap size = 205504
scale = 0.4
验证 width = 247 height= 208

可以看到我们的验证是成功的,接下来再验证一下缩放。我们让宽高都缩小2倍。

@Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inSampleSize = 2; //图片的宽高会压缩2倍
      Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.earth, options);
      Log.e("TAG","图片的宽=618,图片的高=520");
      Log.e("TAG", "bitmap width = " + bitmap.getWidth());
      Log.e("TAG", "bitmap height = " + bitmap.getHeight());
      Log.e("TAG", "bitmap size = " + bitmap.getByteCount());
  }

打印:

图片的宽=618,图片的高=520
bitmap width = 124  // 247/2
bitmap height = 104 // 208/2
bitmap size = 51584

验证也成功,宽高缩小两倍。图片也是造成OOM的原因之一。接下来看看这些图片那么大存储是如何存在我们内存上?

现在可以回答UI的提问了:
图片在内存中运行大小,与给的图片存储大小无关。而是取决于图片的宽高和手机的宽高(dpi)。

Bitmap在内存的创建

看源码前前把大致流程画一下。
在安卓8.0以下版本,内存是创建在java层。8.0以上版本,内存是创建在native层。


image.png

大致流程是:c++是根据指针来分配内存的,所以会有一个原图的指针来开辟一个原图内存。还有一个是最终返回的bitmap,如果原图不需要缩放,就把最终返回的图片指针指向原图的指针。如果需要缩放,则是先看看javaBitmap是否为null,最终要返回的图片新指向一个指针,并且新开辟一个缩放内存,并把原图通过画笔画到缩放内存。

继续跟着JNI层的doDecode()方法看,bitmap如何存储在内存中。

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

  //不同的指针
  //javaBitmap !=null && 需要缩放
  ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
  //javaBitmap !=null
  RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
  //需要缩放 或者 有硬件加速
  SkBitmap::HeapAllocator heapAllocator;
  //默认
  HeapAllocator defaultAllocator;
  SkBitmap::Allocator* decodeAllocator;
  if (javaBitmap != nullptr && willScale) {
      decodeAllocator = &scaleCheckingAllocator;
  } else if (javaBitmap != nullptr) {
      decodeAllocator = &recyclingAllocator;
  } else if (willScale || isHardware) {
      decodeAllocator = &heapAllocator;
  } else {
      decodeAllocator = &defaultAllocator;
  }

//开辟原图内存,tryAllocPixels,通过decodeAllocator指针
if (!decodingBitmap.setInfo(bitmapInfo) ||
          !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
      // SkAndroidCodec should recommend a valid SkImageInfo, so setInfo()
      // should only only fail if the calculated value for rowBytes is too
      // large.
      // tryAllocPixels() can fail due to OOM on the Java heap, OOM on the
      // native heap, or the recycled javaBitmap being too small to reuse.
      return nullptr;
  }

  //获取像素
  SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),
          decodingBitmap.rowBytes(), &codecOptions);

   //最终要返回的图,如果需要缩放,则新指向一个指针创建一个缩放内存,如果不需要缩放,则指向原图内存。
  SkBitmap outputBitmap;
  if (willScale) {
      const float sx = scaledWidth / float(decodingBitmap.width());
      const float sy = scaledHeight / float(decodingBitmap.height());

      //根据javaBitmap决定指向哪个指针
      SkBitmap::Allocator* outputAllocator;
      if (javaBitmap != nullptr) {
          outputAllocator = &recyclingAllocator;
      } else {
          outputAllocator = &defaultAllocator;
      }

      SkColorType scaledColorType = colorTypeForScaledOutput(decodingBitmap.colorType());

      //设置信息
      outputBitmap.setInfo(
              bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
      //开辟缩放内存
      if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
          // This should only fail on OOM.  The recyclingAllocator should have
          // enough memory since we check this before decoding using the
          // scaleCheckingAllocator.
          return nullObjectReturn("allocation failed for scaled bitmap");
      }

      SkPaint paint;
      // kSrc_Mode instructs us to overwrite the uninitialized pixels in
      // outputBitmap.  Otherwise we would blend by default, which is not
      // what we want.
      paint.setBlendMode(SkBlendMode::kSrc);
      paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
      //画笔缩放画上outputBitmap
      SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
      canvas.scale(sx, sy);
      canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
  } else {
      //不缩放,则指向原图
      outputBitmap.swap(decodingBitmap);
  }
...
if (isHardware) {
      sk_sp hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
      return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
              ninePatchChunk, ninePatchInsets, -1);
  }

   //最终调用createBitmap生成bitmap,返回给Java层
  // now create the java bitmap
  return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
          bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

}


总结:首先会创建一个原图内存,然后如果不需要缩放,则把最终的图片指向原图。如果需要缩放,则根据内存的复用判断等,新开辟一个缩放内存。

验证8.0以上图片开辟在java层开辟内存,8.0以上在native层开辟内存:
内存在哪里开辟这里就不进行查看源码验证了,这里简单在写个演示验证一下:
6.0手机演示:

private void createNewBitmap() {
      logIM();
      //相当于开4M图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024, 1024 , Bitmap.Config.ARGB_8888);
     // bitmap.recycle();
      Log.e("TAG","*********");
      Log.e("TAG", "size = " + bitmap.getByteCount() / 1024 / 1024);
      logIM();
  }

  public void logIM() {
      Runtime runtime = Runtime.getRuntime();

      Log.e("TAG","totalSize java层运行总内存 = "+runtime.maxMemory()/1024/1024);
      Log.e("TAG","totalSize java层使用内存 = "+runtime.totalMemory()/1024/1024);

      Log.e("TAG","nativeSize native使用的内存 = "+ Debug.getNativeHeapAllocatedSize()/1024/1024);
  }

在6.0的手机中,打印显示:

totalSize java层运行总内存 = 192
totalSize java层使用内存 = 2
nativeSize native使用的内存 = 3
*********
size = 4
totalSize java层运行总内存 = 192
totalSize java层使用内存 = 5
nativeSize native使用的内存 = 3

private void createNewBitmap() {
      //相当于开1G图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024,1024*256, Bitmap.Config.ARGB_8888);
      Log.e("TAG","size = "+bitmap.getByteCount()/1024/1024);
  }

在6.0的手机中,运行崩溃:

java.lang.OutOfMemoryError: Failed to allocate a 1073741836 byte allocation with 1048576 free bytes and 190MB until OOM
      at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
      at android.graphics.Bitmap.nativeCreate(Native Method)
      at android.graphics.Bitmap.createBitmap(Bitmap.java:831)
      at android.graphics.Bitmap.createBitmap(Bitmap.java:808)
      at android.graphics.Bitmap.createBitmap(Bitmap.java:775)
      at com.haiming.myapplication.MainActivity.createNewBitmap(MainActivity.java:36)

9.0手机演示:

 private void createNewBitmap() {
      logIM();
      //相当于开4M图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024, 1024 , Bitmap.Config.ARGB_8888);
     // bitmap.recycle();
      Log.e("TAG","*********");
      Log.e("TAG", "size = " + bitmap.getByteCount() / 1024 / 1024);
      logIM();
  }

  public void logIM() {
      Runtime runtime = Runtime.getRuntime();
      Log.e("TAG","totalSize java层运行总内存 = "+runtime.maxMemory()/1024/1024);
      Log.e("TAG","nativeSize native使用的内存 = "+ Debug.getNativeHeapAllocatedSize()/1024/1024);
  }
totalSize java层运行总内存 = 384
nativeSize native使用的内存 = 6
*********
size = 4
totalSize java层运行总内存 = 384
nativeSize native使用的内存 = 10

private void createNewBitmap() {
      //相当于开1G图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024,1024*256, Bitmap.Config.ARGB_8888);
      Log.e("TAG","size = "+bitmap.getByteCount()/1024/1024);
  }

在9.0的手机中,提示图片大小为1G,没崩溃。

size = 1024

得出:8.0以上图片内存加载在Native层,内存更多!

Bitmap的回收:

  • 在Android4.0-8.0以下,内存分配在java层,由GC回收。
  • 在Android8.0以上,NativeAllocationRegistry是Android 8.0引入的一种辅助自动回收native内存的一种机制,当Java对象因为GC被回收后,NativeAllocationRegistry可以辅助回收Java对象所申请的native内存。

bitmap.recycler()的作用:
8.0以上手机:

private void createNewBitmap() {
      logIM();
      //相当于开1G图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024, 1024 , Bitmap.Config.ARGB_8888);
      bitmap.recycle();
      Log.e("TAG","*********");
      Log.e("TAG", "size = " + bitmap.getByteCount() / 1024 / 1024);
      logIM();
  }

  public void logIM() {
      Runtime runtime = Runtime.getRuntime();
      Log.e("TAG","totalSize java层运行总内存 = "+runtime.maxMemory()/1024/1024);
      Log.e("TAG","nativeSize native使用的内存 = "+ Debug.getNativeHeapAllocatedSize()/1024/1024);
  }
totalSize java层运行总内存 = 384
nativeSize native使用的内存 = 6
*********
size = 0
totalSize java层运行总内存 = 384
nativeSize native使用的内存 = 5

验证:8.0以上手机可以调用recycle()可以回收native的内存。

8.0以下手机:

private void createNewBitmap() {
      logIM();
      //相当于开4M图片内存
      Bitmap bitmap = Bitmap.createBitmap(1024, 1024 , Bitmap.Config.ARGB_8888);
      bitmap.recycle();
      Log.e("TAG","*********");
      Log.e("TAG", "size = " + bitmap.getByteCount() / 1024 / 1024);
      logIM();
  }

  public void logIM() {
      Runtime runtime = Runtime.getRuntime();

      Log.e("TAG","totalSize java层运行总内存 = "+runtime.maxMemory()/1024/1024);
      Log.e("TAG","totalSize java使用内存 = "+runtime.totalMemory()/1024/1024);
      Log.e("TAG","nativeSize native使用的内存 = "+ Debug.getNativeHeapAllocatedSize()/1024/1024);
  }
totalSize java层运行总内存 = 192
totalSize java使用内存 = 2
nativeSize native使用的内存 = 3
*********
size = 4
totalSize java层运行总内存 = 192
totalSize java使用内存 = 5
nativeSize native使用的内存 = 3

验证:8.0以下手机可以调用recycle()不可以回收java的内存。可见由JVM的GC回收。也可以调用对象的finalze 方法

你可能感兴趣的:(Bitmap的内存大小、创建、回收)