App的内存优化

移动开发 iOS
文章的前篇主要是对两种不同的UIImage工厂方法的分析, 罗列出这些工厂方法的内存管理的优缺点。文章的后篇是本文要说明的重点, 如何结合两种工厂方法的优点做更进一步的节约内存的管理。

[[183249]]

这篇文章是笔者在开发App过程中发现的一些内存问题, 然后学习了YYKit框架时候也发现了图片的缓存处理 (YYKit 作者联系了我, 说明了YYKit重写imageNamed:的目的不是为了内存管理, 而是增加兼容性, 同时也是为了YYKit中的动画服务). 以下内容是笔者在开发中做了一些实验以及总结. 如有错误望即时提出, 笔者会***时间改正.

文章的前篇主要是对两种不同的UIImage工厂方法的分析, 罗列出这些工厂方法的内存管理的优缺点.

文章的后篇是本文要说明的重点, 如何结合两种工厂方法的优点做更进一步的节约内存的管理.

PS

本文所说的 Resource 是指使用imageWithContentsOfFile:创建图片的图片管理方式.

ImageAssets 是指使用imageNamed:创建图片的图片管理方式.

如果你对这两个方法已经了如指掌, 可以直接看UIImage 与 YYImage 的内存问题和后面的内容

[TOC]

UIImage 的内存处理

在实际的苹果App开发中, 将图片文件导入到工程中无非使用两种方式. 一种是 Resource (我也不知道应该称呼什么,就这么叫吧),还有一种是 ImageAssets 形式存储在一个图片资源管理文件中. 这两种方式都可以存储任何形式的图片文件, 但是都有各自的优缺点在内. 接下来我们就来谈谈这两种图片数据管理方式的优缺点.

Resource 与 “imageWithContentsOfFile:”

Resource 的使用方式

将文件直接拖入到工程目录下, 并告诉Xcode打包项目时候把这些图片文件打包进去. 这样在应用的”.app”文件夹中就有这些图片. 在项目中, 读取这些图片可以通过以下方式来获取图片文件并封装成UIImge对象:

  1. NSString *path = [NSBundle.mainBundle pathForResource:@"image@2x" type:@"png"]; 
  2.  
  3. UIImage *image = [UIImage imageWithContentsOfFile:path];  

而底层的实现原理近似是:

  1. + (instancetype)imageWithContentsOfFile:(NSString *)fileName { 
  2.  
  3.     NSUInteger scale = 0; 
  4.  
  5.     { 
  6.  
  7.         scale = 2;//这一部分是取 fileName 中"@"符号后面那个数字, 如果不存在则为1, 这一部分的逻辑省略 
  8.  
  9.     } 
  10.  
  11.     return [[self alloc] initWithData:[NSData dataWithContentsOfFile:fileName scale:scale]; 
  12.  
  13.  

这种方式有一个局限性, 就是图片文件必须在.ipa的根目录下或者在沙盒中. 在.ipa的根目录下创建图片文件仅仅只有一种方式, 就是通过 Xcode 把图片文件直接拖入工程中. 还有一种情况也会创建图片文件, 就是当工程支持低版本的 iOS 系统时, 低版本的iOS系统并不支持 ImageAssets 打包文件的图片读取, 所以 Xcode 在编译时候会自动地将 ImageAssets 中的图片复制一份到根目录中. 此时也可以使用这个方法创建图片.

Resource 的特性

在 Resource 的图片管理方式中, 所有的图片创建都是通过读取文件数据得到的, 读取一次文件数据就会产生一次NSData以及产生一个UIImage, 当图片创建好后销毁对应的NSData, 当UIImage的引用计数器变为0的时候自动销毁UIImage. 这样的话就可以保证图片不会长期地存在在内存中.

Resource 的常用情景

由于这种方法的特性, 所以 Resource 的方法一般用在图片数据很大, 图片一般不需要多次使用的情况. 比如说引导页背景(图片全屏, 有时候运行APP会显示, 有时候根本就用不到).

Resource 的优点

图片的生命周期可以得到管理无疑是 Resource ***的优点, 当我们需要图片的时候就创建一个, 当我们不需要这个图片的时候就让他销毁. 图片不会长期的保存在内存当中, 所以不会有很多的内存浪费. 同时, 大图一般不会长期使用, 而且大图占用内存一般比小图多了好多倍, 所以在减少大图的内存占用中, Resource 做的非常好.

ImageAssets 与 “imageNamed:”

ImageAssets 的设计初衷主要是为了自动适配 Retina 屏幕和非 Retina 屏幕, 也就是解决 iPhone 4 和 iPhone 3GS 以及以前机型的屏幕适配问题. 现在 iPhone 3GS 以及之前的机型都已被淘汰, 非 Retina 屏幕已不再是开发考虑的范围. 但是 plus 机型的推出将 Retina 屏幕又提高了一个水平, ImageAssets 现在的主要功能则是区分 plus 屏幕和非 plus 屏幕, 也就是解决 2 倍 Retina 屏幕和 3 倍 Retina 屏幕的视屏问题.

ImageAssets 的使用方式

iOS 开发中一般在工程内导入两个到三个同内容不同像素的图片文件, 一般如下:

  1. image.png (30 x 30)
  2. image@2x.png (60 x 60)
  3. image@3x.png (90 x 90)

这三张图片都是相同内容, 而且图片名称的前缀相同, 区别在与图片名以及图片的分辨率. 开发者将这三张图片拉入 ImageAssets 后, Xcode 会以图片前缀创建一个图片组(这里也就是 “image”). 然后在代码中写:

  1. UIImage *image = [UIImage imageNamed:@"image"]; 

就会根据不同屏幕来获取对应不同的图片数据来创建图片. 如果是 3GS 之前的机型就会读取 “image.png”, 普通 Retina 会读取 “image@2x.png“, plus Retina 会读取 “image@3x.png“, 如果某一个文件不存在, 就会用另一个分辨率的图片代替之.

ImageAssets 的特性

与 Resources 相似, ImageAssets 也是从图片文件中读取图片数据转为 UIImage, 只不过这些图片数据都打包在 ImageAssets 中. 还有一个***的区别就是图片缓存. 相当于有一个字典, key 是图片名, value是图片对象. 调用imageNamed:方法时候先从这个字典里取, 如果取到就直接返回, 如果取不到再去文件中创建, 然后保存到这个字典后再返回. 由于字典的key和value都是强引用, 所以一旦创建后的图片永不销毁.

其内部代码相似于:

  1. + (NSMutableDictionary *)imageBuff { 
  2.  
  3.     static NSMutableDictionary *_imageBuff; 
  4.  
  5.     static dispatch_once_t onceToken; 
  6.  
  7.     dispatch_once(&onceToken, ^{ 
  8.  
  9.         _imageBuff = [[NSMutableDictionary alloc] init]; 
  10.  
  11.     }); 
  12.  
  13.     return _imageBuff; 
  14.  
  15.  
  16.   
  17.  
  18. + (instancetype)imageNamed:(NSString *)imageName { 
  19.  
  20.     if (!imageName) { 
  21.  
  22.         return nil; 
  23.  
  24.     } 
  25.  
  26.     UIImage *image = self.imageBuff[imageName]; 
  27.  
  28.     if (image) { 
  29.  
  30.         return image; 
  31.  
  32.     } 
  33.  
  34.     NSString *path = @"this is the image path"//这段逻辑忽略 
  35.  
  36.     image = [self imageWithContentsOfFile:path]; 
  37.  
  38.     if (image) { 
  39.  
  40.         self.imageBuff[imageName] = image; 
  41.  
  42.     } 
  43.  
  44.     return image; 
  45.  
  46.  

ImageAssets 的使用场景

ImageAssets 最主要的使用场景就是 icon 类的图片, 一般 icon 类的图片大小在 3kb 到 20 kb 不等, 都是一些小文件.

ImageAssets 的优点

当一个 icon 在多个地方需要被显示的时候, 其对应的UIImage对象只会被创建一次, 而且多个地方的 icon 都将会共用一个 UIImage 对象. 减少沙盒的读取操作. 

  1. + (YYImage *)imageNamed:(NSString *)name { 
  2.  
  3.     if (name.length == 0) return nil; 
  4.  
  5.     if ([name hasSuffix:@"/"]) return nil; 
  6.  
  7.   
  8.  
  9.     NSString *res = name.stringByDeletingPathExtension; 
  10.  
  11.     NSString *ext = name.pathExtension; 
  12.  
  13.     NSString *path = nil; 
  14.  
  15.     CGFloat scale = 1; 
  16.  
  17.   
  18.  
  19.     // If no extension, guess by system supported (same as UIImage). 
  20.  
  21.     NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"]; 
  22.  
  23.     NSArray *scales = [NSBundle preferredScales]; 
  24.  
  25.     for (int s = 0; s count; s++) { 
  26.  
  27.         scale = ((NSNumber *)scales[s]).floatValue; 
  28.  
  29.         NSString *scaledName = [res stringByAppendingNameScale:scale]; 
  30.  
  31.         for (NSString *e in exts) { 
  32.  
  33.             path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e]; 
  34.  
  35.             if (path) break; 
  36.  
  37.         } 
  38.  
  39.         if (path) break; 
  40.  
  41.     } 
  42.  
  43.     if (path.length == 0) return nil; 
  44.  
  45.   
  46.  
  47.     NSData *data = [NSData dataWithContentsOfFile:path]; 
  48.  
  49.     if (data.length == 0) return nil; 
  50.  
  51.   
  52.  
  53.     return [[self alloc] initWithData:data scale:scale]; 
  54.  
  55.  

UIImage 的内存问题

Resource 的缺点

当我们需要图片的时候就会去沙盒中读取这个图片文件, 转换成UIImage对象来使用. 现在假设一种场景:

  1. image@2x.png 图片占用 5kb 的内存
  2. image@2x.png 在多个界面都用到, 且有7处会同时显示这个图片

通过代码分析就可以知道 Resource 这个方式在这个情景下会占用 5kb/个 X 7个 = 35kb 内存. 然而, 在 ImageAssets 方式下, 全部取自字典缓存中的UIImage, 无论有几处显示图片, 都只会占用 5kb/个 X 1个 = 5kb 内存. 此时 Resource 占用内存将会更大.

ImageAssets 的缺点

***次读取的图片保存到缓冲区, 然后永不销毁. 如果这个图片过大, 占用几百 kb, 这一块的内存将不会释放, 必然导致内存的浪费, 而且这个浪费的周期与APP的生命周期同步.

解决方案

为了解决 Resource 的多图共存问题, 可以学习 ImageAssets 中的字典来形成键值对, 当字典中name对应的image存在就不创建, 如果不存在就创建. 字典的存在必然导致 UIImage 永不销毁, 所以还要考虑字典不会影响到 UIImage 的自动销毁问题. 由此可以做出如下总结:

  1. 需要一个字典存储已经创建的 Image 的 name-image 映射
  2. 当除了这个字典外, 没有别的对象持有 image, 则从这个字典中删除对应 name-image 映射

***个要求的实现方式很简单, 接下来探讨第二个要求.

首先可以考虑如何判断除了字典外没有别的对象持有 image? 字典是强引用 key 和 value 的, 当 image 放入字典的时候, image 的引用计数器就会 + 1. 我们可以判断字典中的 image 的引用计数器是否为 1, 如果为 1 则可以判断出目前只有字典持有这个 image, 因此可以从这个字典里删除这个 image.

这样即可提出一个方案 MRC+字典

我们还可以换一种思想, 字典是强引用容器, 字典存在必然导致内部value的引用计数器大于等于1. 如果字典是一个弱引用容器, 字典的存在并不会影响到内部value的引用计数器, 那么 image 的销毁就不会因为字典而受到影响.

于是又有一个方案 弱引用字典

接下来对这两个方案作深入的分析和实现:

方案一之 MRC+字典

该方案具体思路是: 找到一个合适的时机, 遍历所有 value 的 引用计数器, 当某个 value 的引用计数器为 1 时候(说明只有字典持有这个image), 则删除这个key-value对.

***步, 在ARC下获取某个对象的引用计数器:

首先 ARC 下是不允许使用retainCount这个属性的, 但是由于 ARC 的原理是编译器自动为我们管理引用计数器, 所以就算是 ARC 环境下, 引用计数器也是 Enable 状态, 并且仍然是利用引用计数器来管理内存. 所以我们可以使用 KVC 来获取引用计数器:

  1. @implementation NSObject (MRC) 
  2.  
  3.   
  4.  
  5. // 无法直接重写 retainCount 的方法, 所以加了一个前缀 
  6.  
  7. - (NSUInteger)obj_retainCount { 
  8.  
  9.     return [[self valueForKey:@"retainCount"] unsignedLongValue]; 
  10.  
  11.  
  12.   
  13.  
  14. @end  

第二步 遍历 value的引用计数器

  1. // 由于遍历键值对时候不能做添加和删除操作, 所以把要删除的key放到一个数组中 
  2.  
  3. NSMutableArray *keyArr = [NSMutableArray array]; 
  4.  
  5. [self.imageDic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, NSObject * _Nonnull obj, BOOL * _Nonnull stop){ 
  6.  
  7.     NSInteger count = obj.obj_retainCount; 
  8.  
  9.     if(count == 2) {// 字典持有 + obj参数持有 = 2 
  10.  
  11.         [keyArr addObject:key]; 
  12.  
  13.     } 
  14.  
  15. }]; 
  16.  
  17. [keyArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 
  18.  
  19.     [self.imageDic removeObjectForKey:obj]; 
  20.  
  21. }];  

然后处理遍历时机. 选择遍历时机是一个很困难的, 不能因为遍历而大量占有系统资源. 可以在每一次通过 name 创建(或者从字典中获取)时候遍历一次, 但这个方法有可能会长时间不调用(比如一个用户在某一个界面上呆很久). 所以我们可以在每一次 runloop 到来时候来做一次遍历, 同时我们还需要标记遍历状态, 防止第二次 runloop 到来时候***次的遍历还没结束就开始新的遍历了(此时应该直接放弃第二次遍历).代码如下:

  1. CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { 
  2.  
  3.     if (activity == kCFRunLoopBeforeWaiting) { 
  4.  
  5.         static enuming = NO
  6.  
  7.         if (!enuming) { 
  8.  
  9.             enuming = YES; 
  10.  
  11.             // 这里是遍历代码 
  12.  
  13.             enuming = NO
  14.  
  15.         } 
  16.  
  17.     } 
  18.  
  19. }); 
  20.  
  21.   
  22.  
  23. CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes); 

 具体实现请看代码.

方案二之 弱引用字典

在上面那个方案中, 会在每一次 runloop 到来之时开辟一个线程去遍历键值对. 通常来说, 每一个 APP 创建的图片个数很大, 所以遍历键值对虽然不会阻塞主线程, 但仍然是一个非常耗时耗资源的工作.

弱引用容器是指基于NSArray, NSDictionary, NSSet的容器类, 该容器与这些类***的区别在于, 将对象放入容器中并不会改变对象的引用计数器, 同时容器是以一个弱引用指针指向这个对象, 当对象销毁时自动从容器中删除, 无需额外的操作.

目前常用的弱引用容器的实现方式是block封装解封

利用block封装一个对象, 且block中对象的持有操作是一个弱引用指针. 而后将block当做对象放入容器中. 容器直接持有block, 而不直接持有对象. 取对象时解包block即可得到对应对象.

***步 封装与解封

  1. typedef id (^WeakReference)(void); 
  2.  
  3.   
  4.  
  5. WeakReference makeWeakReference(id object) { 
  6.  
  7.     __weak id weakref = object; 
  8.  
  9.     return ^{ 
  10.  
  11.         return weakref; 
  12.  
  13.     }; 
  14.  
  15.  
  16.   
  17.  
  18. id weakReferenceNonretainedObjectValue(WeakReference ref) { 
  19.  
  20.     return ref ? ref() : nil; 
  21.  
  22.  

第二步 改造原容器

  1. - (void)weak_setObject:(id)anObject forKey:(NSString *)aKey { 
  2.  
  3.     [self setObject:makeWeakReference(anObject) forKey:aKey]; 
  4.  
  5.  
  6.   
  7.  
  8. - (void)weak_setObjectWithDictionary:(NSDictionary *)dic { 
  9.  
  10.     for (NSString *key in dic.allKeys) { 
  11.  
  12.         [self setObject:makeWeakReference(dic[key]) forKey:key]; 
  13.  
  14.     } 
  15.  
  16.  
  17.   
  18.  
  19. - (id)weak_getObjectForKey:(NSString *)key { 
  20.  
  21.     return weakReferenceNonretainedObjectValue(self[key]); 
  22.  
  23.  

这样就实现了一个弱引用字典, 之后用弱引用字典代替imageNamed:中的强引用字典即可. 

责任编辑:庞桂玉 来源: iOS大全
相关推荐

2018-07-23 09:26:08

iOS内存优化

2013-09-16 16:56:09

AndroidBitmap内存优化

2021-11-23 10:25:35

性能优化iOS App 启动优化

2017-01-23 21:05:00

AndroidApp启动优化

2010-08-10 10:00:57

Flex内存

2013-12-09 15:21:28

ASOApp Store

2010-08-10 10:17:44

Flex内存

2011-07-28 10:01:19

IOS 内存优化

2023-10-12 07:43:45

2011-08-10 09:06:44

内存内存优化

2009-09-08 09:45:23

App Engine性

2017-03-14 18:48:06

Android性能优化内存优化

2015-05-30 10:04:24

线下公开课51CTO沙龙MDSA

2022-07-05 08:41:03

Redis保存大数据

2017-04-18 21:27:01

AndroidAPP构建速度

2018-06-14 09:35:35

2022-06-10 15:37:24

爱奇艺App网络

2019-09-17 09:21:01

2017-12-14 14:32:30

.Net内存代码

2016-12-22 17:21:11

Android性能优化内存泄漏
点赞
收藏

51CTO技术栈公众号