本文首发于 vivo互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/aRDzmMlkqB14Ty67GJs9vg
作者:Xu Jie

不同Android版本,对一张图片的内存处理方式是不一样的,使用不正确会导致OOM的发生,这篇文章带你梳理内存占用情况,选择适合你的图片加载模式,解决OOM问题。

一、背景

你知道吗

  1. 一张5.48MB,宽高像素为4896*6528的24位的静态图片,放在Android工程目录下面的res/drawable-[density]/ 不同文件夹下面,占据的内存是多少?
  2. 使用Glide加载一张5.48MB,宽高像素为4896*6528的24位的网络图片,占据内存又是多少?
(图:像素为4896*6528的图片)

二、梳理概念

在正式分析下面的内容前,先来看几个概念。

1、屏幕尺寸

指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米。这个值是利用手机屏幕的长和宽,然后利用勾股定理,就可以算出斜边的长了。

2、屏幕像素密度

即每英寸屏幕所拥有的像素数,英文简称ppi, 屏幕像素密度与屏幕尺寸和屏幕分辨率有关,屏幕密度越低在给定物理区域的像素就会较少。Android 将所有屏幕密度分为六组通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。

3、屏幕分辨率

屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点,比如我们经常说的宽高像素为:4896*6528。

上面三个概念模糊吗?我们可以看一下下面这两张图,就可以理清上面三个概念了:

(图:分辨率计算公式)

下面的分析,重要了解的是屏幕像素密度。

三、屏幕密度(dpi)对应关系

屏幕物理区域中的像素量,通常称为 dpi(每英寸点数)。屏幕密度越低在给定物理区域的像素就会较少。Android 将所有屏幕密度分为六组通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。

六种通用密度之间遵循 3:4:6:8:12:16 的缩放比率。

四、代码验证

代码很简单,就是用一个ImageView包含一张背景图片,然后通过转换为Bitmap查看占用内存大小。

布局文件activity_main.xml

布局文件,就是一个ImageView控件,包含一张背景图。

MainAcivity.java

Android有一个特殊的文件夹res/drawable-nodpi/,放在里面的资源,不会被放大或者压缩,按照原大小展示,我们这里也把测试资源放在这个文件夹。

五、图片的内存占用

1、静态图片不区分文件夹内存占用

仍然以宽高像素为:4896*6528=31961088的图片举例,图片原始大小为5.48M,图片资源放在res/drawable-nodpi/下面,这时候找一个vivo X21手机,加载这张图片,占据内存情况为127844352byte:

而图片的原始图片像素总数为31961088,跟内存大小127844352byte好像没什么关系,但是真相是31961088* 4 = 127844352(Byte),原始图片尺寸大小与最终的内存占用大小呈倍数的关系,所以在这里与内存占用大小有直接关系的就是原始图片尺寸大小(例如:480x800),道理我都懂,但是倍数关系是从哪里来的呢,这就要谈论到Bitmap的像素格式了。

Android系统支持4种格式的像素格式,源码在Bitmap.Config中:

为了保证图片质量,官方默认使用ARGB_8888格式,导致图片的每个像素会占用4个Byte大小,所以demo里面的图片占用内存大小就是像素总数*像素格式,就是384000 * 4 = 1536000(Byte),这个时候应该有点成就感了,可以帮助你解决一部分实际项目问题了。

2、静态图片区分文件夹内存占用现象

(1) 静态图片区分文件夹在X21(Android 8.0)上的内存占用

那么问题又来了,放在res/drawable-nodpi/文件夹下没问题,放在其他文件夹下呢?因为我们要适配不同的机器。

仍然以vivo X21举例,x21的目标图片文件夹是res/drawable-xxdpi/,屏幕密度480dpi。

看一下这个图片放在不同的文件夹下面,内存占用情况,单位:M。

可以看到,

  1. 对于分辨率为res/drawable-hdpi/、res/drawable-xhdpi/、res/drawable-xxdpi/三个分辨率来说,图片占据内存基本是一致的,Java层内存没有消耗,而是消耗了native内存。
  2. res/drawable-xxxdpi/分辨率下面的图片,占据内存是最高的,native占据了200M。

(2) 所有的机器,内存占用都是这个规律吗

或许你有这个疑问:

为什么在不同的文件夹下面,图片占据的内存资源基本一致,有的时候却发现不同文件夹下面,内存占据又是不一样的?

在回答这个问题前,你要搞清楚,google在图片加载时候,不同的Android版本,做了native堆栈和Java堆栈的区分。

这里也有个有意思的现象,在Android4.4到Android 8.0以下的机器,当你把这个图片放在不同的文件夹下面时,图片占据的内存是不一样的,那是因为图片内存的加载,是在Java 堆栈,所以你可能会遇到 Java 层面的OOM。

AndroidRuntime: java.lang.RuntimeException: Canvas: trying to draw too large(127844352bytes) bitmap.

8.0之后的内存分配是在native,Java层的bitmap创建之后,实际上像素内存的分配是在native层直接调用calloc,所以其像素分配的是在native heap上, 这也是为什么8.0之后的Bitmap消耗内存可以无限增长,直到耗尽系统内存,也不会提示Java OOM的原因。

3、网络图片加载内存占用现象

(1) Glide加载图片的方法

glide加载图片资源的方式有两个:

  • 无回调,使用如下方式加载
Glide.with(context)
.load(url)
.apply(requestOptions.override(width, height))
.into(imageView);
  • 有回调,使用下面加载方式,区别在into传入simpleTarget,而不是imageview
Glide.with(context)
.asBitmap()
.load(url)
.apply(requestOptions)
.into(simpleTarget);

其中的simpleTarget有两种定义方式:

  • 传入宽、高参数,且大于0
simpleTarget = new SimpleTarget<Bitmap>(width, height) {}
  • 宽、高都为0
simpleTarget = new SimpleTarget<Bitmap>() {} 

(2)SimpleTarget使用错误带来的问题

  • A和B的区别

区别就在于,当你传入了宽高的时候,图片就按照你传入的大小,缓存到了内存(Glide更多级存储大小此处不讨论)。当你不设置宽、高的时候,图片就按照原始的像素大小进行了缓存。

  • 但是我们经常不传入宽、高

这是因为加载网络图片的时候,我们经常不知道宽、高是多少,我们设置本地资源imageview像素的时候,使用了wrap_content或者match_content,不确定最终的宽高,所以我们选择传入width = 0,height = 0,使用glide下载好图片后,再去做对应的设置。

  • 为什么我们一般情况下感受不到A、B的差异

这是因为,网络图片也好、本地图片也好,像素都不会太大,以像素类型为RGB_8888为例,一个1920*1080的图片,在内存占据内存为1920*1080*4Byte = 829440Byte = 7.9M。

此时设置宽、高(正常也就设置个几十dp)与不设置宽高,区别并不大。

  • 崩溃来了

04-27 17:39:53.154 31269-31269/? E/art: Throwing OutOfMemoryError "Failed to allocate a 227278860 byte allocation with 1048576 free bytes and 126MB until OOM"
  • 为什么崩溃?

因为本地的一张图片大小虽然为5.48M,像素为width = 4896 height = 6528,但是在内存占据大小为 4896 * 6528 * 4 = 127844352byte = 120M。这个内存足以使官网app在本来使用内存就高的情况下闪退。

看一下加载这个本地图片时的内存情况,从 320M 到 548M,飙升228M(还有后台事件带来内存波动,引起闪退的根本原因是Graphics的内存飙升)。

  • 怎么解决崩溃?

想办法去掉simpleTarget的B定义方法

如果你不知道需要现实的资源宽高是多少,设置下面这个参数,这样就以当前屏幕宽、高作为最高显示像素,downsample设置为DownsampleStrategy.AT_MOST。

这个表示:

当你的资源原始尺寸大于width * height(屏幕宽、高像素)时,以width * height为准。

当你的资源原始尺寸小于width * height时,以原始尺寸为准。

width * height作为图片保存到内存时的最大像素值。

闪退问题同样解决,此时内存使用情况从 290M 到 340M,增加50M(还有后台事件带来内存波动)。

六、总结

  1. 不同分辨率的静态资源图片放在不同的文件夹下面,不要随便放,会引起内存的异常。
  2. 网络加载框架Glide等,最好根据屏幕宽、高设置需要加载的图片宽、高,不要使用图片原始大小加载,否则容易出现崩溃。

其他:如果你有兴趣,可以验证 Android 8.0以下图片内存占用情况,会发现不一样的天地。

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 联系

Android 加载图片占用内存分析的更多相关文章

  1. 图片--Android加载图片导致内存溢出(Out of Memory异常)

    Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证) ...

  2. Android加载图片导致内存溢出(Out of Memory异常)

    Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证) ...

  3. android 加载图片防止内存溢出

    图片资源: private int fore[]; private int back[]; fore = new int[]{R.drawable.a0, R.drawable.a1, R.drawa ...

  4. 解决android加载图片时内存溢出问题

    尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过jav ...

  5. [Android]异步加载图片,内存缓存,文件缓存,imageview显示图片时增加淡入淡出动画

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/3574131.html  这个可以实现ImageView异步加载 ...

  6. BitmapFactory 加载图片到内存

    Bitmap占用内存分析 Android的虚拟机是基于寄存器的Dalvik,它的最大堆(单个进程可用内存)大小一般是16M,当然不同设备是不一样的,可以查看/system/build.prop文件,[ ...

  7. Android加载图片OOM错误解决方式

    前几天做项目的时候,甲方要求是PAD (SAMSUNG P600 10.1寸 2560*1600)的PAD上显示高分辨率的大图片. SQLITE採用BOLD方式存储图片,这个存取过程就不说了哈,网上一 ...

  8. android 加载图片oom若干方案小结

    本文根据网上提供的一些技术方案加上自己实际开发中遇到的情况小结. 众所周知,每个Android应用程序在运行时都有一定的内存限制,限制大小一般为16MB或24MB(视手机而定).一般我们可以通过获取当 ...

  9. Android加载图片的策略

    实现图片缓存也不难,需要有相应的cache策略.这里我采用 内存-文件-网络 三层cache机制,其中内存缓存包括强引用缓存和软引用缓存(SoftReference),其实网络不算cache,这里姑且 ...

  10. android 加载图片框架--Glide使用详解

    一.简介 Glide,一个被google所推荐的图片加载库,作者是bumptech.这个库被广泛运用在google的开源项目中,包括2014年的google I/O大会上发布的官方app.(PS:众所 ...

随机推荐

  1. 记一次 .NET 某零售管理系统 存储不足分析

    一:背景 1. 讲故事 前几天有位朋友找到我,说他的程序会偶发性的报 存储空间不足,无法处理此命令 的错误,让我帮忙看下到底怎么回事,哈哈,人家是有备而来,dump都准备好了,话不多说,直接分析开干. ...

  2. 一个适用于定制个性化界面的WPF UI组件库

    前言 今天给大家推荐一个能让你用最少的代码来实现期望的UI效果,适用于定制个性化界面的WPF UI组件库:Panuon.WPF.UI. 组件库官方介绍 Panuon.WPF.UI 是一个适用于定制个性 ...

  3. Tomact从认识到安装与详细使用

    一.什么是Tomact? Tomcat是一个开源免费的轻量级Web服务器,它是一个软件程序,主要功能是提供网上信息浏览服务,对HTTP协议的操作进行封装,使得程序员不必对协议进行操作,让Web开发更加 ...

  4. 安装NETDATA集群监控面板

    安装NETDATA集群监控面板 介绍 官方链接 演示网页:https://my-netdata.io/ 官方首页:http://netdata.cloud/ 文档地址:http://docs.netd ...

  5. MySQL搭建主从集群详细步骤~

    一. Docker安装MySQL搭建主从 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] docker run -p 3306:3306 很多 -d --n ...

  6. C++ Qt开发:自定义Dialog对话框组件

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍自定义Dial ...

  7. LR(1)分析法

    SLR(1)方法的出现,解决了大部分的移进和规约冲突.规约和规约的冲突.并且SLR(1)其优点是状态数目少,造表算法简单,大多数程序设计语言基本上都可用SLR(1)文法来描述. 但是仍然有一些文法,不 ...

  8. grpc是基于http/2协议的高性能的rpc框架

    师傅领进门,修行在个人,跟着官方脚手架demo了grpc后,之后就需要扩展前后知识边界,下面总结grpc的前世今生和最佳实践. https://www.cnblogs.com/JulianHuang/ ...

  9. Feign源码解析:初始化过程(三)

    背景 前面两篇讲了下,在一个典型的引入了feign.loadbalancer.nacos等相关依赖的环境中,会有哪些bean需要创建. 其中第一篇讲了非自动配置的bean,第二篇是自动配置的bean. ...

  10. 动态规划问题(四)最长双音序列长度(LBS)

    问题描述 ​ 以一个乱序的数组,求它的最长双音序列长度.双音序列指该序列先递增,再递减. ​ 如: 对于序列 {1, 11, 2, 10, 4, 5, 2, 1},它的最长双音序列长度为 6, 对于序 ...