iOS block深入浅出

2020-04-24  本文已影响0人  FengyunSky

概要

block就是带有自动变量的匿名函数

语法结构如下:

^ 返回值类型 参数列表 表达式

其中返回值类型void时可省略,同理参数列表;

block变量结构同C语言函数指针类似,只是将*换为^符号:

int (^block)(int) = ^(int)(int a){
        NSLog(@"a:%d", a);
};

与通常的变量相同,block变量可赋值操作,也可作为函数的参数传递,也可作为返回值传递;

对于block类型变量,通常使用typedef定义,如下:

typedef int (^Block) (int);
//上面可修改为
Block block = ^(int)(int a){
        NSLog(@"a:%d", a);
}

对于截获自动变量说明,类似c语言中的值传递拷贝:若想对截获的自动变量数值类型变量进行同步修改,需要使用__block说明符。

原理分析

block本质

通过clang(v1100.0.33.17)编译器自带的-rewirte-objc选项将objc代码转换为c++进行分析,且都是基于ARC模式(添加-fobjc-arc),代码如下:

void (^block)(void) = ^{
    printf("block fired\n");
};

block();

转换后的代码核心代码如下:

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;
  __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) {
  printf("block fired\n");
}

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 argc, const char * argv[]) {
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  
  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

先从block实现最基本的结构体说起,其结构体信息如下:

struct __block_impl {
  void *isa;//isa指针
  int Flags;//标志位
  int Reserved;//保留位
  void *FuncPtr;//函数指针
};

其中初始化构造完成后的isa指针指向_NSConcreteStackBlockFuncPtr函数指针指向具体的block实现;如果对runtime原理熟悉的话,这个isa是不是似曾相识,其指向的是类对象进而指向元对象,最后指向root objectruntime通过isa指针使用c语言构造了一套完整的面向对象动态语言。为保持完整的面向对象的体系,block也使用了isa来指向类对象,这里指向的是_NSConcreteStackBlock,后续会对该类对象重点分析,因此,block其实也是一个object对象;

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;
  }
};

struct __main_block_impl_0结构体其实就是c++构造的public对象,包含了__block_impl类实例对象及struct __main_block_desc_0结构体对象(包含了类实例对象的大小);

block的调用函数如下,其中入参默认包含了struct __main_block_impl_0 *类型的__cself,与objcc++中的selfthis不谋而合,即是隐含传递了block的实例对象;

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("block fired\n");
}

具体的block()调用就是调用实例对象中指向的函数指针来实现;

block对象源码分析
在源码Block_private.h中的定义如下:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

typedef void(*BlockInvokeFunction)(void *, ...);
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

c++重写的结构体其实是一样的,结构图(较老版本)如下所示:

Block_layout结构图

__block说明符

对于block中具有截获的自动变量值未使用__block说明符时,其实就是在__main_block_impl_0中添加相应的实例成员,并通过构造函数时通过值传递捕获自动变量值;

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
  printf("block fired, a=%d\n", 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 argc, const char * argv[]) {
  int a = 1;
  //值传参
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

对用使用__block说明符后的转换代码如下:

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

  printf("block fired, a=%d\n", (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(int argc, const char * argv[]) {
  __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

与不含__block说明符不同之处在于:__main_block_impl_0结构体中使用了struc __Block_byref_a_0对象及__block对象__main_block_copy_0__main_block_dispose_0对象copy/dispose拷贝/释放函数,替换了简单地对应的实例变量成员;并且struc __Block_byref_a_0结构体中的成员__Block_byref_a_0 *__forwarding初始化指向了block实例对象成员自身(这个为啥多了该成员变量且指向自身后文阐述),如下图所示

Block_byref结构图
Block_private.h__block源码实现如下:
struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

c++转换的实质的一样的;

上面截获的自动变量需要使用__block说明符来修改其值,但对于静态变量、静态全局变量及全局变量如何呢?

可以从值传递拷贝的原理分析,block匿名函数主要就是用于保存block函数内部变量值用于后续调用,因此存在作用域的问题,对于静态全局变量及全局变量而言,因此不存在访问不到这些变量的情况,即block不会主动去截获这些变量,也就不需要使用__block说明符;但对于静态变量而言,超过作用域就无法访问,因此需要截获此类型变量的地址,具体如下:

//源码如下:
{
    static int a = 1;
  void (^block)(void) = ^{
  printf("block fired, a=%d\n", a);
  };
}
//转换后的c++代码如下
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;
  }
};

block存储域

以上分析的都是局部变量类型的block变量,其类对象类型为_NSConcreteStackBlock,若是静态类型或者全局类型则如何,是否与变量类型存储类型一致。

直接上代码并clang转换分析:

typedef void (^Block)(int);
Block g_block1 = ^(int count){
    printf("block1, count:%d", count);
};
Block g_block2, g_block3;

int main(int argc, const char * argv[]) {
    int a = 1, b = 2;
    g_block2 = ^(int count) {
        printf("block2, count:%d, b:%d", count, b);
    };
  g_block3 = ^(int count){
    printf("block3, count:%d", count);
  };
    
    g_block1(a);
    g_block2(a);
    
    return 0;
}

转换后的关键结构体如下:

//g_block1对应的结构体
struct __g_block1_block_impl_0 {
  struct __block_impl impl;
  struct __g_block1_block_desc_0* Desc;
  __g_block1_block_impl_0(void *fp, struct __g_block1_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//g_block2对应的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int flags=0) : b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//g_block3对应的结构体
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

g_block2 = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b));
g_block3 = ((void (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));

都是全局变量类型block但是其类对象类型不同,对于未截获自动变量的g_block1则为_NSConcreteGlobalBlock,对于需要截获自动变量的g_block2则为_NSConcreteStackBlock;但block类对象类型与《objective-C 高级编程 iOS与OSX多线程和内存管理》存在差异,并且转换后的c++代码未发现引用计数相关的函数调用,如_objc_retainBlock,带着疑惑将源码转换为汇编实现(通过xcode->Product->Perform Action->Assemble "xxxxx")一探究竟:

Assemble
Assemble
而实际的汇编实现block类对象类型为_NSConcreteGlobalBlock,并且对于ARC模式下自动添加了_objc_retainBlock/_objc_release函数调用来实现自动引用计数,实际的xcode编译环节选项中也不存在-rewrite-objc中间生成c++代码的过程(可能老的编译器存在),因此,clang -rewrite-objc转换后的代码可用于参考内部实现,具体要以实际生成的汇编代码为准;

【补充说明】在ARC模式下,_NSConcreteStackBlock类型的block对象都变成了_NSConcreteMallocBlock类型(官方说明中也提到Transitioning to ARC Release Notes),也可以通过demo打印block对象以验证;

#import <Foundation/Foundation.h>
typedef void (^Block)(int);
int main(int argc, const char * argv[])
{
    int a = 1;
    Block blk = ^(int count){
        printf("%d\n", a);
    };
    blk(a);
    NSLog(@"%@", blk);
  
    __weak Block blk1 = ^(int count){
        printf("%d\n", count);
    };
    NSLog(@"%@", blk1);
  
    __weak Block blk2 = ^(int count){
        printf("%d\n", a);
    };
    NSLog(@"%@", blk2);
    
    NSLog(@"%@", ^{printf("%d\n", a);});
    
    return 0;
}

打印结果如下:

block类对象类型
因此,对于ARC模式下,id类型以及对象类型变量隐含着__strong修饰符(默认使用了Block_copy函数拷贝),若block变量表达式含有外部变量,则为_NSConcreteMallocBlock;若表达式不含有外部变量,则为_NSConcreteGlobalBlock;若使用了__weak修饰符或者未赋值给隐含__strong修饰符的变量时,则为_NSConcreteStackBlock;

具体生成_NSConcreteGlobalBlock类对象类型的block场景如下:

与之_NSConcreteStackBlock相对应的存储类型,包括如下:

存储类型如图:


bloc类对象存储布局

那何种场景会是_NSConcreteMallocBlock类型呢,这个不难想象。对于上面的存储类型,若是_NSConcreteStackBlock栈类型,若超过其作用域,则内存会被释放,即无法再使用;要是需要超过其作用域调用,则需要定义为_NSConcreteGlobalBlock或者_NSConcreteMallocBlock,但_NSConcreteGlobalBlock类型受限于定义位置使用不能截获自动变量,因此_NSConcreteMallocBlock堆类型应运而生。

借用《objective-C 高级编程 iOS与OSX多线程和内存管理》书的插图就很容易理解:

block栈copy到堆原理图
那何时block变量会被从栈上复制到堆上?总结如下:

对于手动调用copy方法时,若重复调用会如何?

重复copy

__block变量存储域

上面说明了block变量存储域,对于其表达式中持有__block变量时,__block类型变量(隐含为__strong类型)是否也会被复制到堆上?答案:是,且上文中提到的__forwarding成员变量会指向堆中的结构体实例,因此无论栈上或者堆上的__block变量都可以访问同一个__block变量,如图所示:

__forwarding变化图
参考clang官方文档也可以说明__block变量会自动调用Block_copy()函数拷贝到堆上:

In garbage collected environments, the __weak variable is set to nil when the object it references is collected, as long as the __block variable resides in the heap (either by default or via Block_copy()). The initial Apple implementation does in fact start __block variables on the stack and migrate them to the heap only as a result of a Block_copy() operation.

但对于使用__weak修饰符的__block变量,则不会进行拷贝;

循环引用

ARC末实现block表达式会自动持有外部对象,若外部对象又持有该block对象,就会导致”循环引用“问题,如图所示:

#import <Foundation/Foundation.h>
typedef void (^Block)(int);
@interface MyObject : NSObject
@property (nonatomic, strong) Block blk;
@end

@implementation MyObject
- (id)init {
    self = [super init];
    if (self) {
        _blk = nil;
    }
    
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}
@end

int main(int argc, const char * argv[]) {
    MyObject *obj = [[MyObject alloc]init];
    //方式一:使用__weak修饰符避免引用计数增加
    __weak MyObject *weakObj = obj;
    Block blk = ^(int count){
        NSLog(@"count:%d, blk:%@", count, weakObj.blk);
        //方式二:手动nil释放obj对象,解决循环引用
//        NSLog(@"count:%d, blk:%@", count, obj.blk);
//        obj.blk = nil;
    };
    NSLog(@"blk:%@", blk);
    
    obj.blk = blk;
    obj.blk(1);
    
    return 0;
}
循环引用

解决方法:

小知识

xcode关闭arc

对于单个源文件关闭arc使用fno-objc-arc编译标志,对于整个工程则修改Objective-C Automatic Reference Counting修改为No;

Reference

libclosure-74

《objective-C 高级编程 iOS与OSX多线程和内存管理》

【LLVM】LLVM编译流程

谈Objective-C block的实现

浅谈 block - clang 改写后的 block 结构

史上最详细的Block源码剖析

上一篇 下一篇

猜你喜欢

热点阅读