iOS开发:对Block使用的一次研究总结

Posted wuwuFQ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发:对Block使用的一次研究总结相关的知识,希望对你有一定的参考价值。

在开发中Block是经常使用的,那我们就得知其然,知其所以然。

Block是什么?

Block可以封装一个匿名函数为对象,并捕获上下文所需的数据,并传给目标对象在适当的时候回调。我们使用Block的目的其实就是回调传值,那我们去看看Block的底层,再深入了解一下Block

Block的底层

Block的底层实现是结构体,和类的底层实现类似,都有isa指针,可以把Block当成是一个对象。

Block_layoutblock结构体的底层结构,其源码如下:

// Block 结构体
struct Block_layout 
    //指向表明block类型的类
    void *isa;//8字节
    //用来作标识符的,类似于isa中的位域,按bit位表示一些block的附加信息
    volatile int32_t flags; // contains ref count 4字节
    //保留信息,可以理解预留位置,用于存储block内部变量信息
    int32_t reserved;//4字节
    //函数指针,指向具体的block实现的调用地址
    BlockInvokeFunction invoke;
    //block的附加信息
    struct Block_descriptor_1 *descriptor;
    // imported variables
;

  • isa:指向所属类的指针,也就是Block的类型;
  • flags:标志变量,用于标记Block的类型、状态等
    // flags 标识
    // Values for Block_layout->flags to describe block objects
    enum 
        //释放标记,一般常用于BLOCK_BYREF_NEEDS_FREE做位与运算,一同传入flags,告知该block可释放
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        //存储引用引用计数的 值,是一个可选用参数
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        //低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        //是否拥有拷贝辅助函数,(a copy helper function)决定block_description_2
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        //是否拥有block C++析构函数
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        //标志是否有垃圾回收,OSX
        BLOCK_IS_GC =             (1 << 27), // runtime
        //标志是否是全局block
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        //与BLOCK_HAS_SIGNATURE相对,判断是否当前block拥有一个签名,用于runtime时动态调用
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        //是否有签名
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        //使用有拓展,决定block_description_3
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    ;
    
    
  • reserved:保留变量
  • invoke:block执行时调用的函数指针,Block调用实际上就是调用的invoke
  • descriptor:block的详细描述,包含 copy/dispose 函数,处理block引用外部变量时使用,有三类
    • Block_descriptor_1是必选的
    • Block_descriptor_2Block_descriptor_3 都是可选的
    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 
        uintptr_t reserved;//保留信息
        uintptr_t size;//block大小
    ;
    
    //当block引用对象时,就会新增如下结构:
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 
        // requires BLOCK_HAS_COPY_DISPOSE
        BlockCopyFunction copy;//拷贝函数指针
        BlockDisposeFunction dispose;
    ;
    
    // 当block有签名时,增加Block_descriptor_3:
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;//签名
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT 布局
    ;
    
    

以上关于descriptor的可以从其构造函数中体现,其中Block_descriptor_2Block_descriptor_3都是通过Block_descriptor_1的地址内存平移得到的

static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)

    return aBlock->descriptor;//默认打印

#endif

//  Block 的描述 : copy 和 dispose 函数
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)

    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;//descriptor_1的地址
    desc += sizeof(struct Block_descriptor_1);//通过内存平移获取
    return (struct Block_descriptor_2 *)desc;


//  Block 的描述 : 签名相关
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)

    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) 
        desc += sizeof(struct Block_descriptor_2);
    
    return (struct Block_descriptor_3 *)desc;

  • variables:block范围外的变量,如果block没有调用任何外部变量,该变量就不存在

Block类型

block主要有三种类型

  • 全局块(_NSConcreteGlobalBlock)
  • 栈块(_NSConcreteStackBlock)
  • 堆块(_NSConcreteMallocBlock)

全局区Block

- (void)checkBlock 
    //不引用任何变量
    void(^block1)(void) = ^
        NSLog(@"wuwuFQ:checkBlock");
    ;
    //有形参有返回值
    int(^block2)(int, int) = ^(int a, int b)
        //a + b
        return a + b;
    ;
    //引用静态变量
   static int a = 0;
    void(^block3)(void) = ^
        NSLog(@"wuwuFQ:%d", a);
    ;
    NSLog(@"wuwuFQ:block1%@", block1);
    NSLog(@"wuwuFQ:block2%@", block2);
    NSLog(@"wuwuFQ:block3%@", block3);


Block在不引用外部变量或只引用全局变量或者静态变量就是全局Block

堆区Block

- (void)checkBlock 
    int a = 10;
    void(^block)(void) = ^
        NSLog(@"wuwuFQ:%d", a);
    ;
    NSLog(@"wuwuFQ:%@", block);


此时的block访问外部变量,默认是强引用发生了copy操作,所以是堆区Block

栈区Block

- (void)checkBlock 
    //引用局部变量a
    int a = 0;
    // Assigning block literal to a weak variable; object will be released after assignment
    void (^__weak block)(void) = ^
        NSLog(@"wuwuFQ:%d", a);
    ;
    NSLog(@"wuwuFQ:%@", block);


通过__weak不进行强持有,block就还是栈区block

总结

  • 未引用外部变量时,block无论作为属性还是参数,都在全局区。
  • 引用外部变量时,block声明在栈区,但在block调用、赋值给其它strong修饰的变量时,会发生copy操作,从栈区copy到堆区

Block捕获外部变量

普通变量的捕获

Block捕获外部普通变量(不是__block等修饰的变量)会自动生成一个属性来保存。

#import <UIKit/UIKit.h>
int main(int argc, char * argv[]) 
    int a = 11;
    void (^block)(void) = ^(void)
        NSLog(@"%@",@(a));
    ;
    block();

Block编译时自动生成一个__main_block_impl_0结构体

struct __block_impl 
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
;
struct __main_block_impl_0 
  struct __block_impl impl;// 封装了函数实现的结构体
  struct __main_block_desc_0* Desc;// 里面有内存管理函数,Block_size表示block的大小
  int *static_k; // 捕获到的局部静态变量
  int a; //自动生成a属性来保存捕获的变量a:捕获到的普通局部变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) // 构造函数
    impl.isa = &_NSConcreteStackBlock;//isa指针
    impl.Flags = flags;//
    impl.FuncPtr = fp;//fp函数作为参数传入,保存在FuncPtr指针,这样block()才能调用函数
    Desc = desc;
  
;

__main_block_impl_0结构体自定义临时变量int a用于捕获的外部变量a,这里面的a实际上已经不是block外面的变量a,这同时也解释了为什么在Block里面无法对a进行修改。

__block变量的捕获

	__block int a = 11;
    void (^block)(void) = ^(void)
        a++;
        NSLog(@"%@",@(a));
    ;
    block();

此时的Block的结构体__main_block_impl_0 代码如下:

struct __main_block_impl_0 
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) 
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  
;

此时的__main_block_impl_0和捕获普通对象时的结构基本是一样的,唯一不同在于对外部变量的捕获。多了一个结构体__Block_byref_a_0,这个结构体实际上Block用来捕获__block int a 变量的,其结构体源码如下:

struct __Block_byref_a_0 
  void *__isa;
__Block_byref_a_0 *__forwarding;// 保存变量a的指针
 int __flags;
 int __size;
 int a;// 保存变量a的值
;

可以看出Block不仅捕获__block int a 变量的值,还捕获了a的指针,通过__Block_byref_a_0结构体分别保存在属性int a__Block_byref_a_0 *__forwarding中,这样Block内部和外部指向同一个地址,所以可以修改外部变量。

关于Block的理解

看了很多的文章,也扒了Block底层,那你对Block是怎么理解的呢?

  • Block分为三种类型:全局块(_NSConcreteGlobalBlock)、栈块(_NSConcreteStackBlock)、堆块(_NSConcreteMallocBlock)、
  • Block不访问外部变量,或访问全局变量、静态变量,Block内存在全局区
  • Block访问外部变量,Block声明在栈区,使用时如果发生copy__weak修饰block不会发生copy),Block内存在堆区
  • Block引用局部变量(int a; NSString *str),Block内存在堆区,进行值拷贝,不能修改变量的值
  • Block引用__block修饰的局部变量,Block内存在堆区,触发_Block_byref_copy函数发生指针拷贝,可以修改变量的值
  • Block引用普通对象(self.view; self.name),Block内存在堆区,触发ARCcopy进行指针拷贝,并retain使得引用计数+1 (这里引申一下:其实Block对局部变量也会持有,但是局部变量的作用域小,会随着Block释放掉,只要对象没有持有Block,对象其实也会随着Block释放掉)

变量或对象在Block内部是 __main_block_impl_0 结构体,调用执行 __main_block_func_0 函数,在 __main_block_func_0 函数里面会判断引用类型(全局变量、局部变量、对象、__block修饰、__weak修饰、Block),执行不同的copy原则。

以上是关于iOS开发:对Block使用的一次研究总结的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发-开发总结

iOS底层原理总结 - 探寻block的本质

[iOS开发]block再学习

[iOS开发]block再学习

IOS开发-block的使用

iOS开发-面试总结(十五)