本文纯属个人观点,如有错处,敬请指正,不胜感激。
我们不管是在项目中,还是在面试过程中,总是免不了被问及循环引用的话题,这还是要归于ios的内存管理机制——引用计数。而循环引用常常发生的情景其中之一就是代理和block。这里重点说说代理。
一、代理解决什么问题
场景:
有一个婴儿,他要吃饭才能维持生存,但是,他自己并不能照顾自己。有一个育儿师,她有照顾婴儿的能力,但是她缺少收入。这个时候,婴儿和育儿师之间可以达成协议,婴儿委托给育儿师照顾,婴儿付出报酬。
提取关键词: 婴儿(委托方)、育儿师(代理方)、协议
我们可以根据这个场景,设计程序。
先来定义协议:BabyDelegate.h
协议是声明了一组方法的文件。协议只提供方法的名字,不提供实现。这个跟生活中的协议也是很形象的。协议就是一个文件,这个文件既不会干活儿,也不会发报酬,它就是一个约定。方法相等于协议中的条款。有些方法是必须实现的,有些是可以实现可以不实现的。这都要视具体情况而定。
#import <Foundation/Foundation.h>
@protocol BabyDelegate <NSObject>
-(void)takeCareOfBaby;
@end
\
Baby类
Baby.h:
#import <Foundation/Foundation.h>
#import "BabyDelegate.h"
@interface Baby : NSObject
//遵循协议的成员变量
@property(nonatomic,weak)id<BabyDelegate> delegate;
-(void)eat;
@end
Baby.m
#import "Baby.h"
@implementation Baby
-(void)eat{
//因为该成员变量遵循了协议,所以这里可以调用协议中声明的方法。
//至于方法有没有实现,编译阶段是不关心的,只有运行阶段才会知道。
//一旦方法没有实现,会造成崩溃。所以这里需要加上判断最好。
if ([_delegate respondsToSelector:@selector(takeCareOfBaby)]) {
[_delegate takeCareOfBaby];
}
}
@end
Baby类已经构建完成了。下面是BabySitter类。
BabySitter.h
#import <Foundation/Foundation.h>
#import "BabyDelegate.h"
//注意这里:要想实现协议中的方法,一定要遵守协议
@interface BabySitter : NSObject<BabyDelegate>
@end
BabySitter.m
#import "BabySitter.h"
@implementation BabySitter
//实现协议中声明的方法
-(void)takeCareOfBaby{
NSLog(@"照顾小宝宝");
}
@end
注意观察,Baby、BabySitter以及协议BabyDelegate之间的关系。
到目前为止,Baby、BabySitter分别跟BabyDelegate建立了联系,但是Baby和BabySitter还没有任何的联系。协议的三方都有了,最后就是协议的触发了。
- (void)viewDidLoad {
[super viewDidLoad];
//创建一个育儿师对象
BabySitter * xiaohong = [[BabySitter alloc] init];
//创建一个婴儿对象
Baby * xiaoming = [[Baby alloc] init];
//重点来了,把婴儿和育儿师建立联系
xiaoming.delegate = xiaohong;
//此时,婴儿可以吃饭了,育儿师也可以拿到报酬了
[xiaoming eat];
}
在执行 [xiaoming eat]; 这句代码的时候,执行过程是这样的:
1.先到Baby类中查看是否有eat方法,发现有,进入eat方法内部执行。
2.在eat方法体内部,是一个代理。这个代理的类是xiaohong,也就是babySitter类,那么就去判断该代理中的takeCareOfBaby方法是否在babySitter中进行了实现。如果实现了,[_delegate respondsToSelector:@selector(takeCareOfBaby)]返回true,执行[_delegate takeCareOfBaby]。
3.现在[_delegate takeCareOfBaby]等同于[xiaohong takeCareOfBaby],因为此时delegate已经指向xiaohong了。
4.在BabySitter类中,找到takeCareOfBaby方法,执行方法体内的语句。结果就会打印出“照顾小宝宝”。
现在我们基本可以回答以下几个问题了。
- 为什么Baby类中
@property(nonatomic,weak)id<BabyDelegate> delegate;
需要使用id泛型进行声明呢?
因为直到 xiaoming.delegate = xiaohong;这句话执行之前,delegate的类型都是不确定的。delegate可以指向任意类型。另一方便,无论实现代理的对象是什么类型,都必须要先遵循协议。
- 代理使用strong一定会循环引用?
答案是否定的。
为什么呢?
首先要明白什么是循环引用。
要说循环引用就不得不说ios的内存管理方法或者叫垃圾回收机制——引用计数。
简要说,当一个对象被创建出来之后(不管是怎么创建出来的),至少有一个强指针指向它,它的引用计数都是一个大于0的数。当有别的强引用指针指向这个对象的时候,该对象的引用计数就会增加1。当一个强引用指针从这个对象上移除的时候,这个对象的引用计数就会减1 。直到所有的指针都移除的时候,这个对象的引用计数就等于0了。这个时候,系统就会把这个对象回收掉了。至于系统是什么时候知道这个对象的引用计数等于0的,什么时候回收的,这又是另外的问题了。这里不做深入探讨了。
这里再插入一个小概念:指针和对象。
指针和对象密切相关,但是又是完全不同的东西。
创建个简单的Person类:
//
// Person.h
// TestOC
//
// Created by iOS on 2018/4/20.
// Copyright © 2018年 weiman. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,copy) NSString * name;
@property(nonatomic,assign) NSInteger age;
- (void)play;
- (void)work;
@end
//
// Person.m
// TestOC
//
// Created by iOS on 2018/4/20.
// Copyright © 2018年 weiman. All rights reserved.
//
#import "Person.h"
@implementation Person
-(void)play {
NSLog(@"周末一起去玩耍");
}
-(void)work {
NSLog(@"工作中。。。");
}
@end
main中:
//
// main.m
// TestOC
//
// Created by iOS on 2018/4/20.
// Copyright © 2018年 weiman. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person * tom = [[Person alloc]init];
tom.name = @"Tom";
Person * happy = tom;
NSLog(@"%@, %@",tom, happy);
}
return 0;
}
看这句:
Person * tom = [[Person alloc]init];
从运算符来看,这是一句赋值语句。赋值语句的执行顺序是从右到左。也就是说,先执行 [[Person alloc]init]。
再来分析这句话:
[[Person alloc]init]其实执行了两个方法,一个是类方法alloc,定义在父类NSObject中。另一个是对象方法init,也是定义在父类NSObject中。
[Person alloc]:开辟一段内存空间。
init:对[Person alloc]出来的对象进行初始化。例如给对象age赋初始值0.
(系统如何给person对象开辟的空间,开辟多大空间,空间里都有什么,是如何排列的,初始化的时候都做了什么,成员变量和方法是如何被初始化的,这些问题还有待研究。)
[[Person alloc]init]这句话执行完成之后,我们现在有一个对象了,这个对象是Person类型的,没有指针指向它。
现在再来看赋值语句左边的语句:Person * tom
这一句话是声明了一个Person类型的 指针,指针的名字叫做tom。
最后执行赋值,把右边创建出来的对象地址赋值给指针tom,也就是把指针tom指向了这个创建出来的Person对象。可以使用这个指针访问这个对象的可见的属性和方法。
Person * happy = tom;
理解上上面的概念,这句话就好理解了,把tom指针赋值给happy指针,就是happy指针也指向了tom指向的对象的地址,此时,该对象已经有两个指针引用了。
由于tom和happy这两个指针变量都是局部变量,在方法体执行完之后,这两个指针都会被销毁,当两个指针都销毁的时候,这个Person的对象的引用计数就变成0了,现在系统就把这块空间回收了,这个对象也就销毁了。
哎呀呀呀······扯的有点远了。。。
说了那么多,到底和循环引用有什么关系呢?对象释放的前提是对象本身没有强引用。循环引用会发生就是因为,两个对象中分别持有对方的强引用指针。导致这两个对象都不能释放,这两块内存都不能回收,造成资源浪费,也称为内存泄漏。
内存泄漏为什么总是跟代理相关?
因为代理中持有别的对象的指针,也就是这句:
@property(nonatomic,weak)id<BabyDelegate> delegate;
如果delegate是一个强引用,而实现代理的BabySitter中恰好也有Baby的强引用指针。这个时候,Baby和BabySitter的对象就会造成循环引用。注意,关键来了,循环引用是一个双向的强引用,仅仅把代理声明成strong不一定就会造成循环引用,还要满足另一个条件:代理者中也有被代理者的强引用,这样才会循环引用。
所以,代理声明称strong的不一定会循环引用。
但是,为了安全起见(我们也不知道代理是谁,代理者中会不会有被代理者的强引用指针存在),还是把代理声明成weak的。
- 代理为什么不用assign声明?
weak和assign都是一种“非拥有关系”的指针。通过这两种关键字修饰的变量,都不会改变被引用对象的引用计数。但是,不同的是,在一个对象被释放之后,weak修饰的指针变量会被置为nil,而assign不会。在OC中,向nil发送消息不会崩溃。而使用assign,在对象被释放后,在使用这个指针调用被释放对象,也就是向这个对象发送消息,就会有崩溃出现。
这篇短短的文章断断续续写了好几次,终于写完了。