iOS底层原理 - 探寻block本质

面试题引发的思考:

Q: block的原理是怎样的?本质是什么?

  • block本质上也是一个OC对象,它内部也有一个isa指针。
  • block是封装了 函数调用 以及 函数调用环境 的OC对象。

1. block原理

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void (^block)(int, int) = ^(int a, int b){
            NSLog(@"this is block, a = %d, b = %d", a, b);
            NSLog(@"this is block, age = %d", age);
        };
        age = 20;
        block(1, 2);
    }
    return 0;
}

使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m将代码转化成C++语言:

OC代码转化为C++

将C++中强制转换代码去掉,以便阅读:

简化后代码

(1) 定义block

代码显示:

  • 定义block时调用了__main_block_impl_0函数,并将__main_block_impl_0函数地址赋值给了block。
    即:block底层就是__main_block_impl_0结构体

通过__main_block_impl_0找到__main_block_impl_0函数:

block底层结构 - __main_block_impl_0函数

代码显示:

  • __main_block_imp_0结构体的第一个成员是__block_impl结构体变量,__block_impl结构体的第一个成员是isa
    即:__main_block_imp_0结构体第一个成员是isa,也就是说block的底层就是一个OC对象。

  • __main_block_imp_0结构体内部有一个同名构造函数__main_block_imp_0,会对相关变量赋值并返回一个__main_block_imp_0结构体,然后将结构体的地址赋值给block。

定义block时__main_block_impl_0函数传入的3个参数:
1> _main_block_func_0参数:
_main_block_func_0

很明显,其中的NSLog(...);即我们写的NSLog(@"a = %d, b = %d, age = %d", a, b, age);语句;
即:_main_block_func_0函数把Block中要执行的代码封装到其内部。

2> &_main_block_desc_0_DATA参数:
&_main_block_desc_0_DATA

__main_block_desc_0中包含两个参数:
a> reserved:赋值为0
b> Block_size:存储__main_block_impl_0占用的空间大小。

3> age参数:

age是我们定义的局部变量。block中使用age,所以block会在声明的时候将age作为参数传入,即block会捕获age

因为block在定义时将age值传入存储在__main_block_impl_0结构体中,并在调动block时将age取出来使用,所以在block定义结束后对局部变量进行修改是无法被block捕获的。所以文章开始的代码中输出的age值为10,而非20

下面再看__main_block_impl_0函数:

block底层结构 - __main_block_impl_0函数

由以上分析可知:

  • __block_impl结构体中isa指针存储着_NSConcreteStackBlock地址;
  • _main_block_func_0函数把Block中要执行的代码封装到其内部,FuncPtr则存储着__main_block_func_0函数的地址;
  • Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。

(2) 调用block

// 调用block内部的代码 
// 简化版:block->FuncPtr(block, 1, 2);
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

通过__main_block_impl_0函数结构可知:

FuncPtr_main_block_impl_0第一个成员变量impl的成员变量,但是block调用时却直接通过block找到FuncPtr进行调用,这是为什么呢?

原因在于:(__block_impl *)block将block强制转化为__block_impl类型,而impl__main_block_impl_0结构体的第一个成员,所以impl__main_block_impl_0的首地址是一样的,因此指向_main_block_impl_0的首地址的指针也就可以被强制转换为指向impl的首地址的指针,并找到FuncPtr

FuncPtr则存储着__main_block_func_0函数的地址,因此通过block->FuncPtr()获取__main_block_func_0的地址,对其进行调用,进而执行block中的代码。并且_main_block_func_0函数第一个参数就是__main_block_impl_0类型的指针,也就是说将block传入__main_block_func_0函数中,进而取出block捕获的值。


(3) 总结

通过以上分析,我们对block底层结构有了基本的了解,由此可以分析出其中的结构关系:

block结构体内部之间的关系

2. block的变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制(capture)。

// 全局变量c, d
int c = 30;
static int d = 40;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 局部变量a, b
        // auto变量是声明在函数内部的变量,离开作用域就销毁
        // int a = 10; 则a为auto变量,会自动在前面添加auto关键字
        auto int a = 10;
        static int b = 20;
        void (^block)(void) = ^{
            NSLog(@"a = %d, b = %d, c = %d, d = %d", a, b, c, d);
        };
        a = 1;
        b = 2;
        c = 3;
        d = 4;
        block();
    }
    return 0;
}

// 打印结果
Demo[1234:567890] a = 10, b = 2, c = 3, d = 4

OC代码转化为C++查看变量调用方式:

OC代码转化为C++

(1) 局部变量

1> auto变量:
  • auto变量是声明在函数内部的变量,离开作用域就销毁,局部变量前面自动添加auto关键字。
  • auto变量会捕获到block内部,即block内部会专门新增加一个参数来存储变量的值。
  • auto变量只存在于局部变量中,访问方式为值传递。
2> static变量:
  • static变量在变量的作用域结束时并不会被系统自动回收
  • static变量会捕获到block内部,即block内部会专门新增加一个参数来存储变量的值的地址。
  • static变量只存在于局部变量中,访问方式为地址传递。

(2) 全局变量

全局变量在哪里都可以访问,所以block不用捕获全局变量,直接进行访问。

(3) 总结

block的变量捕获
  • block处理方式不同是由变量的声明周期决定的;
  • 局部变量都会被block捕获,auto变量值传递,static变量指针传递;
  • 全局变量不会被block捕获,直接访问。

Q: 那么以下情况block是否会捕获变量呢?

#import "Person.h"

@implementation Person
- (void)test {
    void (^block)(void) = ^{
        NSLog(@"-------- %p", self);
    };
    block();
}
@end

OC代码转化为C++查看变量调用方式:

OC代码转化为C++

由图可知:self会被block捕获

因为OC方法会默认传递两个参数self_cmd,两者都是局部变量,与我们的结论:局部变量会被block捕获符合。


3. block类型

前文可知:block的本质就是一个OC对象,所以block有类型。

下面我们探寻一下block的类型,首先关闭ARC:

关闭ARC
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
        void (^block)(void) = ^{
            NSLog(@"Hello");
        };

        NSLog(@"%@", [block class]);
        NSLog(@"%@", [[block class] superclass]);
        NSLog(@"%@", [[[block class] superclass] superclass]);
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

// 打印结果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSGlobalBlock
Demo[1234:567890] NSBlock
Demo[1234:567890] NSObject

由打印结果可知:
block最终继承自NSBlock,而NSBlock继承自NSObject,所以block的isa指针是来自于NSObject。也印证了block的本质就是OC对象


(1) block的类型及存放区域:

block的存放区域
  • block有三种类型:__NSGlobalBlock____NSMallocBlock____NSStackBlock__
  • 数据段中的__NSGlobalBlock__直到程序结束才会被回收;
  • 堆中的__NSMallocBlock__需要手动内存管理;
  • 栈中的__NSStackBlock__作用域执行完毕被回收。

(2) block类型总结:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. __NSGlobalBlock__:没有访问auto变量
        void (^block1)(void) = ^{
            NSLog(@"block1");
        };
        NSLog(@"%@", [block1 class]);
        // 2. __NSStackBlock__:访问了auto变量
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2 - %d", age);
        };
        NSLog(@"%@", [block2 class]);
        // 3. __NSMallocBlock__:__NSStackBlock__调用了copy
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}

// 打印结果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSStackBlock__
Demo[1234:567890] __NSMallocBlock__

输出打印结果可知block的三种类型:

上述代码转化为C++去查看源码发现三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。原因是runtime会对其类型进行了转变,所以以runtime运行时类型即打印出的类型为准。

总结可得:

block类型 内存区域 环境 复制效果copy
NSGlobalBlock 数据段 没有访问auto变量 什么也不做,类型不变
NSStackBlock 访问了auto变量 从栈复制到堆,类型改变为__ NSMallocBlock__
NSMallocBlock __ NSStackBlock__调用copy 引用计数增加,类型不变

栈中的__NSStackBlock__访问auto变量,作用域执行完毕被回收,如果调用block时已经销毁其内存,就会出现问题:

void (^block)(void);
void test() {
    // __NSStackBlock__:访问了auto变量
    int age = 10;
    block = ^{
        NSLog(@"age = %d", age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

// 打印结果
Demo[1234:567890] age = -272632568

由打印结果可知:
age没有打印出正确结果,这是因为__NSStackBlock__类型的block存储在栈中,test函数执行完毕后,栈内存中block所占用的内存被系统回收。

我们可以通过copyNSStackBlock类型的block转化为NSMallocBlock类型的block

void (^block)(void);
void test() {
    // __NSStackBlock__ 调用copy转化为 __NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"age = %d", age);
    } copy];
    [block release];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

// 打印结果
Demo[1234:567890] age = 10
  • MRC环境下,经常使用copy将栈上的block拷贝到堆中,然后手动调用release操作将其销毁即可;
  • ARC环境下,编译器会自动将栈上的block进行copy操作,将block复制到堆上。

4. ARC帮我们做了什么?

在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上:

  • block作为函数返回值时;
  • block赋值给__strong指针时;
  • block作为Cocoa API中方法名含有usingBlock的方法参数时;
  • block作为GCD API的方法参数时。

(1) block作为函数返回值时:

typedef void(^MyBlock)(void);
MyBlock myblock() {
    int age = 10;
    return ^{
        NSLog(@"-------- %d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block作为函数返回值
        MyBlock block = myblock();
        block();

        // MRC打印:__NSStackBlock__  (访问了auto变量)
        // ARC打印:__NSMallocBlock__
        NSLog(@"%@", [block class]);
    }
    return 0;
}

block访问auto变量时,block的类型为__NSStackBlock__
ARC将栈上的block进行一次copy操作,将block复制到堆上,并在适当的地方进行release操作,所以ARC打印block为__NSMallocBlock__类型。

(2) 将block赋值给__strong指针时:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block内没有访问auto变量
        MyBlock block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@", [block class]);

        // block内访问了auto变量,但没有赋值给__strong指针
        int age = 10;
        NSLog(@"%@", [^{
            NSLog(@"block1--------- %d", age);
        } class]);

        // block赋值给__strong指针
        MyBlock block2 = ^{
            NSLog(@"block2--------- %d", age);
        };
        NSLog(@"%@", [block2 class]);
    }
    return 0;
}
打印结果

由打印结果可知:
将block赋值给__strong指针时,RAC会自动进行一次copy操作。

(3) block作为Cocoa API中方法名含有usingBlock的方法参数时:

例如:遍历数组的block方法,将block作为参数

        NSArray *array = @[];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        }];

(4) block作为GCD API的方法参数时:

例如:GCD的一次性函数或延迟执行的函数

        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{

        });

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        });

MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容