Objective-C iOS之Block深究

在了解Block之前,我们有必要先了解一下一些基础知识。
  我们都知道,Objective-C是由C语言扩展而来的。在Objective-C中,引用是指向对象的一个指针。即引用是一个变量,也是一个指针,存储的是对象的地址。那么,引用本身其实也是存在地址的。所以引用和引用指向的对象是两个不同的概念。
  1. 在Objective-C中,引用一般分为强引用类型和弱引用类型。即由__strong__weak修饰的引用,不显式指定引用类型时,默认为__strong修饰的强引用类型。强引用会使指向的对象引用计数加1,而弱引用则不会,且弱引用在指向的对象被释放时,会被置为nil。
  2. Block块分为定义时和运行时两种状态。定义时指的是Block块作为类进行编译处理和Block块作为对象进行创建的时候(Block块代码相当于对象方法),这个时候Block块还没有运行。运行时指的是Block块代码执行的时候。(此概念是为了理解Block而定义的,并不是官方定义的。)
  3. 非局部引用指的是定义在Block块外的局部引用,但是在Block块中有该引用的作用域。
  了解完这些基础知识后,我们就可以开始深入了解Block。这篇文章主要研究的是Block块的正确使用,以及为什么要这样使用,并不对Block的实现原理做深入了解。
  首先,对于在Block块中使用非局部引用的情况,我们先看一下下面这个例子:

    NSString *string = @"Smith";
    
    void(^block)() = ^(){
        NSLog(@"%@",string);
    };
    
    string = @"Jackyson";
    block();

我们知道,运行结果是Smith。那么,为什么会是这样呢?我们可以尝试的打印string引用的地址,注意,是string引用的地址,而不是string指向的对象的地址,两者是有很大的不同的。通过下面这个例子:

    NSString *string = @"Smith";
    NSLog(@"1-----string对象:%@,string引用的地址:%p",string,&string);
    
    void(^block)() = ^(){
        NSLog(@"3-----string对象:%@,string引用的地址:%p",string,&string);
    };
    
    string = @"Jackyson";
    NSLog(@"2-----string对象:%@,string引用的地址:%p",string,&string);
    block();

下面是我的运行结果:

2016-09-11 16:43:23.179 Block[73629:3669954] 1-----string对象:Smith,string引用的地址:0x7fff5e6f3a18
2016-09-11 16:43:23.179 Block[73629:3669954] 2-----string对象:Jackyson,string引用的地址:0x7fff5e6f3a18
2016-09-11 16:43:23.180 Block[73629:3669954] 3-----string对象:Smith,string引用的地址:0x7fca60c37260

从上面我们可以看到,在Block块内部的string和Block块外面定义的string是不一样的,它们是两个不同的引用,只是都指向了“Smith”这个字符串对象。
  其实,在Block块内部通过非局部引用访问了外部对象时,因为非局部引用在当前作用域结束时会失效,Block如果不持有外部对象,外部对象会被回收,而为了Block在真正运行时能正确访问外部对象,所以在Block块定义时,会在Block块内部复制非局部引用定义一个内部引用(Block成员变量)。默认情况下没有使用__block修饰,则内部引用是const修饰的引用,这意味着内部引用的值是不可改变的(无法指向新的对象);反之使用__block修饰,则内部引用是没有const修饰的引用,这意味着内部引用的值是可以改变的(可以指向新的对象)。同时,内部引用和非局部引用的强弱类型一致。
  让我们回到上面的例子中,在Block块中通过string访问了“Smith”对象,那么Block块定义时,会自动生成名称为string的内部引用(作用域在Block块内),且该指针的值不可修改(const),即引用不能指向新的对象,所以内部引用string一直指向字符串“Smith”。而非局部引用string指向新的字符串对象“Jackyson”,与内部引用string没有关系,所以就相当于将“Smith”字符串复制一份到Block内,且无法修改。
  那么,我们如果希望内部引用指向新的对象,或者在Block块内外修改引用值能互相影响,应该怎么做呢?答案就是使用__block修饰引用,代码例子如下:

    __block NSString *string = @"Smith";
    NSLog(@"1-----string对象:%@,string引用的地址:%p",string,&string);
    
    void(^block)() = ^(){
        NSLog(@"3-----string对象:%@,string引用的地址:%p",string,&string);
        string = @"SmithJackyson";
        NSLog(@"4-----string对象:%@,string引用的地址:%p",string,&string);
    };
    
    string = @"Jackyson";
    NSLog(@"2-----string对象:%@,string引用的地址:%p",string,&string);
    block();
    NSLog(@"5-----string对象:%@,string引用的地址:%p",string,&string);

下面是我的运行结果:

2016-09-11 17:15:33.063 Block[74333:3683201] 1-----string对象:Smith,string引用的地址:0x7fff5510aa18
2016-09-11 17:15:33.066 Block[74333:3683201] 2-----string对象:Jackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.066 Block[74333:3683201] 3-----string对象:Jackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.067 Block[74333:3683201] 4-----string对象:SmithJackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.068 Block[74333:3683201] 5-----string对象:SmithJackyson,string引用的地址:0x7fb351c1fb08

这里,我对__block修饰符的影响做出了一个假设:
  非局部引用是否使用__block修饰会影响内部引用的访问修饰符,因为Block块也可以作为对象处理,所以当不使用__block修饰时,内部引用是@protected或者@private修饰的成员变量,当使用__block修饰时,内部引用是@public修饰的成员变量。
  这一假设能较好的解释为什么使用__block修饰后,在Block块内外修改引用值能相互影响。如果有人有更好的解释,欢迎与我分享!!!
  从结果分析来看,使用__block修饰string引用,那么在Block块定义时,在Block块内部定义一个内部引用string(@public修饰的成员变量)。那么string指向“Jackyson”字符串对象为什么会影响Block块内string的值呢?因为Block定义后,编译器对Block定义之后使用的string进行一些处理,使用的string是访问到Block块中的内部引用string(类似block->string),所以string指向字符串“Jackyson”就是将Block块内string指向字符串“Jackyson”。同样,在Block内将string指向字符串“SmithJackyson”就是将Block块外的string指向字符串“SmithJackyson”。
  所以__block修饰引用后,在Block块内外改变引用值会相互影响,因为它们是同一个引用。
  值得注意的是,基本数据类型的内存管理不同于对象。对于Block块使用定义在Block块外面的局部变量(基本数据类型)的情况,当不使用__block修饰的时候,那么Block块会定义一个const修饰的内部变量,并将Block块外面的局部变量的值拷贝过来,所以Block块外面修改局部变量的值并不影响Block块的内部变量的值。当使用__block修饰的时候,那么Block块会直接使用外面的局部变量,这样相当于局部变量变为全局变量,在Block块内外修改变量的值会相互影响。
  另外,对于在Block块中使用成员变量的情况,我们再来看一下下面的例子:

@interface ViewController ()
{
    NSString *_string;
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _string = @"Smith";
    NSLog(@"1-----string对象:%@,string引用的地址:%p",_string,&_string);
    NSLog(@"1*****self引用的地址:%p",&self);
    
    void(^block)() = ^(){
        NSLog(@"3-----string对象:%@,string引用的地址:%p",_string,&_string);
        NSLog(@"3*****self引用的地址:%p",&self);
        _string = @"SmithJackyson";
        NSLog(@"4-----string对象:%@,string引用的地址:%p",_string,&_string);
        NSLog(@"4*****self引用的地址:%p",&self);
    };
    
    _string = @"Jackyson";
    NSLog(@"2-----string对象:%@,string引用的地址:%p",_string,&_string);
    NSLog(@"2*****self引用的地址:%p",&self);
    
    block();
    NSLog(@"5-----string对象:%@,string引用的地址:%p",_string,&_string);
    NSLog(@"5*****self引用的地址:%p",&self);
}

@end

下面是我的运行结果:

2016-09-13 17:37:11.162 Block[98324:4200964] 1-----string对象:Smith,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.162 Block[98324:4200964] 1*****self引用的地址:0x7fff57817a28
2016-09-13 17:37:11.163 Block[98324:4200964] 2-----string对象:Jackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 2*****self引用的地址:0x7fff57817a28
2016-09-13 17:37:11.163 Block[98324:4200964] 3-----string对象:Jackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 3*****self引用的地址:0x7f90b0d86fb0
2016-09-13 17:37:11.163 Block[98324:4200964] 4-----string对象:SmithJackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 4*****self引用的地址:0x7f90b0d86fb0
2016-09-13 17:37:11.163 Block[98324:4200964] 5-----string对象:SmithJackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 5*****self引用的地址:0x7fff57817a28

从结果可以看到,Block中使用的_string引用就是成员变量_string,这是因为其实在Block块中是通过self->_string来访问成员变量_string,所以在Block中定义了内部引用self,并通过内部引用self->_string来访问成员变量_string。所以Block块中_string和Block块外_string两个引用是同一个,Block块内外改变引用值会互相影响。
  分析完__block修饰符的作用后,我们再来看看__strong__weak为什么可以避免循环引用呢?
  在前面我们已经了解到了,当Block通过外部引用访问外部对象时,在Block定义的时候,会复制外部引用定义一个内部引用,且内部引用强弱类型与外部引用一致。当对象本身存在一个成员变量持有Block,而Block内部又访问了对象本身,那么可能会造成循环引用。这里我们以UIViewController为例,如下图:

Block块通过self访问外部对象.png

因为可能会出现这么一种情况,当没有外部强引用持有UIViewController对象和Block块时,那么成员变量block和成员变量self默认是无法被置空的,因为一般情况下两个成员变量在Block块和UIViewController对象dealloc时才被置为nil,那么两个对象相互持有,无法被释放。但是如果成员变量block在某时刻可以被置为nil,那么两个对象是能够被释放的。
  那么,为什么使用weakSelf能够防止循环引用呢?因为这样Block块成员变量为weakSelf(弱类型),当没有强引用持有UIViewController时,那么,UIViewController对象会被释放,成员变量block和weakSelf会被置为nil,Block块也会被释放。如下图:


Block块通过weakSelf访问外部对象.png

如果UIViewController对象没有成员变量block持有Block块对象时,为什么我们也需要使用weakSelf呢?这里其实并不是为了防止循环引用,而是希望Block块不持有UIViewController对象,让UIViewController对象能够及时被释放。因为如果Block块持有UIViewController对象,而持有Block块对象的引用没有被置为nil的话,那么Block块没有被释放,也就会一直持有UIViewController对象,造成UIViewController对象无法被及时释放。而我们希望当点击返回按钮时,UIViewController对象能够被及时释放,所以需要使用weakSelf。如下图:


Block块通过weakSelf使外部对象能够被及时释放.png

那么,我们又为什么要使用strongSelf呢?这是因为当Block块回调时,有可能在回调过程中,UIViewController对象被释放了,造成数据的状态和逻辑出现错误。所以需要在Block块执行时,定义一个局部强引用strongSelf来持有UIViewController对象,让Block块执行过程中,UIViewController对象即使没有被其他强引用持有,也无法被释放,确保数据的状态和逻辑不会出现错误。当Block执行完成后,strongSelf就会被置为nil,这时UIViewController对象引用计数为0,就会被释放了。如下图:


Block块使用strongSelf防止外部对象被释放.png

那么,什么情况下需要使用weakSelf和strongSelf呢?一般来说,当我们将Block块作为参数传入方法中,而方法本身进行的是耗时操作(文件读写或网络请求),如果需要Block块执行时对象还存在,那么直接使用self;如果Block块代码是更新界面数据这种非必需的操作,那么使用weakSelf;如果是回调时对象还存在需要执行必要的数据操作和逻辑操作,那么使用weakSelf和strongSelf确保Block执行过程中对象不会被释放掉。即根据Block块代码要进行的操作和操作是不是必要的来决定是使用self、weakSelf或者weakSelf和strongSelf。但是有一点需要注意的是,必须确保Block块在某一时刻会被置为nil,这样持有的对象才能够被释放掉。确保Block执行完后置为nil,或者持有Block块的对象会被释放掉。
  总结来说,当Block块通过外部引用访问外部对象时,在Block块定义的时候,会复制外部引用定义一个强弱类型一致的内部引用。外部引用如果使用__block修饰,则内部引用是可变的,即可以指向新的对象;且访问修饰符为@public,即Block块定义后,使用的不再是外部引用,而是通过类似Block->_internalReference使用内部引用。如果不使用__block修饰,则内部引用是不可变的,即不可以指向新的对象;且访问修饰符为@protected或者@private,即Block块定义后,使用的还是外部引用,与内部引用没有联系,两者互不影响。


转载请注明:作者SmithJackyson

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

推荐阅读更多精彩内容