美文网首页
iOS Blocks 小结

iOS Blocks 小结

作者: z4ywzrq | 来源:发表于2019-12-31 17:59 被阅读0次

原文链接:http://yupeng.fun/2019/12/30/blocks/

本文介绍一下 iOS 中 Block 相关内容,总结 Block 相关的使用方法和注意事项。

Block 概述

Block 也被称作闭包,是不带有名称的函数,匿名函数。相当于是一个代码块,把想要执行的代码封装在代码块里,等需要的时候调用。

Block 表达式语法:

返回值类型(^变量名)(参数) = ^返回值类型(参数){ 表达式 }
int (^myBlock1)(int) = ^(int num) { return num + 1; };
void (^myBlock2)(void) = ^(void){ NSLog(@"无参数,无返回值"); };

Block 常见用法

在 OC 中经常会用到 Block。使用 Block 方便快捷,集中代码块,适用于轻便、简洁的回调,如网络传输等。下面介绍几种常见的用法:

1、声明为属性

可将 block 声明为某个类的属性,在别的地方初始化赋值之后,等待触发回调传值等操作。

@property (nonatomic, copy) void(^blockName)(NSInteger type);
someObj.blockName = ^(NSInteger type) {
    //...
};

//如要是觉得 block 语法书写别扭、不友好,也可使用 typedef 定义一个 block 类型,方便使用:
typedef void(^BlockName)(NSInteger);
@property (nonatomic, copy) BlockName blockName;
2、作为方法参数调用

当有时调用一个耗时的方法处理,不会立即返回,需要时间进行处理,当处理完成后告知调用者处理完毕,可进行后面的操作,在这样的情景下就可使用 block,例如网络请求回调。

- (void)doSomethingParameters:(id)parameters completion:(void (^)(NSInteger type))completion {
    //...
    completion(1);
}

[self doSomethingParameters:nil completion:^(NSInteger type) {
    //处理完成,这里可进行后续操作...
}];

Block 的实质

Block 其实是作为 C语言的代码来进行处理的,通过编译器将 Block 语法转换为相关的 C语言代码,然后进行编译执行。可以通过 clang 来将 OC 代码转换为 C/C++ 代码,执行下面命令:

clang -rewrite-objc main.m

然后生成 main.cpp 文件。可以看到,简单的几行代码,转化为一堆 C语言代码:

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

上面的代码其实就是,将 Block 的匿名函数作为 C语言的函数来处理。
就是使用函数指针调用函数,将函数 __main_block_func_0(__cself) 赋值给 impl.FuncPtr, 然后调用 blk->impl.FuncPtr(blk),参数 __cself 就是指向 blk 自身的指针。

Block 就是 OC 对象

OC 中由类生成对象,就是由该类生成对象的各个结构体,通过对象的成员变量 isa 指针指向该类的结构体实例 objc_class 继承自 objc_object,该结构体实例持有声明的成员变量、方法的名称、方法的实现(函数指针)、属性以及父类的指针。具体内容请参考之前的文章 Objective-C对象解析

Block 就是 OC 对象。 __main_block_impl_0 结构体相当于,基于 objc_object 结构体的 OC 对象的结构体。
值得注意的是,impl.isa = &_NSConcreteStackBlock;
_NSConcreteStackBlock 相当于一个结构体实例,将 Block 作为 OC 对象处理时,关于该类的信息就放在 _NSConcreteStackBlock 中。

Block 截获变量

如下面例子,定义一个 Block,在代码块里打印变量 a,然后修改后调用 Block 打印出的变量 a 的值不变。

NSInteger val = 10;
void (^blockName)(void) = ^{
    NSLog(@"val = %ld", val);
};
blockName(); // val = 10
val = 20;
blockName(); // val = 10

为何修改后打印 a 的值不变,因为 Block 语法的表达式使用的是它之前声明的局部变量 a。Block 表达式截获所使用的局部变量的值,保存了该变量的瞬时值。所以在第二次执行 Block 表达式时,即使已经改变了局部变量 a 的值,也不会影响 Block 表达式在执行时所保存的局部变量的瞬时值。
这就是 Block 变量截获局部变量值的特性。

通过上面的方法转换为 C语言,可以看到在 Block 中使用外面的变量,变量被作为成员变量追加到 __main_block_impl_0 结构体中了。

从上图中可看到,截获自动变量,就是在表达式中所使用的变量被保存到 Block 的结构体中 __cself->val,所以在 Block 之外修改变量 val 的值,Block 表达式里面的 val 值不变。

另外,在使用 C语言数组时要注意,截获自动变量的方法没有实现对 C 数组的截获,可使用指针来解决。

如上图所示,为何会报错呢?
通过上面截获变量的例子可知,变量被赋值给 Block 结构体中的成员变量,因为 C语言数组类型变量不能赋值给数组类型变量:char a[10]={'a'}; char b[10]=a; 这样编译不能通过,所以会报错。

__block 说明符

在 Block 中只能使用保存的局部变量的瞬时值,并不能直接对其进行修改,想要修改需要在局部变量前加 __block 修饰。

__block NSInteger val = 10;
void (^blockName)(void) = ^{
    val = 30;
    NSLog(@"val = %ld", val);
};
blockName(); // val = 30

__block 类似于 static、auto,用于指定将变量值设置到哪个存储域中。auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。

继续转换为 C语言来看一下,如下图:


可看到,使用在变量前加上 __block 说明符后,代码增加了很多,__block 变量变成了__Block_byref_val_0 结构体类型的变量。

struct __Block_byref_val_0 {
  void *__isa;
  __Block_byref_val_0 *__forwarding;
  int __flags;
  int __size;
  NSInteger val;
};

__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 30;

其中结构体的成员变量 *__Block_byref_val_0 __forwarding 是指向自身的指针,通过这个指针来访问变量 __forwarding->val。为何这样使用,下面来介绍。

Block 存储域

前面说过,Block 是作为 OC 对象处理的,impl.isa = &_NSConcreteStackBlock; 该类的信息就放在 _NSConcreteStackBlock 中。
除了 _NSConcreteStackBlock 类型,还有其他两种类型 _NSConcreteGlobalBlock 和 _NSConcreteMallocBlock,这三类所在内存存储区域有区别:

  • _NSConcreteStackBlock 栈区
  • _NSConcreteGlobalBlock 数据区域
  • _NSConcreteMallocBlock 堆区
_NSConcreteGlobalBlock

前面出现的 Block 例子,使用的是 _NSConcreteStackBlock 设置在栈上。
在全局变量使用 Block 时,生成的是 _NSConcreteGlobalBlock 类对象,如下:

void (^blockName)(void) = ^{ NSLog(@"block"); };
int main() {
    blockName();
    return 0;
}

因为在使用全局变量的地方不能使用自动变量,所以就不存在堆自动变量进行截获。此种类型的 Block 结构体实例内容不依赖于执行的状态,所以整个程序只需一个实例,将 Block 用结构体实例设置在与全局变量相同的数据区域中即可。
总结来说就是,在全局变量有 Block 语法时,Block 语法的表达式不使用截获的自动变量时,Block 为 _NSConcreteGlobalBlock 类对象。

_NSConcreteMallocBlock

配置在全局变量上的 Block 在变量作用域外也可以通过指针访问使用,但是配置在栈上的 Block 若其所在变量作用域结束,该 Block 就被废弃,同样的 __block 变量也配置在栈上,若所在变量作用域结束,则该 __block 变量也会被废弃。

因此提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题,从栈上复制到堆上,即使变量作用域结束,堆上的 Block 还可以继续存在。
复制到堆上的 Block 将 _NSConcreteMallocBlock 类对象写入到 Block 结构体实例的成员变量 isa: impl.isa = &_NSConcreteMallocBlock;
__block 变量的结构体成员变量 __forwarding 可以实现无论 __block 变量配置在栈上,还是堆上都能正确的访问 __block 变量。

在 ARC 模式下,编译器会判断,自动生成将 Block 从栈上复制到堆上的代码。但是当,向方法或函数的参数中传递 Block 时,需要手动复制,如下面的代码。

- (NSArray *)getBlockArray {
    int val = 10;
    //需要使用 copy 复制,不然函数执行完成,栈上的 Block 被废弃,执行报错
    return [NSArray arrayWithObjects:
            [^{NSLog(@"blk0: %d", val);} copy],
            [^{NSLog(@"blk1: %d", val);} copy] , nil];
}

NSArray *tempArray = [self getBlockArray];
void(^blk)(void) = [tempArray objectAtIndex:0];
blk();

如果在方法或函数中复制了传递过来的参数,那么就不必再调用该方法或函数前手动复制了,例如,在方法命中含有 usingBlock 时,[array enumerateObjectsUsingBlock:...],或者 GCD 的 API 中,不用手动复制。

当对 Block 调用 copy 方法时,_NSConcreteStackBlock 类会从栈复制到堆上。_NSConcreteGlobalBlock 类什么也不做。_NSConcreteMallocBlock 类引用计数器增加。

什么时候栈上的 Block 会复制到堆上:

  • 调用 Block 的 copy 实例方法时
  • Block 作为函数返回值返回时
  • 将 Block 赋值给有 __strong 修饰符 id 类型的类或 Block类型变量时
  • 在方法名含有 usingBlock 的 Coca 框架方法 或 GCD 的 API 中传递 Block 时

注意下面几种情况,ARC下:


//将 Block 赋值给有 __strong 修饰符 id 类型的类或 Block类型变量时,栈上的 Block 会复制到堆上
        NSInteger val = 3;
        NSLog(@"block = %@", ^{ NSLog(@"val = %ld", val); });
        //block = <__NSStackBlock__: 0x7ffeefbff540>
        
        NSInteger i = 6;
        //捕获变量,和上面的对比,这里将 Block 赋值,会从栈上复制到堆上
        void (^blockName)(void) = ^{ NSLog(@"i = %ld", i); };
        blockName(); // i = 6
        NSLog(@"block = %@", blockName); 
        //block = <__NSMallocBlock__: 0x10280df70>
        
        __block NSInteger val = 3;
        NSLog(@"block = %@", ^{ val = 30; NSLog(@"val = %ld", val); });
        //block = <__NSStackBlock__: 0x7ffeefbff528>
        
        __block NSInteger i = 6;
        void (^blockName)(void) = ^{ i = 60;  NSLog(@"i = %ld", i); };
        blockName(); // i = 60
        NSLog(@"block = %@", blockName); 
        //block = <__NSMallocBlock__: 0x100704250>

//在没有捕获自动变量时, Block 结构体实例是 _NSConcreteGlobalBlock
        NSLog(@"block = %@", ^{ NSLog(@"no val"); });
        //block = <__NSGlobalBlock__: 0x100001028>
        
        static NSInteger val = 3;
        NSLog(@"block = %@", ^{ val = 30; NSLog(@"val = %ld", val); });
        //block = <__NSGlobalBlock__: 0x100001028>
        
        void (^blockName)(void) = ^{ NSLog(@"no val"); };
        blockName();
         NSLog(@"block = %@", blockName);
        //block = <__NSGlobalBlock__: 0x100001048>
        
        static NSInteger i = 6;
        void (^blockName)(void) = ^{ i = 60;  NSLog(@"i = %ld", i); };
        blockName(); // i = 60
        NSLog(@"block = %@", blockName); //block = <__NSGlobalBlock__: 0x100001048>
__block 变量和对象

当 Block 从栈复制到堆时, __block 变量也全部被从栈复制到堆并被 Block 所持有。
若有多个 Block 使用同一个 __block 变量时,任何一个 Block 从栈复制到堆时,__block 变量也会从栈复制到堆,剩下的 Block 从栈复制到堆,被复制的 Block 持有 __block 变量并增加 __block 变量的引用计数。
若配置在堆上的 Block 被废弃,它所使用的 __block 变量也会被释放。

前面提到 __block 变量的结构体成员变量 __forwarding 可以实现无论 __block 变量配置在栈上,还是堆上都能正确的访问 __block 变量。如下例子:

__block NSInteger val = 10;
void (^blockName)(void) = [^{val = 30; NSLog(@"val = %ld", val); } copy];
        
blockName(); // val = 30
NSLog(@" %ld ", val); //30
        
val = 20;
NSLog(@" %ld ", val); //20

__block 变量从栈上复制到堆上,此时会将成员变量 __forwarding 的值替换为复制到堆上的 __block 变量结构体实例的地址。val.__forwarding 使用的是同一个在堆上的值,从而能保证正确的访问同一个 __block 变量。

上图中有两个函数,__main_block_copy_0 和 __main_block_dispose_0,在 Block 复制到堆上和从堆上释放时被调用。将使用到的 __block 变量或者截获的对象复制给 Block 结构体的成员变量,持有对象。这样截获的对象就能够超出其变量作用域而存在。通过参数 BLOCK_FIELD_IS_BYREF 和 BLOCK_FIELD_IS_OBJECT 来区分函数对象类型是 __block 变量还是对象。

id array = [NSMutableArray array];
void (^blk)(void) = ^(){
    //截获对象
    [array addObject:@(1)];
    NSLog(@"Block ! %ld", [array count]);
};
blk();

Block 的循环引用问题

如在 block 中使用了对象, block 会对使用的对象进行持有,如该对象同时持有该 block 则会造成循环引用的问题,互相持有不能释放。


self.blockName = ^() {
    [self.delegate doSomething];
};

//解决方法,在 ARC 下使用 __weak 进行修饰。这样 block 就不持有 self,避免循环引用。
__weak __typeof(self) weakSelf = self;
self.blockName = ^() {
    [weakSelf.delegate doSomething];
};


//下面代码会有内存泄漏吗
- (void)viewDidLoad {
    [super viewDidLoad];
    NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
    id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
                                           object:nil
                                            queue:[NSOperationQueue mainQueue]
                                       usingBlock:^(NSNotification * _Nonnull note) {
        [self doSomething];
        [center removeObserver:token];
        token = nil;
    }];
}

- (void)doSomething {
}

- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}

//__block token,如果不加 __block 在Block里的 [center removeObserver:token]; token 为空。
//因为 token 在执行完后才返回值,所以一开始捕获到的,是返回之前的没有被初始化的。
//加上 __block 是通过指针 __forwarding->token 取值,能够正确访问到。
//上面如果 block 没有执行,则会内存泄漏。center 持有 token,token 持有 block,block 持有 self 也持有 token,
//token 不释放,self 不会释放

//最简单的解决方法就是
__weak typeof(self) wkSelf = self;
id __block __weak wkToken = [wkCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
                                      object:nil
                                       queue:[NSOperationQueue mainQueue]
                                  usingBlock:^(NSNotification * _Nonnull note) {
   [wkSelf doSomething];
   [wkCenter removeObserver:wkToken];
}];

__weak、 __strong 的使用

声明一个对象:

id __strong obj = [[NSObject alloc] init];

//编译器会转换为下面的代码
id __attribute__((objc_ownership(strong))) obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

//上面的代码其实就是下面的函数调用
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj,selector(init));
objc_release(obj);
__weak id weakObj = obj;

//转换为
__attribute__((objc_ownership(weak))) id weakObj = obj;

//相关的调用
id weakObj ;
objc_initWeak(&weakObj,obj);
objc_destoryWeak(&weakObj);


id objc_initWeak(id *object, id value) {   
    *object = nil; 
    return objc_storeWeak(object, value);
}

void objc_destroyWeak(id *object) { 
    objc_storeWeak(object, nil);
}

有关底层实现可查看 clang 文档 http://clang.llvm.org/docs/AutomaticReferenceCounting.html

weak 表是用Hash table实现的, objc_storeWeak 函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把 __weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录。

如果 __weak 引用的原对象如果被释放了,那么对应的 __weak 对象就会被指为nil。就是通过 objc_storeWeak 函数这些函数来实现的。

我们已经知道使用 weakSelf 来解决循环引用的问题,为何有的还需要在 block 里使用 strongSelf ?

__weak __typeof(self) weakSelf = self;
self.blockName = ^() {
        __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [strongSelf.delegate doSomething];
        });
};

有的情况下 block 会延迟调用,在 block 未调用前 self 可能 已经释放掉了,这时再在 block 使用 weakSelf ,weakSelf 为空了。在 block 里面使用的 __strong 修饰的 weakSelf 是为了在函数生命周期中防止 self 提前释放。strongSelf 是一个自动变量当 block 执行完毕就会释放自动变量 strongSelf ,不会对 self 进行一直进行强引用。

总结来说就是,weakSelf 是为了 block 不持有 self,避免循环引用。strongSelf 防止 self 提前释放。

Reference

Objective-C高级编程
深入研究Block

相关文章

网友评论

      本文标题:iOS Blocks 小结

      本文链接:https://www.haomeiwen.com/subject/ebbohctx.html