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为例,如下图:

《Objective-C iOS之Block深究》 Block块通过self访问外部对象.png

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

《Objective-C iOS之Block深究》 Block块通过weakSelf访问外部对象.png

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

《Objective-C iOS之Block深究》 Block块通过weakSelf使外部对象能够被及时释放.png

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

《Objective-C iOS之Block深究》 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

    原文作者:SmithJackyson
    原文地址: https://www.jianshu.com/p/521453ddf037
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞