有哪些内存泄漏是你不知道的?

    作者:课课家教育更新于: 2017-04-28 09:49:07

    大神带你学编程,欢迎选课

      今天这篇文章主要是帮助iOS开发者在检查app中的内存泄漏问题,如果你是大神级别,那就可以忽略本文了,本文大部分内容是比较基础的。

      一、从AFNet说起

      对于ios开发者,网络请求类AFNetWorking是再熟悉不过了,对于AFNetWorking的使用我们通常会对通用参数、网址环境切换、网络状态监测、请求错误信息等进行封装。在封装网络请求类时需注意的是需要将请求队列管理者AFHTTPSessionManager声明为单例创建形式。对于该问题,AFNetWorking的作者在gitHub上也指出建议使用者在相同配置下保证AFHTTPSessionManager只有一个,进行全局管理,因此我们可以通过单例形式进行解决。下方展示部分核心代码:

      Ios代码

      + (AFHTTPSessionManager*)defaultNetManager {

      static AFHTTPSessionManager *manager;

      static dispatch_once_t onceToken;

      dispatch_once(&onceToken, ^{

      manager = [[AFHTTPSessionManager alloc]init];

      manager.responseSerializer = [AFHTTPResponseSerializer serializer];

      });

      return manager;

      }

      Ios代码

      + (void)GET:(NSString*)url parameters:(NSDictionary*)parameter returnData:(void (^)(NSData * resultData,NSError * error))returnBlock{

      //请求队列管理者 单例创建形式 防止内存泄漏

      AFHTTPSessionManager * manager = [HttpRequest defaultNetManager];

      [manager GET:url parameters:parameter progress:^(NSProgress * _Nonnull downloadProgress) {

      } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

      returnBlock(responseObject,nil);

      } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

      returnBlock(nil,error);

      }];

      }

      关于内存泄漏的详解:

      内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。

      内存泄漏形象的比喻是"操作系统可提供给所有进程的存储空间正在被某个进程榨干",最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。所以"内存泄漏"是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

      二、Block循环引用

      Block循环引用的问题已是老经常谈了,至今已有多篇文章详细解释其原理及造成循环引用的原因等,不泛画图或实例列举,这里不一一赘述。总结一句话防止Block循环引用就是要防止对象之间引用的闭环出现。举个开发中的实际例子,就拿很多人在用的MJRefresh说起

      Ios代码

      self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{

      self.page = 1;

      [self.dataArr removeAllObjects];

      [self loadData];

      }];

      若在MJRefresh的执行Block中调用当前self或其所属属性,一定要注意循环引用问题。我们简单分析下MJRefresh为什么会造成循环引用问题:

      点击进入headerWithRefreshingBlock对应方法即可

      #pragma mark - 构造方法

      Ios代码

      + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock

      {

      MJRefreshHeader *cmp = [[self alloc] init];

      cmp.refreshingBlock = refreshingBlock;

      return cmp;

      }

      这里仅有三行代码,无非就是创建了下拉刷新部分View然后返回,这里比较重要的是cmp.refreshingBlock = refreshingBlock;这一句,这里的refreshingBlock是属于MJRefreshHeader的强引用属性,最后header会成为我们自己tableView的强引用属性mj_header,也就是说self.tableView强引用header, header强引用refreshingBlock,如果refreshingBlock里面强引用self,就成了循环引用,所以必须使用weakSelf,破掉这个循环。画图表示为:

    有哪些内存泄漏是你不知道的?_ios知识库_ios开发者_编程语言_课课家教育

      循环引用示意图

      闭环为:

      self--->self.tableView--->self.tableView.mj_header--->self.tableView.mj_header.refreshingBlock--->self

      解决方案大家应该也不陌生

      Ios代码

      __weak typeof(self) weakself = self;

      self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{

      __strong typeof(self) strongself = weakself;

      strongself.page = 1;

      [strongself.dataArr removeAllObjects];

      [strongself loadData];

      }];

      【⚠️strongself是为了防止内存提前释放,有兴趣的童鞋可深入了解,这里不做过多解释了。当然也可借助libextobjc库进行解决,书写为@weakify和@strongify会更方便些。】

      相应的对于自定义View中的一些Block传值问题同样需要注意,与上述类似。

      三、delegate循环引用问题

      delegate循环引用问题比较基础,只需注意将代理属性修饰为weak即可

      Ios代码

      @property (nonatomic, weak) id delegate;

      下图比较形象的说明了使用weak修饰就是为了防止ViewController和UITableView相互强引用内存无法释放的问题:

    下图比较形象的说明了使用weak修饰就是为了防止ViewController和UITableView相互强引用内存无法释放的问题:

      delegate循环引用

      四、NSTimer循环引用

      对于定时器NSTimer,使用不正确也会造成内存泄漏问题。这里简单举个例子,我们声明了一个类TestNSTimer,在其init方法中创建定时器执行操作。

      Ios代码

      #import "TestNSTimer.h"

      @interface TestNSTimer ()

      @property (nonatomic, strong) NSTimer *timer;

      @end

      @implementation TestNSTimer

      - (instancetype)init {

      if (self = [super init]) {

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

      }

      return self;

      }

      - (void)timeRefresh:(NSTimer*)timer {

      NSLog(@"TimeRefresh...");

      }

      - (void)cleanTimer {

      [_timer invalidate];

      _timer = nil;

      }

      - (void)dealloc {

      [super dealloc];

      NSLog(@"销毁");

      [self cleanTimer];

      }

      @end

      在外部调用时,将其创建后5秒销毁。

      Ios代码

      TestNSTimer *timer = [[TestNSTimer alloc]init];

      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

      [timer release];

      });

      最后的执行结果为

    最后的执行结果为

      NSTimer打印结果

      可见TestNSTimer对象并没有正常释放,定时器仍然在无限的执行下去。

      我们都知道定时器使用完毕时需要将其停止并滞空,但cleanTimer方法到底何时调用呢?在当前类的dealloc方法中吗?并不是,若将cleanTimer方法调用在dealloc方法中会产生如下问题,当前类销毁执行dealloc的前提是定时器需要停止并滞空,而定时器停止并滞空的时机在当前类调用dealloc方法时,这样就造成了互相等待的场景,从而内存一直无法释放。因此需要注意cleanTimer的调用时机从而避免内存无法释放,如上的解决方案为将cleanTimer方法外漏,在外部调用即可。

      Iod代码

      TestNSTimer *timer = [[TestNSTimer alloc]init];

      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

      [timer cleanTimer];

      [timer release];

      });

    Iod代码

      打印结果

      五、非OC对象内存处理

      对于iOS开发,ARC模式已发扬光大多年,可能很多人早已忘记当年retain、release的年代,但ARC的出现并不是说我们完全可以忽视内存泄漏的问题。对于一些非OC对象,使用完毕后其内存仍需要我们手动释放。

      举个例子,比如常用的滤镜操作调节图片亮度

      Ios代码

      CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];

      CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];

      [filter setValue:beginImage forKey:kCIInputImageKey];

      [filter setValue:[NSNumber numberWithFloat:.5] forKey:@"inputBrightness"];//亮度-1~1

      CIImage *outputImage = [filter outputImage];

      //GPU优化

      EAGLContext * eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

      eaglContext.multiThreaded = YES;

      CIContext *context = [CIContext contextWithEAGLContext:eaglContext];

      [EAGLContext setCurrentContext:eaglContext];

      CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];

      UIImage *endImg = [UIImage imageWithCGImage:ref];

      _imageView.image = endImg;

      CGImageRelease(ref);//非OC对象需要手动内存释放

      在如上代码中的CGImageRef类型变量非OC对象,其需要手动执行释放操作CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意。

      五、地图类处理

      若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。

      Ios代码

      - (void)clearMapView{

      self.mapView = nil;

      self.mapView.delegate =nil;

      self.mapView.showsUserLocation = NO;

      [self.mapView removeAnnotations:self.annotations];

      [self.mapView removeOverlays:self.overlays];

      [self.mapView setCompassImage:nil];

      }

      六、大次数循环内存暴涨问题

      记得有道比较经典的面试题,查看如下代码有何问题:

      Ios代码

      for (int i = 0; i < 100000; i++) {

      NSString *string = @"Abc";

      string = [string lowercaseString];

      string = [string stringByAppendingString:@"xyz"];

      NSLog(@"%@", string);

      }

      该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

      Ios代码

      for (int i = 0; i < 100000; i++) {

      @autoreleasepool {

      NSString *string = @"Abc";

      string = [string lowercaseString];

      string = [string stringByAppendingString:@"xyz"];

      NSLog(@"%@", string);

      }

      }

      若对autoReleasePool陌生,可查阅相关资料,毕竟不是一两句即可说明的。

      结束语:本篇文章结束,对于iOS基础的开发者一定学到了很多东西,关于更多的知识,欢迎随时登录课课家教育!

ios 更多推荐

课课家教育

未登录