iOS探索 - block 原理

181 阅读12分钟

我们对于事件的传递的方法有代理、通知、block等,其中block应该是我们写的最多的了,GCD、锁、系统动画、AFNetworking、Masonry等系统和第三方库,大部分也使用着block,接下来我们通过源码来分析一下block的底层原理.

block类型

Block根据其类型可以分为三类:

  • 全局Block(NSGlobalBlock
  • 栈Block(NSMallocBlock
  • 堆Block(NSStackBlock) 具体区分原则是: 而其区分的规则是:

如果没有引用局部变量,或者只引用了静态变量或全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针持有,就是堆Block,如果没有则为栈Block。

- (void)testBlock {
    static int testI;
    
    void(^block)(void) = ^{};
    NSLog(@"block - %@",block);
    
    // 使用了静态变量
    void(^block1)(void) = ^{
        testI = 3;
    };
    NSLog(@"block1 - %@",block1);

    // 使用局部变量
    NSInteger i = 1;

    // 没有使用强指针引用过
    void (^__weak block2)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block2 - %@",block2);

    void(^block3)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block3 - %@",block3);

    // 使用局部变量, 又使用静态变量
    void(^block4)(void) = ^{
        NSLog(@"block %ld", i);
        NSLog(@"block static %d", testI);
    };
    NSLog(@"block4 - %@",block4);

    // 使用强指针引用过,再使用若指针引用
    void(^ __weak block5)(void) = block3;
    NSLog(@"block5 - %@",block5);
}

WX20220613-110758@2x.png

block修饰

我们定义block属性,一般都是用copy进行的修饰,原因是block在创建的时候,内存是分配在栈上的,栈上的内存是由系统控制的,随时被销毁,所以需要使用copy将栈上的block拷贝到堆上.

block本质

我们使用clang,将block代码进行编译,看其底层实现

NSString *mallocStr = @"testString";
void(^block)(void) = ^{
    NSLog(@"%@", mallocStr);
};

编译后的样式

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString *mallocStr = __cself->mallocStr; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_d73ece_mi_1, mallocStr);
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mallocStr, (void*)src->mallocStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mallocStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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


NSString *mallocStr = (NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_d73ece_mi_0;

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mallocStr, 570425344));

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

我们看到block编译后变成__main_block_impl_0这种结构体,这个结构体内部的构造函数中有isa = _NSConcreteStackBlock,结构体内有我们使用的变量NSString *mallocStr; 我们可以得到Block的以下特点:

  • block也是个对象(存在isa指针)
  • block出生就是在栈上(isa指针指向_NSConcreteStackBlock)
  • block有捕获变量的能力,该变量已成员变量的形式存放在结构体中(__main_block_impl_0内部有str变量)

我们可以看到block创建就是在栈上,可是我们上面的block应该是堆block,那他是在什么时候变成的堆block呢?

我们在block前进行断电 - 运行程序 - 查看其汇编代码

WX20220613-115024@2x.png 我们可以看到他调用了objc_retainBlock函数,将该函数进行符号断点,我们可以看到其调用了一个_Block_copy的函数 WX20220613-115252@2x.png

// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // 如果 arg 为 NULL,直接返回 NULL
    if (!arg) return NULL;
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // 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
        // 将 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);
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

_Block_copy函数进行了在堆上开辟内存,copy栈block,copy栈block成员变量,isa重新指向堆block,_Block_call_copy_helper内部其实是调用的_Block_object_assign,该方法的实现我们下面再了解.

为什么__weak block2修饰后的仍然是栈block呢? 因为__weak修饰后的block不会调用objc_retainBlock函数,所以没有_Block_copy操作

Block 循环引用

在使用Block的时候,最容易出现的问题就是循环引用,self->block->self相互应用,导致self和block都无法释放.

self.block = ^{
    NSLog(@"%@", self.view);
};

而解决循环引用有一下几种办法:

__weak __strong协作
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    NSLog(@"%@", strongSelf.view);
};

block对象,并没有引用self,在执行block的时候strongSelf的生命周期只有在block内部,在block内部,self的引用计数+1,当执行完block,引用计数-1. 既没有引起循环引用,self在block内部也短暂的持有,避免block执行时外部self释放导致可能的崩溃.

__block 修饰局部变量
__block ViewController *vc = self;
self.block = ^{
    NSLog(@"%@", vc.view);
    vc = nil;
};

使用这种方式,同样也可以解决循环引用,但是要注意,block执行完一次,下一次执行之前要重新复制self,不然会出问题, block内部要将局部变量设为nil,否则还会循环引用,不推荐使用.

参数传递
self.block = ^(ViewController *vc){
    NSLog(@"%@", vc.view);
};
self.block(self);

通过block的参数进行传递,同样可以解决循环引用.

__block原理

上面我们通过clang命令知道,block其实会编译成__main_block_impl_0结构体,该结构体的第一个参数__main_block_func_0会在构造函数中赋值给impl.FuncPtr,这个func_o0我们在上面也得到了,其就是block的实现部分.结构体内部只是对这个实现进行了保存,并没有调用该实现.我们在使用block()时才会执行block的实现.

我们都知道block内部修改局部变量时需要__block修饰,而全局变量并不需要,这是为什么呢? 我们还是用clang来查看其内部实现

int b = 0;
static int c = 0;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int a = 0;
        __block int d = 0;
        void(^block)(void) = ^{
            a++; b++; c++; d++;
            NSLog(@"%d, %d, %d, %d", a, b, c, d);
        };
        block();
    }
    return 0;
}
int b = 0;
static int c = 0;
struct __Block_byref_d_0 {
      void *__isa;
    __Block_byref_d_0 *__forwarding;
     int __flags;
     int __size;
     int d;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __Block_byref_d_0 *d; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, __Block_byref_d_0 *_d, int flags=0) : a(_a), d(_d->__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_d_0 *d = __cself->d; // bound by ref
      int *a = __cself->a; // bound by copy
      (*a)++; b++; c++; (d->__forwarding->d)++;
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_1a3f6f_mi_0, (*a), b, c, (d->__forwarding->d));
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->d, 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, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        static int a = 0;
        __attribute__((__blocks__(byref))) __Block_byref_d_0 d = {(void*)0,(__Block_byref_d_0 *)&d, 0, sizeof(__Block_byref_d_0), 0};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a, (__Block_byref_d_0 *)&d, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_1a3f6f_mi_1, block);
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

我们看到 全局变量和全局静态变量,在func0中是直接使用的;局部静态变量被捕获变量的指针,使用(*a)直接修改指针指向地址的值,局部变量捕获是 __block将变量封装为__Block_byref_d_0结构体,__forwarding指针指向结构体本身(相当于构造函数,如果是对象则会生成copy,dispose,对参数进行复制操作),通过(d->__forwarding->d)的方式修改值.

__attribute__((__blocks__(byref))) __Block_byref_str_0 str = {(void*)0,(__Block_byref_str_0 *)&str, 33554432, sizeof(__Block_byref_str_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};

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

static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment,
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
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];
        ********/
        // 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
        // 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
        _Block_retain_object(object);
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;
      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
       // 使 dest 指向的拷贝到堆上object
        *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 指向的拷贝到堆上的byref
        *dest = _Block_byref_copy(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;
      default:
        break;
    }
}

// 当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      // 如果是 byref
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        // 对 byref 对象做 release 操作
        _Block_byref_release(object);
        break;
      // 如果是 block
      case BLOCK_FIELD_IS_BLOCK:
        // 对 block 做 release 操作
        _Block_release(object);
        break;
      // 如果是对象
      case BLOCK_FIELD_IS_OBJECT:
        // 默认啥也不干,但在 _Block_use_RR() 中可能会被 Objc runtime 或者 CoreFoundation 设置一个 release 函数,里面可能会涉及到 runtime 的引用计数
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

这里是对成员变量的类型进行了分类,如果是对象类型的,直接将对象的增加对象的引用计数,如果是Block类型,会对该Block也进行一次_Block_copy操作,如果是__block修饰的,会调用_Block_byref_copy.

// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
//    被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
//    原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
    // arg 强转为 Block_byref * 类型
    struct Block_byref *src = (struct Block_byref *)arg;
    // 引用计数等于 0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 为新的 byref 在堆中分配内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
        // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
        // one for caller 很好理解,那 one for stack 是为什么呢?
        // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        // 堆上 byref 的 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
        src->forwarding = copy;  // patch stack to point to heap copy
        // 拷贝 size
        copy->size = src->size;
        // 如果 src 有 copy/dispose helper
        if (src->flags & BLOCK_BYREF_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
            // 取得 src 和 copy 的 Block_byref_2
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            // copy 的 copy/dispose helper 也与 src 保持一致
            // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;
            // 如果 src 有扩展布局,也拷贝扩展布局
            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
                copy3->layout = src3->layout;
            }
            // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 如果 src 没有 copy/dispose helper
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // src 已经在堆上,就只将引用计数加 1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    return src->forwarding;
}

最终将byref的栈上的__forwrking指针和堆上的__forwrking指针都指向堆上的结构体,所以block内部修改变量,外部也同时修改了,因为外部的变量指针指向变成了堆上的结构体.

block底层结构

_block_copy等中我们看到会将block强转成Block_layout,block其实就是Block_layout结构体,其内部含有isa,也就是也是对象.

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

struct Block_layout {
    void *isa; // isa指向的是三种类型的block
    volatile int32_t flags; // contains ref count 附加信息,类似掩码
    int32_t reserved; // 保留变量
    // libffi ->
    BlockInvokeFunction invoke; // 函数实现的函数指针
    struct Block_descriptor_1 *descriptor; //存放copy,dispose,block大小,block签名
    // imported variables 捕获的变量
};

Block_descriptor_1、Block_descriptor_2、Block_descriptor_3是三个连续的内存空间,是通过内存偏移来获取相应的值

// 取得 block 中的 Block_descriptor_2,它藏在 descriptor 列表中
// 调用者:_Block_call_copy_helper() / _Block_call_dispose_helper
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock) {
    // Block_descriptor_2 中存的是 copy/dispose 方法,如果没有指定有 copy / dispose 方法,则返回 NULL
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    // 先取得 Block_descriptor_1 的地址
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    // 偏移 Block_descriptor_1 的大小,就是 Block_descriptor_2 的起始地址
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

// 取得 block 中的 Block_descriptor_3,它藏在 descriptor 列表中
// 调用者:_Block_extended_layout() / _Block_layout() / _Block_signature()
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock){
    // Block_descriptor_3 中存的是 block 的签名,如果没有指定有签名,则直接返回 NULL
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    // 先取得 Block_descriptor_1 的地址
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    // 先偏移 Block_descriptor_1 的大小
    desc += sizeof(struct Block_descriptor_1);
    // 如果还有 Block_descriptor_2,就再偏移 Block_descriptor_2 的大小,得到的就是 Block_descriptor_3 的地址
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}