代理详解

本文纯属个人观点,如有错处,敬请指正,不胜感激。

我们不管是在项目中,还是在面试过程中,总是免不了被问及循环引用的话题,这还是要归于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方法,执行方法体内的语句。结果就会打印出“照顾小宝宝”。

现在我们基本可以回答以下几个问题了。

  1. 为什么Baby类中
    @property(nonatomic,weak)id<BabyDelegate> delegate;
    需要使用id泛型进行声明呢?

因为直到 xiaoming.delegate = xiaohong;这句话执行之前,delegate的类型都是不确定的。delegate可以指向任意类型。另一方便,无论实现代理的对象是什么类型,都必须要先遵循协议。

  1. 代理使用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的。

  1. 代理为什么不用assign声明?

weak和assign都是一种“非拥有关系”的指针。通过这两种关键字修饰的变量,都不会改变被引用对象的引用计数。但是,不同的是,在一个对象被释放之后,weak修饰的指针变量会被置为nil,而assign不会。在OC中,向nil发送消息不会崩溃。而使用assign,在对象被释放后,在使用这个指针调用被释放对象,也就是向这个对象发送消息,就会有崩溃出现。

这篇短短的文章断断续续写了好几次,终于写完了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 简介 在某些情况下,我们不希望或是不能直接访问对象 A,而是通过访问一个中介对象 B,由 B 去访问 A 达成目的...
    Tars阅读 896评论 0 1
  • 动态代理的作用通过反射调用代理对象,让其帮我们实现一些非常频繁的操作,如:权限校验和日志记录代理的实现原理:在Ja...
    柒黍阅读 775评论 0 1
  • JDK动态代理详解 java动态代理类 Java动态代理类位于java.lang.reflect包下,一般主要涉及...
    冰火人生阅读 525评论 0 1
  • 怀珍(其三) 将夜戚戚不知所,寒露层层摧心肠。 我笑世人都清醒,趑趄嗫嚅可敢行。 魑魅魍魉何...
    P小通Antonella阅读 78评论 0 0
  • 虽是一朵普通无奇的花,但拍出了晶莹剔透的效果,美丽绽放!
    安和桥_6447阅读 147评论 0 0