block探究

    xiaoxiao2021-12-12  2

    前面提出了一个关于Block的问题,下面我们通过源码先来了解一下block,再来详细分析这个问题

    ======================以下如不特别说明,均是ARC运行环境======================

    首先我们把c语言中的变量分成以下几种:

    自动变量

    函数参数

    静态局部变量

    静态全局变量

    全局变量

    我们首先运行以下程序:

    #include<stdio.h> #import <Foundation/Foundation.h> int globalVar = 1; static int staticGlobalVar = 1; int main() { static int staticVar = 1; int var = 1; printf("初始:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&staticVar,staticVar); printf("var:地址:%p 值:%d\r\n",&var,var); void (^myBlock)(void) = ^{ globalVar++; staticGlobalVar++; staticVar++; printf("block内:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&staticVar,staticVar); printf("var:地址:%p 值:%d\r\n",&var,var); }; globalVar++; staticGlobalVar++; staticVar++; var++; printf("block外:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&staticVar,staticVar); printf("var:地址:%p 值:%d\r\n",&var,var); myBlock(); }

    打印信息如下:

    初始:

    globalVar:地址:0x105f9f290 值:1

    staticGlobalVar:地址:0x105f9f298 值:1

    staticVar:地址:0x105f9f294 值:1

    var:地址:0x7fff59c65414 值:1

    block外:

    globalVar:地址:0x105f9f290 值:2

    staticGlobalVar:地址:0x105f9f298 值:2

    staticVar:地址:0x105f9f294 值:2

    var:地址:0x7fff59c65414 值:2

    block内:

    globalVar:地址:0x105f9f290 值:3

    staticGlobalVar:地址:0x105f9f298 值:3

    staticVar:地址:0x105f9f294 值:3

    var:地址:0x60800004e6c0 值:1

    我们看到,只有var在block内部的地址发生了变化,我们可以猜想var在block内部只是进行了值的拷贝,因此在执行block的时候var的值依然是它的初始值1;

    block实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极为普通的C语言源代码被编译。

    现在我们就通过源码来验证一下,我们通过clang工具将含有Block语法的源代码变换为C++源代码:

    clang -rewrite-objc 文件名

    我们截取最要部分如下:

    struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; int globalVar = 1; static int staticGlobalVar = 1; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *staticVar; int var; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticVar, int _var, int flags=0) : staticVar(_staticVar), var(_var) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *staticVar = __cself->staticVar; // bound by copy int var = __cself->var; // bound by copy globalVar++; staticGlobalVar++; (*staticVar)++; printf("block内:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&(*staticVar),(*staticVar)); printf("var:地址:%p 值:%d\r\n",&var,var); } 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() { static int staticVar = 1; int var = 1; printf("初始:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&staticVar,staticVar); printf("var:地址:%p 值:%d\r\n",&var,var); void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticVar, var)); globalVar++; staticGlobalVar++; staticVar++; var++; printf("block外:\r\n"); printf("globalVar:地址:%p 值:%d\r\n",&globalVar,globalVar); printf("staticGlobalVar:地址:%p 值:%d\r\n",&staticGlobalVar,staticGlobalVar); printf("staticVar:地址:%p 值:%d\r\n",&staticVar,staticVar); printf("var:地址:%p 值:%d\r\n",&var,var); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); }

    分析:

    我们的myBlock通过__main_block_impl_0结构体的构造函数创建并初始化,我们先来看看它的命名,main代表Block所属的函数,0代表该Block在该函数中出现的顺序,如果还有其他Block,则为1,2...依次往后排。

    __main_block_impl_0结构自身其实就两个变量,一个是__block_impl结构,一个是__main_block_desc_0结构指针,其他的变量都是捕获进来的,我们先看看__block_impl结构:

    struct __block_impl { void *isa;//和Class的isa一样 int Flags;//一些标识,下面会讲到 int Reserved;//预留的 void *FuncPtr;//指向Block实际执行的代码块 };

    本例中isa指向_NSConcreteStackBlock,Flags为0,FuncPtr指向__main_block_func_0,

    看到isa我们直接想到的就是每个类都有个Class类型的isa变量,

    typedef struct objc_class *Class;

    我从原始的Objc2的源码发现_NSConcreteStackBlock的定义如下:

    struct objc_class _NSConcreteStackBlock;

    说明Block的实际其实也是Object-C的对象,它的isa所指向的_NSConcreteStackBlock中存储了该类的信息。

    除了_NSConcreteStackBlock,Block还有_NSConcreteMallocBlock,_NSConcreteGlobalBlock两种类型,后面两种类型上面情况会出现呢?待会我们会讲到。

    上面我们看到Block的调用实际上就是执行FuncPtr指针所指的函数

    我们继续看看__main_block_desc_0结构

    static struct __main_block_desc_0 { size_t reserved;//预留的 size_t Block_size;//Block的大小 }

    本例中Block_size就是__main_block_impl_0的大小

    __main_block_func_0中的__cself相当于C++中的this指针,是编译器自动传进去的。

    我们从__main_block_impl_0结构体可以发现多了两个变量

    int *staticVar; int var;

    这两个变量中从Block的构造函数创建可以看出,staticVar捕获的是指针,而var捕获的是数值,因此,我们在Block外边对var进行自加操作,是不会影响Block内部的var的,而静态全局变量和全局变量完全不用捕获就可以直接使用,因为他们是全局的。

    这里我没有在Block内部对var++是因为编译器会报错,Block内部只能修改修饰为__block的变量,也就是我们上一篇文章讲到的题目,现在我们来分析一下:

    通过clang工具将其转换成C++如下:

    struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_i_0 *i; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref (i->__forwarding->i) ++; NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_zb4rs_592rqcsch9nb65xxgm0000gn_T_main_806a08_mi_1,&(i->__forwarding->i),(i->__forwarding->i)); } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0}; NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_zb4rs_592rqcsch9nb65xxgm0000gn_T_main_806a08_mi_0,&(i.__forwarding->i),(i.__forwarding->i)); void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344)); (i.__forwarding->i)++; NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_zb4rs_592rqcsch9nb65xxgm0000gn_T_main_806a08_mi_2,&(i.__forwarding->i),(i.__forwarding->i)); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); }

    我们对比这两个例子的编译器源码,发现__main_block_desc_0中多了copy和dispose两个函数指针变量,分别指向__main_block_copy_0和__main_block_dispose_0。

    570425344 就是0x22000000,也就是下面的BLOCK_HAS_COPY_DISPOSE|BLOCK_HAS_DESCRIPTOR

    enum { BLOCK_REFCOUNT_MASK = (0xffff), BLOCK_NEEDS_FREE = (1 << 24), BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), /* Helpers have C++ code. */ BLOCK_IS_GC = (1 << 27), BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_DESCRIPTOR = (1 << 29) };

    我们看到i被转换成了__Block_byref_i_0对象,并且Block捕获了这个对象,并且是通过指针的形式捕获进去的,程序里面所有对i的访问的都是通过i.__forwarding->i来实现的,这个__forwarding是个啥,创建它有啥意义,现在我们就从这个__forwarding来揭开谜团。

    首先我们来看个问题

    Block中可以存有超过其变量作用域的被截获对象的自动变量,变量作用域结束的同时,原来的自动变量被废弃,因此Block中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量,如何解决这个问题呢?

    Block通过将Block和__block变量从栈上复制到堆上的方法来解决这个问题。

    我们通过” clang -S -fobjc-arc 文件名 “将源码转换为汇编代码,我们会看到_objc_retainBlock的调用,如下:

    movq %rax, -48(%rbp) leaq -80(%rbp), %rdi callq _objc_retainBlock movq %rax, -40(%rbp) movq 16(%rax), %rcx

    我们再来看看_objc_retainBlock的实现:

    id objc_retainBlock(id x) { return (id)_Block_copy(x); }

    其实就是对Block执行了copy,将栈上的Block拷贝到了堆上,我们继续看看内部实现,

    void *_Block_copy(const void *arg) { return _Block_copy_internal(arg, WANTS_ONE); }

    最终是调用的_Block_copy_internal函数:

    源码在这里:http://llvm.org/svn/llvm-project/compiler-rt/trunk/lib/BlocksRuntime/runtime.c

    /* Copy, or bump refcount, of a block. If really copying, call the copy helper if present. */ static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; if (!arg) return NULL; aBlock = (struct Block_layout *)arg; //如果block是创建在堆上,引用计数加1,并返回当前block if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } //这个不用管 else if (aBlock->flags & BLOCK_IS_GC) { // GC refcounting is expensive so do most refcounting here. if (wantsOne && ((latching_incr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK) == 1)) { // Tell collector to hang on this - it will bump the GC refcount version _Block_setHasRefcount(aBlock, true); } return aBlock; } //如果block是全局的,直接返回 else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } //如果block是栈上的,那么拷贝到堆上 if (!isGC) { //根据block的大小在堆上申请一块内存 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; //对result初始化,将栈上的block的数据拷贝到堆上创建的block中去 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // 重新设置标识符和isa,表示result为堆上创建的block result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; result->isa = _NSConcreteMallocBlock; //如果block有copy和dispose实现,则调用copy方法,也就是我们之前说到的__main_block_desc_0中多出来的copy和dispose if (result->flags & BLOCK_HAS_COPY_DISPOSE) { //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock); (*aBlock->descriptor->copy)(result, aBlock); // do fixup } return result; } //以下请无视 else { //… } }

    那么我们来看看copy方法的实现

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

    里面调用了_Block_object_assign,我们通过svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt获取源码

    void _Block_object_assign(void *destAddr, const void *object, const int flags) { //printf("_Block_object_assign(*%p, %p, %x)\n", destAddr, object, flags); if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) { if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) { _Block_assign_weak(object, destAddr); } else { // do *not* retain or *copy* __block variables whatever they are _Block_assign((void *)object, destAddr); } } else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) { // copying a __block reference from the stack Block to the heap // flags will indicate if it holds a __weak reference and needs a special isa _Block_byref_assign_copy(destAddr, object, flags); } // (this test must be before next one) else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) { // copying a Block declared variable from the stack Block to the heap _Block_assign(_Block_copy_internal(object, flags), destAddr); } // (this test must be after previous one) else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) { //printf("retaining object at %p\n", object); _Block_retain_object(object); //printf("done retaining object at %p\n", object); _Block_assign((void *)object, destAddr); } } 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_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */ BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */ BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */ }; static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { struct Block_byref **destp = (struct Block_byref **)dest; struct Block_byref *src = (struct Block_byref *)arg; if (src->forwarding->flags & BLOCK_IS_GC) { ; // don't need to do any more work } //前面通过forwarding初始化的时候flags是置0的 else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) { //printf("making copy\n"); // src points to stack bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)); // if its weak ask for an object (only matters under GC) struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak); copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack //这两条为关键语句,解释了为什么block外的i和初始的i地址发生了变化,将堆上的i的forwarding指向自己,将栈上的i的forwarding指向了堆上的i,这样无论通过栈还是堆上的i的forwarding访问的都是堆上的i,因此block创建以后,block外和block内的i的地址,编译器源码看到都是通过&(i.__forwarding->i)来访问的,因此最后输出的都是堆上的i的i实例变量的地址,而初始的i输出的地址还是栈上的i的i实例变量的地址。这样,就实现了无论__block变量配置在栈上还是堆上,都能够正确访问__block变量。 copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier) src->forwarding = copy; // patch stack to point to heap copy copy->size = src->size; if (isWeak) { copy->isa = &_NSConcreteWeakBlockVariable; // mark isa field so it gets weak scanning } if (src->flags & BLOCK_HAS_COPY_DISPOSE) { // Trust copy helper to copy everything of interest // If more than one field shows up in a byref block this is wrong XXX copy->byref_keep = src->byref_keep; copy->byref_destroy = src->byref_destroy; (*src->byref_keep)(copy, src); } else { // just bits. Blast 'em using _Block_memmove in case they're __strong _Block_memmove( (void *)©->byref_keep, (void *)&src->byref_keep, src->size - sizeof(struct Block_byref_header)); } } // already copied to heap else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } // assign byref data block pointer into new Block _Block_assign(src->forwarding, (void **)destp); }

    最终执行的是_Block_assign。相当于值拷贝。

    static void _Block_assign_default(void *value, void **destptr) { *destptr = value; }

    前面讲到Block有以下三种类型,

    _NSConcreteStackBlock 存储在栈上:ARC下weak指针指向的block还在栈上

    _NSConcreteMallocBlock 存储在堆上: _NSConcreteMallocBlock只需要对_NSConcreteStackBlock进行copy操作就可以获取,ARC下由于对象创建默认都是__strong类型的,因此创建block也一样,会直接在堆上创建

    _NSConcreteGlobalBlock 存储在数据区:1.全局的Block;2.不捕获自动变量(也就是Block内部没有用到自动变量,可以使用全局变量,静态全局变量和静态局部变量)

    我们再来看看哪些情况编译器会自动将Block从栈复制到堆上

    typedef int (^blk_t)(int); blk_t func(int rate) { return ^(int count){return rate*count;}; }

    汇编源码如下:

    Ltmp2: .cfi_def_cfa_register %rbp subq $48, %rsp movl
    转载请注明原文地址: https://ju.6miu.com/read-900177.html

    最新回复(0)