Android源码解析之Bitmap占用内存正确的计算公式 你知道吗

Bitmap

  • 前言
  • Bitmap简介
  • 像素存储方式
  • 图片压缩格式
  • Bitmap内存计算
    • 获取Bitmap所占内存
    • 计算所占内存
      • 举例
      • Bitmap.getAllocationByteCount
      • Bitmap.getByteCount
      • Bitmap.getRowBytes
      • Bitmap.Bitmap_rowBytes
      • SkBitmap.cpp
      • 内存计算公式
      • BitmapFactory.decodeResource
      • BitmapFactory.decodeResourceStream
      • BitmapFactory.cpp
      • 修正后的内存计算公式
      • 补充
  • 划重点

前言

做移动端应用开发的朋友或多或少为Bitmap头疼过,茶不思饭不想就为了想出一个高效加载Bitmap的方法,为什么会有这样的情况呢?

毫无疑问,太尼玛吃内存了这玩意,Android手机本来内存就这么点,而且还是多个应用共享,稍微不注意给你送个惊喜,来个OutofMemoryError,不要太酸爽哦,所以作为移动软件开发者,解决好Bitmap是一个必须跨过去的坎,那么今天就来聊聊它,不过先补充下Bitmap相关的基础知识

Bitmap简介

我们先了解下什么是Bitmap

Bitmap从字面意思理解是位图,位图又叫点阵图或像素图,计算机屏幕上的图是由屏幕上的发光点(即像素)构成的,每个点用二进制数据来描述其颜色与亮度等信息,这些点是离散的,类似于点阵。多个像素点的色彩组合就形成了图像,称之为位图。位图在放大到一定程度时会发现它是由一个个小方格组成的,这些小方格被称为像素点,计算机存储位图实际上是存储图像的各个像素的位置和颜色数据等信息,所以图像越清晰,说明像素越多,相应的存储容量也越大

在Android中一个Bitmap对象其实是对位图的抽象,它可以从文件系统,资源文件夹,网络等获取,作用对象可以是JPG图片,也可以是PNG图片等,Bitmap对象包括像素点,长宽等信息

像素存储方式

Bitmap内部有一个枚举类Config,它描述了像素的存储方式, 这会影响质量(颜色深度)以及显示透明/半透明颜色的能力。
提供了四种存储方式

  • ARGB_8888:四个通道即A,R,G,B,其中A指半透明的alpha,RGB是颜色通道,每个通道以八位精度存储;这种方式下每个像素占用四个字节,可以提供最高质量的图片,但是最耗内存
  • ARGB_4444:通道同上,但是每个通道以四位存储;这种方式下每个像素占用两个字节,该模式存储图片质量差,失真明显,但是耗费内存小且拥有Alpha通道
  • RGB_565:只有RGB三个通道,R通道以五位精度存储,G通道以六位精度存储,B通道以5位精度存储;这种方式每个像素以两个字节存储;当使用不需要高保真的不透明Bitmap时,此配置可能很有用;为了获得更好的结果,应该应用抖动属性
  • ALPHA_8:只有A通道,每个像素占用一个字节内存,不过只有透明度,不存储颜色信息

ARGB_4444失真严重,已经被Google抛弃,在API13中已弃用,从KITKAT(API19)开始,创建Bitmap默认使用ARGB_8888;ALPHA_8使用场景特殊,比如设置遮盖效果等;不需要设置透明度,RGB_565是个不错的选择;既要透明度,对图片质量要求又高,那选ARGB_8888

图片压缩格式

Bitmap内部有一个枚举类CompressFormat,给我们指定了三种Bitmap压缩格式

  • JPEG:全称Joint Photographic Expert Group,即联合照片专家组;文件后辍名为".jpg"或".jpeg";是一种有损压缩格式,压缩比率通常在10:1到40:1之间,压缩比越大,品质就越低
  • PNG:全称Portable Network Graphics,即便携式网络图形;文件后缀名为“.png”;是一种无损压缩格式,支持高级别无损耗压缩和alpha 通道透明度,主要用于小图标,透明背景等;但是其压缩比没有jpeg大,且色彩复杂情况下压缩后文件较大
  • WEBP:由Google推出的新格式,同时提供了有损压缩与无损压缩;无损压缩,相同质量的webp比PNG小大约26%;有损压缩,相同质量的webp比JPEG小25%-40%,但是WebP格式图像的编码时间比JPEG格式图像长8倍; 支持GIF

其中JPEG有一个升级版JPEG2000,其压缩率比JPEG高约30%左右,同时支持有损和无损压缩。JPEG2000格式有一个极其重要的特征在于它能实现渐进传输,即先传输图像的轮廓,然后逐步传输数据,不断提高图像质量,让图像由朦胧到清晰显示

Bitmap内存计算

本文基于API24

获取Bitmap所占内存

Android各个版本获取Bitmap内存方法不一样

  • int getAllocationByteCount ():API19(Android4.4)及以后,返回用于存储此Bitmap像素的已分配内存的大小,该值在Bitmap的生命周期内不会改变;如果重新使用它来解码较小尺寸的其他Bitmap,或者通过手动重新配置,这可能会大于getByteCount()的结果,比如调用了reconfigure(int,int,Config),setWidth(int),setHeight(int),setConfig(Bitmap.Config)和BitmapFactory.Options.inBitmap。 如果未以这种方式修改位图,则此值将与getByteCount()返回的值相同
  • int getByteCount ():API12及以后,返回可用于存储此Bitmap像素的最小字节数。从KITKAT(API19)开始,此方法的结果不再用于确定位图的内存使用情况。 请参阅getAllocationByteCount()
  • getRowBytes()*getHeight():其中getRowBytes返回Bitmap每行所占像素的字节数,注意这是存储在native内存中的;从KITKAT(API19)开始,此方法不应用于计算位图的内存使用情况,请参阅getAllocationByteCount();所以这个结果与getHeight()相乘就是Bitmap所占的总内存

现在Android 都更新到9.0了,4.4及以下版本市场占有率不足5%,所以尽情使用getAllocationByteCount方法获取Bitmap像素所占内存吧

在这里插入图片描述

上图统计数据时间为2018年6月28号,由Google统计针对全世界所有的Android手机占比情况;从这点也可以看出在新建项目的时候将minSdkVersion设置为19(Android 4.4)是个不错的选择

计算所占内存

在讲到计算Bitmap内存的时候会涉及到DisplayMetrics的两个变量,如下:

  • density:The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5”x2” screen), providing the baseline of the system’s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc.
    This value does not exactly follow the real screen size (as given by xdpi and ydpi, but rather is used to scale the size of the overall UI in steps based on gross changes in the display dpi. For example, a 240x320 screen will have a density of 1 even if its width is 1.8”, 1.3”, etc. However, if the screen resolution is increased to 320x480 but the screen size remained 1.5”x2” then the density would be increased (probably to 1.5).
  • densityDpi:The screen density expressed as dots-per-inch.

这里可以简单理解:

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();
  • 放在-hdpi目录下,加载这张图片获取到的height=824,width=824,size=2715904
  • 放在-xxhdpi目录下,加载这张图获取到的height=412,width=412,size=678976
  • 放在-xxxhdpi目录下,加载这张图获取到的height=309,width=309,size=381924

很奇怪对吧,同一张图片只是放在不同的目录居然会占用不同的内存,那具体什么原理呢?我们从源码来追溯下

Bitmap.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()

Bitmap.getByteCount

public final int getByteCount() {
        return getRowBytes() * getHeight();
}

这个方法是返回可用于存储Bitmap像素的最小字节数,最大可达46,340 x 46,340,其中getHeight方法会返回Bitmap对象的mHeight,也就是图片的高度(单位为px),而getRowBytes方法返回的是图片的像素宽度与色彩深度的乘积

继续追踪

Bitmap.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

Bitmap.Bitmap_rowBytes

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

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的设备的,对应关系如下

  • ldpi(低)~120dpi
  • mdpi(中)~160dpi
  • hdpi(高)~240dpi
  • xhdpi(超高)~320dpi
  • xxhdpi(超超高)~480dpi
  • xxxhdpi(超超超高)~640dpi

Android源码解析之Bitmap占用内存正确的计算公式 你知道吗_第1张图片
具体可以到官网查看支持多种屏幕

了解了这些对应关系后,那这些值是怎么决定Bitmap占用的内存的呢?我们从加载方法中去一探究竟

BitmapFactory.decodeResource

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,那到底是怎么处理内存的,进入此方法再看

BitmapFactory.decodeResourceStream

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

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

  • 放在-hdpi目录下,加载这张图片获取到的height=824,width=824,size=2715904
  • 放在-xxhdpi目录下,加载这张图获取到的height=412,width=412,size=678976
  • 放在-xxxhdpi目录下,加载这张图获取到的height=309,width=309,size=381924

计算公式 = bitmapWidth * bitmapHeight * ColorType(默认是4)


现在从上面的源码可以得出新的计算公式 :

(int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType

新公式中的density 指的是资源所在目录的densityDpi,如下

  • ldpi(低)~120dpi
  • mdpi(中)~160dpi
  • hdpi(高)~240dpi
  • xhdpi(超高)~320dpi
  • xxhdpi(超超高)~480dpi
  • xxxhdpi(超超超高)~640dpi

targetDensity 指的是设备屏幕的densityDpi = 480

现在来计算下各个目录的值,见证奇迹的时刻来了

  • 放在-hdpi目录下,(int)(412 * 480 / 240 + 0.5f) * (int) (412 * 480 / 240 + 0.5f) * 4 = 824 * 824 * 4 = 2715904
  • 放在-xxhdpi目录下 (int)(412 * 480 / 480 + 0.5f) * (int) (412 * 480 / 480 + 0.5f) * 4 = 412 * 412 * 4 = 678976
  • 放在-xxxhdpi目录下 (int)(412 * 480 / 640 + 0.5f) * (int) (412 * 480 / 640 + 0.5f) * 4 = 309 * 309 * 4 = 381924

看到没有,我的天啊,终于找到正确答案了,现在你有没有get到Bitmap内存的正确计算方式呢

补充

有人可能觉得这个0.5f加了有什么用呢,这里也没体现出来啊,要知道我们这里的设备屏幕densityDpi都是很规整的,但是总会有一些不正规厂家出的非正版手机,比如某pin多多上面的一块钱手机,当然了,这里只是开玩笑,但是targetDensity / density结果可能是小数,比如小数位是0.5+这种,而图片的尺寸,都是以 int 类型为单位,如果不加0.5个,对于小数位大于0.5的值就也会直接舍去,影响最终结果的精准性。所以 Android 为了规避这样的问题,做了个容差值,修正结果

到这里我们可以知道了一个Bitmap占用多少内存跟以下这些因素有关

  • 图片本身的像素宽高
  • 图片像素存储方式或者说色彩格式,比如ARGB_8888,ARGB_4444等
  • 原始文件存放的资源目录,比如xhdpi 和 xxhdpi等
  • 使用设备屏幕的densityDpi

知道了Bitmap内存占用的原理,那么一个很直接的问题就来了,怎么降低Bitmap在应用中所占内存或者怎么高效加载Bitmap呢?这个就不在这篇文章继续了,太长了,写的我眼都花了,休息会,放到下一篇文章中解析

划重点

memory = (int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType

(小提示:Android默认会从与屏幕相匹配的 densityDpi的目录去寻找图片资源,如果没有,就往更高densityDpi的目录去寻找,开发者切勿放到低densityDpi的目录,这会放大占用内存)

你可能感兴趣的:(【Framework源码解析】)