OS X和iOS 内存优化

2021-05-17  本文已影响0人  人生看淡不服就干

基础优化策略

MyGlobalInfo* GetGlobalBuffer() {
    static MyGlobalInfo* sGlobalBuffer = NULL;
    if ( sGlobalBuffer == NULL ) {
            sGlobalBuffer = malloc( sizeof( MyGlobalInfo ) );
     }
     return sGlobalBuffer;
}
  1. 从可用页(free list)获取一页,并清零初始化。
  2. 将该物理页记录到VM Object的resident pages中。
  3. 通过修改叫做pmap的结构体, 将虚拟页映射到物理页。(pmap包含了CPU/MMU用来映射地址的页表)

内存页的最小粒度为4K或16K,所以尽量分配其整数倍大小,避免浪费内存。

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:方法则不会进行缓存,用完立即释放掉了。优化建议:

  1. 对于经常需要使用的小图,可以放到Assets.xcassets中,使用imageNamed:方法。
  2. 对于不经常使用的大图,不要放到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

典型堆栈

image

VM_ALLOCATE

这部分基本是对开发者自行分配的大内存进行检查。

__TEXT

优化措施:清理冗余代码,缩小代码段体积。

__DATA

可执行二进制的可写入静态区,主要包含

针对使用场景的优化措施

图像优化

图片占用的内存大小实际与其分辨率相关的,如果一个像素点占用4个byte的话,width * height * 4 / 1024 / 1024 MB。

参考:WWDC 2018 Session 219:Image and Graphics Best Practices

imageNamed和imageWithContentsOfFile

  1. UIImage的imageNamed:方法会将图片数据缓存在内存中,缓存使用的时NSCache,收到内存警告会释放。
  2. 而imageWithContentsOfFile:方法则不会进行缓存,不需要的时候就立即释放掉了。

所以建议:

  1. 对于频繁使用的小图,可以放到Assets.xcassets中,使用imageNamed:方法。
  2. 对于不经常使用的大图,不要放到Assets.xcassets中,且使用imageWithContentsOfFile:方法。

UIImage的异步解码和渲染

UIImage只有在屏幕上渲染(self.imageView.image = image)的时候,才去解码的,解码操作在主线程执行。所以,如果有非常多(如滑动界面下载大量网络图片)或者较大图片的解码渲染操作,则会阻塞主线程。

可以通过如下方式,避免图片使用时候的一些阻塞、资源消耗过大、频繁解码等的情况。

  1. 异步下载网络图片,进行内存和磁盘缓存
  2. 对图片进行异步解码,将解码后的数据放到内存缓存
  3. 主线程进行图片的渲染

异步解码的详细实现,可以查看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中会根据场景自动选择最合适的渲染格式,更合理地使用内存。

另一个方式,采用UIGraphicsBeginImageContextWithOptionsUIGraphicsGetImageFromCurrentImageContext得到的图片,每个像素点都需要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:

  1. kCGImageSourceCreateThumbnailFromImageAlways
  2. kCGImageSourceThumbnailMaxPixelSize
  3. kCGImageSourceShouldCache 可以设置为NO,避免缓存解码后的数据。默认为YES。
  4. kCGImageSourceShouldCacheImmediately 可以设置为YES,避免在需要渲染的时候才做图片解码。默认是NO,不会立即进行解码渲染,而是在渲染时才在主线程进行解码。

而该downsampling过程非常占用CPU资源,一定要放到异步线程去执行,否则会阻塞主线程。

缓存优化

对于缓存数据或可重建数据,尽量使用NSCache或NSPurableData,收到内存警告时,系统自动处理内存释放操作,并且是线程安全的。

使用SDWebImage同时开启内存和磁盘缓存时,若收到内存警告,则内存缓存的image被清除。

加载超大图片的正确姿势

对于一些微信长图/微博长图之类的,或者一些需要展示全图,然后拖动来查看细节的场景,可以使用 CATiledLayer 来进行分片加载,避免直接对图片的所有部分进行解码和渲染,以节省资源。在滑动时,指定目标位置,映射原图指定位置的部分图片进行解码和渲染。

进入后台

释放占用较大的内存,再次进入前台时按需加载。防止App在后台时被系统杀掉。

一般监听UIApplicationDidEnterBackground的系统通知即可。

ViewController相关的优化

对于UITabBarController这样有多个子VC的情况,切换tab时候,如果不显示的ViewController依然占用较大内存,可以考虑释放,需要时候再加载。

UIView相关的优化

EXC_RESOURCE_EXCEPTION异常

iOS中没有交换空间,而是采用了JetSam机制。

当App使用的内存超出限制时,系统会抛出EXC_RESOURCE_EXCEPTION异常。

内存泄漏

内存泄漏,有些是能通过工具检测出来的。而还有一些无法检测,需要自行分析。

通常对象间相互持有或者构成环状持有关系,则会引起循环引用。

常见的有对象间引用、委托模式下的delegate,以及Block引起的。 其中block里捕获了当前对象obj的带下划线的私有变量,也会强引用了obj, 如果obj再持有block,那就会环引用了。

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimerAction) userInfo:nil repeats:YES];

Timer不仅会持有target,也会持有userInfo对象, 如果target也直接或间接的持有了timer,则会造成环引用。

解决方法:

  1. 引入WeakContainer作为代理,弱持有target对象
  2. 通过扩展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
上一篇 下一篇

猜你喜欢

热点阅读