基础优化策略
- 延迟分配&懒分配
MyGlobalInfo* GetGlobalBuffer() {
static MyGlobalInfo* sGlobalBuffer = NULL;
if ( sGlobalBuffer == NULL ) {
sGlobalBuffer = malloc( sizeof( MyGlobalInfo ) );
}
return sGlobalBuffer;
}
-
高效初始化内存
malloc分配的小块内存,并不会保证清零初始化,一般会配上memset来初始化。但memset会强制将虚拟内存映射到触发物理内存,如果短时间内并不需要写入数据,会额外增加内存开销。而calloc
会保留要分配的虚拟地址空间,直到真正使用的时候才会映射到物理内存并清零初始化,而且只是初始化要用到的内存页。所以建议使用calloc
代替malloc+memset
-
复用频繁使用的大内存
如果你的计算需要频繁创建大的临时buffer,可以考虑复用buffer而不是每次重新分配。即使每次使用的buffer大小可能不一样,也可以通过realloc
方法来扩展已有Buffer。多线程环境下,最好将Buffer放入线程私有存储里thread-local storage
,避免多个线程同时操作同一Buffer。
缓存了Buffer减少了内存分配的次数,但也可能造成Footprint长期较大,所以仅适合需频繁分配Buffer的场景。 -
及时释放无用的内存
及时释放无用的内存,尤其要清理内存泄露问题。 -
小内存分配
malloc分配内存块的最小粒度为16字节,举例,当你需要分配4字节时,malloc
返回的是16字节的内存块;当你需要24字节时,返回的是32字节的内存块(16的整数倍)。所以我们设计数据结构时尽量占用16字节的整数倍。 -
大内存分配
malloc分配大内存时(包含多个内存页),会自动使用vm_allocate
来获取内存,而该过程只是分配了虚拟地址空间,并未立即分配对应的物理内存。当代码想要读写该内存区域某个地址时,会触发缺页错误,此时内核会进行以下操作:
- 从可用页(free list)获取一页,并清零初始化。
- 将该物理页记录到VM Object的resident pages中。
- 通过修改叫做
pmap
的结构体, 将虚拟页映射到物理页。(pmap包含了CPU/MMU用来映射地址的页表)
内存页的最小粒度为4K或16K,所以尽量分配其整数倍大小,避免浪费内存。
-
批量分配
如果需要分配多个同等大小的内存块,可以使用malloc_zone_batch_malloc
,它比多次调用malloc
要高效的多,尤其当内存块较小时(<4K)。这个方法会尽力分配请求的块数,但最终返回的块数可能少于请求的,所以需要仔细判断返回结果。 -
批量释放
所有内存分配都是在某个zone范围内,zone可以理解为一段可变大小的虚拟内存。你可以在zone里分配多个内存块,然后一次性释放整个zone,比单独释放每个内存块要高效的多。 -
延迟拷贝
通过memcpy
或memmove
拷贝内存叫做即时拷贝,源内存块和目标内存块需要同时存在内存中。当拷贝较大内存块时,增加了应用的整体内存占用和内存换出的几率。
如果拷贝完内存后并不需要里面使用,可以使用vm_copy
实现延迟拷贝。vm_copy
并不创建真实的内存块,而是通过修改虚拟内存映射,来表示目标内存区域是源内存区域的一个写时复制版本。为了实现延迟拷贝,内核需要将源内存页从虚拟内存空间中清理掉(物理内存还在)。下一次进程再访问源内存页时,会触发soft fault
,内核会将该内存页重新映射回虚拟内存。处理soft fault
跟即时拷贝性能损耗差不多,所以只在发生拷贝后长时间不再访问数据的情况下优势明显。 -
iOS低内存告警
iOS的虚拟内存没有换出磁盘的机制,所以需要依赖应用去释放内存。当iOS的可用内存页少于某阈值后,会尝试释放未修改的内存页(Clean Memory),如果需要的话也会终结一些切换到后台的应用。过程中可能也会向运行中的应用发送低内存告警通知。应用收到该通知时,需要尽可能清理不必要的内存。比如根页面为UITabViewCtroller的应用,可以先移除未展示的Tab, 下次需要展示时再重新加载。
清理缓存数据时要注意Memory Compression
带来的影响,避免清理已经压缩过的数据时触发了解压操作,反而增加了内存。建议用NSCache
代替NSDictionary
,使用NSPurgableData
代替NSData
。
NSCache 分配的内存实际上是 Purgeable Memory,可以由系统自动释放。NSCache 与 NSPureableData 的结合使用既能让系统根据情况回收内存,也可以在内存清理的同时移除相关对象。
针对具体内存Region的优化
IOKit
这部分主要是图片、OpenGL纹理、CVPixelBuffer等,比如通常是OpenGL的纹理,glTexImage2d调用产生的。iOS系统有相关释放接口,但可能释放不及时。
显存可能被映射到某块虚拟内存,因此可以通过IOKit来查看纹理增长情况。iOS的显存就是内存,而OSX才区分显存和内存。"
Graphics Memory (VRAM)
• iOS uses a Unified Memory Architecture — GPU and CPU share Physical Memory
• Graphics Driver allocates Virtual Memory for its resources
• Most of this is Resident and Dirty
纹理是在内核态分配的,不计算到Allocations里边,但是也记为Dirty Size。创建一定数量纹理后,到达极限值,则之后创建纹理就会失败,App可能不会崩溃,但是出现异常,花屏,或者拍后页白屏。
通常情况下,开发者已经正确调用了释放内存的操作,但是OpenGL驱动自己做了优化,使得内存并未真正地及时释放掉,仅仅是为了重用。
Some drivers may keep the storage allocated so that they can reuse it for satisfying future allocations (rather than having to allocate new storage – a common misunderstanding this behaviour leads to is people thinking they have a memory leak), other drivers may not.
VM:ImageIO_IOSurface_Data
典型堆栈:
VM:ImageIO_PNG_Data
典型堆栈
UIImage的imageNamed:
方法会将图片数据缓存在内存中。而imageWithContentsOfFile:
方法则不会进行缓存,用完立即释放掉了。优化建议:
- 对于经常需要使用的小图,可以放到Assets.xcassets中,使用imageNamed:方法。
- 对于不经常使用的大图,不要放到Assets.xcassets中,且使用imageWithContentsOfFile:方法。
如果对于多图的滚动视图,渲染到imageView中后,可以使用autoreleasepool来尽早释放:
for (int i=0;i<10;i++) {
UIImageView *imageView = xxx;
NSString *imageFile = xxx;
@autoreleasepool {
imageView.image = [UIImage imageWithContentsOfFile:imageFile];
}
[self.scrollView addSubview:imageView];
}
VM:Image IO
典型堆栈:
VM:IOAccelerator
典型堆栈
VM:CG raster data
典型堆栈:
光栅数据,即为UIImage的解码数据。SDWebImage将解码数据做了缓存,避免渲染时候在主线程解码而造成阻塞。
优化措施:
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];
VM:CoreAnimation
典型堆栈:
mach_vm_allocate
vm_allocate
CA::Render::Shmem::new_shmem
CA::Render::Shmem::new_bitmap
CABackingStorePrepareUpdates_
CABackingStoreUpdate_
invocation function for block in CA::Layer::display_()
x_blame_allocations
[CALayer _display]
CA::Context::commit_transaction
CA::Transaction::commit()
[UIApplication _firstCommitBlock] _block_invoke_2
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRunLoopDoBlocks
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
UIApplicationMain
main
start
UIKit渲染数据,大小跟UIView/CALayer尺寸有关。
优化措施:不要用太大的UIView和CALayer。
VM: CoreUI image data
典型堆栈
imageVM_ALLOCATE
这部分基本是对开发者自行分配的大内存进行检查。
__TEXT
优化措施:清理冗余代码,缩小代码段体积。
__DATA
可执行二进制的可写入静态区,主要包含
- 非const的static变量
- 全局变量
针对使用场景的优化措施
图像优化
图片占用的内存大小实际与其分辨率相关的,如果一个像素点占用4个byte的话,width * height * 4 / 1024 / 1024 MB。
参考:WWDC 2018 Session 219:Image and Graphics Best Practices。
imageNamed和imageWithContentsOfFile
- UIImage的imageNamed:方法会将图片数据缓存在内存中,缓存使用的时NSCache,收到内存警告会释放。
- 而imageWithContentsOfFile:方法则不会进行缓存,不需要的时候就立即释放掉了。
所以建议:
- 对于频繁使用的小图,可以放到Assets.xcassets中,使用imageNamed:方法。
- 对于不经常使用的大图,不要放到Assets.xcassets中,且使用imageWithContentsOfFile:方法。
UIImage的异步解码和渲染
UIImage只有在屏幕上渲染(self.imageView.image = image)的时候,才去解码的,解码操作在主线程执行。所以,如果有非常多(如滑动界面下载大量网络图片)或者较大图片的解码渲染操作,则会阻塞主线程。
可以通过如下方式,避免图片使用时候的一些阻塞、资源消耗过大、频繁解码等的情况。
- 异步下载网络图片,进行内存和磁盘缓存
- 对图片进行异步解码,将解码后的数据放到内存缓存
- 主线程进行图片的渲染
异步解码的详细实现,可以查看SDWebImage的SDImageCoderHelper.m文件。
适当使用autoreleasepool
如果对于多图的滚动视图,渲染到imageView中后,可以使用autoreleasepool来尽早释放:
for (int i=0;i<10;i++) {
UIImageView *imageView = xxx;
NSString *imageFile = xxx;
@autoreleasepool {
imageView.image = [UIImage imageWithContentsOfFile:imageFile];
}
[self.scrollView addSubview:imageView];
}
复制代码
UIGraphicsImageRenderer
建议使用iOS 10之后的UIGraphicsImageRenderer
来执行绘制任务。该API在iOS 12中会根据场景自动选择最合适的渲染格式,更合理地使用内存。
另一个方式,采用UIGraphicsBeginImageContextWithOptions
与UIGraphicsGetImageFromCurrentImageContext
得到的图片,每个像素点都需要4个byte。可能会有较大内存空间上的浪费。
Downsampling
对于一些场景,如UIImageView尺寸较小,而UIImage较大时,直接展示原图,会有不必要的内存和CPU消耗。
- 之前的方式
将大图缩小的时候,即downsampling的过程,一般需要将原始大图加载到内存,然后做一些坐标空间的转换,再生成小图。此过程中,如果使用UIGraphicsImageRenderer的绘制操作,会消耗比较多的资源。
UIImage *scaledImage = [self scaleImage:image newSize:CGSizeMake(2048, 2048)];
- (UIImage *)scaleImage:(UIImage *)image newSize:(CGSize)newSize {
// 这一步只是根据size创建一个bitmap的上下文,参数scale比较关键。
UIGraphicsBeginImageContextWithOptions(newSize, NO, 1);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 79.7
UIGraphicsEndImageContext(); // 15.7MB
return newImage;
}
UIGraphicsBeginImageContextWithOptions需要跟接收参数相关的context消耗,消耗的内存与三个参数相关。其实不大。
关键在于:UIImage的drawInRect:方法在绘制时,会将图片先解码,再生成原始分辨率大小的bitmap,内存峰值可能很高。这一步的内存消耗非常关键,如果图片很大,很容易就会增加几十MB的内存峰值。
这种方式的耗时不多,主要是内存消耗巨大。
- 推荐的方式
使用ImageIO的接口,避免调用UIImage的drawInRect:方法执行带来的中间bitmap的产生。可以在不产生Dirty Memory的情况下,直接读取图像大小和元数据信息,不会带来额外的内存开销。其内存消耗即为目标尺寸需要的内存。
static func downsampling(imageWith imageData: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithData(imageData as CFData, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels,
kCGImageSourceShouldCacheImmediately: false
] as CFDictionary
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
/// Core Foundation objects returned from annotated APIs are automatically memory managed in Swift
/// you do not need to invoke the CFRetain, CFRelease, or CFAutorelease functions yourself.
return UIImage(cgImage: downsampledImage)
}
其中,有一些选项设置downsampleOptions:
- kCGImageSourceCreateThumbnailFromImageAlways
- kCGImageSourceThumbnailMaxPixelSize
- kCGImageSourceShouldCache 可以设置为NO,避免缓存解码后的数据。默认为YES。
- kCGImageSourceShouldCacheImmediately 可以设置为YES,避免在需要渲染的时候才做图片解码。默认是NO,不会立即进行解码渲染,而是在渲染时才在主线程进行解码。
而该downsampling过程非常占用CPU资源,一定要放到异步线程去执行,否则会阻塞主线程。
缓存优化
对于缓存数据或可重建数据,尽量使用NSCache或NSPurableData,收到内存警告时,系统自动处理内存释放操作,并且是线程安全的。
使用SDWebImage同时开启内存和磁盘缓存时,若收到内存警告,则内存缓存的image被清除。
加载超大图片的正确姿势
对于一些微信长图/微博长图之类的,或者一些需要展示全图,然后拖动来查看细节的场景,可以使用 CATiledLayer
来进行分片加载,避免直接对图片的所有部分进行解码和渲染,以节省资源。在滑动时,指定目标位置,映射原图指定位置的部分图片进行解码和渲染。
进入后台
释放占用较大的内存,再次进入前台时按需加载。防止App在后台时被系统杀掉。
一般监听UIApplicationDidEnterBackground的系统通知即可。
ViewController相关的优化
对于UITabBarController这样有多个子VC的情况,切换tab时候,如果不显示的ViewController依然占用较大内存,可以考虑释放,需要时候再加载。
UIView相关的优化
- 避免尺寸过大
UIView尺寸过大时,如果全部绘制,则会消耗大量内存,以及阻塞主线程。常见的场景如微信消息的超长文本,则可将其分割成多个UIView,然后放到UITableView中,利用cell的复用机制,减少不必要的渲染和内存占用。 - 避免创建冗余的位图 'Backing Store'
减少drawRect
的方法实现,使用一些backgroundColor
等属性并不会创建backing store
- 避免触发离屏渲染
使用cornerRadius
并不会离屏渲染(开辟临时绘制buffer),但使用mask
会触发离屏渲染。
EXC_RESOURCE_EXCEPTION异常
iOS中没有交换空间,而是采用了JetSam机制。
当App使用的内存超出限制时,系统会抛出EXC_RESOURCE_EXCEPTION异常。
内存泄漏
内存泄漏,有些是能通过工具检测出来的。而还有一些无法检测,需要自行分析。
- 循环引用
通常对象间相互持有或者构成环状持有关系,则会引起循环引用。
常见的有对象间引用、委托模式下的delegate,以及Block引起的。 其中block里捕获了当前对象obj的带下划线的私有变量,也会强引用了obj, 如果obj再持有block,那就会环引用了。
- NSTimer
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimerAction) userInfo:nil repeats:YES];
Timer不仅会持有target,也会持有userInfo对象, 如果target也直接或间接的持有了timer,则会造成环引用。
解决方法:
- 引入WeakContainer作为代理,弱持有target对象
- 通过扩展NSTimer的Category,引入带block的初始化方法,而block里弱持有了target
关于NSTimer可以参考更详细的这篇博客:比较一下iOS中的三种定时器
- 其他场景
一些滥用的单例,尤其是包含了不少block的单例,很容易产生内存泄漏。排查时候需要格外细心。
离屏渲染
我们经常会需要预先渲染文字/图片以提高性能,此时需要尽可能保证这块 context 的大小与屏幕上的实际尺寸一致,避免浪费内存。可以通过 View Hierarchy 调试工具,打印一个 layer 的 contents 属性来查看其中的 CGImage(backing image)以及其大小。layer的contents属性即可看到其CGImage(backing store)的大小。
Offscreen rendering is invoked whenever the combination of layer properties that have been specified mean that the layer cannot be drawn directly to the screen without pre- compositing. Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed.
离屏渲染未必会导致性能降低,而是会额外加重GPU的负担,可能导致一个V-sync信号周期内,GPU的任务未能完成,最终结果就是可能导致卡顿。
iOS系统对于Release环境下的优化
实际的release环境下,Apple会对一些场景自动优化,如release环境下,申请50MB的Dirty Memory,但实际footprint和resident不会增加50MB,具体Apple怎么做的不清楚。
减少缺页次数
App启动时,加载相应的二进制文件或者dylib到内存中。当进程访问一个虚拟内存page,但该page未与物理内存形成映射关系,则会触发缺页中断,然后再分配物理内存。过多的缺页中断会导致一定的耗时。
二进制重排的启动优化方案,是通过减少App启动时候的缺页中断次数,来加速App启动。
字节对齐
当定义object的时候,尽量使得内存页对齐也会有帮助。小内存属性放一起,大内存属性放一起。
文字渲染
WWDC(WWDC18 219和416)上的结论,即黑白的位图像素只占用 1 个字节,比 4 字节节省 75% 的空间。
image
网友评论