概述
-
我们为什么减少内存占用?
为了更好的用户体验
内存是有限且系统共享的资源,一个程序占用更多,系统和其他程序所能用的就更少。程序启动前都需要先加载到内存中,并且在程序运行过程中的数据操作也需要占用一定的内存资源。减少内存占用也能同时减少其对 CPU 时间维度上的消耗,从而使不仅你所开发的 App,其他 App 以及整个系统也都能表现的更好。
-
可以减少的内存占用有哪些?
从苹果的开发者文档里可以看到,一个 app 的内存分三类:
- Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
- Abandoned memory: Memory still referenced by your application that has no useful purpose.
- Cached memory: Memory still referenced by your application that might be used again for better performance.
之前在做后台开发的时候解决最多的问题就是Leaked memory,但是在客户端开发的时候会发现,Abandoned memory跟Cached memory渐渐地成为了内存消耗的主力
-
如何选取内存占用指标?
- 系统APP 活动监视器-内存
- terminal 使用footprint命令
- objective-c 使用task_vm_info_data_t中的phys_footprint
以上三种方式能得到相似的结果,也是WWDC2013 704 Building Efficient OS X Apps推荐的内存占用获取方式,附上最后一种方式的代码
task_vm_info_data_t task_infos = {0}; info_count = TASK_VM_INFO_COUNT; if (task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&task_infos, &info_count) == KERN_SUCCESS) { memory_footprint = task_infos.phys_footprint; }
分析工具
-
静态代码扫描
- Xcode自带Analyze
适合检查leaked memory,对malloc或者new方式分配的内存或者未定义的变量等检查效果较好
- Xcode自带Analyze
-
MLeaksFinder
- 适合检查如NSViewController被释放了了,但它的view没被释放,或者一个NSView被释放了了,但它的某个subview没被释放。对手机上频繁切换页面的APP优化效果明显。
-
微信内存监控
Matrix-iOS 当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:
- WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。
- WCMemoryStatPlugin: 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。
内存这块主要使用WCMemoryStatPlugin,得到的内存分配文件相比较与Instruments有部分数据缺失,需要自己编写脚本符号化得到的内存分配文件,适合监控线上的内存情况,可以发现一些偶现的内存问题
Instruments leaks
- 可以看到单纯的内存泄露并不多,看来大都属于在运行过程中不断的申请内存,但是在很长一段时间内没有释放,或者是直到程序退出时,才释放申请的内存。
- Instruments Allocations
- 可以看到自应用开始详细的内存分配,不过占用空间过大,不能持续运行。
- Memory Graph
- 它对比Instruments Allocations,开启后不会迅速产生大量日志文件,导致应用程序卡死。而且他提供丰富的命令,几乎涵盖了,Instruments中Allocations跟leaks的所有功能,并且使用命令对当前应用程序中分配的内存类型等进行简单的统计分析,快速的定位应用程序中分配最多的内存是什么,以及是如何分配的很方便。
内存优化的方法论
-
在介绍了上述几种工具之后,我还希望可以借助一些工具,对内存占用进行一些分析操作,比如对分配的内存进行分类,排序,查看其分配堆栈,查看其被谁持有。
左图就是由此产生的内存问题分析方法,具体操作是打开APP,持续使用,期间可以随时导出Memory Graph文件,使用VMMap查看文件,对文件中的内存分配进行分类排序,取其中分配最大的一块内存的首地址使用Malloc_History跟Leaks进行分析,可以得到这块内存分配的详细堆栈以及当前是被谁引用,接下来就是对这块内存进行优化,通过不断的优化当前内存分配的大头,带来APP内存的巨大下降。
通过上述方法进行分析,发现APP中内存占用几个大类
- 界面的渲染
- 图片的压缩
- 各种缓存
- bug
方案
接下来我们逐个分析,介绍APP的内存占用
界面的渲染
- view的定制通过重写drawrect消耗大量内存
-
为何重写drawrect消耗大量内存
当我们重写drawrect时,会促使Core Animation创建一个Open GL纹理,并将你使用CoreGraphics框架的绘图操作数据放到纹理的位图数据中。
计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。当我们重写drawrect时,CPU会创建backing-store进行渲染,随后拷贝到GPU的显存(VRAM)
上图就是一个BackingStore的绘制过程,首先是window窗口触发重绘,准备重绘区域等大的一个基于bitmap的上下文,然后从父view开始,遍历所有的子view进行层层绘制,每一个绘制操作完成之后,才开始下一个绘制。
观察drawRect方法,如发现是一些点线条,背景色等的绘制,我们就可以使用CAShapeLayer+CGPath的方式进行绘制,CAShapeLayer是苹果提供的一个对opengl es的一个封装,可以完全满足我们的需要。
上图就是使用CAShapeLayer+CGPath的绘制方式替换之前通过Core Graphics的绘制方式的流程对比,一个CAShapeLayer 不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。并且CAShapeLayer渲染快速,使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
- 离屏渲染介绍
首先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区,其中涉及到两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。可以直接将图层合成到帧的缓冲区中(在屏幕上);当帧缓冲区图片被复用的时候,可以提升性能。- 帧缓冲区介绍
帧缓冲区(显存或者内存上一段空间):是由像素组成的二维数组,每一个存储单元对应屏幕上的一个像素,整个帧缓冲对应一帧图像即当前屏幕画面。帧缓冲通常包括:颜色缓冲,深度缓冲,模板缓冲和累积缓冲。这些缓冲区可能是在一块内存区域,也可能单独分开,看硬件。
- 帧缓冲区介绍
- Core Animation介绍
Core Animation的核心是OpenGL ES的一个抽象物,Core Animation的layer对应着OpenGL ES的texture,Core Animation可以有子layer,所以我们能看到他是一个图层树。在图像显示过程中,Core Animation的主要任务是判断出哪些图层需要被(重新)绘制,然后交由OpenGL ES将这些图层合并、显示到屏幕上。
图片的压缩
-
减少图片压缩产生的内存占用峰值
- 图片的压缩逻辑改造
使用ImageIO方式对图片进行压缩,无需解码bitmap
下面附上实现代码
NSMutableDictionary* options = [NSMutableDictionary dictionary]; [options safeSetObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways]; [options safeSetObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kCGImageSourceCreateThumbnailWithTransform]; [options safeSetObject:(__bridge id)kCFBooleanFalse forKey:(__bridge id)kCGImageSourceShouldCache]; [options safeSetObject:[NSNumber numberWithFloat:maxPixelSize] forKey:(__bridge id)kCGImageSourceThumbnailMaxPixelSize]; CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
- 图片的压缩逻辑改造
各种缓存
- 减少APP内缓存
- 减少单个缓存的大小
- 减少缓存时间,及时释放
- 减少缓存数量
bug
- bug修复
推荐阅读: