Objective-C block 深入了解

本文中代码所依赖的环境是

Xcode 9.0 Apple LLVM 9.0.0 ARC环境

block的类型

Objective-C中block有三种类型:

  • __NSGlobalBlock__
  • __NSStackBlock__
  • __NSMallocBlock__

以上是通过NSLog打印不同类型log的输出结果。从结果可以看出分别对应着全局block栈block堆block

NSGlobalBlock

当block中没有使用block外部的任何局部变量时,即为全局block。全局block在内存的全局数据区

int a = 111;
// block without captured variable
block_type block = ^{
    int b = 0;
    printf("a : %d, globalVar:%d", b, globalVar);//此处使用了block内部的局部变量和全局变量
};
NSLog(@"block with no captured auto variable :%@", block);// block with no captured variable :<__NSGlobalBlock__: 0x1000020b8>

通常情况全局block使用的情况比较少。

NSStackBlock 和 NSMallocBlock

栈和堆block使用情况比较多。

栈block: 使用了(捕获)局部变量的block在创建之初,就是栈block。block在内存的栈区

int a = 111;
NSLog(@"stack block : %@", ^{NSLog(@"a:%d", a);}); // stack block : <__NSStackBlock__: 0x7ffedff74a98>

其实栈block不止以上情况会出现,文章后面会看到其他一些情况也会看到stack block

堆block: 栈block在一些时机,会copy到堆区中,即为堆block。堆block可以实现,当超出block所在的代码块区域时仍能保留并执行。

NSLog(@"malloc block : %@", [^{NSLog(@"a:%d", a);} copy]);// malloc block : <__NSMallocBlock__: 0x60400024f180>

细看NSStackBlock 和 NSMallocBlock

上面只是大体了解了下几种block,现在我提出了一些在使用block时经常遇到的问题:

  1. block如何实现捕获局部变量?
  2. 为什么直接捕获的局部变量不能修改,而使用__block修饰的变量则可以被修改?
  3. 使用weakSelf来避免循环引用时,是不是一定要配合strongSelf使用?

block如何实现捕获局部变量

可以通过查看block内部的实现来一探究竟,比如使用

clang -rewrite-objc block.m

该命令是将oc代码转为c++实现代码,因为oc或block对象本质上是一些结构体。如果提示cannot create __weak reference because the current deployment target does not support weak错误可以加上一些参数试下clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations block.m

通过将oc代码转为底层的结构体实现,能够分析出block捕获局部变量的过程。相关文章比较多,可以参考文末的参考。此处不再赘述,直接说结论:

block会将局部变量拷贝一份,作为自己的成员变量

其实这也可以解释,为什么在block中无法修改捕获到的局部变量,因为block中使用的变量其实已经不再是外部的局部变量了,而是block自己的成员变量。但我们期望的是修改外部变量,所以你改block的成员变量有啥用啊?索性编译器直接提示你,不能改!

__block修饰的变量为什么可以修改

__block的变量同样也会被block捕获,但注意,block会将局部变量包一层,可以认为包成了一个结构体,然后将结构体的指针作为block的成员变量。block通过该指针访问局部变量,既然是指针,那么block中也就可以修改外部的局部变量了。

文字多了太枯燥,上两张图缓和一下:

非_ _block变量
_ _block变量

图片来自唐巧的《谈Objective-C block的实现》

其实,非block变量和block变量在block的区别是 值传递 和 引用传递

block的内存管理

关于第三个问题,要涉及到block的内存管理

大家都知道,block循环引用一般是 self -> block -> self(或者self.property)这种结构导致互不释放资源。在此之前,有一个前置的问题是block为什么可以被持有?又为什么可以持有self?

因为堆block可以像oc对象一样,栈block是不行的

前面有提到,捕获了局部变量的block创建之初都是栈block,栈block就像一个函数一样,函数执行完,函数中的局部变量就都出栈,内存中就不存在了。但实际当中,我们的block可能要在函数执行完,仍要保留一段时间,比如网络请求:

NSURLSession *session;
NSURLRequest *request;
[session dataTaskWithRequest:request
           completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
           //do something    
}];

block能够保证超出作用域后仍能保留的原因其实是,栈block被copy到了堆中,堆block和oc对象类似,也是通过引用计数来进行内存管理

新的问题来了:

  • 谁来copy栈block到堆中?
  • 谁来管理堆block的引用计数?

栈block拷贝到堆中

本文只针对ARC环境,ARC环境系统API几乎为我们做了绝大多数copy工作:

  1. 当block被赋值给强引用时
  2. 当函数返回的是block时
  3. Cocoa框架中方法名含有usingBlock
  4. 一些没有usingBlock的系统方法也可以比如上面的网络请求
  5. GCD所有的方法
  6. 显示地对block执行copy方法

来一段代码瞅瞅

int a = 111;

// strong block with captured variable
void(^block2)(void) = ^{
    NSLog(@"a:%d", a);```};
NSLog(@"strong block with captured auto variable:%@", block2);// strong block with captured variable:<__NSMallocBlock__: 0x1004249f0>

// weak block with captured variable
__weak void(^block1)(void) = ^{
    NSLog(@"a:%d", a);
};
NSLog(@"weak block with captured auto variable:%@", block1);// weak block with captured variable:<__NSStackBlock__: 0x7ffeefbff550>

// get block from method
NSLog(@"get block from method : %@", [self getBlock]);// get block from method : <__NSMallocBlock__: 0x600000447a40>

// copy block explicitly
NSLog(@"stack block : %@", ^{NSLog(@"a:%d", a);}); // stack block : <__NSStackBlock__: 0x7ffedff74a98>
NSLog(@"malloc block : %@", [^{NSLog(@"a:%d", a);} copy]);// malloc block : <__NSMallocBlock__: 0x60400024f180>

// block as argument
[self printBlock:^{
    NSLog(@"%d", a);
}];

- (void)printBlock:(block_type)block {
    NSLog(@"block as argument : %@", block);// block as argument : <__NSStackBlock__: 0x7ffeeca47ac0>
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"block in dispatch_asyn:%@", block);// block in dispatch_asyn:<__NSMallocBlock__: 0x604000646330>
    });
}

- (block_type)getBlock {
    int a = 123;
    return ^{NSLog(@"%d", a);};
}

代码中能够看到在将block赋值给弱引用将block当做参数传递时也是stack block

strongSelf在避免循环引用中是否必须?

先举个避免循环引用的🌰

__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // do something
};
  • self.block中使用self为什么会产生循环引用

    在block拷贝到堆中时,block捕获到self,并把self拷贝到block内部作为自己成员变量(即使block中引用的是self.property,block内部访问该property时仍然是通过self->property的方式进行访问,所以仍然是捕获的self),同时会执行能够强持有self的操作,即使得self引用计数+1。block执行结束后,由于self持有block,所以不会释放,self由于被block的成员变量强持有,所以也不会被释放。于是循环引用

  • 先简单说下使用weakSelf为什么能避免循环引用:

    block捕获了weakSelf这个局部变量,当做自己的成员变量,但由于是weak的,所以作为block的成员变量的weakSelf,并不会强持有self(即不会让self的引用计数+1)。

  • 接下来,另一个问题是:strongSelf会不会造成循环引用呢?

    不会的,因为strongSelf是block内部的局部变量,strongSelf被赋值时,由于是强引用,所以会强持有self,让self的引用计数+1,但block执行结束后strongSelf的生命周期结束,self的引用计数-1,也就不会造成循环引用了。

那么strongSelf的必要性就容易解释了,执行block中的某些逻辑时,如果self释放了可能会造成严重的问题,为了执行block时不让self释放,我们要用strongSelf这个强引用局部变量控制着self。

至于会造成什么严重问题,请看下面两个🌰

__weak typeof(self) weakSelf1 = self;
block_type block4 = ^{

    // 例子1
    NSLog(@"weakSelf : %@", weakSelf); // weakSelf : <MyObject: 0x60000001fb10>
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"weakSelf in dispatch : %@", weakSelf1);// weakSelf in dispatch : (null)
    });
    
    //例子2,该例来自唐巧的博客
    // 如果正在执行networkReachabilityStatusBlock时,self释放了,多半情况下会崩溃
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
         weakSelf.networkReachabilityStatus = status;
       akSelf.networkReachabilityStatusBlock(status);
         }
     };
};
self.block = block4;

__strong typeof(weakSelf) strongSelf = weakSelf; 此处__strong是必要的,如果不写,则转换成c++源码后是 MyObject *const __weak strongSelf = weakSelf; 这样也就起不到对self强引用的作用

__weak typeof(self) weakSelf1 = self;
block_type block4 = ^{
    typeof(weakSelf) strongSelf = weakSelf1; /*注意:此处并没有使用__strong */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"strongSelf in dispatch : %@", strongSelf); /* strongSelf in dispatch : (null) */
    });
};
self.block = block4;
  • block中__strong typeof(self) strongSelf = weakSelf;的写法会导致对self的强引用吗?

不会。这要弄清楚什么是typeof()
- 首先,typeof()不是Objective-C,也不是标准C语言的操作符,是扩展的特性,需要有编译器支持才可以
- 另外,typeof()在编译期间决定类型,编译后的代码已经没有self相关内容了,所以不会对self有强引用

项目中例子分析

拿项目代码中block例子分析一把

- (void)startTask {
    Task *task = [self startTaskWithCompletion:^{
        NSLog(@"task : %@", task);// 此时block捕捉到的是未初始化的task,即nil。相当于值传递
        // do something with task
    }];
}

分析过程:

  1. 代码中的赋值过程是,先执行startTaskWithCompletion:,再对task赋值
  2. 初始化block时,task还是nil
  3. 所以block中task成员变量也是nil
  4. 赋值方法执行完后,task指向了新的task对象,但block中的task由于是值拷贝,所以还是nil
  5. 之后代码执行到block中时,task还是nil

解决方案:

改用引用传递,
Task *task -> _ _block Task *task

参考

Objective-C高级编程:iOS与OS X多线程和内存管理

AutomaticReferenceCounting-llvm官方文档

WorkingwithBlocks

Objective-C 拾遗:从Heap and Stack到Block

谈Objective-C block的实现

iOS 面试题(三):为什么 weakSelf 需要配合 strong self 使用

Swift与OC真正去理解Block解决循环引用的技巧

Block 梳理与疑问

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

推荐阅读更多精彩内容