App的内存优化

文章的前篇主要是对两种不同的UIImage工厂方法的分析, 罗列出这些工厂方法的内存管理的优缺点。文
首页 新闻资讯 行业资讯 App的内存优化

[[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对象:

复制

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

  • 2.

  • 3.

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

复制

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

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 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”). 然后在代码中写:

复制

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

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

ImageAssets 的特性

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

其内部代码相似于:

复制

+ (NSMutableDictionary *)imageBuff {      static NSMutableDictionary *_imageBuff;      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          _imageBuff = [[NSMutableDictionary alloc] init];      });      return _imageBuff;  }     + (instancetype)imageNamed:(NSString *)imageName {      if (!imageName) {          return nil;      }      UIImage *image = self.imageBuff[imageName];      if (image) {          return image;      }      NSString *path = @"this is the image path"//这段逻辑忽略      image = [self imageWithContentsOfFile:path];      if (image) {          self.imageBuff[imageName] = image;      }      return image;  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

ImageAssets 的使用场景

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

ImageAssets 的优点

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

复制

+ (YYImage *)imageNamed:(NSString *)name {      if (name.length == 0) return nil;      if ([name hasSuffix:@"/"]) return nil;         NSString *res = name.stringByDeletingPathExtension;      NSString *ext = name.pathExtension;      NSString *path = nil;      CGFloat scale = 1;         // If no extension, guess by system supported (same as UIImage).      NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];      NSArray *scales = [NSBundle preferredScales];      for (int s = 0; s count; s++) {          scale = ((NSNumber *)scales[s]).floatValue;          NSString *scaledName = [res stringByAppendingNameScale:scale];          for (NSString *e in exts) {              path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];              if (path) break;          }          if (path) break;      }      if (path.length == 0) return nil;         NSData *data = [NSData dataWithContentsOfFile:path];      if (data.length == 0) return nil;         return [[self alloc] initWithData:data scale:scale];  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 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 来获取引用计数器:

复制

@implementation NSObject (MRC)     // 无法直接重写 retainCount 的方法, 所以加了一个前缀  - (NSUInteger)obj_retainCount {      return [[self valueForKey:@"retainCount"] unsignedLongValue];  }     @end
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

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

复制

// 由于遍历键值对时候不能做添加和删除操作, 所以把要删除的key放到一个数组中  NSMutableArray *keyArr = [NSMutableArray array];  [self.imageDic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, NSObject * _Nonnull obj, BOOL * _Nonnull stop){      NSInteger count = obj.obj_retainCount;      if(count == 2) {// 字典持有 + obj参数持有 = 2          [keyArr addObject:key];      }  }];  [keyArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {      [self.imageDic removeObjectForKey:obj];  }];
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

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

复制

CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {      if (activity == kCFRunLoopBeforeWaiting) {          static enuming = NO;          if (!enuming) {              enuming = YES;              // 这里是遍历代码              enuming = NO;          }      }  });     CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes);
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

 具体实现请看代码.

方案二之 弱引用字典

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

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

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

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

***步 封装与解封

复制

typedef id (^WeakReference)(void);     WeakReference makeWeakReference(id object) {      __weak id weakref = object;      return ^{          return weakref;      };  }     id weakReferenceNonretainedObjectValue(WeakReference ref) {      return ref ? ref() : nil;  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

第二步 改造原容器

复制

- (void)weak_setObject:(id)anObject forKey:(NSString *)aKey {      [self setObject:makeWeakReference(anObject) forKey:aKey];  }     - (void)weak_setObjectWithDictionary:(NSDictionary *)dic {      for (NSString *key in dic.allKeys) {          [self setObject:makeWeakReference(dic[key]) forKey:key];      }  }     - (id)weak_getObjectForKey:(NSString *)key {      return weakReferenceNonretainedObjectValue(self[key]);  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

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

23    2017-02-14 17:00:39    iOS App 内存优化