Android 图片内存占用过大?不存在的...

转载请注明出处:http://blog.csdn.net/hjf_huangjinfu/article/details/79281829



概述

        Android 平台的内存,一直都是比较珍贵的,防止内存占用过多,再怎么强调也不为过的。目前绝大多数互联网 App,都需要加载显示很多图片,如果方式不当,这些图片会消耗掉大量内存,所以如何降低图片的内存消耗,就比较重要了。


1、图片数据

        想要研究图片占用内存,首先需要分析一下图片数据。

        一张图片是由众多个像素点组成的。

        每一个像素点的数据,或者说每一个颜色都是由 三基色+透明度 组成的,每一个基色或者透明度,我们称之为一个通道

        每一个通道也有取值范围,比如 8bit 的,就是 0-255,4bit 的,就是 0-16,它们的区别在于,取值范围大的,色彩分辨率比较高,也就是颜色比较细腻。

        我们用 PictureData 表示图片数据,用 PixelData 表示像素点数据的话,那么结构是这样的:

//图片数据
public class PictureData {
    PixelData[] pixelData;
}
//每一个像素点的数据,每一个颜色都是由三基色+透明度组成的,每一个基色或者透明度,我们称之为一个通道
public class PixelData {
    byte[] alpha; //透明通道
    byte[] red;   //红色通道
    byte[] green; //绿色通道
    byte[] blue;  //蓝色通道
}

1.1、图片文件格式

        上节分析了图片数据的组成,这一节分析一下图片文件的组织方式。每个图片都是需要保存这些像素数据的,我们可以把一张图片看成是一个二维矩阵,每一个像素点就是矩阵中的一个数据,我们可以简单的按行从左到右,列从上到下的顺序,依次存储每个像素点数据,这样就可以把一张图片数据保存到文件,这就是 bmp 格式的图片。其他常见的图片格式还有 jpeg、png、webp 等等,其实它们都是文件格式。从像素点组成方式的维度来看,它们的区别就是像素点组成方式的不同,共同的目标都是压缩数据,毕竟需要降低图片传输成本,里面涉及了一些压缩算法,这里不做讨论。


1.2、像素数据以及格式

        上一节聚焦于众多像素点如何组成图片,这一节主要聚焦于每个像素点的内部表示。
        直观来说就是,每个像素点是由多个通道组成的,那么:

                第一,内部如何组织这些通道的数据。

                第二,每个通道的取值范围。

        这就是像素格式,做 Android 开发的人可能都听过 ARGB_8888 这个词,这个词就包含了上面提到的数据信息,ARGB 表示,透明度-红色-绿色-蓝色,这几个通道的数据,按照这个顺序排列,8888 表示,每个通道的数据用 8bit 来保存,也就是取值范围为 0-255。当然还有很多像素格式,Android 中常见的还有 RGB_565。其他格式的就不具体说明了。
参考链接:https://msdn.microsoft.com/en-us/library/windows/desktop/ee719797(v=vs.85).aspx


1.3、有损压缩和无损压缩

        上面提到了一个压缩的概念,这一节简单再介绍一下图片压缩的一些概念。第一个就是各种图片格式,也就是众多像素点数据如何组织的问题,比如第一种,把连续的颜色值一样(各通道值都一样)的像素点表达成(个数、颜色)的这种数据格式,就能压缩不少空间。还有很多图片压缩方式,具体不做介绍。第二个就是大家常说的有损压缩和无损压缩,什么概念呢,比如之前用 8bit 存储红色通道数据,所以有256种红色,但是现在想降低大小,改成 4bit 存储,所以就只有16种红色,所以可表达的颜色数量就会变少,这种颜色通道数据丢失的压缩方法,就叫做有损压缩,颜色数据完整的压缩方式叫做无损压缩。这里是两个维度,文件格式维度和像素格式维度。图片压缩是不会改变像素点个数的。



2、Android图片加载原理

2.1、图片在内存中的表示以及空间计算

        不管图片以什么文件格式存储,当它需要被使用的时候,它就要被加载到内存中,就需要解压,把每一个像素点的每一个通道数据都存储在内存中。至于如何存储,就是由像素格式来确定的,比如 ARGB_8888,那么就会使用4个字节,依次存储 ARGB 这4个通道的数据,然后每个像素点依次存储。
        所以,如何计算一张图片在内存中需要占用多少空间,先计算像素点,像素点个数=宽度*高度。然后计算每个像素点需要多少空间存储,比如ARGB_8888就需要4个字节,那么所需空间大小就是 空间(字节)=宽度*高度*4,比如RGB_565就需要2个字节,那么所需空间大小就是 空间(字节)=宽度*高度*2。
        误区一:图片文件大小不是太大,只有几十KB,不会占用多少内存。错,文件只有几十KB,是因为它内部色彩单纯,压缩率比较高,但是长宽都不变。我们App中有一个图片链接,该图只有110KB,看起来很无害的样子,后台就上传了,然后它的尺寸是3240*1260,然后它又是 ARGB_8888 的图片,所以计算一下,3240 * 1260 * 4 = 16329600 byte = 15.6M,哈哈,杀手级图片。


2.2、图片加载和显示流程

        我们使用各种方式,把一个图片从各种来源(res、assets、文件、url 等等),显示到一个 ImageView 或者其他地方的时候,过程究竟是如何的呢?这一节来分析一下。
        加载并显示图片的流程大家都知道,BitmapFactory.decode....系列方法,拿到bitmap后,再显示到控件上。
        图片加载和显示是2个不同的流程:
                加载流程,就是按照指定的参数,把图片加载到内存中。
                显示流程,就是把上一步加载进来的图片数据,经过预处理,显示到控件上面。
        BitmapFactory.Options 就是控制这两个流程的参数,里面类似于 inXxx 的属性,就是控制加载流程的,比如 inPreferredConfig,类似于 outXxx 的属性,就是控制显示流程的,比如 outConfig。





3、如何降低内存占用

        根据前面分析的结果,想要降低内存占用,就需要控制 in 的过程,也就是控制图片加载到内存中的解码参数,当图片数据加载到内存中后,不管你怎么显示,控件设置成10 * 10也好,只是绘制效果问题,原始图片还是在内存中。如果以低尺寸加载,高尺寸绘制,就会造成图片模糊的问题。

3.1、降低图片尺寸

        前面我们知道,图片在内存中的空间,是由尺寸和像素格式决定的,所以缩小尺寸可以降低图片质量。因为当图片尺寸大于控件尺寸的时候,加载全尺寸图片到内存中,没有什么好处。

        其实官方也是推荐我们这么做的,https://developer.android.com/topic/performance/graphics/load-bitmap.html

        官方推荐的图片降尺寸策略为,找到一个最大的整数值 n,让 inSampleSize = 2^n,保持 bitmapWidth / inSampleSize >= viewWidth; bitmapHeight / inSampleSize >= viewHeight;
        举个例子,比如一个 ImageView 尺寸为 400 * 200,一个 Bitmap 尺寸为 900 * 500,那么 inSize 的值为 2。        

        现在的流行图片加载库,比如 Glide,都是支持这些功能的。

        注意点,看下面一段代码,很常规的写法。

<ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/icon_add_note_1"/>
静态引用一个res目录下的资源时,系统默认的加载参数中,inSampleSize=0,系统会把它当做1处理,下面是BitmapFactory.cpp中的doDecode的部分代码:

sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// Correct a non-positive sampleSize.  sampleSize defaults to zero within the
// options object, which is strange.
if (sampleSize <= 0) {
    sampleSize = 1;
}
        所以,这种常规写法,会把图片全尺寸加载进来,如果你认为apk就那么大,图片能有多大?那么请看误区一。

        图片尺寸主要是 inSampleSize 来控制,具体使用方式,不做介绍。

        能使用代码手动加载替代静态资源引用的话,也会改善内存占用。

        注意点:上图有介绍说同一张图片的数据是供多个绘制要求共用的,但是当你以不同的两套尺寸加载同一张图片到内存中时,会有2份以不同采样率处理过的图片数据,所以这里要注意,假如你有十几个尺寸各不相同的控件按照自己的尺寸加载同一张图片时,哈哈,可能比全尺寸加载进来占用的更多。


3.2、降低图片质量

        现在来看影响图片占用内存的第二个因素,像素点格式。系统默认是使用ARGB_8888来接收像素数据。这部分有兴趣可以查看Android源码,主要集中在BitmapFactory.cpp中的doDecode函数,以及Chromium项目的Skia图形库。

        这个流程主要是通过inPreferredConfig来控制。

Bitmap.Config代码如下:

public enum Config {
    /**
     * Each pixel is stored as a single translucency (alpha) channel.
     * This is very useful to efficiently store masks for instance.
     * No color information is stored.
     * With this configuration, each pixel requires 1 byte of memory.
     */
    ALPHA_8     (1),
    /**
     * Each pixel is stored on 2 bytes and only the RGB channels are
     * encoded: red is stored with 5 bits of precision (32 possible
     * values), green is stored with 6 bits of precision (64 possible
     * values) and blue is stored with 5 bits of precision.
     */
    RGB_565     (3),
    /**
     * @deprecated Because of the poor quality of this configuration,
     *             it is advised to use {@link #ARGB_8888} instead.
     */
    @Deprecated
    ARGB_4444   (4),
    /**
     * Each pixel is stored on 4 bytes. 
     */
    ARGB_8888   (5),
    /**
     * Each pixels is stored on 8 bytes. 
     */
    RGBA_F16    (6),
    HARDWARE    (7);
}

        纵观Bitmap.Config所示,Android平台就对外暴露了这几种像素格式,想要降低画质,就需要把ARGB_8888降低为RGB_565。

        所以,你以为事情就这么简单的结束了?

        误区二:RGB_565不是你想用,想用就能用。加载策略不允许丢失透明度数据,比如jpeg,天然不含透明数据的格式,你用RGB_565完全没问题。但是png、webp这种带有透明度数据的格式,想要强制以RGB_565加载进来,是不行的。

        比如,这是Skia库(chromium项目,Android在用)的webp解码器部分源码:

if (info) {
    // FIXME: Is N32 the right type?
    // Is unpremul the right type? Clients of SkCodec may assume it's the
    // best type, when Skia currently cannot draw unpremul (and raster is faster
    // with premul).
    *info = SkImageInfo::Make(features.width, features.height, kN32_SkColorType,
                              SkToBool(features.has_alpha) ? kUnpremul_SkAlphaType
                                                          : kOpaque_SkAlphaType);
}

    switch (dst.colorType()) {
        // Both byte orders are supported.
        case kBGRA_8888_SkColorType:
        case kRGBA_8888_SkColorType:
            return true;
        case kRGB_565_SkColorType:
            return src.alphaType() == kOpaque_SkAlphaType;
        default:
            return false;
    }

        大概意思就是如果该图片有透明度数据,那么就不可以使用RGB_565格式。
        但是,我就想用RGB_565来降低图片质量怎么办,好办,对于那些视觉上没有透明度的图片,使用 cwebp -noalpha 压缩,丢掉透明度通道,不影响视觉。

        webp详细信息:https://developers.google.com/speed/webp/

        png也可以通过处理,去掉alpha通道。



4、React Native 页面图片占用内存过大问题

        我们的 App,有些用 React Native 编写的页面,占用内存比较高,经过研究后发现,好多类似于上面那个杀手级的图片存在于内存中,所以,要想办法干掉他们。

        怎么干,由于使用 React Native 的原生写法,使用 <Image> 标签,所以我们不好弄 BitmapFactory 来自定义加载。反正就是想办法降尺寸或者降质量。

        在 ReactNative 0.46.4 版本后,React Native 提供了 resizeMethod 的属性,来提供一个图片裁剪接口。

        把 resizeMethod 设置为 resize 后,把图片降尺寸后再加载到内存中,就会减少很多内存开销。

        网络图片,需要把自带的 FrescoModule 的 配置项,也就是 ImagePipelineConfig 对象,把它的 downSampleEnable 设置为true。


        在RN中使用 ListView 或者 FlatList 加载 Image 时,这个 ListView 并不是像 Android 中的 ListView 一样,有回收机制,RN 的 ListView,运行时实现为 ReactScrollView 嵌套 ReactViewGroup,所以,图片不会被回收。目前版本0.53.0,在实现上面有缺陷,RN 内部会引用所有 View,即使它不可见,也确实它不在 View 树中,但是 RN 内部有引用,没法释放。ReactViewGroup 中有一个 mAllChildren ,会引用所有 View。可以考虑使用自己包裹的 Android 原生 ListView 或者 RecyclerView 。



5、res 目录下面的各种分辨率的 drawable 目录陷阱

        图片放到不同分辨率目录中,从内存层面的影响有多大呢?很大。

        比如,你把一个图片放在 mdpi 目录中,然后静态引用(在 xml 中以src引用)了该资源,比如你的机器 dpi 是 420 的,那么,恭喜你,你中枪了。

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
    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 = (float) targetDensity / density;
    }
}

// Scale is necessary due to density differences.
if (scale != 1.0f) {
    willScale = true;
    scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
    scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
        看看,因为 mdpi = 160, 所以 420 / 160 = 2.6(近似)。所以,你的图片尺寸加载到内存中,长宽都会被扩大到 2.6 倍,所以,内存占用是 2.6 * 2.6 = 6.76 倍。

        因为系统需要做分辨率适配,所以,这是显而易见的。

        该是什么分辨率的图片,就要放到指定的分辨率目录下。

        最佳实践就是准备一套高分辨率图片,放到高分辨率目录下,然后向做降尺寸操作。


6、Glide缓存问题

        一张大图,比如启动页的欢迎图片,一般尺寸都是跟手机分辨率差不多,比如是 1920 * 1080,假如是 ARGB_8888 的,那么它在内存中大小大约为 7.92M,但是业务要求如此,不能降尺寸,也不能去掉透明度。但是使用Glide加载后,你会把它放到 Glide 的缓存里面,但是这个缓存是强引用的,那么,哈哈,如果一直触碰不到它的缓存临界区,也就是说,Glide 不会清理它的缓存,然后这种过眼即逝的高清大图,就长期霸占着你的内存,但是你却基本上不会再看到它了,多么痛的领悟。


结论

        总之,遇到图片占用内存过大的问题,都可以想办法解决了,本质方案就是降尺寸或者降质量。对症下药。







  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值