block的数据结构

前言

Clang分析下block的底层结构

Clang看block

代码

int age = 30;
void (^yang)(void) = ^{
    NSLog(@"%d", age);
};
yang();

clang

// 存在isa  说明block是个对象
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 能捕获外部变量 有函数的效果
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zm_558cwfjs099fbm2r8kxg8wt00000gt_T_main_bfedde_mi_0, age);
        }

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(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 30;
        // 指向一个地址  说明block是个指针
        void (*yang)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        ((void (*)(__block_impl *))((__block_impl *)yang)->FuncPtr)((__block_impl *)yang);


        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

block生成的结构体

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

block的实现

void (*yang)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

block的匿名函数

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

block的调用

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

分析

  • 存在isa 说明block是个对象
  • 能捕获外部变量有函数的效果
  • 指向一个地址 说明block是个指针

完整分析 借一张网图
block的数据结构


LLDB调试block

将Clang编译出来的结构体copy到我们的mian文件,侨接一个结构体变量zyblock 通过lldb调试下block结构体的内部

main代码

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
 
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};
 
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        int age = 30;
        void (^yang)(void) = ^{
            NSLog(@"%d", age);
        };
        struct __main_block_impl_0 *zyblock = (__bridge struct __main_block_impl_0 *)yang;
        
        // 断点
        yang();
       
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

断点一
block的数据结构

断点二
block的数据结构
开启汇编
block的数据结构

分析

内部层级
block的数据结构
block大括号内部的代码的第一行的地址跟FuncPtr指针指向的地址是一样的

问题

block大括号内的代码是如何存放的?
跟FuncPtr指针有什么关系?

我们带着问题往下看
源码和cpp代码对照
1、 block 捕获的外部变量
2、 定义的block yang
3、 调用block
block的数据结构

生成 block ^{}

 // =右边的 ^{}  被编译成 ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age))   
void (^yang)(void) = ^{ NSLog(@"%d",age);};


void (*yang)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 讲 yang 指向  __main_block_impl_0结构体地址

分析

  • =右边的 ^{} 被编译成 ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age))
  • 将yang 指向 __main_block_impl_0结构体地址
  • 传递了__main_block_func_0 __main_block_desc_0_DATA age 3个参数
  • 将地址强转位 ((void (*)()) 类型
  • 将地址赋给 yang 指针

__main_block_impl_0

c++中 如果struct 有自定义构造方法且构造方法后面跟列表初始化 只能写到实现中

定义

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

实现

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  
  // 自定义构造方法  : 这个符号表示 后面跟了初始化列表    
  // 就是把外界传入的值先给 _age   然后  age = _age  这些属于c++ 语法  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_func_0

// 编译会把block块的代码放到这个函数里面
// 函数加static关键字 函数地址固定 函数在全局区  
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_zm_558cwfjs099fbm2r8kxg8wt00000gt_T_main_9da3e9_mi_0, age);
}

block的数据结构
参数传递如图:
block的数据结构
分析

  • __main_block_func_0函数存放的是block大括号里面的代码
    该函数被直接赋值给__main_block_impl_0构造函数的第一个参数fp指针
    fp又赋值给__block_impl结构体中的FuncPtr变量

  • FuncPtr变量指向__main_block_func_0函数 既block大括号内的代码
    也就是你在block 内部写不写代码 编译器都会创建一个 fun0函数

__main_block_desc_0

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { , sizeof(struct __main_block_impl_0)};

参数传递如图:
block的数据结构
分析

结构体有两个成员变量,同时定义了一个结构体变量__main_block_desc_0_DATA并对其赋值,reserved赋值为0,Block_size赋值为sizeof(struct __main_block_impl_0) 也就是结构体的内存大小

block 调用

__block_impl

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

block的数据结构

分析

  • 结构体有4个成员变量第一个成员变量为isa指针,我们知道这是oc对象(实例、类、元类)的专属标志,因为__block_impl 在__main_block_impl_0 内部 所以__main_block_impl_0是一个oc对象,而oc对象的本质就是在内存中为结构体
  • yang是一个 __main_block_impl_0 结构体指针
  • yang是一个结构体指针 所以访问成员FuncPtr使用 ->

问题
1、FuncPtr 并不是__main_block_impl_0 结构体的成员变量 他是__block_impl的成员变量 不应该是 yang->impl.FuncPtr吗?
2、yang进行了强制转换(__block_impl *)yang,而__block_impl结构体中是存在FuncPtr变量的 但这完全是两个不同的结构体,也不能强制转换引用啊?

父指子强转类型

c/c++经常会使用父指子的访问方式
如果父的前几个成员和子的前几个成员内存布局和顺序一致,可以使用父指子

如图
block的数据结构
__main_block_impl_0结构体在__block_impl结构体的大小范围内跟__block_impl结构体是完全重合的(其实就是同一片内存)只不过__main_block_impl_0结构体大小要比__block_impl结构体大 经过强制转换后block完全可以直接引用FuncPtr成员变量

侨接结构体
OC强转类型的使用
假设我们通过函数指针来调用 funyang 函数

代码

struct Car {
    NSString *isa;
    int price;
    int year;
    void *ptr;
    
}car;

struct Person {
    struct Car car;
    int age;
}person;

void funyang(struct Car *xmcar) {
    NSLog(@"1");
}

找到funyang函数地址我们有下面几种方式

- (void)test1 {
    person.car.ptr = (void *)funyang;
    struct Person *yang = &person;
    void *p1 = &funyang;
    void *p2 = person.car.ptr;
    void *p3 = yang->car.ptr;
    void *p4 = ((struct Car *)yang)->ptr;
}

输出

(lldb) p p1
(void *) $0 = 0x000000010bcabd70
(lldb) p p2
(void *) $1 = 0x000000010bcabd70
(lldb) p p3
(void *) $2 = 0x000000010bcabd70
(lldb) p p4 
(void *) $3 = 0x000000010bcabd70
(lldb)

这里我们为了帮助理解上面的block 我们选择强转方式

// yang 本身是 Person结构体指针 相当于上面的 __main_block_impl_0
// 讲yang转化为 Car结构指针 相当于上面的 __block_impl
((struct Car *)yang)->ptr;   

使用

- (void)test {
    
    // 给结构体成员赋值
    person.car.isa = @"模拟block";
    person.car.price = 20;
    person.car.year = 2021;
    
    // 讲funyang的函数地址 赋给 ptr
    person.car.ptr = (void *)funyang;
    person.age = 30;
    
    // 下面我通过指针访问 ptr
    struct Person *yang = &person;
    // 正常访问是这样
    yang->car.ptr;
    
    // 如果我强转成Car之后访问是这样
    ((struct Car *)yang)->ptr;
    
    /*
        我们要访问的函数 void funyang(struct Car *xmcar)
        函数类型 void (*)(struct Car *)
        参数类型 (struct Car *)
     */
 
    // 用p4指向ptr   ptr是函数指针 存的是funyang函数地址
    void *p4 = ((struct Car *)yang)->ptr;
    
    // 把p4强转成一个函数类型 ((void (*)(struct Car *)) p4);
    ((void (*)(struct Car *)) p4);
    
    // 调用函数并传入参数  参数类型是funyang函数的参数类型
    ((void (*)(struct Car *)) p4)((struct Car *)yang);
    
    // 把p4去掉
    ((void (*)(struct Car *)) ((struct Car *)yang)->ptr)((struct Car *)yang);
	
    

我们还可以这么访问

 // 我们还可以在用一个结构体指针 再次间接访问
    struct Car *xiaoyang = (struct Car *)yang;
    ((void (*)(struct Car *))((struct Car *)xiaoyang)->ptr)((struct Car *)yang);

输出

2021-11-24 20:40:10.167551+0800 07_本质[61067:32686116] 1
2021-11-24 20:40:10.167699+0800 07_本质[61067:32686116] 1

分析
yang指针调用1次 xiaoyang指针调用1次

如果要访问的函数参数是Person类型


void funyang(struct Person *xmcar) {
    NSLog(@"1");
}

调用

    struct Person *nb = &person;
    nb->car.ptr = (void *)funyang;
    ((void (*)(struct Person *)) nb->car.ptr)(nb);
	((void (*)(struct Person *)) (nb->car).ptr)(nb);
    void * p5 = nb->car.ptr;
	((void (*)(struct Person *)) p5)((struct Person *) nb);
	((void (*)(struct Person *)) nb->car.ptr)((struct Person *) nb);

理解了上面的oc例子 再看 block代码就简单多了,其实就是

代码

void (^yang)(void) = ^{};
yang();

编译生成

void (*yang)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)yang)->FuncPtr)((__block_impl *)yang);

简化

void (*yang)(void) = &__main_block_impl_0(参数1,参数2,参数3)
yang->FuncPtr(参数)

举例

在苹果的源码里面,经常使用这样的方式,理解了block 其他地方也一样 我们以dyld源码为例

block的数据结构

block的数据结构
block的数据结构
block的数据结构

block内存分布

图一源码

block的数据结构
图二 查看Block匿名函数地址
block的数据结构
分析

1、找到p对象地址
2、找到结构体__main_block_impl_0地址
3、找到__main_block_impl_0结构体中 _FuncPtr 地址

图三 查看 block结构体大小 和 i捕获的auto变量 int a 的值
block的数据结构
分析

1、找到p对象地址
2、p对象偏移8字节找到结构体__main_block_impl_0地址
3、_main_block_impl_0首地址也是__block_impl结构体地址 __block_impl占24字节
4、_main_block_impl_0首地址偏移24字节是__main_block_desc_0结构体地址__main_block_desc_0 占据8字节
5、__main_block_desc_0首地址偏移4字节 就是Blocksize 地址 blocksize = 24+8+4 = 36
6、_main_block_impl_0首地址偏移 32字节就是 int a的地址

结论

  • block本质是一个oc对象,以block_impl_0结构体形式存放在内存中
  • block本身也是一个指针,存放的是该结构体的内存地址
  • 遇到 ^{} 编译器内部会生成一个 fun0函数 把我们写的代码块放到 fun0函数中 ,然后把fun()函数的入口地址 存放到 block_impl_0 结构中的成员变量 FuncPtr 中
  • FuncPtr Func代表函数 prt代表指针地址 ptr point adress
  • block_impl是函数调用结构体 我们使用block() 进行block调用时候其实就是找 func地址
上一篇:Java日期解析具有微秒或纳秒精度


下一篇:[鸿蒙]鸿蒙开发之HML(三):block 标签