先简要学习下dpi的计算和dp转px的计算
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层。
大致流程是: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 方法