Block相关杂记

1.Block的特性和使用场景

Block 是一种闭包语法,将代码像对象一样传递,最重要的特性是,Block 可以访问定义范围内的全部变量。
Block 可以在多种场合使用,常见的场合包括但不限于通知回调、动画、多线程等。

2.Block的结构和类型研究

对 Block 稍微了解的话,就会知道 Block 会在编译过程中,会被当作结构体进行处理。
其大致的结构如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

其中isa指针就指向表明 Block 类型的类。

根据 Block 在内存中的位置,一般可分为三种类型:

_NSConcreteGlobalBlock:全局的静态 block ,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的 block。这个类型的 block 要么是空 block ,要么是不访问任何外部变量的 block 。它既不在栈中,也不在堆中,我理解为它可能在内存的全局区。

_NSConcreteStackBlock:保存在栈中的 block,当函数返回时被销毁。该类型的 block 有闭包行为,也就是有访问外部变量,并且该 block 只且只有有一次执行,因为栈中的空间是可重复使用的,所以当栈中的 block 执行一次之后就被清除出栈了,所以无法多次使用。

_NSConcreteMallocBlock:保存在堆中的 block,当引用计数为0时被销毁。该类型的 block 都是由 _NSConcreteStackBlock 类型的 block 从栈中复制到堆中形成的。该类型的 block 有闭包行为,并且该 block 需要被多次执行。当需要多次执行时,就会把该 block 从栈中复制到堆中,供以多次执行。

以上内容引用自 Objective-C中的Block

为了验证以上结论,这里写了两个 block ,然后通过 clang 将其翻译成 C 语言。

A:

^{ printf("Hello, World!\n"); } ();

这是个空 block ,不涉及外部变量的拷贝。
通过 clang 翻译后,得到如下关键区域代码:

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) {
 printf("Hello, World!\n"); }

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)};

从C代码中可以看到,isa 指针指向的是 _NSConcreteStackBlock ,按照之前的理论,应该是指向 _NSConcreteGlobalBlock 。
这里通过查阅相关资料可知:

由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC 。所以这里我们看到 isa 指向的还是 _NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型。

关于是否开启 ARC 对于 block 类型的影响的问题,在 ARC 开启的情况下,将只会有 _NSConcreteGlobalBlock 和 _NSConcreteMallocBlock 类型的 block。
比如我们将第二段代码中的 blk() 进行打印,可以得到以下信息:

2018-05-16 11:29:57.405094+0800 BlockTest[7696:7587452] <__NSMallocBlock__: 0x60000004d1a0>

证明以上结论正确。

B:

 __block int val = 0;
 void (^blk)(void) = ^{val = 1;};
 blk();

第二个例子是一个有外部变量访问的 block 。
通过clang 翻译之后,得到如下C代码:

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;}
static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_1(struct __main_block_impl_1*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

isa 指向 _NSConcreteStackBlock,说明这是一个分配在栈上的实例。

NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。

3.常见的非Retain Cycle的Block类型

正常情况下,当 block 不是 self 的属性时,self 不持有 block ,不会发生循环引用,如:

void (^blkk)(void) =  ^{
    NSLog(@"self==%@",self);
};
blkk();

另外,调用系统类方法时,也不会发生循环引用,比如使用 UIView 动画:

[UIView animateWithDuration:0.5 animations:^{
    NSLog(@"self==%@",self);
}];

还有一种情况,比如使用了系统的 NSOperation 对象,如下面这段示例代码:

self.queue = [[NSOperationQueue alloc] init];
self.operation = [[NSOperation alloc] init];
self.operation.completionBlock = ^{
    NSLog(@"self==%@",self);
};
[self.queue addOperation:self.operation];

这时,在 completionBlock 中,编译器甚至已经给了我们 retain cycle 的警告,但是实际运行后可以得知,这里并不会发生循环引用,具体的原因在查阅苹果关于 NSOperation 的文档后,得到以下这段解释:

In iOS 8 and later and macOS 10.10 and later, this property is set to nil after the completion block begins executing.

由此得知 Apple 在内部做了置空处理,所以这里可以放心使用。

4.Block的循环引用问题(retain cycle)

只要 Block 的内部引用了 self 或 self 的变量、属性,就会对 self 带来直接或间接的强引用,如果 self 又通过某种方式直接或间接的对 Block 进行了强引用,则造成循环引用(retain cycle),带来内存泄漏问题。

Demo A:

@implementation TestViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    Tester *tester = [[Tester alloc] init];
    [tester run];

    _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotificationKey" 
                                                            object:nil queue:nil 
                                                        usingBlock:^(NSNotification *n){
                                                            NSLog(@"%@",self);
                                                        }];
}

- (void)dealloc {
    if (_observer) {
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}
@end

向通知中心注册一个观察者,在 dealloc 方法中解除。
在注册通知的时候,block 中使用了 self,因此,self 被 block retain,在解除通知之前,block 一直被通知中心持有,则 _observer 持有了 block 的一份拷贝,而 _observer 始终被 self 持有,所以 self 同时持有了 block。
至此,形成循环引用,self 不会被释放,dealloc 方法也不会走,通知也就不会被解除。

Demo B:

#import "TestB.h"
@interface TestA:NSObject
@property (nonatomic, strong) TestB *testB;
@end

@implementation TestA
- (void)test {
    //retain cycle demo
    _testB = [[TestB allock] init];
    [_testB testWithBlock:^(NSError *error){
        NSLog(@"%@",self);
    }];
}

- (void)dealloc {
    NSLog(@"dealloc");
}
@end
typedef void (^TestBlock)(NSError *error);
@interface TestB:NSObject
@property (nonatomic, copy) TestBlock testBlock;
@end

@implementation TestB
- (void)testWithBlock:(TestBlock)completion {
    _testBlock = completion;
}
@end

在 TestA 中,TestA 持有了 _testB,_testB 持有其属性 testBlock,而在 testWithBlock 方法中,TestA 中的 block 又通过参数 completion 赋给了 testBlock,因此,间接造成在 TestA 中 self 对 block 的强引用。而在 block 内部,又对 self 进行了强引用,所以形成循环引用。
上例是故意制造的 retain cycle,在这个简单的 demo 中,这么做可能毫无意义,但是在实际开发中,TestB 中的 _testBlock 很可能在其他地方被使用,造成容易被疏忽的循环引用问题。

总结:
循环引用的形成,根本原因只有一条,就是 self 和 block 之间直接或者间接的互相持有了对方,分析问题的时候,只需要抓住这个宗旨,循序渐进,找到变量之间的持有关系,就会发现隐藏的问题。

5. weak-strong dance

对于在使用block过程中产生的循环引用问题,苹果官方给出了一种解决方案,weak-strong dance。
以Demo A为例,解决此处的循环引用可以采用如下方式:

__weak TestViewController *weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotificationKey" 
                                                        object:nil queue:nil 
                                                    usingBlock:^(NSNotification *n){
                                                      TestViewController *strongSelf = weakSelf;
                                                      if (strongSelf) {
                                                        NSLog(@"%@",strongSelf);
                                                      }
                                                    }];

首先定义对 self 的弱引用 weakSelf,当 self 被释放时,weakSelf 会变为 nil。
然后在 block 中使用 weakSelf,考虑到多线程情况,这里使用强引用 strongSelf 来持有 weakSelf,此时如果 self 不为 nil 即 retain self,以防止在后面使用的时候被释放。使用 strongSelf 的时候需要进行 nil 判断,在多线程的情况下,可能在对 strongSelf 赋值的时候,weakSelf 已经 nil 了。
通过这种手法,block 就不会持有 self,从而打破循环引用。
此外,strongSelf 的作用会保持到 block 执行完成,清理 block 栈的时候,strongSelf 会被 release,所以在 block 内定义的 strongSelf 是被 block 持有的,帮助 block 持有 self,相当于 self 的引用计数+1,并跟随 block 的执行完毕而销毁。

6.使用 weak-strong dance 的注意事项

在使用 weak-strong dance 的时候,需要注意一些情况。
比如异步网络请求,使用 GCD 延迟执行一段代码:

@implementation TestViewController
- (void)test {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", self);
    });
}

- (void)dealloc {
    NSLog(@"dealloc");
}
@end

假设 TestViewController 被 push 进来之后立即执⾏ test ⽅法,然后立即 pop 回去,这里不会立即执⾏ dealloc ⽅法,而是先等待5s执行 block,之后再⾛ dealloc ⽅法。

使用 weak-strong dance 后:

@implementation TestViewController
- (void)test {
    __weak TestViewController *weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      TestViewController *strongSelf = weakSelf;
      NSLog(@"%@", self);
    });
}

- (void)dealloc {
    NSLog(@"dealloc");
}
@end

pop的时候,会立即执行dealloc,5s后又会执行block,不过此时self已经为nil了。如果此时在block中又进行了其它操作,并且使用到了strongSelf的话,必然会造成crash。
因此,如上一条所述,在block中使用weak-strong dance时,要做好nil判断。

总结:
在使⽤ weak-strong dance 时,首先需要清楚的是,使⽤的场合是什么,目的⼜是什么,多线程环境下,使用了 weak-strong dance 后,如果在给 block ⾥面的 strongSelf 赋值的时候,weakSelf 已经 nil 了,代码就不执⾏了。也就是说,如果需要 block 中的代码⽆论何时都必须执行,就不该使⽤ weak-strong dance,⽽如果 block 中不是必须执⾏的代码,那么即使 weakSelf 为 nil 了,也⽆所谓了,正如⻚面都销毁了,是否执⾏加载数据的代码,就变的毫⽆意义了,此时只需要做好判断,不让程序崩溃,该 return 就 return 吧。

7.其它应对retain cycle的做法

优秀的开发者,不会把循环引用的问题抛给使⽤者,也不应该把责任推给API的调⽤者。所以在产生循环引用的情况下,开发者应该自⾏找到一个适当的时机解除retain cycle。
以Demo B为例,在 TestB 中,testBlock 在使⽤后,需要及时将其置空,比如在回调结束后执⾏_testBlock = nil,这样,只要 block 运⾏完毕,retain cycle就解除了,这⼀切都在内部实现,不需要也不应该暴露给调用者。
再⽐如,AFNetworking(3.x以下版本)中,在做网络请求的时候会使用这个方法:

- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                              failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    ...
    self.completionBlock = ^{
            ...
               if (success) {
                   dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                       success(self, responseObject);
                   });
               }
            ...
          }
    ...
}

success 这个 block 被 AFHTTPRequestOperation 对象持有。其实循环引⽤在一开始的时候被建立了,只不过在 block 执⾏完成之后,循环引⽤又被⼿动打破了。如何打破?因为AFN的作者封装了一个completionBlock,使用了一个dispatch_group,无论传进来的是什么,最终都会在回调之后主动打破循环引用。

- (void)setCompletionBlock:(void (^)(void))block {
    [self.lock lock];
    if (!block) {
        [super setCompletionBlock:nil];
    } else {
        __weak __typeof(self)weakSelf = self;
        [super setCompletionBlock:^ {
            __strong __typeof(weakSelf)strongSelf = weakSelf;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
            dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
            dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop

            dispatch_group_async(group, queue, ^{
                block();
            });

            dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
                [strongSelf setCompletionBlock:nil];
            });
        }];
    }
    [self.lock unlock];
}

总结:
retain cycle本身并不一定是糟糕的,他可以延迟self的销毁,最关键的依旧是,在合适的时候手动打破它。

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

推荐阅读更多精彩内容