深入了解Block的奥秘

前言

block可以叫回调代码块,是iOS开发中至关重要的形式之一。不同的编程语言都会用到block, 只是体现形式有所不同,例如c/c++函数指针javascript叫它闭包。它用简单的方式帮我们解决了很多复杂的问题。但是还是一些区别:

  • block的代码是内联的,效率高于函数调用
  • block对于外部变量默认是只读属性
  • block被Objective-C看成是对象处理
  • block可读性更高,相比于delegate思路不会打断

block初识

先从一个简单的需求来说:传入两个数,并且计算这两个数的和,为此创建了这样一个block:

int (^sum)(int a, int b) = ^(int a, int b) {
    return a + b;
};
783864-3ad5d92333756aa7 (1).jpeg

block如何将变量传递及持有

测试代码:

传递原则:

  1. 捕获对象是基础类型变量,如int, double类型时,是值传递。
  2. 只有用__block修饰,变量才可以被修改,可以理解为指针传递。

我们来验证一下,变量a能否在block赋值之后被修改

    int a = 10;
    _foo.testBlock= ^() {
        _testView.backgroundColor = nil;//持有了self
        NSLog(@"a:%d", a);//10还是20?
    };
    a+= 10;
    _foo.testBlock();//block调用

输出结果: 10,它没有被外界改变。
表面上看起来block里面的a和外面的a一个东西,但实际上相当于生成了一个新的变量a' = a; 所以a值改变,a'不会跟着变;a'值改变,a也不会变。
解决方法:可以__block来修饰int a,也就是

__block int a = 10

最终a的值就是20,它被外界改变了,__block帮我们了解决问题。

但是Why? block内部是以什么形式存在,并捕获值的呢?接下来我们要一探究竟。

准备工作:clang命令

大家可以用clang(或者gcc) -rewrite-objc xxxxx.m命令来查看转化成的c++代码来了解内幕。如果你引用了UIKIt库,这个命令会报错,那个因为命令里没有指定sdk的版本,此时用下面的命令完美解决:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

__block的奥秘

不带__block的代码转化为cpp代码:

    int a = 10;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, a, 570425344)));

    a+= 10;

    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

带__block的代码转化为cpp代码:

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//对a的封装进行初始化

    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, (__Block_byref_a_0 *)&a, 570425344)));//这里把封装的结构体的地址传递了进去

    (a.__forwarding->a)+= 10;//a+10

        ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

__Block_byref_a_0的定义如下, 它对a进行了封装

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;//---->它就是传递进来的a
};

我们发现编译器把int a封装成了一个叫__Block_byref_a_0的结构体,int a只是个迷惑人的假象,所有对a的操作都是对结构体的操作。并且用&符号将结构体的地址传递给了block,所以后面a被修改时,block里面的结构体的a也被同时修改。

Block的父子层级关系及常见类别

1.我们可以打印一个Global block的类及父类的名字:(这段代码摘自facebook的FBRetainCycleDetector)

static Class _BlockClass() {
  static dispatch_once_t onceToken;
  static Class blockClass;
  dispatch_once(&onceToken, ^{
    void (^testBlock)() = [^{} copy];
    blockClass = [testBlock class];
    while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
      blockClass = class_getSuperclass(blockClass);
    }
    [testBlock release];
  });
  return blockClass;
}

结果是 **NSObject -> NSBlock ->__NSGlobalBlock **
事实上block有三种形式:

  • __NSGlobalBlock 全局 (未捕获变量)
  • __NSStackBlock 栈 捕获变量
  • __NSMallocBlock 堆 捕获变量

在 ARC 中,捕获外部了变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量在这个过程中会执行 _Block_copy 将原有的 NSStackBlock 变成 NSMallocBlock;但是如果 block 没有被赋值给某个变量,那它的类型就是 NSStackBlock;没有捕获外部变量的 block 的类会是 NSGlobalBlock 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。

2.那什么时候在堆上,什么时候在栈上呢?
在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:

  1. 调用Block的copy方法
  2. 将Block作为函数返回值时
  3. 将Block赋值给__strong修改的变量时
  4. 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时

3.block用strong修饰还是copy修饰呢?
实际上调用retain方法时, block会调用copy方法,所以这两种修饰是相同的。但是为了语义的明确,推荐用copy修饰。

 self.testBlock = ^() {
 }

反编译代码(也可以打断点,切换到汇编模式查看汇编代码)

 void -[Foo setTestBlock:](void * self, void * _cmd, void * arg2) {
 objc_storeStrong(var_18, arg2);
 rax = objc_retainBlock(0x0);
 rdi = self->_testBlock;
 self->_testBlock = rax;
 [rdi release];
 objc_storeStrong(0x0, 0x0);
 return;
 }

runtime源码如下,实际上还是调用了block_copy方法

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

循环引用

开头说过,block在iOS开发中被视作是对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放:

    self.foo.testBlock= ^() {
        self.view.backgroundColor = nil;//持有了self
        NSLog(@"a:%d", a);//10还是20?
    };

严格意义上讲循环引用是因为形成了一个环状引用,参与者可能是多个,并非只有2个,这会造成这个环上的所有对象都无法被释放

解决方法:用__weak来修饰对象,这样对象被捕获后不会被强引用,引用计数器不发生变化,然后在真正用要到的时候再strong强引用,防止在使用过程中对象突然释放

    __weak __typeof__ (self) wself = self;
    self.foo.testBlock = ^() {
        __strong __typeof (wself) sself = wself;
        sself.view.backgroundColor = [UIColor whiteColor];
    };

结语

还是那句话:源码下面无秘密
苹果底层对于block实现真是煞费苦心。我们了解了原理,用起来会更加得深应手。

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

推荐阅读更多精彩内容