OC底层原理探索之block分析下

702 阅读6分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

block底层编译

我们在main函数写一个最简单的block,然后转换为cpp文件查看一下。

int main(){
    __block int a = 18
   // __Block_byref_a_0 int a = 18; 2者相等
    void(^block)(void) = ^{
        a++;
        printf(" - %d",a);
    };
    
    block();
    return 0;
}
int main(){
    // int a = 18;
     __Block_byref_a_0 a = {
     (void*)0,
     (__Block_byref_a_0 *)&a,
      0,
      sizeof(__Block_byref_a_0),
      18};
    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这个结构体的构造函数。

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

我们观察这个结构体,里面有一个参数a,如果block没有捕获外部变量,重新xcrun一下,发现这个结构体里面就没有了成员变量a。所以得出结论:block捕获就生成了相应的成员变量 ​

还有一个细节,在编译阶段的时候默认的是一个NSConcreteStackBlock,但是我们上一节了解到如果捕获了外部变量,应该是一个堆区的Block,所以在运行时的有一些操作把栈变成了堆。 ​

__main_block_impl_0的第一个参数__main_block_func_0是什么?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; 

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

看样子有点像是block里面的执行函数,把它赋值给了结构体__main_block_impl_0中的impl.FuncPtr,后面block调用的时候执行的也是这个ptr(__block_impl *)block)->FuncPtr,这也是block的函数式保存 ​

__block做了什么

我们加了__block之后,编译发现多了一个这个结构体

  __Block_byref_a_0 a = {
       (void*)0,
       (__Block_byref_a_0 *)&a,
       0,
       sizeof(__Block_byref_a_0),
       18
   };

上面的写法是__Block_byref_a_0结构体的初始化

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

此时的__forwarding = &a也就是指向了a的地址。在上面的__main_block_impl_0初始化中, a(_a->__forwarding)__Block_byref_a_0 a__forwarding指针默认传递给了成员变量a,所以此时的__Block_byref_a_0 *a = __cself->a;这里的a的地址和外部参数a的地址一样,这也就是为什么加了__block修饰生成了__Block_byref_a_0 的结构体,传给block的是指针的地址,所以能达到修改同一片内存空间的效果。 ​

block汇编分析得到签名copy的过程

打开debug汇编模式,定位到了一个函数objc_retainBlock,打开libobjc源码 image.png 在工程中下符号断点_Block_copy定位到了libsystem_blocks.dylib这个库里面,使用真机环境在retain的时候打印下寄存器的值 image.png 所以在经过了这个函数之后,block就从栈copy到了堆。signature这个签名是block的签名@? image.png

Block_layout结构

我们在libclosure中找到了这个_Block_copy函数

void *_Block_copy(const void *arg) {
 		//.... 下面就是堆和栈的 但是我们知道编译阶段的默认是个栈区
        // Its a stack block.  Make a copy. -> heap
        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 -- 对象 isa 联合体位域
        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);
        // isa重新标记为堆区
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

这这里我们找到了block的结构体Block_layout

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

点击这个Block_descriptor_1发现并没有上面lldb读出来的copydispose函数,但是我们却在Block_descriptor_2结构体中发现了这两个函数。

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

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy; // 8
    BlockDisposeFunction dispose; // 8
};

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

全局搜索找到Block_descriptor_2的get方法,这里平移Block_descriptor_1内存空间大小就等得到Block_descriptor_2同理再平移一个Block_descriptor_2的大小可以得到Block_descriptor_3 这里的2和3都是可选的

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

所以上面控制台打印的那个block有签名和copy``dispose函数,所以有Block_descriptor_2Block_descriptor_3,我们可以查看x0的内存地址来验证下

image.png

第一个0x00000001f7d94580Block_layout的isa,平移两个8字节得到了0x000000019e4aca38也就是invoke函数的地址,Block_descriptor_1的内存为block再平移8字节到了0x00000001fd099a18,查看当前的内存空间。

image.png

可以跟上面对比验证下蓝色框出来的0x000000019e49e308地址就是copy的地址,红色框出来的0x000000019e49e310就是dispose的地址。第一行就是Block_descriptor_1内存空间16个字节,第二行是 Block_descriptor_2的内存空间16个字节,第三行开始就是Block_descriptor_3的内存空间。我们用const char *接收一下,也能拿到block的签名

image.png

block捕获外部变量

继续回到我们之前的cpp文件找到Block_descriptor_1Block_descriptor_2对应的结构

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

所以此时的Block_descriptor_2``copy = __main_block_copy_0

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

也就得出实际Block_descriptor_2 copy的时候调用的是_Block_object_assign函数,在libclosure源码中搜索这个函数,找到了block对外部捕获变量的类型的注释BLOCK_FIELD_IS_OBJECT

image.png

上面代码块__main_block_copy_0当中的BLOCK_FIELD_IS_BYREF=8 因为是用__block修饰的int变量。

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: // oc对象类型
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK: // block  值拷贝
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
		//...
      default:
        break;
    }
}

BLOCK_FIELD_IS_OBJECT:普通的对象类型_Block_retain_object_default = fn默认交给系统级别的ARC操作。*dest指向捕获变量object的地址空间,是两个不同的东西 BLOCK_FIELD_IS_BLOCK:值拷贝 BLOCK_FIELD_IS_BYREF: 进入到_Block_byref_copy代码里

// src points to stack
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
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy
copy->size = src->size;
(*src2->byref_keep)(copy, src);

这里的copy是一个临时变量, 此时这里的Block_byref *copy 所有的属性重新赋值,等于外部的a,(*src2->byref_keep)(copy, src)把结构体里面的对象重新保存。 此时copy的地址和捕获的变量的地址是一样的,这也就是为什么__block修饰的对象是指针拷贝。 总结: 如果是被__block修饰的变量,首先block会copy一下,从栈区到了堆区。接下来block捕获变量Block_byref,并且对Block_byref进行拷贝,并且Block_byref针对里面的object进行变量的copy,这个也就是__block三层拷贝的原理。

Block flag标识

  • 第1位,释放标记,-般常用BLOCKNEEDSFREE做位与操作,一同传人Flags,告知该block可释放。
  • 低16位,存储引用计数的值;是一个可选用参数
  • 第24位,低16是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值;
  • 第25位,是否拥有拷贝辅助函数(a copy helper function);
  • 第26位,是否拥有block析构函数;
  • 第27位,标志是否有垃圾回收;//0SX
  • 第28位,标志是否是全局block;
  • 第30位,与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于runtime时动态调用。