美文网首页
最后一次学习Block!!!

最后一次学习Block!!!

作者: 8fe8946fa366 | 来源:发表于2018-03-09 16:07 被阅读40次

    前言:block这个东西我学了太多次了,这是最后一次学他,我发誓要学明白!

    1.Block是什么

    block其实是仅使用c语言源码就实现了带有自动变量值的匿名函数。这一概念在其它语言中叫做closure(闭包)。

    2.Block的语法

    ^返回值(参数列表){表达式}

    ⚠️:block可以省略返回类型,省略返回类型不代表block的返回值类型就是void。而是要看block里有没有return语句,如果有return语句返回值类型就是这个类型,如果没有return语句返回值类型就是void。

    ⚠️:block省略参数的条件是不使用参数,如果使用了参数就不能省略

    3.声明block类型变量

    声明block类型变量和声明c语言中的函数指针非常类似:

    返回值(^block类型变量名)(形参类型)

    🌰:int(^blk)(int) = ^void (int count){return count+1;};

    由^开始的Block语法生成的Block被赋值给block类型的变量blk中。

    int(^blk)(int) 这个东西就相当于int a,可以作为:局部变量、函数参数、静态变量、静态全局变量、全局变量。

    可以使用typedef来简化block的表达。

    typedef  int(^blk )(int);这样就可以用blk来代替block的声明。

    4.__block说明符

    如果直接在block内部给在block外部声明的局部变量赋值,编译阶段会报错。

    ⚠️:为什么不能在block内部直接修改局部变量的值(赋值),因为在block里只是截获了自动变量的值,也就是说我们在block外部打印局部变量的内存地址和在block内部打印局部变量的内存地址会发现它们的内存地址是不一样的,因此无法赋值,赋值也不是給原来的局部变量赋的。局部变量在block里是值传递的,而不是像静态变量那样直接传递了指针。

    想要赋值的话,必须在局部变量前加__block说明符。

    ⚠️:只有局部变量是这样,静态局部变量、全局变量、静态全局变量都不需要。

    ❓:为什么不需要,因为静态变量和全局变量在程序运行的过程中在内存里只有一个内存地址,所以给他们赋值就是在给原本的变量赋值。

    ❓:为什么局部变量不能像静态变量那样用指针访问呢?

    因为静态变量的生命周期是程序运行的整个过程,而局部变量不是,当超过变量的作用域以后,局部变量就被销毁了。很多情况下,block是延后执行的,这个时候指向的局部变量已经销毁了,就会crash。

    ❓:用静态变量实现值的修改为什么不可以?这个问题我还没有回答

    __block存储域类说明符,用来指定将变量值设置到哪个存储区域。通过原码我们发现,__block修饰的变量变成了结构体实例(已经不是原来的基本数据类型),这个结构体里持有原来的局部变量,存储在栈上

    Block的实质是栈上Block的结构体实例。

    __block的实质是栈上__block变量的结构体实例。

    __forwarding是干什么的?

    我们首先要明白一个道理,就是当Block内的__block变量随着Block的复制从栈被复制到了堆以后,这个时候栈里和堆里都有Block和内部的__block变量。那么在有些情况下,我们想要访问的是栈内的__block变量,有时访问的是堆内的__block变量。__block变量结构体在栈里的时候,成员变量__forwarding指向自己本身的指针。当__block变量被复制到堆里以后,栈里的成员变量__forwarding指针指向堆上的__block变量结构体的指针,堆里的__forwarding指向自己本身的指针。

    __forwarding实现了无论__block变量配置在栈上还是堆上,都能正确的访问__block变量。

    使用__block变量的block被复制到堆上以后,__block变量也被复制到堆上。如果有多个block同时使用一个__block变量,那么这些block被复制到堆上以后都持有这个__block变量,只有当所有持有__block变量的block都被销毁以后,__block变量才会被销毁。

    如果在block中使用了_block对象,那么block被从栈复制到堆的时候,里面的_block对象也会被复制到堆中,如果block本来就在堆中,那么再次被复制的时候对于内部的_block对象也没有影响。

    5.OC中对象和类的实质

    oc中的对象其实是由objc_object这个结构体表示的。

    typedef struct objc_object{

    Class isa;

    }*id;

    id为objc_object结构体的指针类型。因为oc是一个动态语言,在运行时才确定对象到底属于哪一个类,在运行时就是通过isa指针,看这个指针指向了那一个类,这个对象就属于哪一个类。

    oc中的类其实是由objc_class这个结构体表示的。其实oc中的类,也是一种对象,因此结构体都是以isa指针开始的。

    struct objc_class{

    Class isa;

    };

    各类的结构体就是基于objc_class结构体的class_t结构体,class_t结构体的声明如下:

    struct class_t{

    struct class_t *isa;

    struct class_t *superclass;

    Cache cache;

    IMP* *Vtable;

    uintptr_t data_NEVER_USE;

    }

    该结构体实例持有声明的成员变量,方法名称,方法的实现(函数指针),属性,父类的指针。

    通过block结构体我们可以判断,block就是oc对象。

    6.block是如何截获局部变量的呢

    在执行block语句的时候,block内部的局部变量值被保存到了block的结构体实例(即block自身)中。

    通过查看原码可以发现,局部变量在block结构体里面。

    7.Block存储域

    Block对应的类:为什么说Block会有不同的类呢,因为Block本质上也是一个对象。

    _NSConcreteStackBlock 存储在栈上

    _NSConcreteGlobalBlock 存储器在程序的数据区域(和全局变量一样)

    _NSConcreteMallocBlock 存储在堆上

    在运行时,block的isa指针会动态的指向block对应的类。

    如果block存储在数据区域:

    🌰:void (^blk) (void) = ^{printf(...);};

    int main(){};

    block被存在数据区域,因为是全局变量所以不存在截获局部变量,整个运行过程中block的存储区域不变,都是在数据区域,整个程序中只有一个实例。

    ❓:什么情况下block会存储在数据区域:

    1.在写全局变量的地方写block语法

    2.block语法的表达实例不使用应截获的局部变量

    ❓:Block的实质是一个对象,那为什么Block在超出了作用域以后依然可以存在呢?

    如果Block的类型是_NSConcreteGlobalBlock,也就是这个block存储在程序的数据存储区,那么从作用域外也可以通过指针去访问。

    如果Block的类型是_NSConcreteStackBlock,也就是这个Block存储在栈区,那么吵过变量作用域以后这个Block就被销毁了。

    把Block复制到堆上,变量作用域结束以后,栈上的Block被销毁,堆上的Block依然存在。

    相当于改变了Block的isa指针:imp1.isa = &_NSConcreteMallocBlock;

    ❓:什么情况下block会存储在堆里:

    在arc环境下,编译器会自动进行判断,把block从栈上复制到堆上。

    ⚠️:有一种情况编译器无法自动copy block,就是blcok作为方法或函数的参数传递的时候。这个时候需要手动进行copy。

    但是如果方法或者函数中适当地复制了传递过来的参数,就不需要手动复制了。比如说方法名中含有usingBlock的Cocoa框架方法,或者gcd的api中传递block时。🌰:block语法可以直接调用copy,[  ^(void)(int a){}  copy];

    block类型变量也可以直接调用copy,typedef int (^blk) (int);

    blk block = ^(int a){};

    block =  [block copy];

    ⚠️:除了下面这三种情况,都需要手动把block从栈copy到堆。

    1.block作为函数返回值返回

    2.将block复制给类的附有__strong修饰符的id类型或block类型成员变量

    3.向方法名中含有usingBlock的cocoa框架方法或gcd的api传递block时

    🙋🌰:举一个例子,block作为函数参数传递的时候需要手动copy,否则超过变量作用域无法访问:

    -(id)getBlockArray{ int val = 10; return [[NSArray alloc] initWithObjects:^{NSLog(@"block0:%zd",val);},^{NSLog(@"block1:%zd",val);}, nil];}

    -(void)callgetBlockArray{ id obj = [self getBlockArray]; typedef void(^blk_t) (void); blk_t blk = (blk_t)[obj objectAtIndex:0]; blk();}

    在第二个方法里访问返回的block的时候,由于block作为函数参数传递,编译器无法自动把它copy到堆里,那么这个时候访问栈上的block已经被销毁了,因此crash。要避免这种问题需要手动copyblock。

    return [[NSArray alloc] initWithObjects:[^{NSLog(@"block0:%zd",val);}copy],[^{NSLog(@"block1:%zd",val);}copy], nil];

    ⚠️:这也就解释了,为什么Block离开了作用域以后依然存在。使用了copy以后,block中截获的对象也可以超出其变量作用域而存在。

    如果block在栈里,访问外界的对象,不会对对象进行retain操作。

    如果block在堆里,访问外界对象,就会对对象进行retain操作。

    如果外界变量加了__block,哪怕block在堆中,也不会对外界对象进行retain操作。就不会使内存管理产生问题。

    8.block的循环引用

    ❓:什么情况下造成循环引用♻️

    a对象和b对象互相强引用(互相持有),a对象想要销毁的时候需要调用dealloc方法,因为他被b对象持有,所以需要b对象向a对象发送release消息。但是b对象只有调用dealloc方法的时候才能发送release消息,b对象调用dealloc方法就需要a对象给他发release消息,这样两个对象互相等待对方发送release消息,造成了循环引用。

    堆内存有两种持有方式:

    外部指针指向一段堆内存,栈对堆的引用。

    一段内存中的某个指针指向另一段内存,堆对堆的引用。

    造成循环引用的根本原因是堆对堆的引用。

    ❓:什么情况下是堆对堆引用?

    a是b的属性,block对block所截获的变量的持有,容器类对其包含对象的持有。

    如果在block中使用附有_strong修饰符的对象类型自动变量,那么当block从栈复制到堆的时候,block持有该对象,容易造成循环引用。

    有些很明显的循环引用编译器可以检查出来,会在编译时报错。

    通过使用weak可以打破循环引用

    通过使用__block可以打破循环引用,使用__block的优点:

    通过__block可以控制对象的持有周期

    在执行block时可以动态的决定是否将nil或其它对象赋值在__block变量中

    缺点就是必须执行block。

    关于使用weak的一个深层次的探讨:

    一个稍微复杂的循环引用

    如果在block内部不用strong修饰符去修饰weak会造成一种问题:

    如果b在十秒之内pop回a,b会立刻执行dealloc,block打印出的是null,也就是weak会造成内存的提前回收。

    解决方法就是在block内部用strong去修饰weakSelf。这样做不会造成循环引用,因为strongSelf是在block内部生命的,存在栈里,block没有持有他。

    strongSelf把classb的引用计数加一,这样他就不会执行dealloc。block执行完,strongSelf被回收,classb执行dealloc方法。

    相关文章

      网友评论

          本文标题:最后一次学习Block!!!

          本文链接:https://www.haomeiwen.com/subject/cpoyfftx.html