block的结构和原理

在 Block的类型和循环引用一文中,我们简单探索了block的类型和其循环引用。在本文中,我们将继续探索block的结构和原理。

Block结构

我们在代码中定义如下函数:

        void (^block)(void) = ^{

            printf("Hello");
        };

        block();

我们先将其编译其c++文件

xcrun -sdk iphoneos  clang -arch arm64 -rewrite-objc main.m -o main.cpp

我们将其main函数部分的c++代码单独拿出来:

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;
  }
};
__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("Hello ");
        }

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

我们可以看出其为__main_block_impl_0的结构体,在其构造函数中,运用了函数式编程思想将函数作为参数传入到block构造函数中。

Block捕获变量

无__block修饰

我们修改代码,打印变量a的值

 int a = 10;
 void (^block)(void) = ^{
      printf("Hello %d", a);
 };

 block();

同样将其编译为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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("Hello %d", 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)};                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          

在编译时期,就会捕获变量,将捕获的变量作为结构体的属性。在block进行调用时,会调用FuncPtr函数

block->FuncPtr(block) 

在调用时,block会作为参数,在block调用时,能够拿到block对象,通过block对象拿到属性值。此时 a的值是通过int a = __cself->a;的方式获取的。是一个 值拷贝

使用__block修饰

下面我们将变量使用__block修饰,从而修改变量a的值,然后我们来看下转化为c++文件后的变化。

        __block int a = 10;
        void (^block)(void) = ^{
            a++;
            printf("Hello %d", a);
        };

        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

            (a->__forwarding->a)++;
            printf("Hello %d", (a->__forwarding->a));
        }

经过__block修饰 之后多了一个 __Block_byref_a_0结构体,在block的构造函数中将__Block_byref_a_0的内存地址进行传入。
在函数调用的时候修改变量a的值,是通过如下方式进行修改变量a的值的

  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a)++;
            printf("Hello %d", (a->__forwarding->a));

通过 __Block_byref结构体里面保存的 变量指针,找到该变量进行操作。这样达到了在block调用时,能够修改外界变量的值。

Block的内存变化

在开始这一结探索之前我们先想一下以下问题:

  • 1,block为什么用copy修饰比用 strong修饰更好一些?
  • 2,block是如何从栈区拷贝到堆区的?
  • 3,block从栈区拷贝到堆区的时机是什么?

我们接着以上面的代码做为示例进行探讨。

    int a = 10;
    void (^block)(void) = ^{        \\  代码1
        NSLog(@"Hello - %d", a);
    };

    block(); \\  代码2

我们分别在 代码1代码2行设置两个断点。在Xcode的 Debug ->Debug Workflow中选中 Always Show Disassembly,来查看其混编代码。

混编代码.png

我们使用的是 真机设备,查看的是arm64架构下的混编。在第一个断点处,我们来查看其寄存器的值:

(lldb) register read
General Purpose Registers:
        x0 = 0x00000001eb34ef70  libsystem_blocks.dylib`_NSConcreteStackBlock
        x1 = 0x00000001d9800598  
        x2 = 0x0000000000000001
        x3 = 0x000000016b8d1e00
        x4 = 0x0000000000000010
        x5 = 0x000000016b8d1a0f
        x6 = 0x000000016b8d1b00
        x7 = 0x0000000000000000
        x8 = 0x0000000104534010  BlockTest`__block_descriptor_36_e5_v8�?0l
        x9 = 0x0000000104531fec  BlockTest`__29-[ViewController viewDidLoad]_block_invoke at ViewController.m:20
       x10 = 0x000000000000000a
       x11 = 0x00000002830fa308
       x12 = 0x0000000000000000
       x13 = 0x0000000000000000
       x14 = 0x00000001d97fce46  
       x15 = 0x00000001df8ae420  (void *)0x00000201bc228e46
       x16 = 0x00000001a14ddbbc  UIKitCore`-[UIViewController viewDidLoad]
       x17 = 0x00000001046dd0a4  libMainThreadChecker.dylib`__trampolines + 15992
       x18 = 0x0000000000000000
       x19 = 0x0000000000000000
       x20 = 0x0000000115d0a5e0
       x21 = 0x00000001eb392000  _MergedGlobals.9 + 16
       x22 = 0x00000001d970fcb7  
       x23 = 0x0000000000000001
       x24 = 0x0000000000000001
       x25 = 0x00000001e8377000  UIKitCore`_UITouchForceMessage._maximumPossibleForce
       x26 = 0x0000000115e07cb0
       x27 = 0x0000000280bde380
       x28 = 0x00000001d97192ee  
        fp = 0x000000016b8d1c70
        lr = 0x0000000104531f6c  BlockTest`-[ViewController viewDidLoad] + 64 at ViewController.m:18:5
        sp = 0x000000016b8d1c10
        pc = 0x0000000104531f8c  BlockTest`-[ViewController viewDidLoad] + 96 at ViewController.m:20:27
      cpsr = 0x60000000

读取通用寄存器x0的值:

(lldb) register read x0
      x0 = 0x00000001eb34ef70  libsystem_blocks.dylib`_NSConcreteStackBlock

x0寄存器现在存放的是 block,其类型为 _NSConcreteStackBlock,该数据结构在 libsystem_blocks.dylib里面。

我们跳过第一个断点,进入第二个断点

混编代码二.png

我们从汇编代码中可以看出,在block调用前,调用了 objc_retainBlock函数,此时,我们来查看block的类型。

(lldb) register read x0
      x0 = 0x000000028204d7d0
(lldb) po 0x000000028204d7d0
<__NSMallocBlock__: 0x28204d7d0>
 signature: "v8@?0"
 invoke   : 0x104e61fec (/private/var/containers/Bundle/Application/F19DA240-D120-4161-8FAA-AC62918B124D/BlockTest.app/BlockTest`__29-[ViewController viewDidLoad]_block_invoke)

在经过 objc_retainBlock函数后,block由 _NSConcreteStackBlock类型变为 __NSMallocBlock__类型。

我们通过objc4libclosure源码来查看一下objc_retainBlock函数究竟做了什么?

objc_retainBlock

通过查看 objc4源码我们可知其实现为:

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

_Block_copy函数在libclosure


void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock; \\1 

    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
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) { \\ 2
        return aBlock; 
    }
    else {
        // Its a stack block.  Make a copy.  \\ 3
        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;
    }
}

Block_layout

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
  • 1,block的底层数据结构为 Block_layout结构。
  • 2,通过BLOCK_IS_GLOBAL标识来判断是否为 NSGlobalBlock,如果是,不做任何处理。
  • 3,如果是非NSGlobalBlock,就会将其从栈区拷贝到堆区。
    具体流程如下:
  • 3.1: 根据block的size申请内存空间。
  • 3.2: 对block的标志符flag,invoke函数等进行拷贝。
  • 3.3: 修改已拷贝block的isa指针,指向_NSConcreteMallocBlock

通过对上面的探索,我们可以知道我们前面提出的三个问题的答案,
1: block捕获外界变量时,会由栈区拷贝堆区,因此使用copy修饰,更好一些。
2: block是通过_Block_cppy函数,拷贝过去的。
3: block拷贝的时机是:该block将要调用之前。

你可能感兴趣的:(block的结构和原理)