Block

Block

20210329001.png

Block定义以及表达式

在iOS开发中针对于Objective-C我们经常提到Block,对于Swift来说就是闭包,今天主要是探索Block,所以先有个疑问,什么是Block?为什么要用Block?

首先Block是一个OC对象,其内部也是isa指针,Block封装了函数实现以及函数上下文的OC对象

使用Block是为了将函数的调用以及实现合并到一起

Block的声明:返回值(^Block名称)(参数列表){Block回调实现}

Block的调用:Block名称(Block参数)

Block分类

在我们程序运行过程中,Block的使用是非常广泛的,比如数组遍历enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {},masonry等很多优秀三方库中也会发现Block的身影,所以我们探索下的使用场景以及在什么场景下Block是什么类型

在我们iOS系统中Block分为6种:

_NSConcreteStackBlock:栈Block

_NSConcreteMallocBlock:堆Block

_NSConcreteGlobalBlock:全局Block

_NSConcreteAutoBlock:在GC环境下,当对象被__weak,__block修饰,并且从栈复制到堆时,block会被标记为该模式

_NSConcreteFinalizingBlock:在GC环境下,当block被复制时,如果block有ctors&dtors时,则转换为该模式

_NSConcreteWeakBlockVariable:与_NSConcreteFinalizingBlock反之则转换为该模式

Block的结构

Block作为一个OC的对象,是一个结构体类型,其结构如下:

struct Block_layout {
    void *isa;
    volatile int32_t flags;
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
};

知识点:

1、通过上面Block的结构体,就能得出我们之所以说Block也是OC对象的原因,因为Block内部也有isa指针

2、其中用到了一个修饰符volatile这个在系统结构体里面很常见,这个修饰符的意思就是告诉编译器,我修饰的属性,不需要你去优化,包括存储的空间也不需要你去优化。而且volatile修饰符保证了不同线程对这个属性进行操作的时候的可见性,即一个线程修改了变量的值,这个新值对其他线程来说是立即可见的。

3、GlobalBlock:位于全局区,在Block内部不使用外部变量,或者只使用静态变量和全局变量

4、MallocBlock:位于堆区,在Block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy修饰的变量

5、StackBlock:位于栈区,与MallocBlock一样,可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者Copy修饰的变量

Block源码探索

首先下载block源码,然后我们打开项目后,然后创建一个TARGET,然后打开main.m文件,在里面申明并调用简单的block,代码如下:

int main() {
    void(^block0)(void) = ^{
        NSLog(@"==================\n");
    };
    block0();
}

然后在block申明处添加断点,同时打开汇编模式,运行程序,然后进入断点后,点击step over然后会发现进入了_Block_copy函数,然后打开源码,找到_Block_copy函数的实现,代码如下:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 arg 为 NULL,直接返回 NULL
    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    // 获取Block签名
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 aBlock 内存上的数据全部复制新开辟的 result 上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

在这个函数中主要进行以下操作:

1、进行强转,变成Block_layout结构体对象。

2、然后调用Block_descriptor_3函数获取签名信息,在这个函数内部首先判断是否有签名信息,用到了Block给构体中flags属性,这个属性与BLOCK_HAS_SIGNATURE(1左移30位)进行或运算,然后判断结果是否为0,如果结果为0,那么表示没有签名信息,直接返回null,如果不为0,获取Block的descriptor指针,然后指针偏移Block_descriptor_1结构体大小,然后继续判断flags属性或运算BLOCK_HAS_COPY_DISPOSE(1左移25位)得到的结果是否为0,如果不为0,指针再次偏移Block_descriptor_2结构体大小,最后得到Block_descriptor_3的地址,然后进行强转后返回Block_descriptor_3对象,这样外部就可以直接访问到Block_descriptor_3结构体对象的属性。

3、然后判断Block是否在堆上,如果在堆上Block的引用计数加1,然后返回Block对象

4、如果Block是全局Block,不做任何处理直接返回Block对象

5、如果Block是栈区Block,那么先在堆上开辟一块和Block大小相同的空间,如果开辟空间失败,直接返回null,如果开辟成功,直接将block的数据全部复制到新开辟的堆上block,重新定义调用指针。然后将堆上block的flags中的BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0,然后标记堆block并且引用计初始化为1,然后调用_Block_call_copy_helper方法,复制栈block的属性到堆block上,修改堆block的isa指针为堆block类型。

Block实现以及变量捕获

我们都知道Block会捕获外部变量,那么是怎么捕获的,Block在编译时发生了什么?怎么找到外部变量的?怎么找到Block块的实现的?下面我们通过编译cpp文件去探索一下Block。

1、在Block中不使用任何变量或者常量,然后使用clang -rewrite-objc main.m -o main.cpp将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:

编译前:

#import 

int main() {
    void(^block0)(void) = ^{
        NSLog(@"==================\n");
    };
    block0();
}

编译后(删除额外代码后):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// Block块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_ac9b9b_mi_0);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main() {
    // Block的申明
    void(*block0)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    
    // Block的调用
    (block0->FuncPtr)(block0);
}

先看下main函数,在main函数中,Block的申明以及调用。

Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用__main_block_impl_0结构体的初始化方法,传入两个参数,一个是Block块实现函数,一个是Block信息。

__main_block_func_0函数中很简单就是打印日志。

__main_block_impl_0结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。

2、在Block中使用外部局部变量,然后使用clang -rewrite-objc main.m -o main.cpp将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:

编译前:

#import 

int main() {
    
    int a = 10;
    
    void(^block0)(void) = ^{
        NSLog(@"==================%d\n",a);
    };
    block0();
}

编译后(删除额外代码后):

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;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_5f4e61_mi_0,a);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {

    int a = 10;

    void(*block0)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
    
    (block0->FuncPtr)(block0);
}

先看下main函数,在main函数中,Block的申明以及调用。

Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用__main_block_impl_0结构体的初始化方法,传入三个参数,一个是Block块实现函数,一个是Block信息,一个是局部变量a传入了局部变量a的值。

__main_block_impl_0结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息,最重要的是__main_block_impl_0中定义了一个属性a,在析构函数中将对外部变量的值赋值给了a。

__main_block_func_0函数中定义了一个临时变量a然后使用__cself->a进行赋值,然后进行打印a的值,而且这块定义的a是一个全新的局部变量,不是外部定义的局部变量a,这也就是为什么在Block申明之后,Block调用之前修改局部变量的值,不影响Block块内部输出的值的原因,因为Block在捕获局部变量的时候传递了一个值,所以当编译器编译时就已经捕获了局部变量的值,并且赋值给了内部属性a,然后在Block实现中再次赋值给了Block块内部的局部变量a,所以外部不管怎么修改,都不影响Block内部捕获的值。

3、在Block中使用外部全局变量,然后使用clang -rewrite-objc main.m -o main.cpp将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:

编译前:

#import 

int a = 10;

int main() {
    
    void(^block0)(void) = ^{
        NSLog(@"==================%d\n",a);
    };
    block0();
}

编译后(删除额外代码后):

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

int a = 10;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_aac8d9_mi_0,a);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {

    void(*block0)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    (block0->FuncPtr)(block0);
}

先看下main函数,在main函数中,Block的申明以及调用。

Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用__main_block_impl_0结构体的初始化方法,传入两个参数,一个是Block块实现函数,一个是Block信息。

__main_block_impl_0结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。

__main_block_func_0函数中直接使用了全局变量,并不是捕获值,因为在__main_block_impl_0结构体中并没有申明属性,也没有对全局变量进行操作,而是直接使用全局变量,所以这就是为申明全局变量在Block申明后修改会影响Block内部的原因。

4、在Block中使用外部进过__block修饰的局部变量,然后使用clang -rewrite-objc main.m -o main.cpp将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:

编译前:

#import 

int main() {
    
    __block int a = 10;
    
    void(^block0)(void) = ^{
        NSLog(@"==================%d\n",a);
    };
    block0();
}

编译后(删除额外代码后):

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_379bf1_mi_0,(a->__forwarding->a));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {

    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

    void(*block0)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    
    (block0->FuncPtr)(block0);
}

先看下main函数,在main函数中,Block的申明以及调用。

__block int a = 10;经过编译后变成了__Block_byref_a_0结构体对象,通过结构体的析构函数将a的地址以及值都进行捕获。

Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用__main_block_impl_0结构体的初始化方法,传入四个参数,一个是Block块实现函数,一个是Block信息,一个是__Block_byref_a_0对象的地址,一个是flags。

__main_block_impl_0结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。

__main_block_desc_0_DATA中对捕获的值进行了拷贝,而且是地址拷贝,也就是让a的引用计数+1防止提前释放

__main_block_func_0函数中通过定义__Block_byref_a_0结构体对象然后获取值,然后访问结构体对象中局部变量a的值进行操作。

堆Block

虽然通过上面的源码编译查看后Block的isa都是指向栈Block,但是在程序运行中,我们通过断点调试查看的时候,发现Block只要没有被copy或者捕获使用__block修饰的变量,都是_NSConcreteGlobalBlock全局Block,copy出来的Block以及捕获了使用__block修饰的变量时,就变成了_NSConcreteMallocBlock堆Block。具体如下图所示:

20210402001.png

你可能感兴趣的:(Block)