Objective-C block 底层详解

笔者前几天做了一道笔试题:默认情况下 block 是不能修改外面的 auto 变量的,解决办法? 思索了好久才想起可以用 __block 或者 static 修饰符来修饰 auto 变量。看来有必要好好探索一下 block 的相关知识。

目录

1. block 的使用

1.1 什么是 block ?

block 的声明

block 的定义

block 的调用

2. block 的底层数据结构

3. block 的变量捕获机制 

3.1 auto 自动变量

3.2 static 类型的局部变量 

3.3 全局变量

为什么局部变量需要捕获,而全局变量不用呢?

3.4 __block 修饰的变量

3.4.1 __block 的使用:

3.4.2  __block 修饰符


​​​​​​​

1. block 的使用


1.1 什么是 block ?

块,封装了函数调用以及调用环境的 OC 对象。

  • block 的声明

// 1.
@property (nonatomic, copy) void(^myBlock1)(void);
// 2.BlockType:类型别名
typedef void(^BlockType)(void);
@property (nonatomic, copy) BlockType myBlock2;
// 3.
    // 返回值类型(^block变量名)(参数1类型,参数2类型,...)
    void(^block)(void);
  • block 的定义

    // ^返回值类型(参数1,参数2,...){};
    // 1.无返回值,无参数
    void(^block1)(void) = ^{
        
    };
    // 2.无返回值,有参数
    void(^block2)(int) = ^(int a){
        
    };
    // 3.有返回值,无参数(不管有没有返回值,定义的返回值类型都可以省略)
    int(^block3)(void) = ^int{
        return 3;
    };
    // 以上Block的定义也可以这样写:
    int(^block4)(void) = ^{
        return 3;
    };
    // 4.有返回值,有参数
    int(^block5)(int) = ^int(int a){
        return 3 * a;
    };
  • block 的调用

    // 1.无返回值,无参数
    block1();
    // 2.有返回值,有参数
    int a = block5(2);

2. block 的底层数据结构


通过 Clang 将以下的 OC 代码转换为 C++代码

// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
//main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"我是 block");
        };
        block();
    }
    return 0;
}

转换为 C++ 代码 : 

struct __main_block_impl_0 {
  struct __block_impl impl;// block 结构体
  struct __main_block_desc_0* Desc;// block 的描述对象
/*
block 的构造函数
 ** 返回值:__main_block_impl_0 结构体
 ** 参数一:__main_block_func_0 结构体
 ** 参数二:__main_block_desc_0 结构体的地址
 ** 参数三:flags 标识位
*/
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;// &_NSConcreteStackBlock 表示存储在栈上
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block 结构体
struct __block_impl {
  void *isa;//block 的类型
  int Flags;
  int Reserved;
  void *FuncPtr;// block的执行函数指针,指向__main_block_func_0
};

//封装了 block 中的代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_03dcda_mi_0);
        }


static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;// block 所占的内存空间
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
/*
         ** void(^block)(void) = ^{
                NSLog(@"调用了block");
            };
         ** 定义block的本质:
         ** 调用__main_block_impl_0()构造函数
         ** 并且给它传了两个参数 __main_block_func_0 和 &__main_block_desc_0_DATA
         ** __main_block_func_0 封装了block里的代码
         ** 拿到函数的返回值,再取返回值的地址 &__main_block_impl_0,
         ** 把这个地址赋值给 block
         */
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
/*
         ** block();
         ** 调用block的本质:
         ** 通过 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函数地址,直接调用
         */    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

3. block 的变量捕获机制 

  • 对于全局变量,不会捕获到 block 内部,访问方式为直接访问;
  • 对于 auto 类型的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为值传递;
  • 对于 static 修饰的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为指针传递。
  • 对于对象类型的局部变量,block 会连同其所有权修饰符一起捕获。

3.1 auto 自动变量

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"%d",age);
        };
        block();
    }
    return 0;
}
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_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_40c716_mi_0,age);
        }

可以看出:

  • 在 __main_block_impl_0 结构体中会自动生成一个相同的 age 变量
  • 构造函数 __main_block_impl_0( ) 中多出了一个 age 参数,用来捕获外部的变量

由于是传递方式为值传递,所以我们在 block 外部修饰 age 变量时,不会影响到 block 中的 age 变量

3.2 static 类型的局部变量 

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

    static int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    age = 20;
    block();
    // 20
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_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_cb7943_mi_0,(*age));
        }

可以看出:

  • __main_block_impl_0 结构体中生成了一个相同类型的 age 指针
  • __main_block_impl_0 构造函数多了个参数,用来捕获外部的 age 变量的地址

由于传递方式是指针传递,所以修改 局部变量 age 时,age 的值会随之变化

3.3 全局变量

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int height = 10;
static int age = 20;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"%d,%d",height,age);
        };
        block();
    }
    return 0;
}
int height = 10;
static int age = 20;

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_7a340f_mi_0,height,age);
        }

可以看出:

  • __main_block_impl_0 结构体中,并没有自动生成 age 或 height 全局变量,也就是说没有将变量捕获到 block 内部

那么就有一个问题:

为什么局部变量需要捕获,而全局变量不用呢?

  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static 变量会一直保存在内存中, 所以捕获其地址即可。

3.4 __block 修饰的变量


3.4.1 __block 的使用:

终于来到那个问题:

默认情况下 block 是不能修改外面的 auto 变量的,解决办法?

  • 变量用 static 修饰(原因:捕获 static 类型的局部变量是指针传递,可以访问到该变量的内存地址)
  • 全局变量
  • __block(我们只希望临时用一下这个变量临时改一下而已,而改为 static 变量和全局变量会一直在内存中)

3.4.2  __block 修饰符

  • __block 可以用于解决 block 内部无法修改 auto 变量值的问题;
  • __block 不能修饰全局变量、静态变量;
  • 编译器会将 __block 变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));
  • 加 __block 修饰不会修改变量的性质,它还是 auto 变量;
  • 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;
  • 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        void(^block)(void) = ^{
            age = 20;
            NSLog(@"block-%d",age);
        };
        block();
        NSLog(@"%d",age);
    }
    return 0;
}
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_75529b_mi_0,(age->__forwarding->age));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

可以看出:

  • 编译器会将 __block 修饰的变量包装成一个__Block_byref_age_0对象;
  • 以上age = 20;的赋值过程为:通过 block 结构体里的(__Block_byref_age_0)类型的 age 指针,找到__Block_byref_age_0结构体的内存(即被 __block 包装成对象的内存),把__Block_byref_age_0结构体里的 age 变量的值改为20。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值