iOS循环引用

3 阅读3分钟

循环引用(Retain Cycle)是iOS开发中一个非常关键且容易出现的内存泄漏问题。以下是一些最常见的场景:

  1. Block捕获外部对象

    • 场景:在对象内部定义一个Block,并在Block内部使用了该对象的属性或方法。

    • 原因:如果对象强引用了这个Block(例如将Block赋值给对象的属性),而Block又强引用了对象自身(通过self),就会形成循环引用。

    • 示例

      @interface MyClass : NSObject
      @property (nonatomic, strong) NSString *name;
      @end
      
      @implementation MyClass
      
      - (void)doSomething {
          // 这里的self被block捕获,形成了循环引用
          void (^myBlock)(void) = ^{
              NSLog(@"Name is: %@", self.name); // block强引用了self
          };
      
          // 如果将block赋值给self的属性(强引用)
          self.block = myBlock; // self -> block -> self (循环引用!)
      }
      
      @end
      
    • 解决方法:使用__weak修饰符捕获self,避免强引用。

      - (void)doSomething {
          __weak typeof(self) weakSelf = self; // 使用weakSelf
          void (^myBlock)(void) = ^{
              // block内使用weakSelf代替self
              NSLog(@"Name is: %@", weakSelf.name);
          };
          self.block = myBlock;
      }
      
  2. Delegate模式

    • 场景:A对象持有B对象的引用(强引用),而B对象又持有A对象的引用(通常是delegate属性)。

    • 原因:如果A和B都使用强引用对方,则形成循环引用。

    • 示例

      @interface ViewController : UIViewController
      @property (nonatomic, strong) UITableView *tableView;
      @end
      
      @interface CustomTableViewDataSource : NSObject <UITableViewDataSource>
      @property (nonatomic, weak) ViewController *viewController; // 使用weak避免循环引用
      @end
      
      @implementation ViewController
      
      - (void)viewDidLoad {
          [super viewDidLoad];
          self.tableView.dataSource = [[CustomTableViewDataSource alloc] init];
          // 如果CustomTableViewDataSource的属性是strong的,且指向self,则可能产生循环引用
          // 但通常dataSource使用weak,所以这里不会直接造成循环引用
      }
      
      @end
      
    • 解决方法:确保Delegate属性使用weak修饰符,这是约定俗成的做法。如上例所示,viewController属性应该声明为weak

  3. 定时器(NSTimer)

    • 场景:对象A创建了一个NSTimer,并将timer的target设置为自身(self)。

    • 原因:如果timer的target是self,那么timer会强引用self;同时,如果timer被添加到RunLoop中,RunLoop也会强引用timer。如果对象A也强引用了timer,就会形成循环引用。

    • 示例

      @interface MyClass : NSObject
      @property (nonatomic, strong) NSTimer *timer;
      @end
      
      @implementation MyClass
      
      - (void)startTimer {
          // 这里将timer的target设置为自己,容易形成循环引用
          self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
          // self -> timer -> self (循环引用!)
      }
      
      - (void)timerFired:(NSTimer *)timer {
          // ...
      }
      
      - (void)dealloc {
          // 必须在dealloc中invalidate timer,否则可能在timer fire时访问已释放的对象
          [self.timer invalidate];
          self.timer = nil;
      }
      
      @end
      
    • 解决方法

      • 使用weak修饰符(如iOS 10+的NSTimer支持weak)。
      • 或者将timer的target设置为一个不持有timer的中间对象。
      • 更重要的是,在对象销毁前调用[timer invalidate]
  4. 自定义的强引用关系

    • 场景:开发者在代码中手动建立了对象之间的强引用关系。

    • 原因:如果A强引用B,B强引用A,就会形成循环引用。

    • 示例

      @interface Parent : NSObject
      @property (nonatomic, strong) Child *child;
      @end
      
      @interface Child : NSObject
      @property (nonatomic, strong) Parent *parent;
      @end
      
      @implementation Parent
      
      - (void)addChild {
          self.child = [[Child alloc] init];
          self.child.parent = self; // 造成循环引用
      }
      
      @end
      
    • 解决方法:通常在这种情况下,其中一个方向应该使用weak引用。例如,Childparent属性应声明为weak

总结来说,循环引用的核心在于两个或多个对象之间存在相互的强引用关系。解决的关键是打破这种强引用链,通常使用weakunowned来避免强引用,特别是在Block、Delegate、定时器等场景中。同时,遵循良好的编程习惯,如及时清理资源(如移除KVO观察者、销毁定时器等),也是防止循环引用的重要手段。