Bitmap的大小计算
- 一张 300x400 的 jpeg 图片,我把它放到 drawable-xxhdpi 目录下,在1280x720,5英寸的手机上,它占用内存是多少
官方给的是5英寸实际屏幕尺寸我们可以自己获取,我的实际是4.589英寸
public double getScreenPhysicalSize() {
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
double diagonalPixels = Math.sqrt(Math.pow(dm.widthPixels, 2) + Math.pow(dm.heightPixels, 2));
return diagonalPixels / (160 * dm.density);
}
我们知道 图片大小 = 宽 * 高 * 单个像素点所占字节数,那么这么一算大小应该是 300* 400 *4 = 240000 ,但最终调用代码 Bitmap.getByteCount() 发现是 213600。算错的原因是,这里的宽高不是图片资源的宽高
实际大小和手机分辨率与放到的文件夹有关
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.meinv, options);
Log.e("TAG", "美女图片占用的宽的大小是:" + src.getWidth());
Log.e("TAG", "美女图片占用的高的大小是:" + src.getHeight());
Log.e("TAG", "美女图片占用的内存的大小是:" + src.getByteCount());
实际大小=bitmap.width() * bitmap*height() * 4。(RGB:4 ,565:2)
所以上 面图片的 实际大小是=200* 267*4=213600
Bitmap java层源码分析
- 首先看简单的getWidth方法
public final int getWidth() {
if (mRecycled) {
Log.w(TAG, "Called getWidth() on a recycle()'d bitmap! This is undefined behavior!");
}
return mWidth;
}
// called from JNI
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}
mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;
mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
sPreloadTracingNumInstantiatedBitmaps++;
sPreloadTracingTotalBitmapsSize += nativeSize;
}
}
实际调用的是native层源码
- 我们看下Bitmap的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);
}
//代码省略...
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();
}
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);
}
这里需要注意两个方法opts.inDensity和opts.inTargetDensity
opts.inDensity指的是你放到的目录:
目录 | 像素范围 | 图标尺寸 |
---|---|---|
mdpi | 120dpi-160dpi | 48x48 |
hdpi | 160dpi~240dpi | 72x72 |
xdpi | 240-320dpi | 96x96 |
xxhdpi | 320-480dpi | 144x144 |
xxxhdpi | 480-640dpi | 192x192 |
屏幕分辨率px,屏幕密度dpi:与屏幕尺寸和屏幕的分辨率有关
以160dpi为基准,1dpi=1px
那么240dpi,1dpi=240/160=1.5px
屏幕尺寸:屏幕尺寸指屏幕对角线的的长度,单位是英寸,1英寸=2.54厘米
所以我们放在xxhdpi的图片的实际大小是480,所以opts.inDensity的值是480
opts.inTargetDensity指的是我们手机的dpi,我的手机是1280x720,5英寸的小米手机,所以计算结果应该是开根号(1280* 1280+720*720)/4.589=320
- 继续追踪源码
步骤就省略了,最终实际调用的是native层的源码
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
opts里面有两个上面我提到的非常重要的参数opts.inTargetDensity和opts.inDensity
Bitmap native层源码分析
查看native源码方式一、下载Android版本的源码,我们默认的Android sdk下的源码不行的,具体下载大家可以去这个百度网盘https://pan.baidu.com/s/1ngsZs
二、去这个网站看http://androidxref.com/
可以直接搜BitmapFactory.cpp,也可以去文件夹下找
源码分析
首先找native注册gMethod方法
static const JNINativeMethod gMethods[] = {
{ "nativeDecodeStream",
"(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
(void*)nativeDecodeStream
},
//代码省略
};
nativeDecodeStream源码
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
jobject bitmap = NULL;
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
//实际是这个方法
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
return bitmap;
}
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
std::unique_ptr<SkStreamRewindable> streamDeleter(stream);
int sampleSize = 1;
//是否只是获取图片的大小
bool onlyDecodeSize = false;
SkColorType prefColorType = kN32_SkColorType;
//硬件加速
bool isHardware = false;
//复用
bool isMutable = false;
//缩放
float scale = 1.0f;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;
sk_sp<SkColorSpace> prefColorSpace = nullptr;
//上面java分析的时候可以知道肯定不为空
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (sampleSize <= 0) {
sampleSize = 1;
}
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
env->SetObjectField(options, gOptions_outConfigFieldID, 0);
env->SetObjectField(options, gOptions_outColorSpaceFieldID, 0);
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
jobject jcolorSpace = env->GetObjectField(options, gOptions_colorSpaceFieldID);
prefColorSpace = GraphicsJNI::getNativeColorSpace(env, jcolorSpace);
isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
//计算缩放比例
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//获取我们的dpi,之前我们传入xxhdpi下图片的dpi=480
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=当前设备的dpi/文件目录下的dpi
//scale= opts.inTargetDensity/opts.inDensity=320/480=0.66
scale = (float) targetDensity / density;
}
}
}
//Android 8.0需要注意
if (isMutable && isHardware) {
doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
return nullObjectReturn("Cannot create mutable hardware bitmap");
}
...
SkISize size = codec->getSampledDimensions(sampleSize);
//我们图片实际的宽高 300*400
int scaledWidth = size.width();//300
int scaledHeight = size.height();//400
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
//一般默认会进来,如果java设置了insampleSize=2,则实际会缩放四倍
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
if (options != NULL) {
jstring mimeType = encodedFormatToString(
env, (SkEncodedImageFormat)codec->getEncodedFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in encodedFormatToString()");
}
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
SkColorType outColorType = decodeColorType;
// Scaling can affect the output color type
if (willScale || scale != 1.0f) {
outColorType = colorTypeForScaledOutput(outColorType);
}
jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(outColorType);
if (isHardware) {
configID = GraphicsJNI::kHardware_LegacyBitmapConfig;
}
jobject config = env->CallStaticObjectMethod(gBitmapConfig_class,
gBitmapConfig_nativeToConfigMethodID, configID);
env->SetObjectField(options, gOptions_outConfigFieldID, config);
env->SetObjectField(options, gOptions_outColorSpaceFieldID,
GraphicsJNI::getColorSpace(env, decodeColorSpace, decodeColorType));
//java中如果调用 options.inJustDecodeBounds=true,则返回null但是会设置宽高
if (onlyDecodeSize) {
return nullptr;
}
}
//刚刚我们计算是2/3
if (scale != 1.0f) {
willScale = true;
//实际结果=(300*2/3+0.5f)=200.5
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
//实际结果=(400*2/3+0.5f)=266.66
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
// 判断是否有复用的 Bitmap
android::Bitmap* reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
//不需要缩放的时候
HeapAllocator defaultAllocator;
RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
//需要缩放的时候
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator* decodeAllocator;
if (javaBitmap != nullptr && willScale) {
//开辟内存
decodeAllocator = &scaleCheckingAllocator;
} else if (javaBitmap != nullptr) {
decodeAllocator = &recyclingAllocator;
} else if (willScale || isHardware) {//7.0没有
decodeAllocator = &heapAllocator;
} else {
decodeAllocator = &defaultAllocator;
}
....
//原图 开辟内存
SkBitmap decodingBitmap;
if (!decodingBitmap.setInfo(bitmapInfo) ||
!decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
//开辟内存和设置失败的话返回null
return nullptr;
}
//真正要返回的图片
SkBitmap outputBitmap;
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// Set the allocator for the outputBitmap.
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)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc);
paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
//画笔缩放画上去的
SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
//直接复制
outputBitmap.swap(decodingBitmap);
}
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
options.width和options.height返回的是图片的宽高,并不是bitmap最终的宽高
Bitmap内存分配
内存如果需要缩放都是开辟在native层中,如果不需要缩放,8.0以下开辟在java内存中
-
8.0分配内存源码
主要这个方法outputBitmap.tryAllocPixels(outputAllocator, NULL),而outputBitmap实际是SkBitmap,所以我们看下SkBitmap.cpp中tryAllocPixels源码
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
HeapAllocator stdalloc;
if (nullptr == allocator) {
allocator = &stdalloc;
}
return allocator->allocPixelRef(this, ctable);
}
我们从刚才的参数outputAllocator往上看
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
//实际是defaultAllocator
outputAllocator = &defaultAllocator;
}
HeapAllocator defaultAllocator;
所以实际调用的是 HeapAllocator中的allocPixelRef
可以在中搜http://androidxref.com/
HeapAllocator实际是Graphics.cpp中
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
return !!mStorage;
}
static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, SkColorTable* ctable, AllocPixeRef alloc) {
const SkImageInfo& info = bitmap->info();
if (info.colorType() == kUnknown_SkColorType) {
LOG_ALWAYS_FATAL("unknown bitmap configuration");
return nullptr;
}
size_t size;
// we must respect the rowBytes value already set on the bitmap instead of
// attempting to compute our own.
const size_t rowBytes = bitmap->rowBytes();
if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
return nullptr;
}
//分配内存到native层
auto wrapper = alloc(size, info, rowBytes, ctable);
if (wrapper) {
wrapper->getSkBitmap(bitmap);
// since we're already allocated, we lockPixels right away
// HeapAllocator behaves this way too
bitmap->lockPixels();
}
return wrapper;
}
Bitmap的销毁recycle
通过上 面源码分析我们知道,Bitmap 其实占三个部分对象,一个是 Java Bitmap 对象,还有一个是 Native Bitmap 对象,还有一个对象数组,Java Bitmap 对象肯定是垃圾回收机制来管理了,那 Native Bitmap 对象会在什么时候回收?
Google提供了recycle方法,但是只对8.0以上的有用,7.0以下没有用(因为开辟的是java内存)
实际最终调用的是
private static native boolean nativeRecycle(long nativeBitmap);
继续追踪8.0的Bitmap.cpp源码
static const JNINativeMethod gBitmapMethods[] = {
{ "nativeRecycle", "(J)Z", (void*)Bitmap_recycle },
}
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
void freePixels() {
mInfo = mBitmap->info();
mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
mAllocationSize = mBitmap->getAllocationByteCount();
mRowBytes = mBitmap->rowBytes();
mGenerationId = mBitmap->getGenerationID();
mIsHardware = mBitmap->isHardware();
mBitmap.reset();
}
Bitmap内存复用
Bitmap不断反复开辟和销毁内存,很容易造成内存抖动,所以Google提供了解决办法就是允许bitmap复用
它也有几个限制条件:
- 被复用的 Bitmap 必须为 Mutable(通过 BitmapFactory.Options 设置)
- 在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,其中BitmapFactory.Options#inSampleSize 字段必须设置为 1。例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从4.4(sdk 19)开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
- 新申请的bitmap与旧的bitmap必须有相同的解码格式,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。
logMemory();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.meinv, options);
options.inBitmap = bitmap1;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.meinv, options);
Log.e("TAG",bitmap1+"\t"+bitmap2);
logMemory();
private void logMemory() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
Log.e("TAG", "AvailMem :" + memoryInfo.availMem / 1024 / 1024);
Log.e("TAG", "lowMemory:" + memoryInfo.lowMemory);
Log.e("TAG", "NativeHeapAllocatedSize :" + Debug.getNativeHeapAllocatedSize() / 1024 / 1024);
}