iOS-MRC

MRC初学

  • 前引
    • 关于引用计数:
    • 内存管理的思考方式:
  • 正文
    • dealloc
    • 野指针和空指针
      • release自己不持有的对象并没有导致崩溃
    • @property参数
    • autoreleasepool
      • 使用 autorelease 有什么好处呢?
      • autorelease 的原理实质上是什么?
      • autorelease 的创建方法
        • 使用 NSAutoreleasePool 创建
        • 使用 @autoreleasepool 创建
      • autorelease 的使用方法
      • autorelease 的注意事项
      • 自动释放池的嵌套使用
      • autorelease 的注意事项
    • MRC 中避免循环引用
    • 多个对象内存管理思想
      • 玩家没有使用过房间
      • 一个玩家使用一个游戏房间的情况
      • 一个玩家使用一个房间 r 后,换到另一个房间 r2 的情况
      • 一个玩家使用一个房间,不再使用房间,将房间释放掉之后,再次使用该房间的情况
    • 循环引用
      • 自循环引用
      • 相互循环引用
      • 多循环引用
      • 常见循环引用问题及其解决方法
        • delegate
          • 解决方法
        • block
          • 解决方法
          • 解决方法

MRC的意思是手动管理内存(Manual Reference Counting)。

前引

关于引用计数:

OC 语言使用引用计数来管理内存每个对象都有个可以递增或递减的计数器,如果想某个对象继续存活,那就递增其引用计数,用完之后,就递减其计数,计数变为0,就销毁。

系统根据引用计数器的机制来判断对象是否需要被回收。在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。

内存管理的思考方式:

iOS-MRC_第1张图片

正文

dealloc

  • 当一个对象的引用计数器值为 0 时,这个对象即将被销毁,其占用的内存被系统回收。
  • 对象即将被销毁时系统会自动给对象发送一条 dealloc 消息(因此,从 dealloc 方法有没有被调用,就可以判断出对象是否被销毁)

dealloc 使用注意:
不能直接调用 dealloc 方法。
一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)。

使用一般来说我们不可以直接调用dealloc方法,但是我们可以重写dealloc方法,在其中释放我们自己添加的监听等无法通过ARC来释放的内容。

关于重写dealloc

在MRC中,需要注意:

一般会重写 dealloc 方法,在这里释放相关资源,dealloc 就是对象的遗言
一旦重写了 dealloc 方法,就必须调用 [super dealloc],并且放在最后面调用。

- (void)dealloc {
    NSLog(@"Person dealloc");
    // 注意:super dealloc 一定要写到所有代码的最后面
    [super dealloc]; 
}

野指针和空指针

只要一个对象被释放了,我们就称这个对象为「僵尸对象(不能再使用的对象)」。关于_NSZombie

当一个指针指向一个僵尸对象(不能再使用的对象),我们就称这个指针为「野指针」。

只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 执行完引用计数为 1。
 
        [p release]; // 执行完引用计数为 0,实例对象被释放。
        [p release]; // 此时,p 就变成了野指针,再给野指针 p 发送消息就会报错。
        [p release]; // 报错
    }
    return 0;
}

为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 执行完引用计数为 1。
 
        [p release]; // 执行完引用计数为 0,实例对象被释放。
        p = nil; // 此时,p 变为了空指针。
        [p release]; // 再给空指针 p 发送消息就不会报错了。
        [p release];
    }
    return 0;
}

release自己不持有的对象并没有导致崩溃

iOS-MRC_第2张图片

在这里插入代码片int main(int argc, const char * argv[]) {
    @autoreleasepool {
       	NSObject* obj = [[NSObject alloc] init];
    	[obj release]; 
   		[obj release];   
    }
    return 0;
}

例如像如上的代码,可能会出现不报错的情况。iOS-MRC_第3张图片
当你添加了一句NSLog之后,却又报错了。。。。。

并不是加了一句NSLog之后就一定会造成程序crash的,如果那句新加的NSLog没有占用原来obj的内存,那下一句NSLog依旧能够响应发送给obj的消息,结果会类似第一种代码所产生的结果。

所以说,两种情况都是有可能发生的,至于到底发生哪种情况,完全取决于何时系统会清理掉obj占用的内存,也可以说取决于“运气”,因为这个时间是不确定的。

还有如果给其一个自动释放池的销毁那加上断点 其输出的结果可能不同应该也是这个原因

https://blog.csdn.net/m0_46110288/article/details/117995287

@property参数

在成员变量前加上 @property,系统就会自动帮我们生成基本的 setter / getter 方法,但是不会生成内存管理相关的代码。

@property (nonatomic) int val;

同样如果在 property 后边加上 assign,系统也不会帮我们生成 setter 方法内存管理的代码,仅仅只会生成普通的 getter / setter 方法,默认什么都不写就是 assign。

@property(nonatomic, assign) int val;
/* = 
@property(nonatomic) int val;
*/

如果在 property 后边加上 retain,系统就会自动帮我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法。

@property(nonatomic, retain) Room *room;

autoreleasepool

autorelease 是一种支持引用计数的内存管理方式,只要给对象发送一条 autorelease 消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的「所有对象」做一次 release 操作。

注意:这里只是发送 release 消息,如果当时的引用计数(reference-counted)依然不为 0,则该对象依然不会被释放。

autorelease 方法会返回对象本身,且调用完 autorelease 方法后,对象的计数器不变。

Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1

使用 autorelease 有什么好处呢?

  1. 不用再关心对象释放的时间
  2. 不用再关心什么时候调用release

autorelease 的原理实质上是什么?

autorelease 实际上只是把对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,当该 pool 被释放时,该 pool 中的所有对象会被调用 release 方法。

autorelease 的创建方法

使用 NSAutoreleasePool 创建

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池

使用 @autoreleasepool 创建

@autoreleasepool
{ // 开始代表创建自动释放池
 
} // 结束代表销毁自动释放池

autorelease 的使用方法

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
在这里插入代码片@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)

autorelease 的注意事项

并不是放到自动释放池代码中,都会自动加入到自动释放池

@autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
}

在自动释放池的外部发送 autorelease 不会被加入到自动释放池中

autorelease 是一个方法,只有在自动释放池中调用才有效。

@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
 
// 正确写法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }
 
// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}

自动释放池的嵌套使用

  • 自动释放池是以栈的形式存在。
  • 由于栈只有一个入口,所以调用 autorelease 会将对象放到栈顶的自动释放池。

栈顶就是离调用 autorelease 方法最近的自动释放池。

@autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}
  • 自动释放池中不适宜放占用内存比较大的对象。

尽量避免对大内存使用该方法,对于这种延迟释放机制,还是尽量少用。
不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升。

// 内存暴涨
@autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
        Person *p = [[[Person alloc] init] autorelease];
    }
}
// 内存不会暴涨
for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
    }
}

autorelease 的注意事项

  1. 不要连续调用 autorelease。
@autoreleasepool {
 // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
  1. 调用 autorelease 后又调用 release(错误)。
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
}

MRC 中避免循环引用

定义两个类 Person 类和 Dog 类

Person 类:

#import <Foundation/Foundation.h>
@class Dog;
 
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end

Dog 类:

#import <Foundation/Foundation.h>
@class Person;
 
@interface Dog : NSObject
@property(nonatomic, retain)Person *owner;
@end

执行以下代码:

int main(int argc, const char * argv[]) {
    Person *p = [Person new];
    Dog *d = [Dog new];
 
    p.dog = d; // retain
    d.owner = p; // retain  assign
 
    [p release];
    [d release];
 
    return 0;
}

就会出现 A 对象要拥有 B 对象,而 B 对应又要拥有 A 对象,此时会形成循环 retain,导致 A 对象和 B 对象永远无法释放。

那么如何解决这个问题呢?

不要让 A retain B,B retain A。
让其中一方不要做 retain 操作即可。
当两端互相引用时,应该一端用 retain,一端用 assign。

多个对象内存管理思想

多个对象之间往往是通过setter方法产生联系的,其内存管理的方法也是通过setter、delloc方法实现管理的。

我们可以举一个例子来帮助我们理解这个过程:
比如qq麻将,我们打麻将需要三个人和一个房间,所以我们可以定义房间为Room类对象,然后定义玩家为Person类对象,玩家对象拥有 _room 作为成员变量。
一个玩家对象,如果想要玩游戏,就要持有一个房间对象,并保证在使用房间期间,这个房间对象一直存在,并且在游戏房间没人的时候,还需要将这个房间对象释放。

那么房间具体的引用情况有哪些呢:

  • 只要一个玩家想使用房间(进入房间),就需要对这个游戏房间的引用计数器 +1。
  • 只要一个玩家不想再使用房间(离开房间),就需要对这个游戏房间的引用计数器 -1。
  • 只要还有至少一个玩家在用某个房间,那么这个游戏房间就不会被回收,引用计数至少为 1。
  • 只要没有玩家在房子里了,那么这个房间就会被回收

iOS-MRC_第4张图片

我们可以看到玩家三个玩家对象都持有房间对象,所以房间对象的引用为3。

我们将刚刚说的两个类对的代码写出来:
Room类:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Room : NSObject
@property int number;
@end

NS_ASSUME_NONNULL_END

Person类:

#import <Foundation/Foundation.h>
#import "Room.h"
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
    Room *_room;
}

- (void)setRoom:(Room *)room;
- (Room *)room;
@end

NS_ASSUME_NONNULL_END

玩家没有使用过房间

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    	//1.创建两个对象
        Room *r = [[Room alloc] init];
        Person *p = [[Person alloc] init];
        //给房间号赋值
        r.number = 808;
        //释放两个对象
        [r release];
        [p release];
    }
    return 0;
}

我们可以看到在上述代码中Person类创建的对象没有对房间进行持有,也就是玩家虽然创建出来了但是却没有使用过房间,上述代码运行时内存使用情况如图所示:

在这里复习两个知识点:

栈:存放基本类型 的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】
堆:存放使用new创建的对象,全局变量

我们在来看上面的例子,通过上图可以发现Room 实例对象和 Person 实例对象之间没有相互联系,所以各自释放不会报错。等两个对象释放以后,内存的情况如图所示:

最后由于引用计数变为0了,各自实例对象的内存就会被系统回收。

一个玩家使用一个游戏房间的情况

在调用 setter 方法的时候,因为 Room 实例对象多了一个 Person 对象引用,所以应将 Room 实例对象的引用计数 +1 才对,即 setter 方法应该像下边一样,对 room 进行一次 retain 操作。

- (void)setRoom:(Room *)room { // 调用 room = r;
    // 对房间的引用计数器 +1
    [room retain];
    _room = room;
}

然后我们在main函数里完成一下一个玩家使用一个游戏房间的情况:

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Room *r = [[Room alloc] init];
        Person *p = [[Person alloc] init];
        r.number = 808;
        // 将房间赋值给玩家,表示玩家在使用房间
        // 玩家需要使用这间房,只要玩家在,房间就一定要在
        p.room = r;// [p setRoom:r]
        
        [r release];
        // 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
        [p release];
    }
    return 0;
}

此时我们的内存分配情况就应该为:

其实还是很好理解的,我们主要来理解一下引用计数这部分,Room创建实例对象引用计数➕1,然后Person创建实例对象Person的引用计数也➕1同时Person通过setter方法对Room实例对象进行了持有,所以此时Room的引用计数再➕1变为了2。

然后我们看,Room的实例对象释放了对应的Room的引用计数就要➖1,此时内存的分配情况为:

然后执行代码 [p release];,释放Person实例对象。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在 delloc 里边对房间再进行一次 release 操作。

这样对房间对象来说,每一次 retain / alloc 操作都对应一次 release 操作。

- (void)dealloc {
    // 人释放了, 那么房间也需要释放
    [_room release];
    NSLog(@"%s", __func__);
 
    [super dealloc];
}

最终内存情况变为了:

一个玩家使用一个房间 r 后,换到另一个房间 r2 的情况

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Room *r = [[Room alloc] init];
        Person *p = [[Person alloc] init];
        r.number = 808;
        // 将房间赋值给玩家,表示玩家在使用房间
        // 玩家需要使用这间房,只要玩家在,房间就一定要在
        p.room = r;
        
        [r release];
        Room *r2 = [[Room alloc] init];
        r2.number = 404;
        p.room = r2;
        [r2 release];    // 释放房间 r2
        
        // 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
        [p release];
    }
    return 0;
}

在第一个Room实例对象释放后,内存情况为:

接着我们进行了第二个房间的创建以及Person实例对象通过setter方法持有第二个Room实例对象。此时我们的内存情况变为了:

在我们执行完所有代码,我们可以发现内存情况变为了:

此时为什么r还持有Room的实例对象呢,原因其实很简单,我们调用了两次Person的setter方法但是只delloc了一次,问题出在哪呢?当r释放的时候,我们的p并没有释放,所以不会调用delloc方法,所以就造成了上述结果,那我们应该怎么办呢?我们可以在调用 setter 方法的时候,对之前的变量进行一次 release 操作。具体 setter 方法代码如下:

- (void)setRoom:(Room *)room { // room = r
        // 将以前的房间释放掉 -1
        [_room release];
 
        // 对房间的引用计数器 +1
        [room retain];
 
        _room = room;
    }
}

这样我们在第二次调用setter方法的时候会先将之前通过setter方法增加的引用计数减掉,就不会出现刚刚那种情况了。

所以内存情况就变为了:

一个玩家使用一个房间,不再使用房间,将房间释放掉之后,再次使用该房间的情况

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 创建两个对象
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.number = 808;
 
        // 2. 将房间 r 赋值给玩家 p
        p.room = r; // [p setRoom:r]
        [r release];    // 释放房间 r
 
        // 3. 再次使用房间 r
        p.room = r;
        [r release];    // 释放房间 r
        [p release];    // 释放玩家 p
    }
    return 0;
}

执行完以下代码:

// 1.创建两个对象
Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.number = 808;
 
// 2.将房间赋值给人
p.room = r; // [p setRoom:r]
[r release];    // 释放房间 r

内存情况为:

然后再执行 p.room = r;,因为 setter 方法会将之前的 Room 实例对象先释放掉,所以此时内存表现为:

此时 _room、r 已经变成了一个野指针。之后再对野指针 r 发出 retain 消息,程序就会崩溃。所以我们在进行 setter 方法的时候,要先判断一下是否是重复赋值,如果是同一个实例对象,就不需要重复进行 release 和 retain。换句话说,如果我们使用的还是之前的房间,那换房的时候就不需要对这个房间再进行 release 和 retain。则 setter 方法具体代码如下:

- (void)setRoom:(Room *)room { // room = r
    // 只有房间不同才需用 release 和 retain
    if (_room != room) {    // 0ffe1 != 0ffe1
        // 将以前的房间释放掉 -1
        [_room release];
 
        // 对房间的引用计数器+1
        [room retain];
 
        _room = room;
    }
}

因为 retain 不仅仅会对引用计数器 +1, 而且还会返回当前对象,所以上述代码可最终简化成:

- (void)setRoom:(Room *)room { // room = r
    // 只有房间不同才需用 release 和 retain
    if (_room != room) {    // 0ffe1 != 0ffe1
        // 将以前的房间释放掉 -1
        [_room release];
 
        _room = [room retain];
    }
}

所以就这样我们得到了setter最终的形式,这也是多个对象内存管理的思想。

循环引用

对于引用计数,它有一个致命的缺陷:它不能很好的解决循环引用的问题

对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。

对于循环引用,也分为几种类型:

  1. 自循环引用
  2. 相互循环引用
  3. 多循环引用

自循环引用

假如有一个对象,内部强持有它的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用。

iOS-MRC_第5张图片

相互循环引用

对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用。

iOS-MRC_第6张图片

多循环引用

假如类中有对象1…对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用。

iOS-MRC_第7张图片

常见循环引用问题及其解决方法

delegate

例如:我们平时经常用的协议传值,如果我们委托方的delegate属性使用strong强引用,就会造成代理方和委托方互相强引用出现循环引用问题。代理方强引用委托方对象,并且委托方对象中的delegate属性又强引用代理方对象,这就造成了循环引用问题。

@property (nonatomic, strong) id <MyCustomDelegate> delegate;
解决方法

为了解决这个问题,我们只需要将委托方的delegate属性改为weak修饰就行了,这样委托方的delegate就不会强引用代理方对象了,简单解决了这个循环引用问题。

@property (nonatomic, weak) id <MyCustomDelegate> delegate;

block

解决方法
  1. 并不是所有block都会产生循环引用,block是否产生循环引用是需要我们去判断的,例如:
//这样是不会产生循环引用,因为这个block不被self持有,是被UIView的类对象持有,这个block和self没有任何关系,所以可以任意使用self。
[UIView animateWithDuration:0.0 animations:^{
    [self viewDidLoad];
}];
  1. self -> reachabilityManager -> block -> self,才会产生循环引用,并且Xcode会给出循环引用warning,例如:
//self -> reachabilityManager -> block -> self 都是循环引用
    self.reachabilityManager.stateBlock = ^(int number){
        NSLog(@"%@",self. reachabilityManager);
    };
//或者(block内部没有显式地出现"self",只要你在block里用到了self所拥有的东西,一样会出现循环引用!)
    self.reachabilityManager.stateBlock = ^(int number){
        NSLog(@"%@",_ reachabilityManager);
    };

综合上述来看,要判断block是否造成了循环引用,我们要看block中的引用的变量和block外部引用block的变量会不会形成一个强引用的闭环,以此来判断block是否造成了循环引用的问题。

解决方法

解决它其实很简单,无非就是self引用了blockblock又引用了self嘛,让他们其中一个使用weak修饰不就行了:

__weak __typeof(self) weakSelf = self;
[self.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
    NSLog(@"%@",weakSelf.reachabilityManager);
}];

但是仅仅使用__weak修饰self存在一个缺陷:__weak可能会导致内存提前回收weakSelf,在未执行NSLog()时,weakSelf就已经被释放了,然后执行NSLog()时就打印(null)

所以为了解决这个缺陷,我们需要这样在block内部再用__strong去修饰weakSelf

__weak __typeof(self) weakSelf = self;
[self.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    NSLog(@"%@",strongSelf.reachabilityManager);
}];

这就是我们平时所说的强弱共舞。

我们发现上述这个方法确实解决所有问题,但是可能会有两个不理解的点:
即使用weakSelf又使用strongSelf,这么做和直接用self有什么区别?为什么不会有循环引用?这是因为block外部的weakSelf是为了打破环循环引用,而block内部的strongSelf是为了防止weakSelf被提前释放,strongSelf仅仅是block中的局部变量,在block执行结束后被回收,不会再造成循环引用。

这么做和使用weakSelf有什么区别?唯一的区别就是多了一个strongSelf,而这里的strongSelf会使self的引用计数+1,使得self只有在block执行完,局部的strongSelf被回收后,self才会dealloc

  1. NSTimer
    在使用NSTimer时我们会遇到很多循环引用问题,比如下面一段代码:
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}

- (void)doSomething {
    
}

- (void)dealloc {
    [self.myTimer invalidate];
    self.myTimer = nil;
}

这是典型的循环引用,因为myTimer会强引用self,而 self又持有了timer,所有就造成了循环引用。那有人可能会说,我使用一个weak指针,比如:

__weak typeof(self) weakSelf = self;
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];

但是其实并没有用,因为不管是weakSelf还是strongSelf,最终Runloop强引用NSTimer其也就间接的强引用了对象,结果就会导致循环引用。

那怎么解决呢?有两点:

(1)让视图控制器对NSTimer的引用变成弱引用
(2)让NSTimer对视图控制器的引用变成弱引用

分析一下两种方法,第一种方法如果控制器对NSTimer的引用改为弱引用,则会出现NSTimer直接被回收,所以不可使,因此我们只能从第二种方法入手。

主要有如下三种方式:

一:使用中间类

创建一个继承NSObject的子类MyTimerTarget,并创建开启计时器的方法。

// MyTimerTarget.h
#import <Foundation/Foundation.h>
@interface MyTimerTarget : NSObject
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
@end

// MyTimerTarget.m   
#import "MyTimerTarget.h"
@interface MyTimerTarget ()
@property (assign, nonatomic) SEL outSelector;
@property (weak, nonatomic) id outTarget;
@end

@implementation MyTimerTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
      MyTimerTarget *timerTarget = [[MyTimerTarget alloc] init];
      timerTarget.outTarget = target;
      timerTarget.outSelector = selector;
      NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timerTarget selector:@selector(timerSelector:) userInfo:userInfo repeats:repeats];
      return timer;
}   
- (void)timerSelector:(NSTimer *)timer {
      if (self.outTarget && [self.outTarget respondsToSelector:self.outSelector]) {
        [self.outTarget performSelector:self.outSelector withObject:timer.userInfo];
      } else {
        [timer invalidate];
      }
}
@end

// 调用方 
@interface ViewController ()
@property (nonatomic, strong) NSTimer *myTimer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}

- (void)doSomething {
    
}

- (void)dealloc {
    NSLog(@"MyViewController dealloc");
}
@end

VC强引用timer,因为timertargetMyTimerTarget实例,所以timer强引用MyTimerTarget实例,而MyTimerTarget实例弱引用VC,解除循环引用。这种方案VC在退出时都不用管timer,因为自己释放后自然会触发timerSelector:中的[timer invalidate]逻辑,timer也会被释放。

二:使用类方法

我们还可以对NSTimer做一个category,通过blocktimertargetselector绑定到一个类方法上,来实现解除循环引用:

// NSTimer+MyUtil.h
#import <Foundation/Foundation.h>
@interface NSTimer (MyUtil)
+ (NSTimer*)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats;
@end

// NSTimer+MyUtil.m
#import "NSTimer+MyUtil.h"
@implementation NSTimer (MyUtil)
+ (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats {
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)MyUtil_blockInvoke:(NSTimer *)timer {
    void (^block)(void) = timer.userInfo;
      if (block) {
         block();
      }
}
@end

//  调用方
@interface ViewController ()
@property (nonatomic, strong) NSTimer *myTimer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{
            NSLog(@"doSomething");
      } repeats:YES];
}
- (void)dealloc {
    if (_myTimer) {
        [_myTimer invalidate];
    }
    NSLog(@"MyViewController dealloc");
}
@end

这种方案下,VC强引用timer,但是不会被timer强引用,但有个问题是VC退出被释放时,如果要停掉timer需要自己调用一下timerinvalidate方法。

三:使用 weakProxy

创建一个继承NSProxy的子类MyProxy,并实现消息转发的相关方法。NSProxy是iOS开发中一个消息转发的基类,它不继承自NSObject。因为他也是Foundation框架中的基类,通常用来实现消息转发,我们可以用它来包装NSTimertarget,达到弱引用的效果。

// MyProxy.h
#import <Foundation/Foundation.h>
@interface MyProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end

// MyProxy.m
#import "MyProxy.h"

@interface MyProxy ()
@property (weak, readonly, nonatomic) id weakTarget;
@end
@implementation MyProxy

+ (instancetype)proxyWithTarget:(id)target{
    return [[MyProxy alloc] initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target {
    _weakTarget = target;
    return self;
}
- (void)forwardInvocation:(NSInvocation*)invocation {
    SEL sel = [invocation selector];
    if (_weakTarget &&[self.weakTarget respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.weakTarget];
    }
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
    return [self.weakTarget methodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.weakTarget respondsToSelector:aSelector];
}

@end

//  调用方
@interface ViewController ()
@property (nonatomic, strong) NSTimer *myTimer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:[MyProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];
}
- (void)dealloc {
    if (_myTimer) {
        [_myTimer invalidate];
    }
    NSLog(@"MyViewControllerdealloc");
}
@end

上面的代码中,了解一下消息转发的过程就可以知道-forwardInvocation: 是会有一个NSInvocation对象,这个NSInvocation对象保存了这个方法调用的所有信息,包括Selector名,参数和返回值类型,最重要的是有所有参数值,可以从这个NSInvocation对象里拿到调用的所有参数值。这时候我们把转发过来的消息和weakTargetselector信息做对比,然后转发过去即可。

这里需要注意的是,在调用方的dealloc中一定要调用timerinvalidate方法,因为如果这里不清理timer,这个调用方dealloc被释放后,消息转发就找不到接收方了,就会crash。

你可能感兴趣的:(ios,objective-c,开发语言)