阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

内存占用过高怎么办?iOS 图片内存优化指南

  • 2020-09-25
  • 本文字数:3459 字

    阅读完需:约 11 分钟

内存占用过高怎么办?iOS图片内存优化指南

导语 | 一般来说,在 App 的内存占用中,图片很容易成为其中的大头。特别是在图片相关的 App 中,稍不注意就容易引发内存占用过高的问题。本文将就 iOS 图片类应用的内存优化展开讨论,希望与大家一同交流。文章作者:张恒铭,腾讯终端开发工程师。

一、内存优化的必要性

事实上,因为目前 iPhone 配备的内存越来越高,当内存占用过高时,并不一定会超过系统设定的阈值而引发强杀进程。


但这并不意味着减少内存占用是没有意义的,因为当内存占用过高时,很容易引起一系列的副作用。最直接的表现是 App Crash,当然还有很多更为深远的副作用。

1. FOOM

FOOM 是最直接的影响了,当内存占用过多导致整个系统的可用内存不足时,App 所在的进程容易被杀掉。而且相比于一般的 Crash 来说,FOOM 更难以检测,并且也更难排查。

2. 限制并发数量

如果一个任务占用了过多的内存,但总的内存是有限的,那么任务的并发数将会受到直接限制。表现上就是 App 里某个功能可同时执行的数量有限,或者可以同时显示的内容有数量限制。


同时,因为内存是有限资源,当占用内存过多时,会容易导致操作系统杀掉其它 App 的进程来给当前的 App 提供足够的内存空间,这对用户体验是不利的。

3. 增加耗电

由于 iOS 系统的 Memory Compressor 的存在,当可用内存不足时,一部分 Dirty Page 会被压缩存储到磁盘中,当用到这部分内存时,再从磁盘里加载回来。这会造成 CPU 花费更多的时间来等待 IO, 间接提高 CPU 占用率,造成耗电。

二、原因分析

1. 图片显示原理

图片其实是由很多个像素点组成的,每个像素点描述了该点的颜色信息。这样的数据是可以被直接渲染在屏幕上的,称之为 Image Buffer。


事实上,由于图片源文件占用的存储空间非常大,一般在存储时候都会进行压缩,非常常见的就是 JPEG 和 PNG 算法压缩的图片。


因此当图片存储在硬盘中的时候,它是经过压缩后的数据。经过解码后的数据才能用于渲染,因此需要将图片显示在屏幕上的话,需要先经过解码。解码后的数据就是 Image Buffer 。



当图片显示在屏幕上时,会复制显示区域的 Image Buffer 去进行渲染。

2. 图片真实占用内存

对于一张正在显示在屏幕上的,尺寸为 1920*1080 的图片来说,如果采用 SRGB 的格式(每个像素点的颜色由 red,green,blue,alpha 一个共 4 个 bytes 来决定)的话,那么它占用的内存为:


1920 * 1080 * 4 = 829440 bytes
复制代码


也就是说,一张非常普通的图片,解码后占用的内存就是 7.9 MB,这是非常夸张的。而图片显示时所占的内存大小是与尺寸和颜色空间正相关的,与压缩算法、图片格式、图片文件的大小没有关联。

三、解决方式

1. 避免将图片放在内存里

对于不显示在屏幕上的图片,在绝大部分时间里,其实是没有必要放在内存里的。解码后的 UIImage 是非常大的,对于不需要显示的图片是不需要解码的。而对于不显示在屏幕上的图片,一般也没有必要继续持有着 UIImage 对象。

2. 图片缩放

图片缩放是很常见的处理方式,一般来说,常见的思想可能是重新画一张小一点的图片,往往是用 UIGraphicsBeginImageContextWithOptions 的方式:


extension UIImage {        public func scaling(to size:CGSize) -> UIImage? {            let drawScale = self.scale            UIGraphicsBeginImageContextWithOptions(size, false, drawScale)            let drawRect:CGRect = CGRect(origin:.zero,size:size)            draw(in: drawRect)            let result = UIGraphicsGetImageFromCurrentImageContext()            UIGraphicsEndImageContext()            return result        }    }
复制代码


这种方式存在以下问题:


第一,默认是 SRGB 的格式,也就是说每个像素需要占 4 个 bytes 的空间,对于一些黑白或者仅有 alpha 通道的数据来说是没有必要的。


第二,需要将原图片完全解码后渲染出来,原图片的解码会造成内存占用的高峰。


对于问题一的解决,可以使用新的 UIGraphicsImageRenderer 的方式,这种情况下框架会自动帮你选择对应的颜色格式,减少不必要的消耗。


extension UIImage {    func scaling(to size:CGSize) -> UIImage? {        let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: size))        return renderer.image { context in            self.draw(in: context.format.bounds)        }    }}
复制代码


这种方式在一定的场景有所优化,但是没有解决问题二中存在的内存峰值的问题。由于处理前的图片并不一定展示在屏幕上,解码后的数据是冗余信息,因此应该避免图片的解码。


对于峰值过高的问题,最直接的思想是采用流式的方式进行处理。而底层的 ImageIO 的接口就采用了这种方式:


func resizedCgImage(url:URL,for size: CGSize) -> CGImage? {        let options: [CFString: Any] = [            kCGImageSourceShouldCache:false,            kCGImageSourceCreateThumbnailFromImageAlways: true,            kCGImageSourceCreateThumbnailWithTransform: true,            kCGImageSourceShouldCacheImmediately: true,            kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)        ]                guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),            let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)            else {                                return nil        }                return image    }
复制代码

3. 降低峰值

通过 ARC 管理内存的对象,注册在某个 Autoreleasepool 中,Autoreleasepool 在 drain 的时候释放已经没有使用的对象。


一般没有进行特殊处理的话,会在 Runloop 结束后,有一次 Autoreleasepool 的 drain 操作,而这次 Runloop 中生成的对象也是由这个 Autoreleasepool 来管理的。这部分的原理有很多的文章介绍,这里就不多赘述了。


在图片批量处理的过程中,由于还在一个 Runloop 里,此时引用计数为 0 的对象是不会被释放的。因此需要在每次循环后触发 Autoreleasepool 的 drain 操作:


for image in images {    autoreleasepool {   operation()    }}
复制代码

4. 裁剪显示的图片

在很多场景下,图片是不会完整的显示出来的,例如下图所示的情况:



在这种情况中,即使给 UIImageView 一张完整的图片,最后渲染的时候也只会截取显示区域的 Image Buffer 去进行渲染。


这就意味着,区域外的数据,其实是没有必要的。因此在这种场景下,其实只需要裁减显示区域的图片即可。


举个例子,以前面提到 1920 * 1080 的图片为例, 显示时需要占用的内存为 829440 bytes。如果它是以 ScaleAspectFill 的方式放置在一个 300 x 300 的 UIImageView 中时,那么其实一张 300 x 300 的图片就足以展示,而此时这张图片占用的内存为 360000 bytes, 仅为前者的 43% 。


func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary        let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale        let downsampleOptions =            [kCGImageSourceCreateThumbnailFromImageAlways: true,             kCGImageSourceShouldCacheImmediately: true,             kCGImageSourceCreateThumbnailWithTransform: true,             kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary        let downsampledImage =            CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!        return UIImage(cgImage: downsampledImage)    }
复制代码

四、效果对比

在 App 进行优化前,是先将图片的原图显示出来,并且持有这些图片直到处理完毕。


在处理方式上,采用了 UIGraphicsBeginImageContextWithOptions 的方式来进行图片的缩放。因此造成了持续的高内存占用,峰值可以达到 600 MB 。



经过上述优化后,已经有了比较大的改观。同样的操作,总的内存占用为 221 MB,仅为之前的 36.4% 。


参考资料

[1] iOS Memory Deep Dive:


https://developer.apple.com/videos/play/wwdc2018/416


[2] Image and Graphics Best Practices:


https://developer.apple.com/videos/play/wwdc2018/219


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接


内存占用过高怎么办?iOS图片内存优化指南


2020-09-25 14:004705

评论

发布
暂无评论
发现更多内容

防患于未然,华为云数据灾备解决方案保护企业数据安全

YG科技

迎政策东风,华为云为企业“等保”建设打开想象空间

神奇视野

华为云数据灾备方案,助力政企数据无忧

神奇视野

AI 圈新晋顶流:AutoGPT,Star量近8万,远超PyTorch

Openlab_cosmoplat

人工智能 开源社区 autogpt

抛弃丑陋的try-catch,优雅处理异常

Java你猿哥

Java SSM框架 异常处理 try-catch

产教融合| 赛意信息·讯方·深信息产教融合交流研讨会暨国产工业软件人才培养战略合作示范基地揭牌仪式圆满举办

科技热闻

等保2.0时代,华为云助力客户做好等保合规

神奇视野

MYSQL 主从复制如何保证数据一致性

做梦都在改BUG

Java MySQL 数据库 主从复制

React Native 9个好用的开发工具盘点

Onegun

React Native 移动开发 跨端框架

selenium源码通读·9 |webdriver/common/desired_capabilities.py-DesiredCapabilities类分析

Python 测试 自动化测试 源码剖析 selenium

阿里云何川:计算巢产品规划与生态策略解读

云布道师

计算巢

阿里十亿级并发系统设计+java性能优化实战

做梦都在改BUG

Java 性能调优 并发系统设计

抵御数据风险、保障业务安全,就看华为云数据灾备

YG科技

开源即时通讯IM框架MobileIMSDK的微信小程序端技术概览

JackJiang

网络编程 IM 即时通信

阿里云大数据架构师推荐学习的Flink入门与实战

做梦都在改BUG

大数据 flink

Web平台规模化部署高效编码格式的实践和思考

阿里技术

视频编解码

华为云安全建设安全云生态 保全企业运营安全

神奇视野

华为云数据灾备,助力企业业务极速恢复

YG科技

实例分享| anyRTC 部署南京某区城市运行“一网统管”综合调度系统

anyRTC开发者

音视频 指挥调度 融合通信 快对讲 综合调度

MySQL中,当update修改数据与原数据相同时会再次执行吗?

Java你猿哥

Java MySQL SSM框架 update

同时斩获7大offer的终极原因,全靠这套Java笔记

程序知音

Java 后端 JVM java面试 Java面试八股文

等保2.0来临,华为云助力企业更好应对等保合规

神奇视野

Redis布隆过滤器的原理和应用场景,解决缓存穿透

做梦都在改BUG

Java redis 缓存 布隆过滤器

Mybatis 通过接口实现 sql 执行原理解析

做梦都在改BUG

Java mybatis SQL执行

终于学完阿里架构师推荐413页微服务分布式架构基础与实战笔记

做梦都在改BUG

Java 架构 分布式 微服务

GreptimeDB v0.2 正式发布 | 50%+ PromQL 兼容、写入性能优化、Dashboard with Playground

Greptime 格睿科技

云原生 时序数据库 PromQL 国产时序数据库

手机穿戴设备能力共享,提升丰富交互体验

HMS Core

HMS Core

全栈开发实战|​名片管理系统的设计与实现(SSM + JSP)

TiAmo

管理系统 数据库开发 全栈开发 SQL Server 三周年连更

105道Java面试题,认真思考对你面试很有价值

Java你猿哥

Java spring 面试题 java面试 Java面经

华为云数据灾备方案,为数据安全铸造铜墙铁壁

神奇视野

GrowingIO—UEI模型:万物皆可“事件化”

科技热闻

内存占用过高怎么办?iOS图片内存优化指南_大前端_云加社区_InfoQ精选文章