默认
打赏 发表评论 11
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
Android程序员的痛你永远不懂(二):如何减少Bitmap内存占用?
阅读(67322) | 评论(11 收藏 淘帖1
微信扫一扫关注!

前言


Bitmap有关的内存占用、OOM等问题一直是新Android程序员心中挥之不去的阴影,本文依据实践经验着重总结如何减少Bitmap的内存占用方法。如果您希望透彻理解Bitmap到底占用多少内存,请查看此文《Android程序员的痛你永远不懂(一):Bitmap到底占用多大内存?》。

推荐阅读:用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]》。

正确理解Android上的Jpg和Png


说到这里,肯定会有人会说,我们用 jpg 吧,jpg 格式的图片不应该比 png 小么?这确实是个好问题,因为同样一张图片,jpg 确实比 png 会多少小一些(甚至很多),原因很简单,jpg 是一种有损压缩的图片存储格式,而 png 则是 无损压缩的图片存储格式,显而易见,jpg 会比 png 小,代价也是显而易见的。

可是,这说的是文件存储范畴的事情,它们只存在于文件系统,而非内存或者显存。说得简单一点儿,我有一个极品飞车的免安装硬盘版的压缩包放在我的磁盘里面,这个游戏是不能玩的,我需要先解压,才能玩——jpg 也好,png 也好就是个压缩包的概念,而我们讨论的内存占用则是从使用角度来讨论的。所以,jpg 格式的图片与 png 格式的图片在内存当中不应该有什么不同。

肯定有人有意见,jpg 图片读到内存就是会小,还会给我拿出例子。当然,他说的不一定是错的。因为 jpg 的图片没有 alpha 通道!!所以读到内存的时候如果用 RGB565的格式存到内存,这下大小只有 ARGB8888的一半,能不小么。。。

不过,抛开 Android 这个平台不谈,从出图的角度来看的话,jpg 格式的图片大小也不一定比 png 的小,这要取决于图像信息的内容:JPG 不适用于所含颜色很少、具有大块颜色相近的区域或亮度差异十分明显的较简单的图片。对于需要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大。

如果仅仅是为了 Bitmap 读到内存中的大小而考虑的话,jpg 也好 png 也好,没有什么实质的差别。但二者的差别主要体现在:

  • alpha 你是否真的需要?如果需要 alpha 通道,那么没有别的选择,用 png。
  • 你的图色值丰富还是单调?就像刚才提到的,如果色值丰富,那么用jpg,如果作为按钮的背景,请用 png。
  • 对安装包大小的要求是否非常严格?如果你的 app 资源很少,安装包大小问题不是很凸显,看情况选择 jpg 或者 png(不过,我想现在对资源文件没有苛求的应用会很少吧。。)
  • 目标用户的 cpu 是否强劲?jpg 的图像压缩算法比 png 耗时。这方面还是要酌情选择,前几年做了一段时间 Cocos2dx,由于资源非常多,项目组要求统一使用 png,可能就是出于这方面的考虑。

跑题了,我们其实想说的是怎么减少内存占用的。。。

使用inSampleSize


有些朋友一看到这个肯定就笑了。采样嘛,我以前是学信号处理的,一看到 Sample 就抽抽。哈哈开个玩笑,这个采样其实就跟统计学里面的采样是一样的,在保证最终效果满足要求的前提下减少样本规模,方便后续的数据采集和处理。这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出地目标可能相对较小,对图片分辨率、大小要求不是非常的严格。

举个例子:

我们现在有个需求,要求将一张图片进行模糊,然后作为 ImageView 的 src 呈现给用户,而我们的原始图片大小为 1080*1920,如果我们直接拿来模糊的话,一方面模糊的过程费时费力,另一方面生成的图片又占用内存,实际上在模糊运算过程中可能会存在输入和输出并存的情况,此时内存将会有一个短暂的峰值。


这时候你一定会想到三个字母在你的脑海里挥之不去,它们就是『OOM』。既然图片最终是要被模糊的,也看不太情况,还不如直接用一张采样后的图片,如果采样率为 2,那么读出来的图片只有原始图片的 1/4 大小,真是何乐而不为呢??
BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);

不过,inSampleSize的数值不是随意决定的,具体的计算方法可参见此文:http://www.cnblogs.com/kobe8/p/3877125.html,或百度“Android inSampleSize 计算”。

使用矩阵


用到 Bitmap 的地方,总会见到 Matrix。这时候你会想到什么?其实想想,Bitmap 的像素点阵,还不就是个矩阵,真是你中有我,我中有你的交情啊。那么什么时候用矩阵呢?

我信记住一个原则:

大图小用用采样,小图大用用矩阵。


还是用前面模糊图片的例子,我们不是采样了么?内存是小了,可是图的尺寸也小了啊,我要用 Canvas 绘制这张图可怎么办?当然是用矩阵了:

1方式一


Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0f, 0f);
//如果使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0,0, paint);

需要注意的是:在使用搭载 5.1.1 原生系统的 Nexus6 进行测试时发现,如果使用 Canvas 的 setMatrix 方法,可能会导致与矩阵相关的元素的绘制存在问题,本例当中如果使用 setMatrix 方法,bitmap 将不会出现在屏幕上。因此请尽量使用 canvas 的 scale、rotate 这样的方法,或者使用 concat 方法。

2方式二


Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);

这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。如果我要把图片放到 ImageView 当中呢?一样可以,请看:
Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);

合理选择Bitmap的像素格式


其实前面我们已经多次提到这个问题。ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。

我们先看下有多少种格式可选:

格式描述
ALPHA_8只有一个alpha通道
ARGB_4444这个从API 13开始不建议使用,因为质量太差
ARGB_8888ARGB四个通道,每个通道8bit
RGB_565每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit

这几个当中:

- ALPHA8:没必要用,因为我们随便用个颜色就可以搞定的。
- ARGB4444:虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃,失宠了。。『又要占省内存,又要看着爽,臣妾做不到啊T T』。
- ARGB8888:是最常用的,大家应该最熟悉了。
- RGB565:看到这个,我就看到了资源优化配置无处不在,这个绿色。其实如果不需要 alpha 通道,特别是资源本身为 jpg 格式的情况下,用这个格式比较理想。

索引位图(Indexed Bitmap)


索引位图,每个像素只占 1 Byte,不仅支持 RGB,还支持 alpha,而且看上去效果还不错!等等,请收起你的口水,Android 官方并不支持这个。

是的,你没看错,官方并不支持:
public enum Config {
    // these native values must match up with the enum in SkBitmap.h

    ALPHA_8     (2),
    RGB_565     (4),
    ARGB_4444   (5),
    ARGB_8888   (6);
 
    final int nativeInt;
}

不过,Skia 引擎是支持的,不信你再看:
enum Config {
   kNo_Config,   //!< bitmap has not been configured
     kA8_Config,   //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)
 
   //看这里看这里!!↓↓↓↓↓
    kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors  
    kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing)
    kRLE_Index8_Config,
 
    kConfigCount
};

其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值,所以,如果我们能够拿到这个索引是不是就可以了?对不起,拿不到。

不过呢,在 png 的解码库里面有这么一段代码:
bool SkPNGImageDecoder::getBitmapColorType(png_structp png_ptr, png_infop info_ptr,
                                       SkColorType* colorTypep,
                                       bool* hasAlphap,
                                       SkPMColor* SK_RESTRICT theTranspColorp) {
png_uint_32 origWidth, origHeight;
int bitDepth, colorType;
png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,
             &colorType, int_p_NULL, int_p_NULL, int_p_NULL);
 
#ifdef PNG_sBIT_SUPPORTED
  // check for sBIT chunk data, in case we should disable dithering because
  // our data is not truely 8bits per component
  png_color_8p sig_bit;
  if (this->getDitherImage() && png_get_sBIT(png_ptr, info_ptr, &sig_bit)) {
#if 0
    SkDebugf("----- sBIT %d %d %d %d\n", sig_bit->red, sig_bit->green,
             sig_bit->blue, sig_bit->alpha);
#endif
    // 0 seems to indicate no information available
    if (pos_le(sig_bit->red, SK_R16_BITS) &&
        pos_le(sig_bit->green, SK_G16_BITS) &&
        pos_le(sig_bit->blue, SK_B16_BITS)) {
        this->setDitherImage(false);
    }
}
#endif
 
if (colorType == PNG_COLOR_TYPE_PALETTE) {
    bool paletteHasAlpha = hasTransparencyInPalette(png_ptr, info_ptr);
    *colorTypep = this->getPrefColorType(kIndex_SrcDepth, paletteHasAlpha);
    // now see if we can upscale to their requested colortype
    //这段代码,如果返回false,那么colorType就被置为索引了,那么我们看看如何返回false
    if (!canUpscalePaletteToConfig(*colorTypep, paletteHasAlpha)) {
        *colorTypep = kIndex_8_SkColorType;
    }
} else {
...... 
}
return true;
}

canUpscalePaletteToConfig 函数如果返回false,那么 colorType 就被置为 kIndex_8_SkColorType 了。
static bool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
  switch (dstColorType) {
    case kN32_SkColorType:
    case kARGB_4444_SkColorType:
        return true;
    case kRGB_565_SkColorType:
        // only return true if the src is opaque (since 565 is opaque)
        return !srcHasAlpha;
    default:
        return false;
}
}

如果传入的 dstColorType 是 kRGB_565_SkColorType,同时图片还有 alpha 通道,那么返回 false~~咳咳,那么问题来了,这个dstColorType 是哪儿来的??就是我们在 decode 的时候,传入的 Options 的 inPreferredConfig。下面是实验时间。

准备:
在 assets 目录当中放了一个叫 index.png 的文件,大小192*192,这个文件是通过 PhotoShop 编辑之后生成的索引格式的图片。

代码:
try {
   Options options = new Options();
   options.inPreferredConfig = Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeStream(getResources().getAssets().open("index.png"), null, options);
   Log.d(TAG, "bitmap.getConfig() = " + bitmap.getConfig());
   Log.d(TAG, "scaled bitmap.getByteCount() = " + bitmap.getByteCount());
   imageView.setImageBitmap(bitmap);
} catch (IOException e) {
    e.printStackTrace();
}

程序运行在 Nexus6上,由于从 assets 中读取不涉及前面讨论到的 scale 的问题,所以这张图片读到内存以后的大小理论值(ARGB8888):192 * 192 *4=147456

好,运行我们的代码,看输出的 Config 和 ByteCount:
D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864

先说大小为什么只有 36864,我们知道如果前面的讨论是没有问题的话,那么这次解码出来的 Bitmap 应该是索引格式,那么占用的内存只有 ARGB 8888 的1/4是意料之中的;再说 Config 为什么为 null。官方说:

public final Bitmap.Config getConfig ()
Added in API level 1
If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.


看来这个法子还真行啊,占用内存一下小很多。不过由于官方并未做出支持,因此这个方法有诸多限制,比如不能在 xml 中直接配置,,生成的 Bitmap 不能用于构建 Canvas 等等。

总结


本文主要讨论了如何运行时获取 Bitmap 占用内存的大小,如果事先根据 Bitmap 的格式、读取方式等算出其占用内存的大小,后面又整理了一些常见的 Bitmap 使用建议。突然好像说,是时候研究一下 Skia 引擎了。

全站即时通讯技术资料分类


[1] 网络编程基础资料:
TCP/IP详解 - 第11章·UDP:用户数据报协议
TCP/IP详解 - 第17章·TCP:传输控制协议
TCP/IP详解 - 第18章·TCP连接的建立与终止
TCP/IP详解 - 第21章·TCP的超时与重传
理论经典:TCP协议的3次握手与4次挥手过程详解
理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程
计算机网络通讯协议关系图(中文珍藏版)
NAT详解:基本原理、穿越技术(P2P打洞)、端口老化等
UDP中一个包的大小最大能多大?
Java新一代网络编程模型AIO原理及Linux系统AIO介绍
NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战
NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战
>> 更多同类文章 ……

[2] 有关IM/推送的通信格式、协议的选择:
为什么QQ用的是UDP协议而不是TCP协议?
移动端即时通讯协议选择:UDP还是TCP?
如何选择即时通讯应用的数据传输格式
强列建议将Protobuf作为你的即时通讯应用数据传输格式
移动端IM开发需要面对的技术问题(含通信协议选择)
简述移动端IM开发的那些坑:架构设计、通信协议和客户端
理论联系实际:一套典型的IM通信协议设计详解
58到家实时消息系统的协议设计等技术实践分享
>> 更多同类文章 ……

[3] 有关IM/推送的心跳保活处理:
Android进程保活详解:一篇文章解决你的所有疑问
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
为何基于TCP协议的移动端IM仍然需要心跳保活机制?
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
>> 更多同类文章 ……

[4] 有关WEB端即时通讯开发:
新手入门贴:史上最全Web端即时通讯技术原理详解
Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
SSE技术详解:一种全新的HTML5服务器推送事件技术
Comet技术详解:基于HTTP长连接的Web端实时通信技术
WebSocket详解(一):初步认识WebSocket技术
socket.io实现消息推送的一点实践及思路
>> 更多同类文章 ……

[5] 有关IM架构设计:
浅谈IM系统的架构设计
简述移动端IM开发的那些坑:架构设计、通信协议和客户端
一套原创分布式即时通讯(IM)系统理论架构方案
从零到卓越:京东客服即时通讯系统的技术架构演进历程
蘑菇街即时通讯/IM服务器开发之架构选择
腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT
微信技术总监谈架构:微信之道——大道至简(演讲全文)
如何解读《微信技术总监谈架构:微信之道——大道至简》
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
17年的实践:腾讯海量产品的技术方法论
>> 更多同类文章 ……

[6] 有关IM安全的文章:
即时通讯安全篇(一):正确地理解和使用Android端加密算法
即时通讯安全篇(二):探讨组合加密算法在IM中的应用
即时通讯安全篇(三):常用加解密算法与通讯安全讲解
即时通讯安全篇(四):实例分析Android中密钥硬编码的风险
传输层安全协议SSL/TLS的Java平台实现简介和Demo演示
理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)
微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解
来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享
>> 更多同类文章 ……

[7] 有关实时音视频开发:
即时通讯音视频开发(一):视频编解码之理论概述
即时通讯音视频开发(二):视频编解码之数字视频介绍
即时通讯音视频开发(三):视频编解码之编码基础
即时通讯音视频开发(四):视频编解码之预测技术介绍
即时通讯音视频开发(五):认识主流视频编码技术H.264
即时通讯音视频开发(六):如何开始音频编解码技术的学习
即时通讯音视频开发(七):音频基础及编码原理入门
即时通讯音视频开发(八):常见的实时语音通讯编码标准
即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述
即时通讯音视频开发(十):实时语音通讯的回音消除技术详解
即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解
即时通讯音视频开发(十二):多人实时音视频聊天架构探讨
即时通讯音视频开发(十三):实时视频编码H.264的特点与优势
即时通讯音视频开发(十四):实时音视频数据传输协议介绍
即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况
即时通讯音视频开发(十六):移动端实时音视频开发的几个建议
即时通讯音视频开发(十七):视频编码H.264、V8的前世今生
简述开源实时音视频技术WebRTC的优缺点
良心分享:WebRTC 零基础开发者教程(中文)
>> 更多同类文章 ……

[8] IM开发综合文章:
移动端IM开发需要面对的技术问题
开发IM是自己设计协议用字节流好还是字符流好?
请问有人知道语音留言聊天的主流实现方式吗?
IM系统中如何保证消息的可靠投递(即QoS机制)
谈谈移动端 IM 开发中登录请求的优化
完全自已开发的IM该如何设计“失败重试”机制?
微信对网络影响的技术试验及分析(论文全文)
即时通讯系统的原理、技术和应用(技术论文)
开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀
>> 更多同类文章 ……

[9] 开源移动端IM技术框架资料:
开源移动端IM技术框架MobileIMSDK:快速入门
开源移动端IM技术框架MobileIMSDK:常见问题解答
开源移动端IM技术框架MobileIMSDK:压力测试报告
开源移动端IM技术框架MobileIMSDK:Android版Demo使用帮助
开源移动端IM技术框架MobileIMSDK:Java版Demo使用帮助
开源移动端IM技术框架MobileIMSDK:iOS版Demo使用帮助
开源移动端IM技术框架MobileIMSDK:Android客户端开发指南
开源移动端IM技术框架MobileIMSDK:Java客户端开发指南
开源移动端IM技术框架MobileIMSDK:iOS客户端开发指南
开源移动端IM技术框架MobileIMSDK:Server端开发指南
>> 更多同类文章 ……

[10] 有关推送技术的文章:
iOS的推送服务APNs详解:设计思路、技术原理及缺陷等
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
扫盲贴:认识MQTT通信协议
一个基于MQTT通信协议的完整Android推送Demo
求教android消息推送:GCM、XMPP、MQTT三种方案的优劣
移动端实时消息推送技术浅析
扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别
绝对干货:基于Netty实现海量接入的推送服务技术要点
移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)
为何微信、QQ这样的IM工具不使用GCM服务推送消息?
>> 更多同类文章 ……

[11] 更多即时通讯技术好文分类:
http://www.52im.net/forum.php?mod=collection&op=all

(原文链接:点此查看

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

标签:Android
上一篇:Android程序员的痛你永远不懂(一):Bitmap到底占用多大内存?下一篇:开源NIO框架八卦——到底是先有MINA还是先有Netty?

本帖已收录至以下技术专辑

推荐方案
评论 11
引用:JackJiang 发表于 2017-12-13 12:37
手机屏幕那么小,显示5000*5000以上的分辨率没有意义啊,变通一下嘛

如果做压缩的话, 降低分辨率的话, 就会变模糊呀, 其实想要的效果就是该文章的ios版解决方案, 看来还得自己研究了
签名: 又来看看了
引用:Decin 发表于 2017-12-13 11:06
对于这个问题, 在网上的资源比较稀少, 苹果官方只有一个很久之前的MRC环境的demo, 但也苦于对CoreGraphic ...

手机屏幕那么小,显示5000*5000以上的分辨率没有意义啊,变通一下嘛
引用:JackJiang 发表于 2017-12-13 10:59
什么样的问题?

对于这个问题, 在网上的资源比较稀少, 苹果官方只有一个很久之前的MRC环境的demo, 但也苦于对CoreGraphic框架不了解, 没能真正运用, 不知道业界像QQ, 微信展示大图的方案是怎么样的
签名: 又来看看了
引用:JackJiang 发表于 2017-12-13 10:59
什么样的问题?

就是加载大图的问题, 项目需要展示大图, 不丢细节的同时做到低内存浏览, 一般显示的是5000*5000以上的分辨率, 也就是很多单反拍摄的照片, 生成位图太大, 在几张切换浏览时大图内存瞬间爆表, 目前还不太懂苹果的渲染系统, 所以想请教前辈
签名: 又来看看了
引用:Decin 发表于 2017-12-13 10:12
有没有ios版的啊, 目前遇到ios版的问题

什么样的问题?
引用:JackJiang 发表于 2016-03-18 13:45
超过10M图片真不多见,你可以看看Bitmap如何计算内存占用的,估计一样是OOM问题吧
http://www.52im.net/ ...

有没有ios版的啊, 目前遇到ios版的问题
签名: 又来看看了
引用:JackJiang 发表于 2016-03-18 13:45
超过10M图片真不多见,你可以看看Bitmap如何计算内存占用的,估计一样是OOM问题吧
http://www.52im.net/ ...

同感!同感!
引用:vdreamx 发表于 2016-03-18 05:43
好像图片超过10M都会出现一个瓶颈问题,这个有啥解决方案吗?

超过10M图片真不多见,你可以看看Bitmap如何计算内存占用的,估计一样是OOM问题吧
http://www.52im.net/thread-147-1-1.html
好像图片超过10M都会出现一个瓶颈问题,这个有啥解决方案吗?

作者对Bitmap的使用还是深有体会的,看来也是踩了不少坑。
示例代码讲解,其实还不知道该怎么做?
inSampleSize 怎么取值?固定个宽高度,然后用实际宽高度除以宽高度

想了解QQ发送照片的压缩实现...
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部