iOS 的内存管理

788 阅读16分钟

MRC

一个OC对象, 比如创建 cat 对象,它的引用计数默认 = 1,调用 [cat retain] 引用计数 + 1, [cat release] 引用计数 - 1。如果最后 cat 对象的引用计数 = 0,那么对象会被销毁,释放占用的内存空间。但是如果一个对象由于没有在合适的时候调用 release 操作,那么它就会在内存中一直待着造成内存泄漏。

(对象的引用计数存放在自己的isa指针里面,如果存放满了,会额外有一个SideTable存放)

那么如何安全的使用OC对象,保证不会发生内存泄露问题?由于现在开发默认是 ARC 模式,所以通常情况我们开发的时候是不用考虑对象的 retain 和 release 操作的,编译器 LLVM + Runtime 已经默认帮我们做了这些操作(会在合适的位置插入 retain 和 release)。但是如果是使用 MRC 模式则需要自己考虑对象的生命周期了。

为了测试 MRC 的开发环境,如下图关闭 ARC 模式:

下面是一个简单的测试代码

Cat 类:

@interface Cat : NSObject

@end

#import "Cat.h"

@implementation Cat

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [super dealloc];
}

@end

Person 类:

@interface Person : NSObject
{
    Cat *_cat;
}

- (void)setCat:(Cat *)cat;
- (Cat *)cat;

@end

#import "Person.h"

@implementation Person

- (void)setCat:(Cat *)cat {
    // 判断对象不相等
    if (_cat != cat) {
        // 释放老的,之前引用的 cat 引用计数 - 1
        [_cat release];
        
        // 引用新的,cat 引用计数 + 1
        _cat = [cat retain];
    }
}

- (Cat *)cat {
    return _cat;
}

- (void)dealloc
{
    // 先释放自己成员变量
    [_cat release];
    _cat = nil;

    NSLog(@"%s", __func__);
    
    // 后调用父类的
    [super dealloc];
}

@end

在上面的代码中,person 对象拥有一个 cat 对象。

测试:

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Cat.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init]; // person 引用计数 +1,person.retainCount = 1
        Cat *cat = [[Cat alloc] init]; // cat 引用计数 +1,结果 = 1, cat.retainCount = 1
        
        [person setCat:cat]; // cat 引用计数 +1,cat.retainCount = 2
        
        [person release]; // person 调用 release,person.retainCount = 0, cat.retainCount = 1
        [cat release]; // cat 调用 release,引用计数 -1,cat.retainCount = 0
    }
    return 0;
}

运行输出:

2021-02-08 15:04:31.353056+0800 MRC[19798:2222965] -[Person dealloc]
2021-02-08 15:04:31.353323+0800 MRC[19798:2222965] -[Cat dealloc]
Program ended with exit code: 0

可以看到如果是使用 MRC 模式,自己在合适的位置插入 retain 和 release 操作也是能管理对象的生命周期的。

对象添加到 autorelease

在上面的代码中,我们写了这样的代码:

Person *person = [[Person alloc] init];
[person release];

其实可以写成这样:

// 把对象添加到自动释放池,让自动释放池在合适的是否释放添加的对象
Person *person = [[[Person alloc] init] autorelease];

所以结果可以是这样:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[[Person alloc] init] autorelease];
        Cat *cat = [[[Cat alloc] init] autorelease];
        
        [person setCat:cat];
    }
    return 0;
}

使用属性 property 简化代码

在上面的例子代码中,为 person 对象 添加了一个 cat 对象作为成员变量,然后还自己实现了 cat 对象的 get 和 set 方法。

使用属性修改 Person 类,结果如下:

@interface Person : NSObject

@property (nonatomic, strong) Cat *cat;

@end

@implementation Person

- (void)dealloc
{
    // 先释放自己成员变量
    [_cat release];
    _cat = nil;

    NSLog(@"%s", __func__);
    
    // 后调用父类的
    [super dealloc];
}

@end

可以看到我们去掉了成员变量和它的get和 set方法,也就不用在它的 set 方法里面自己添加 release 和 retain 操作了。这是因为使用 @property 声明属性以后,编译器为我们做了这些操作,编译器为我们实现的代码和我们自己添加的set 和 get 里面的实现是大致一样的。

总结

  • 对象创建后的默认引用计数 = 1;
  • 使用 alloc, new, copy, mutableCopy 方法返回的对象,引用计数会 +1;
  • 对象的引用计数-1操作可以使用 release 或者使用 autorelease。

Copy

为什么要使用 copy 拷贝操作?当然是为了创建一个和原有对象互不影响的对象来使用。

其中:

  • copy 方法返回一个不可变的副本;
  • mutableCopy 方法返回一个可变的副本(可修改内容)。

下面是初始对象为 NSString 的测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 创建一个不可变字符串对象
        NSString *obj1 = @"abc";
        
        // 由于 obj1 是一个不可变的对象,而 NSString *obj2 也是一个不可变的对象,
        // 所以可以直接指向同一快内存,这样也不会互相影响,只需要创建一个新的 obj2 指针即可(节约内存)
        NSString *obj2 = [obj1 copy];
        
        // 由于 obj1 是一个不可变的对象,而 NSMutableString *obj3 是一个可变的对象,
        // 谁也不知道 obj3 对象以后的内存内容是否会修改,所以需要创建一块新的内存来保存 obj3
        NSMutableString *obj3 = [obj1 mutableCopy];
        
        [obj3 appendString:@"xyz"];
        
        NSLog(@"地址: \nobj1 = %p, \nobj2 = %p, \nobj3 = %p", obj1, obj2, obj3);
        NSLog(@"内容: \nobj1 = %@, \nobj2 = %@, \nobj3 = %@", obj1, obj2, obj3);
    }
    return 0;
}

可以看到输出:

2021-02-08 15:55:11.835020+0800 Copy[28818:2276669] 地址: 
obj1 = 0x100004018, 
obj2 = 0x100004018, 
obj3 = 0x108125a50
2021-02-08 15:55:11.835317+0800 Copy[28818:2276669] 内容: 
obj1 = abc, 
obj2 = abc, 
obj3 = abcxyz
Program ended with exit code: 0

其中 obj1 对象和 obj2 指向同一个内存地址,它们都是不可变的字符串对象。obj3 指向了一个新的内存地址。

这符合上面说的,执行 copy 操作创建的对象都是互相不会影响的。

下面是初始对象为 NSMutableString 的测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建一个可变字符串对象
        NSMutableString *obj1 = [[NSMutableString alloc] initWithString:@"abc"];
        
        // 由于 obj1 是一个可变的对象,而 NSString *obj2 是一个不可变的对象,
        // 所以为了保证 obj1 的修改不会影响到 obj2对象,obj2 需要一个新内存,也就是需要创建一个新对象
        NSString *obj2 = [obj1 copy];
        
        // obj1 可变的对象,*obj3 也是是一个可变的对象,
        // 为了互相的修改不影响对象,创建新内存
        NSMutableString *obj3 = [obj1 mutableCopy];
        
        [obj3 appendString:@"xyz"];
        
        NSLog(@"地址: \nobj1 = %p, \nobj2 = %p, \nobj3 = %p", obj1, obj2, obj3);
        NSLog(@"内容: \nobj1 = %@, \nobj2 = %@, \nobj3 = %@", obj1, obj2, obj3);
    }
    return 0;
}

输出:

2021-02-08 15:58:20.795606+0800 Copy[28947:2279634] 地址: 
obj1 = 0x107b0d1b0, 
obj2 = 0xa950e25f2b193af1, 
obj3 = 0x107b0d200
2021-02-08 15:58:20.795901+0800 Copy[28947:2279634] 内容: 
obj1 = abc, 
obj2 = abc, 
obj3 = abcxyz
Program ended with exit code: 0

其中 obj1、 obj2、obj3 指向不同的内存地址,它们的修改都互不影响。

如果测试 NSArray NSMutableArray 和 NSDictionary NSMutableDictionary 的copy操作结果,可以看到也是有类似的结果。

日常说的浅拷贝就是指针拷贝,深拷贝就是堆内存拷贝。

下面是常用 OC 可变和不可变对象的拷贝归纳表:

copymutableCopy
NSString得到 NSString / 浅拷贝得到 NSMutableString / 深拷贝
NSMutableString得到 NSString / 深拷贝得到 NSMutableString / 深拷贝
NSArray得到 NSArray / 浅拷贝得到 NSMutableArray / 深拷贝
NSMutableArray得到 NSArray / 深拷贝得到 NSMutableArray / 深拷贝
NSDictionary得到 NSDictionary / 浅拷贝得到 NSMutableDictionary / 深拷贝
NSMutableDictionary得到 NSDictionary / 深拷贝得到 NSMutableDictionary / 深拷贝

copy 修饰属性

如下所示:

@interface Person : NSObject

@property (nonatomic, copy) NSArray *cats;

@end

@implementation Person

// 这个是在 MRC 添加的测试代码。可以不用自己写
- (void)setCats:(NSArray *)cats {
    if (_cats != cats) {
        [_cats release];
        _cats = [cats copy]; // copy 修饰属性编译器做的操作
    }
}

@end

如果属性是 @property (nonatomic, copy) NSArray *cats 这样 copy 修饰的,那么在执行set方法的时候编译器干了如上面的代码所做的事情。它创建的是一个不可变类型的副本。

也就是说 cats 数组如果声明如下:

@interface Person : NSObject

@property (nonatomic, copy) NSMutableArray *cats;

@end

使用的时候:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.cats = [[NSMutableArray array] init];
        [person.cats addObject:[[Cat alloc] init]];
    }
    return 0;
}

程序会崩溃,产生如下错误:

2021-02-08 16:29:00.042251+0800 Copy[32298:2304888] -[__NSArray0 addObject:]: unrecognized selector sent to instance 0x7fff8018ca70
...

原因很简单,copy 修饰属性的话,编译器为我们做的cats属性 的 set方法赋值的时候产生的是一个不可变的副本对象,也就是一个 NSArray 对象。

自己的对象添加 copy 功能

如果要想为自己创建的OC对象也实现 copy 功能,那么需要实现 NSCopying 协议。

(如果不实现NSCopying协议而调用了 copy 方法,那么会有 unrecognized selector 错误抛出)

下面是一个简单例子:

// 实现 NSCopying 协议
@interface Person : NSObject <NSCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

// 实现这个 copy 方法
- (id)copyWithZone:(struct _NSZone *)zone {
    Person *person = [[Person allocWithZone:zone] init];
    person.name = self.name;
    person.age = self.age;
    return  person;
}

- (NSString *)description {
    return [NSString stringWithFormat:@"name = %@, age = %lu", self.name, (unsigned long)self.age];
}

@end

测试:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *tom = [[Person alloc] init];
        tom.name = @"tom";
        tom.age = 12;
        
        NSLog(@"tom info: %p, %@", tom, tom);
        
        Person *jerry = [tom copy];
        jerry.name = @"jerry";
        jerry.age = 9;
        
        NSLog(@"jerry info: %p, %@", jerry, jerry);
    }
    return 0;
}

输出:

2021-02-08 16:42:02.523688+0800 Copy[32959:2317427] tom info: 0x100726bb0, name = tom, age = 12
2021-02-08 16:42:02.523960+0800 Copy[32959:2317427] jerry info: 0x100726c50, name = jerry, age = 9
Program ended with exit code: 0

可以看到此时 tom 和 jerry 两个对象互不影响。

Weak

我们知道weak修饰的对象属性,对象执行set方法的时候引用计数不会+1,而是直接进行内存的指向。当指向对象的引用计数 = 0,进行 dealloc 操作以后会在自己的isa指针里面查看一下是否有weak类型属性指向自己,如果有那么去把weak对象设置为nil。

具体的到 objc 的源代码里面查看一下实现:

具体查看流程如下:

dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance -> ...

下面是一个关键截图 (objc 源代码版本 objc4-781):

找到 dealloc 方法入口:

对象销毁weak表的入口:

找到和对象关联的 SideTable,清空里面的数据,把所有和该对象关联的 weak 对象都设置为nil:

SideTable 的结构:

SideTable 其中:

  • refcnts: 存放对象在isa指针满了放不下的引用计数;

  • weak_tables: 存放所有和该对象关联的 weak 修饰对象。

Autorelease

之前说过在MRC环境下,cat 对象的释放可以调用release 释放:

[cat release];

也可以创建的时候调用 autorelease:

Cat *cat = [[[Cat alloc] init] autorelease];

那么对象调用 autorelease 干了啥呢?

添加测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 记得在设置里面关闭 arc进行测试
        NSObject *obj = [[[NSObject alloc] init] autorelease];
    }
    return 0;
}

使用 clang -rewrite-objc main.m -o main.cpp 指令生成对应的C++代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }
    return 0;
}

可以看到 @autoreleasepool { ... } 包含的代码插入了这样一个东西 __AtAutoreleasePool

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

可以看到 autoreleasepool 开始的时候执行 objc_autoreleasePoolPush, 退出的时候执行 objc_autoreleasePoolPop。

再到 objc 源代码里面查看一下 autoreleasepool 的调用情况,可以看到相关代码:

通过源代码可以看到自动释放池主要的数据结构有:__AtAutoreleasePool, AutoreleasePoolPage。调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理。

其中 AutoreleasePoolPage 的数据结构如下:

class AutoreleasePoolPage : private AutoreleasePoolPageData {
...
...
}

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
	magic_t const magic;
	__unsafe_unretained id *next; // 指向下一个可存储对象地址的内存
	pthread_t const thread;
	AutoreleasePoolPage * const parent; // 上一个 Page
	AutoreleasePoolPage *child; // 下一个 Page
	uint32_t const depth;
	uint32_t hiwat;

	AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
		: magic(), next(_next), thread(_thread),
		  parent(_parent), child(nil),
		  depth(_depth), hiwat(_hiwat)
	{
	}
};

可以看出 AutoreleasePoolPage 是一个双向链表的数据结构。

查看源代码 autoreleasepool 的调用过程大致如下:

  • 调用 push 方法,将一个 POOL_BOUNDARY 入栈,并且返回它存放的内存地址;

  • 添加对象地址到 AutoreleasePoolPage 里面,修改 next 指针的指向下一个可插入的地址,如果当前 Page 存放满了,那么到下一页 Page 储存;

  • 调用 pop 方法。传入之前push操作保存的 POOL_BOUNDARY 地址,然后 release 从当前地址到之前 POOL_BOUNDARY 地址之间保存的对象。

AutoreleasePoolPage 里面可以进行 autoreleasepool 的嵌套。比如:

    @autoreleasepool { // push
        NSObject *obj = [[[NSObject alloc] init] autorelease];
        
        @autoreleasepool { // push
            NSObject *obj1 = [[[NSObject alloc] init] autorelease];
            NSObject *obj2 = [[[NSObject alloc] init] autorelease];
        } // pop
        
    } // pop

它可以在当前的 AutoreleasePoolPage 重复进行 push 操作添加 POOL_BOUNDARY。

Autoreleasepool 和 Runloop 的关系

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%s", __func__);
    
    NSLog(@"--------- 1");
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
    }
    NSLog(@"--------- 2");
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}

@end

输出:

2021-02-08 19:49:13.596164+0800 Runloop[4966:1424777] -[ViewController viewDidLoad]
2021-02-08 19:49:13.596212+0800 Runloop[4966:1424777] --------- 1
2021-02-08 19:49:13.596231+0800 Runloop[4966:1424777] -[Cat dealloc]
2021-02-08 19:49:13.596246+0800 Runloop[4966:1424777] --------- 2
2021-02-08 19:49:13.598306+0800 Runloop[4966:1424777] -[ViewController viewWillAppear:]
2021-02-08 19:49:13.602113+0800 Runloop[4966:1424777] -[ViewController viewDidAppear:]

修改为如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%s", __func__);
    
    NSLog(@"--------- 1");
    Cat *cat = [[Cat alloc] init];
    NSLog(@"--------- 2");
}

输出:

2021-02-08 19:50:00.721015+0800 Runloop[4968:1425283] -[ViewController viewDidLoad]
2021-02-08 19:50:00.721054+0800 Runloop[4968:1425283] --------- 1
2021-02-08 19:50:00.721073+0800 Runloop[4968:1425283] --------- 2
2021-02-08 19:50:00.721086+0800 Runloop[4968:1425283] -[Cat dealloc]
2021-02-08 19:50:00.723261+0800 Runloop[4968:1425283] -[ViewController viewWillAppear:]
2021-02-08 19:50:00.727038+0800 Runloop[4968:1425283] -[ViewController viewDidAppear:]

打断点查看函数调用栈:

autoreleasepool 里面管理的对象会在runloop的运行循环的时候对对象进行 release 释放。

它们的大致关系:

  • iOS在主线程的Runloop中注册了2个Observer;

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();

  • 第2个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop();

  • 简单来说就是app启动的时候添加一个 autoreleasepool 到 runloop,退到后台销毁老的 autoreleasepool,创新新的 autoreleasepool, 杀掉app的时候销毁 autoreleasepool。

补充

Tagged Pointer

为什么要使用 Tagged Pointer?主要是能够用于优化NSNumber、NSDate、NSString等小对象的存储。要想使用一个正常的对象,需要在堆空间给它一个内存区域存数据,然后还需要一个指针保存对象的堆空间地址。如果使用 Tagged Pointer,那么对于一个小对象就能直接在指针里面保存数据和和该对象的类型的标示了,这样就能够不动态分配堆空间,节约内存,提高查找效率了。

查看一个例子:

@interface ViewController ()

@property (nonatomic, copy) NSString *small;
@property (nonatomic, copy) NSString *normal;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // tagged pointer
    self.small = [NSString stringWithFormat:@"abc"];
    NSLog(@"small class type = %@", [self.small class]);
    
    // 正常对象
    self.normal = [NSString stringWithFormat:@"abcabcabcabcabc"];
    NSLog(@"normal class type = %@", [self.normal class]);

    NSLog(@"--- done ---");
}

@end

输出:

2021-02-19 16:58:35.311570+0800 Timer[23580:329038] small class type = NSTaggedPointerString
2021-02-19 16:58:35.311683+0800 Timer[23580:329038] normal class type = __NSCFString
2021-02-19 16:58:35.311766+0800 Timer[23580:329038] --- done---

可以看到当字符串的内容比较少的时候,它会是 NSTaggedPointerString 类型。

一个崩溃的例子:

@interface ViewController ()

@property (nonatomic, copy) NSString *small;
@property (nonatomic, copy) NSString *normal;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // tagged pointer
    self.small = [NSString stringWithFormat:@"abc"];
    NSLog(@"small class type = %@", [self.small class]);
    
    // 正常对象
    self.normal = [NSString stringWithFormat:@"abcabcabcabcabc"];
    NSLog(@"normal class type = %@", [self.normal class]);
    
    // 对象的set方法赋值...多线程调用,(会崩溃)
    for (int i = 0; i < 1000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.normal = [NSString stringWithFormat:@"ccccccccccccccc"];
        });
    }
    
    // tagged pointer 赋值...类似基本数据类型的赋值,不需要对象的 release 和 retain(不会崩溃)
    for (int i = 0; i < 1000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.small = [NSString stringWithFormat:@"123"];
        });
    }
    NSLog(@"--- done ---");
}

@end

奔溃截图:

例子中为什么会崩溃?这是因为一个正常的对象的set方法会先释放老的对象,再引用新的对象。而多线程会导致线程安全问题,导致可能释放对象的时候它已经不存在了。

解决办法有:对象声明为 atomic 属性,或者加锁处理保证线程安全。

NSTimer CADisplayLink 的使用注意

一个 NSTimer 会导致引用循环的例子:

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    
    [self.timer invalidate];
}

@end

如果此时是在 UINavigationController 的层级中测试运行,可以看到 dealloc 方法不会调用,所以 ViewController 也就不会释放。这是因为 ViewController 和 NSTimer 互相强引用导致的。那么把 ViewController 声明为 weak 传入 NSTimer 初始化方法中能解决强引用问题吗?答案是不能,因为 NSTimer 的 timerWithTimeInterval 初始化方法里面还是会强引用 ViewController。

引用关系图:

解决 NSTimer 引用循环的方式 1:

使用 block 形式的初始化方法:

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:true block:^(NSTimer * _Nonnull timer) {        [weakSelf timerRun];
    }];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    
    [self.timer invalidate];
}

@end

运行测试可以看到 dealloc 方法可以正常调用。它们的引用关系是这样的:

不会导致引用循环。

解决 NSTimer 引用循环的方式 2:

从上面的引用循环关系图可知只要在 ViewController 和 NSTimer 之间插入第三方添加弱引用就可以打破引用循环,那么完全可以自己自定义一个第三方实现类似功能。下面是一个示例代码:

创新第三方 DemoProxy 类(继承 NSObject):

@interface DemoProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;

// 记得弱引用
@property (nonatomic, weak) id target;

@end

@implementation DemoProxy

+ (instancetype)proxyWithTarget:(id)target {
    DemoProxy *proxy = [[DemoProxy alloc] init];
    proxy.target = target;
    return proxy;
}

// 使用 Runtime 的消息转发直接实现消息的调用
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end

测试代码:

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 使用 DemoProxy 作为中间的弱引用媒介对象
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[DemoProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    
    [self.timer invalidate];
}

@end

此时的关系图:

测试运行也可以看到是能打破引用循环的。

其实 iOS 是有一个默认的 NSProxy 代理类的,它和 NSObject 是同一个类层级。它能够直接实现方法的转发,不用走 Runtime 那一大堆的方法查找。

下面是 DemoProxy 继承 NSProxy 的实现例子:

@interface DemoProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@property (nonatomic, weak) id target;

@end

@implementation DemoProxy

+ (instancetype)proxyWithTarget:(id)target {
    DemoProxy *proxy = [DemoProxy alloc]; // NSProxy 没有 init 方法
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

安全使用 CADisplayLink:

CADisplayLink 是一个搭配 Runloop 使用的计时器,调用的时候和屏幕的刷帧频率一致。参考上面的 NSTimer 使用,可以实现类似的打破强引用的使用方式:

@interface ViewController ()

@property (nonatomic, strong) CADisplayLink *displayLink;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 调用频率和屏幕的刷帧频率一致,60FPS
    self.displayLink = [CADisplayLink displayLinkWithTarget:[DemoProxy proxyWithTarget:self] selector:@selector(timerRun)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    
    [self.displayLink invalidate];
}

@end

一个需要注意的是 NSTimer 和 CADisplayLink 的调用需要 Runloop 的配合使用,但是如果 Runloop 里面的任务过于繁重可能会导致它们的调用时间不精确,要想使用更加精确的定时器功能可以查看使用 GCD 的 DISPATCH_SOURCE_TYPE_TIMER,GCD 的 定时器没有依赖 Runloop。