ios block原理分析

Block本质上也是一个OC对象,它内部也有个isa指针,它是封装了函数调用以及函数调用环境的OC对象。
闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部上下文变量「也就是自由变量」,Block也是 Objective-C 对于闭包的实现。

一、block的种类

block总共有6种,来源libclosure源码。

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

我们首先介绍三种常见的block:

    void (^block1)(void) = ^{
        NSLog(@"-----");
    };
   
    int a = 10;
    void (^block2)(void) = ^{
        NSLog(@"----- %d", a);
    };
    
    NSLog(@"%@",block1);
    NSLog(@"%@",block2);
    NSLog(@"%@",^{
        NSLog(@"----- %d", a);
    });

打印结果:

2020-05-06 21:38:29.777677+0800 Block[16006:269295] <__NSGlobalBlock__: 0x101acb090>
2020-05-06 21:38:29.777890+0800 Block[16006:269295] <__NSMallocBlock__: 0x600001aa97a0>
2020-05-06 21:38:29.778054+0800 Block[16006:269295] <__NSStackBlock__: 0x7ffeee1334c8>

1.2 block循环引用

block的循环引用处理方式无非weakstrong__block修饰,不再详述,这里介绍一种传参解决方式。

typedef void(^KIBlock)(ViewController *);
@property (nonatomic, copy) KIBlock block;

self.block = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };

二、 clang分析

2.1 block的本质

先写一个简单的block实现,然后clang分析一下。

int main(void) {
    
    void (^block)(void) = ^{
        NSLog(@"-----");
    };
    block();
    
    return 0;
}

执行clang -rewrite-objc main.m -o main.cpp命令后,整理main.cpp文件:

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

//block结构体
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_yp_28c9wg8n09d25v3gszz9j_dstr91j7_T_main_3be554_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(void) {

    //block的声明  构造函数
    //原始:void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    
    //block调用实现
    //原始:((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    (block->FuncPtr)(block);
    

    return 0;
}

通过clang分析,我们不难发现,block本质就是一个结构体,一个对象;也解释了为什么需要执行block()实现调用。

2.2 值拷贝

声明一个int变量,并在block内打印

int main(void) {
    
    int a = 6;
    void (^block)(void) = ^{
        NSLog(@"----- %d", a);
    };
    block();
    
    return 0;
}

clang之后:

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) {
        //__cself是block结构体即自身,将block拷贝的值赋给新的局部变量a
        int a = __cself->a; // bound by copy

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

我们发现block结构体多了一个属性a,并通过构造函数将外部变量_a的值赋给a,并在函数实现时重新生成了一个局部变量a
所以block内部打印的a和外部的a是两个不同的变量,直接在block内部执行a++是危险的操作,有引发变量作用域混淆的风险,因此也不被允许。

2.3 指针拷贝

如果想要在block内部执行a++该怎么操作呢,那么就需要对a执行__block修饰。

int main(void) {
    
    __block int a = 6;
    void (^block)(void) = ^{
        a++;
        NSLog(@"----- %d", a);
    };
    block();
    
    return 0;
}

同样我们clang分析:

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

    (a->__forwarding->a)++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_yp_28c9wg8n09d25v3gszz9j_dstr91j7_T_main_540f42_mi_0, (a->__forwarding->a));
}

有没有发现,多了一个结构体__Block_byref_a_0,下面我们整理一下main方法:

int main(void) {

    /**
     struct __Block_byref_a_0 {
         void *__isa;
         __Block_byref_a_0 *__forwarding;
         int __flags;
         int __size;
         int a;
     };
     */
    __Block_byref_a_0 a = {
        (void*)0,
        (__Block_byref_a_0 *)&a,
        0,
        sizeof(__Block_byref_a_0),
        6
    };
    
    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修饰之后的变量a,编译成了一个结构体__Block_byref_a_0,并保存原始的变量值,然后传递一个结构体指针给block。

三、内存和签名

在了解block内存和签名前,我们先从openSource找一份block的开源源码libclosure-73。

3.1 block数据结构

首先我们来看下block的基础数据:

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

关于flags:

// Values for Block_layout->flags to describe block objects
enum {
    //释放标记,一般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    //存储引用计数的值,是一个可选用参数
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    //是否拥有copy、dispose辅助函数
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    //是否拥有析构函数
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    //是否是全局block
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    //与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

关于签名:

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

// 可选
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};
// 可选
#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
};

3.2 lldb调试

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

断点进入objc_retainBlock,然后读取x0寄存器「必须真机」register read x0

objc_retainBlock.png

x0寄存器.png

可以看到此时为__ NSStackBlock__即栈block,继续执行,会进入_Block_copy,在最后一行return代码处打断点:
_Block_copy.png

return.png

此时为__ NSMallocBlock__。

3.3 签名

block是匿名函数,那么作为一个函数,block肯定也有自己的签名,上面lldb调试打印的结果已经有体现。
我们在讲述block的源码中提到block有两个可选的参数Block_descriptor_2Block_descriptor_3。而block的签名信息就放在Block_descriptor_3中,一个名为signature的元素。
从上面的打印结果找到签名

signature: "v8@?0"

其中v表示返回值是void@?表示未知的对象,即为block。这和方法签名是有所不同的,方法签名一般是v@:这样的形式(此处只说返回值为void的场景),:表示SEL

四、StackBlock到MallocBlock的copy实现

在上面LLDB调试的时候,我们看到了一个函数_Block_copy,下面我们看下具体实现,代码已经加了注释。

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 栈 -> 堆 研究拷贝
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        //引用计数处理,没有走truntime下层,自己处理
        //内部实现为什么是+2:因为”BLOCK_DEALLOCATING = (0x0001)“,0x0001这个位置已经被占用
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        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
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

五、__blcok原理

我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。
__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中,进而在block内部也可以修改外部变量的值。
下面我们打印指针地址验证一下:

    __block int a = 0;
    NSLog(@"定义前:%p", &a);         //栈区
    void (^blcok)(void) = ^{
        a = 1;
        NSLog(@"block内部:%p", &a);    //堆区
    };
    NSLog(@"定义后:%p", &a);         //堆区
    blcok();
2020-05-09 12:07:37.350556+0800 block[8223:91646] 定义前:0x7ffee5d78cf0
2020-05-09 12:07:37.351701+0800 block[8223:91646] 定义后:0x60000011df58
2020-05-09 12:07:37.351863+0800 block[8223:91646] block内部:0x60000011df58

0x7:栈地址; 0x6:堆地址; 0x1:全局区。

下面我们通过源码分析,先来一段简单的block代码:

#import 

int main(void) {
    __block NSString *name = [NSString stringWithString:@"name"];
    
    void(^block)(void) = ^ {
           NSLog(@"--- %@", name);
       };
    
    block();
    
    return 1;
}

clang编译处理后的代码:

struct __Block_byref_name_0 {
    void *__isa;
    __Block_byref_name_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    NSString *name;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_name_0 *name; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_name_0 *_name, int flags=0) : name(_name->__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_name_0 *name = __cself->name; // bound by ref

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->name, 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(void) {
    //__attribute__((__blocks__(byref))) __Block_byref_name_0 name = {(void*)0,(__Block_byref_name_0 *)&name, 33554432, sizeof(__Block_byref_name_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_yp_28c9wg8n09d25v3gszz9j_dstr91j7_T_main_3b3b70_mi_0)};
    __Block_byref_name_0 name = {
        (void*)0,
        (__Block_byref_name_0 *)&name,
        33554432,
        sizeof(__Block_byref_name_0),
        __Block_byref_id_object_copy_131,
        __Block_byref_id_object_dispose_131,
        ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_yp_28c9wg8n09d25v3gszz9j_dstr91j7_T_main_3b3b70_mi_0)
        
    };
    
    
    //void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
    void(*block)(void) = &__main_block_impl_0(__main_block_func_0,
                                              &__main_block_desc_0_DATA,
                                              (__Block_byref_name_0 *)&name,
                                              570425344);

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

    return 1;
}

编译后,在main中,主要多了两个方法:__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131,一个结构体:&__main_block_desc_0_DATA

5.1 __Block_byref_id_object_copy_131

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

这里调用的是_Block_object_assign(下面具体分析),方法内部目标对象偏移了40字节,由于此时目标对象就是__Block_byref_name_0,偏移40字节正好是NSString *name。也就是说此时对name对象的内存地址做了一次copy。

struct __Block_byref_name_0 {
    void *__isa;                                        // 8
    __Block_byref_name_0 *__forwarding;                 // 8
    int __flags;                                        // 4
    int __size;                                         // 4
    void (*__Block_byref_id_object_copy)(void*, void*); // 8
    void (*__Block_byref_id_object_dispose)(void*);     // 8
    NSString *name;
};

5.2 &__main_block_desc_0_DATA

block的构造函数传进来一个结构体&__main_block_desc_0_DATA,该结构体在初始化的时候传入了__main_block_copy_0方法进行拷贝操作,它里面只有一个方法_Block_object_assign,实现如下(runtime.cpp文件):

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...   截获的是对象 
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable  截获的是block变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable   截获的是__block修饰的对象
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers  截获的是__weak修饰的对象
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

//
// When Blocks or Block_byrefs hold objects(自动捕获到变量) then their copy routine helpers use this entry point
// to do the assignment.
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        //截获的变量是对象,只需要赋值,引用计数不做任何处理,因为对象的引用计数是runtime底层自己处理的。
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
        //如果截获的变量是block对象,调用_Block_copy方法。
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
            
        *dest = _Block_byref_copy(object);
        break;

        ........
}

如果变量是__block修饰的对象,调用_Block_byref_copy方法。
它会重新申请一块堆内存,然后将截获的对象也就是上述例子中的__Block_byref_name_0结构体赋值给新的结构体,并将copy的对象和源对象的forwarding指针都指向新生成的结构体。其实也就是对__block修饰的对象做了一次拷贝动作,然后让他们都指向同一块内存区域达到修改其中一个两个都改变的目的。

static struct Block_byref *_Block_byref_copy(const void *arg) {
    // 创建一个临时变量
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack 
        // 1.申请堆内存空间
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        
        // 2. 给新申请的空间赋值
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        
        // copy的对象和源对象都指向堆内存的拷贝地址
        copy->forwarding = copy; // patch heap copy to point to itself 堆拷贝指向自己
        src->forwarding = copy;  // patch stack to point to heap copy 栈指向堆拷贝
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // 处理desc2 内存偏移取值 
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                // 处理desc3
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        } else {
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    } else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

copy最终的代码调用了一个方法byref_keep,那么这个方法是干什么的呢?
我们点击查看这个方法,发现他是Block_byref结构体的第五个参数,我们重新回到main.cpp文件找到__Block_byref_name_0结构体,在main方法初始化的时候,第五个参数正好对应__Block_byref_id_object_copy_131,即 byref_keep = __Block_byref_id_object_copy_131,完成对block修饰变量的内存拷贝,这个方法在前面已经介绍。

由上,我们可以得出结论:__block修饰的外部变量,在block内部可以修改,是因为发生了3次拷贝:

  1. _Block_copy,block的拷贝,从栈内存到堆内存。
  2. __main_block_copy_0,对新生成的结构体的拷贝。__block修饰的变量会生成一个名为__Block_byref_xxx_0结构体,将原来的进行了封装,然后把整个结构体地址指针传入block内部。(即,在block内部,把block外部的__Block_byref_xxx_0结构体,copy一份新的,然后block内外的结构体forwarding指针都指向新copy的结构体,保持block内外一致)
  3. __Block_byref_id_object_copy_131,对原来的(__block修饰)对象的内存的拷贝,存入第二步copy的结构体。

参考文档
苹果官方文档 Blocks and Variables
底层源码 libclosure-73

你可能感兴趣的:(ios block原理分析)