深入分析iOS问题-performSelector:onThread:withObject:waitUntilDone

今天检查内存泄露的问题,发现误用系统API的问题。导致内存泄露。这个问题还是比较常见的,我觉得还是记下了,分享给大家

现场还原

–FYSingleInstance类主要代码

// 初始化上下文
+ (FYSingleInstance *) S {
    static FYSingleInstance * instance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!instance) {
            instance = [[FYSingleInstance alloc] init];
        }
    });

    return instance;
}


/* * 停止线程 */
+ (void) stopThread:(id) thread withUserId:(id) userId {
    // 停止初始化线程
    @try {
        [[FYSingleInstance S] performSelector:@selector(stop:) onThread:thread withObject:userId waitUntilDone:NO];
    }
    @catch (NSException * exception) {
        NSLog(@"%@", exception.callStackSymbols);
    }
}

–使用代码用例

[FYSingleInstance stopThread:_currentThread withUserId:nil];

内存泄露现象

如果不断调用上述代码,就看到不断有内存泄露。

请看下图:

深入分析iOS问题-performSelector:onThread:withObject:waitUntilDone_第1张图片

抽丝剥茧开始

好奇怪为啥这么多16 bytes在内存中,而且使用代码用例调用越多,这些16bytes的内存越多。

内存泄露就是这个吧。

查找原因

performSelector:onThread:withObject:waitUntilDone这个执行原理.
直接上源码

- (void) performSelector: (SEL)aSelector
                onThread: (NSThread*)aThread
              withObject: (id)anObject
           waitUntilDone: (BOOL)aFlag
                   modes: (NSArray*)anArray
{
  GSRunLoopThreadInfo   *info;
  NSThread          *t;

  if ([anArray count] == 0)
  {
      return;
  }

  t = GSCurrentThread();
  if (aThread == nil)
  {
      aThread = t;
  }
  info = GSRunLoopInfoForThread(aThread);
  if (t == aThread)
  {
      /* Perform in current thread. */
      if (aFlag == YES || info->loop == nil)
      {
          /* Wait until done or no run loop. */
         [self performSelector: aSelector withObject: anObject];
      }
      else
      {
          /* Don't wait ... schedule operation in run loop. */
      [info->loop performSelector: aSelector
                               target: self
                             argument: anObject
                                order: 0
                                modes: anArray];
      }
  }
  else
  {
      GSPerformHolder   *h;
      NSConditionLock   *l = nil;

      if ([aThread isFinished] == YES)
      {
          [NSException raise: NSInternalInconsistencyException
                      format: @"perform on finished thread"];
      }
      if (aFlag == YES)
      {
         l = [[NSConditionLock alloc] init];
      }

      h = [GSPerformHolder newForReceiver: self
                 argument: anObject
                 selector: aSelector
                    modes: anArray
                     lock: l];
      [info addPerformer: h];
      if (l != nil)
      {
          [l lockWhenCondition: 1];
          [l unlock];
          RELEASE(l);
          if ([h isInvalidated] == YES)
          {
              RELEASE(h);
              [NSException raise: NSInternalInconsistencyException
                          format: @"perform on finished thread"];
          }
          /* If we have an exception passed back from the remote thread, * re-raise it. */
          if (nil != h->exception)
          {
              NSException       *e = AUTORELEASE(RETAIN(h->exception));

              RELEASE(h);
              [e raise];
          }
      }
      RELEASE(h);
    }
}

重点关注下面代码

      h = [GSPerformHolder newForReceiver: self
                 argument: anObject
                 selector: aSelector
                    modes: anArray
                     lock: l];
      [info addPerformer: h];

内部实现是创建了一个对象,并且把这个对象放到runloopinfo的数组中,创建这个对象的成员变量就是之前传入的参数。

泄露原因

对于iOS,线程驱动都是通过runloop驱动的。当runloop运行时,都是运行该线程空间中的runloop任务,也就是源码中的runloopinfo数组中的内容。当一个任务完成后,该任务对象就会释放了,对应的内存也会释放。如果线程停止了,runloop就停止工作,那么还是可以通过之前说的函数给runloopinfo数组加入任务。但是由于runloop不在运行,所以任务对象不会被处理,那么也不会从runloopinfo数组中删除,对应的内存也不会被释放。

结合今天遇到的问题,进行测试,调用该代码对应的stop函数不会被调用到,说该线程停止运行了。所以runloopinfo数组中任务不会被处理,这样就出现的不断内存增长。

大胆推测,如果[FYSingleInstance stopThread:_currentThread withUserId:nil];这里WithUserId参数传入对象,那么该对象不会被被释放,因为它被任务对象持有了内存。

马上实现。

调用代码修改为:

    FYTempData* tmpData = [[FYTempData alloc] init];
    tmpData.log = @"lalallalalalla";
    [FYSingleInstance stopThread:_currentThread withUserId:tmpData];

FYTempData实现具体为

@implementation FYTempData
{
    char* buffer;
}

- (void)dealloc
{
    free(buffer);
    NSLog(@"dealloc --- FYTempData");
}


-(id)init
{
    self = [super init];

    if (self) {
        buffer = (char*)malloc(sizeof(char) * 1024*1024);
    }

    return self;
}
@end

运行现象:每一次1M的内存泄露

深入分析iOS问题-performSelector:onThread:withObject:waitUntilDone_第2张图片


内存,不断上升




每次调用,就有1M内存,调用了16次就有1M内存

使用总结

  • 对于今天case,最快的修改就是(也许大家有很多从设计角度解决的方案)
+ (void) stopThread:(id) thread withUserId:(id) userId {
    // 停止初始化线程
    @try {
        NSThread* oneThread = thread;

        if (![oneThread isFinished] && ![oneThread isCancelled]) {
            [[FYSingleInstance S] performSelector:@selector(stop:) onThread:thread withObject:userId waitUntilDone:NO];
        }
    }
    @catch (NSException * exception) {
        NSLog(@"%@", exception.callStackSymbols);
    }
}
  • 对于传入参数

    • 如果系统类型,比如array,dictionary
      请使用 []NSArray arrayWithObjects:xxx],等方法创建一个有autorelease属性的对象。
    • 如果是自定义的类,希望深拷贝一个新的对象,当参数传入。
  • 对于NSThread使用,如果要加入新的任务,请要确定,该Thread还在运行中,runloop还在执行。因为通过实现,及时把把thread实例设置为nil,内部的内存还是不会释放。

By the way

欢迎大家一起讨论

转发请注明出处,谢谢

你可能感兴趣的:(ios,内存泄露)