做移动端应用开发的朋友或多或少为Bitmap头疼过,茶不思饭不想就为了想出一个高效加载Bitmap的方法,为什么会有这样的情况呢?
毫无疑问,太尼玛吃内存了这玩意,Android手机本来内存就这么点,而且还是多个应用共享,稍微不注意给你送个惊喜,来个OutofMemoryError,不要太酸爽哦,所以作为移动软件开发者,解决好Bitmap是一个必须跨过去的坎,那么今天就来聊聊它,不过先补充下Bitmap相关的基础知识
我们先了解下什么是Bitmap
Bitmap从字面意思理解是位图,位图又叫点阵图或像素图,计算机屏幕上的图是由屏幕上的发光点(即像素)构成的,每个点用二进制数据来描述其颜色与亮度等信息,这些点是离散的,类似于点阵。多个像素点的色彩组合就形成了图像,称之为位图。位图在放大到一定程度时会发现它是由一个个小方格组成的,这些小方格被称为像素点,计算机存储位图实际上是存储图像的各个像素的位置和颜色数据等信息,所以图像越清晰,说明像素越多,相应的存储容量也越大
在Android中一个Bitmap对象其实是对位图的抽象,它可以从文件系统,资源文件夹,网络等获取,作用对象可以是JPG图片,也可以是PNG图片等,Bitmap对象包括像素点,长宽等信息
Bitmap内部有一个枚举类Config,它描述了像素的存储方式, 这会影响质量(颜色深度)以及显示透明/半透明颜色的能力。
提供了四种存储方式
ARGB_4444失真严重,已经被Google抛弃,在API13中已弃用,从KITKAT(API19)开始,创建Bitmap默认使用ARGB_8888;ALPHA_8使用场景特殊,比如设置遮盖效果等;不需要设置透明度,RGB_565是个不错的选择;既要透明度,对图片质量要求又高,那选ARGB_8888
Bitmap内部有一个枚举类CompressFormat,给我们指定了三种Bitmap压缩格式
其中JPEG有一个升级版JPEG2000,其压缩率比JPEG高约30%左右,同时支持有损和无损压缩。JPEG2000格式有一个极其重要的特征在于它能实现渐进传输,即先传输图像的轮廓,然后逐步传输数据,不断提高图像质量,让图像由朦胧到清晰显示
本文基于API24
Android各个版本获取Bitmap内存方法不一样
现在Android 都更新到9.0了,4.4及以下版本市场占有率不足5%,所以尽情使用getAllocationByteCount方法获取Bitmap像素所占内存吧
上图统计数据时间为2018年6月28号,由Google统计针对全世界所有的Android手机占比情况;从这点也可以看出在新建项目的时候将minSdkVersion设置为19(Android 4.4)是个不错的选择
在讲到计算Bitmap内存的时候会涉及到DisplayMetrics的两个变量,如下:
这里可以简单理解:
densityDpi的意思是屏幕上每英寸有多少点,注意不是像素点,可以理解为绝对屏幕密度,比如160dpi的屏幕的densityDpi值就是160
density是相对屏幕密度,一个DIP(设备独立像素)在160dpi屏幕上等于一个像素,我们以160dpi为基准线,density的值即为相对于160dpi屏幕的相对屏幕密度。比如,160dpi屏幕的density值为1, 320dpi屏幕的density值为2
在 DisplayMetrics 当中,这两个的关系是线性的
density | 1 | 1.5 | 2 | 3 | 3.5 | 4 |
---|---|---|---|---|---|---|
densityDpi | 160 | 240 | 320 | 480 | 560 | 640 |
一张实际像素大小是412*412的图片,运行在ZTE BV0800手机上,手机density是3,densityDpi是480
使用如下API加载图片并获取相关参数
Bitmap value = BitmapFactory.decodeResource(getResources(),R.mipmap.app_logo);
int height = value.getHeight();
int width = value.getWidth();
int size = value.getAllocationByteCount();
很奇怪对吧,同一张图片只是放在不同的目录居然会占用不同的内存,那具体什么原理呢?我们从源码来追溯下
public final int getAllocationByteCount() {
if (mBuffer == null) {
return getByteCount();
}
return mBuffer.length;
}
这个mBuffer 是个int数组,用于Bitmap的备份缓冲,当进行reconfigure(int,int,Config),setWidth(int),setHeight(int),setConfig(Bitmap.Config)等操作时,这个就会有值,并且会大于getByteCount()值
默认情况下会走到getByteCount()
public final int getByteCount() {
return getRowBytes() * getHeight();
}
这个方法是返回可用于存储Bitmap像素的最小字节数,最大可达46,340 x 46,340,其中getHeight方法会返回Bitmap对象的mHeight,也就是图片的高度(单位为px),而getRowBytes方法返回的是图片的像素宽度与色彩深度的乘积
继续追踪
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);
}
private static native int nativeRowBytes(long nativeBitmap);
到这里就需要进入native层了
这个方法定义在/frameworks/base/core/jni/android/graphics/Bitmap.cpp
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
return static_cast(bitmap->rowBytes());
}
class LocalScopedBitmap {
public:
explicit LocalScopedBitmap(jlong bitmapHandle)
: mBitmapWrapper(reinterpret_cast(bitmapHandle)) {}
BitmapWrapper* operator->() {
return mBitmapWrapper;
}
void* pixels() {
return mBitmapWrapper->bitmap().pixels();
}
bool valid() {
return mBitmapWrapper && mBitmapWrapper->valid();
}
private:
BitmapWrapper* mBitmapWrapper;
};
class BitmapWrapper {
public:
BitmapWrapper(Bitmap* bitmap)
: mBitmap(bitmap) { }
void freePixels() {
mInfo = mBitmap->info();
mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
mAllocationSize = mBitmap->getAllocationByteCount();
mRowBytes = mBitmap->rowBytes();
mGenerationId = mBitmap->getGenerationID();
mIsHardware = mBitmap->isHardware();
mBitmap.reset();
size_t rowBytes() const {
if (mBitmap) {
return mBitmap->rowBytes();
}
return mRowBytes;
}
void getSkBitmap(SkBitmap* outBitmap) {
assertValid();
mBitmap->getSkBitmap(outBitmap);
}
private:
sk_sp mBitmap;
SkImageInfo mInfo;
bool mHasHardwareMipMap;
size_t mAllocationSize;
size_t mRowBytes;
uint32_t mGenerationId;
bool mIsHardware;
};
这里的LocalScopedBitmap其实是对BitmapWrapper这样一个对象做了封装,然后BitmapWrapper类内部维护了SkBitmap,Java中的Bitmap在native层其实是一个SKBitmap对象
这个类可以去这里看skia/src/core/SkBitmap.cpp
int SkBitmap::ComputeBytesPerPixel(SkBitmap::Config config) {
int bpp;
switch (config) {
case kNo_Config:
bpp = 0; // not applicable
break;
case kA8_Config:
case kIndex8_Config:
bpp = 1;
break;
case kRGB_565_Config:
case kARGB_4444_Config:
bpp = 2;
break;
case kARGB_8888_Config:
bpp = 4;
break;
default:
SkDEBUGFAIL("unknown config");
bpp = 0; // error
break;
}
return bpp;
}
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
return width * SkColorTypeBytesPerPixel(ct);
}
为什么ARGB_888占4个字节,就是在上面函数定义的,看到这里基本上差不多可以得出一个结论了:
Bitmap所占内存 计算公式 = bitmapWidth * bitmapHeight * ColorType
这里面的ColorType跟图片的像素存储方式有关,比如ARGB_8888方式存储,一个像素占用4个字节,那我们计算下上面那种图片,它本身实际像素宽度高度是 412 * 412;加载Bitmap默认的类型是ARGB_8888,那么所占内存就是
412 * 412 * 4 = 678976
可以看出跟上面给出的第二种结果相同,但是放在其它两个目录后的所占内存就变了,为什么呢?
还记得上面说的手机两个参数和目录名称吗,这不是随便瞎说的,因为Bitmap所占用内存绝不仅仅只跟图片本身(像素点数和色彩格式)有关,还跟具体设备的屏幕参数(density和densityDpi)和所在目录有关
当前手机是确定的,参数density是3,densityDpi是480,现在只有具体放在哪个目录是可以变的,其实放在哪个目录是对应相应densityDpi和density的设备的,对应关系如下
了解了这些对应关系后,那这些值是怎么决定Bitmap占用的内存的呢?我们从加载方法中去一探究竟
public static Bitmap decodeResource(Resources res, int id) {
return decodeResource(res, id, null);
}
public static Bitmap decodeResource(Resources res, int id, Options 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) {
} 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;
}
这里只是通过openRawResource获取资源流,然后调用decodeResourceStream方法对流进行解码和适配后返回Bitmap,那到底是怎么处理内存的,进入此方法再看
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options 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
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
这些配置属性稍后再讲
第二步,配置Options的两个属性inDensity和inTargetDensity,这两个属性是影响内存开辟的重要因素,那这两个属性表示啥呢?
inDensity:资源所在目录的densityDpi(对应关系在上面)
inTargetDensity:当前设备屏幕的densityDpi
第三步,调用decodeStream方法
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
if (is == null) {
return null;
}
Bitmap bm = null;
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层了 ,这个方法定义在/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
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);
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
return bitmap;
}
接下来调用doDecode,这个方法有300多行,这里就给出一些重点部分,这个方法很重要,因为我们通过BitmapFactory.Options设置的一些属性是怎么加载Bitmap的都在这个方法体现
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
std::unique_ptr 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 prefColorSpace = nullptr;
// 根据开发者设置的选项参数从新赋值
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// 纠正sampleSize值
if (sampleSize <= 0) {
sampleSize = 1;
}
//是否只解码图片大小
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
// 初始化
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)) {
//这是在java层BitmapFactory.decodeResourceStream中设置的density
const int density = env->GetIntField(options, gOptions_densityFieldID);
//这是在java层BitmapFactory.decodeResourceStream中设置的targetDensity
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
//这个没有配置
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//根据targetDensity和density得到一个缩放值,用于Canvas绘制所用
scale = (float) targetDensity / density;
}
}
}
if (isMutable && isHardware) {
doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
return nullObjectReturn("Cannot create mutable hardware bitmap");
}
// 创建编解码器.
NinePatchPeeker peeker;
std::unique_ptr codec(SkAndroidCodec::NewFromStream(
streamDeleter.release(), &peeker));
if (!codec.get()) {
return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
}
// 不允许将ninepatch解码为565.在过去,解码到565会抖动,我们不想预先抖动ninepatch ,
//因为我们知道它们会被拉伸。我们不再抖动565解码,但我们继续阻止ninepatch解码到565,以保持旧的行为
if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
prefColorType = kN32_SkColorType;
}
// 确定输出大小
SkISize size = codec->getSampledDimensions(sampleSize);
//图片原始宽高
int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;
// 如有必要,精细缩放步骤.
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
// 设置解码colorType
SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);
sk_sp decodeColorSpace = codec->computeOutputColorSpace(
decodeColorType, prefColorSpace);
// 如果开发者只需要大小,设置选项并返回
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);
jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(decodeColorType);
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));
if (onlyDecodeSize) {
return nullptr;
}
}
// 将上面计算的缩放比列进一步精确
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast(scaledWidth * scale + 0.5f);
scaledHeight = static_cast(scaledHeight * scale + 0.5f);
}
......
SkBitmap outputBitmap;
if (willScale) {
// 根据缩放后的宽高除以图片真实宽高,得到canvas缩放比例
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// 为outputBitmap设置分配器
SkBitmap::Allocator* outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &defaultAllocator;
}
SkColorType scaledColorType = decodingBitmap.colorType();
// 如果alphaType是kUnpremul并且图像具有alpha,则颜色可能不正确,因为Skia尚不支持绘制到/来自未预乘的位图
outputBitmap.setInfo(
bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
if (!outputBitmap.tryAllocPixels(outputAllocator)) {
// 因为OOM失败
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);
}
......
// 返回Java Bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
分为如下几步:
首先会拿到density和targetDensity这两个值,然后根据它们计算出缩放比例
scale = (float) targetDensity / density
通过sampleSize值计算出缩小后的宽高
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
如果scale不等于1,也就是density和targetDensity不相等,那就再从新计算加载的Bitmap的宽高
scaledWidth = static_cast(scaledWidth * scale + 0.5f);
scaledHeight = static_cast(scaledHeight * scale + 0.5f);
根据计算后的宽高,除以图片真实宽高,得到canvas宽高缩放比例
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
最后通过sx和sy将canvas缩放,然后将读到内存的Bitmap画上去,这样Bitmap最后也缩放了相应比例
到此Bitmap内存的计算原理就结束了,我们回到上面的例子看看:
一张实际像素大小是412*412图片,运行在ZTE BV0800手机上,手机density是3,densityDpi是480
计算公式 = bitmapWidth * bitmapHeight * ColorType(默认是4)
现在从上面的源码可以得出新的计算公式 :
(int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType
新公式中的density 指的是资源所在目录的densityDpi,如下
targetDensity 指的是设备屏幕的densityDpi = 480
现在来计算下各个目录的值,见证奇迹的时刻来了
看到没有,我的天啊,终于找到正确答案了,现在你有没有get到Bitmap内存的正确计算方式呢
有人可能觉得这个0.5f加了有什么用呢,这里也没体现出来啊,要知道我们这里的设备屏幕densityDpi都是很规整的,但是总会有一些不正规厂家出的非正版手机,比如某pin多多上面的一块钱手机,当然了,这里只是开玩笑,但是targetDensity / density结果可能是小数,比如小数位是0.5+这种,而图片的尺寸,都是以 int 类型为单位,如果不加0.5个,对于小数位大于0.5的值就也会直接舍去,影响最终结果的精准性。所以 Android 为了规避这样的问题,做了个容差值,修正结果
到这里我们可以知道了一个Bitmap占用多少内存跟以下这些因素有关
知道了Bitmap内存占用的原理,那么一个很直接的问题就来了,怎么降低Bitmap在应用中所占内存或者怎么高效加载Bitmap呢?这个就不在这篇文章继续了,太长了,写的我眼都花了,休息会,放到下一篇文章中解析
memory = (int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType
(小提示:Android默认会从与屏幕相匹配的 densityDpi的目录去寻找图片资源,如果没有,就往更高densityDpi的目录去寻找,开发者切勿放到低densityDpi的目录,这会放大占用内存)