·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> app软件开发 >> IOS开发 >> iOS开发日记31-Block终极篇

iOS开发日记31-Block终极篇

作者:佚名      IOS开发编辑:admin      更新时间:2022-07-23

今天博主有一个Block的需求,遇到了一些困难点,在此和大家分享,希望能够共同进步.

1.什么是Block 
     Block是一个C级别的语法以及运行时的一个特性,和标准C中的函数(函数指针)类似,但是其运行需要编译器和运行时支持,从ios4.0开始就很好的支持Block. 

2.在iOS开发中,什么情况下使用Block 
     Block除了能够定义参数列表、返回类型外,还能够获取被定义时的词法范围内的状态(比如局部变量),并且在一定条件下(比如使用__block变量)能够修改这些状态。此外,这些可修改的状态在相同词法范围内的多个block之间是共享的,即便出了该词法范围(比如栈展开,出了作用域),仍可以继续共享或者修改这些状态。通常来说,block都是一些简短代码片段的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调.

3.Block的基本定义

语法不再多说,各位看官自行百度.

4.Block的原理

block其实就是一个函数指针,block可以看做是匿名函数的函数指针来对待。

5.关键字__block

Block会捕获外部变量,但是当你试图在Block里面修改捕获的外部变量时。就是出现编译错误,解决的一种办法是将外部变量使用 __block 修饰符修饰。下面是添加 __block 修饰的外部变量代码: 


#include <stdio.h>

int main(int argc, const char * argv[]) {

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

    return 0;
}

该代码可进行编译。变换后如下:


struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
}; // [1]
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *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_0(struct __main_block_impl_0 *__cself) 
{
    __Block_byref_val_0 *val = __cself->val; // bound by ref
   (val->__forwarding->val) = 1; 
} // [2]
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
  _Block_object_assign((void*)&dst->val, 
  (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
  _Block_object_dispose((void*)src->val, BLOCK_FIELD_IS_BYREF*/);
}
static struct __main_block_desc_0 
{
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 
  0, 
  sizeof(struct __main_block_impl_0), 
  __main_block_copy_0, 
  __main_block_dispose_0
};
int main(int argc, const char * argv[]) 
{
  __Block_byref_val_0 val = {		
    (void*)0,
    (__Block_byref_val_0 *)&val,
     0, 
     sizeof(__Block_byref_val_0), 
     10
  };
  blk = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);
  return 0;
}

我们可以发现加上 __block 说明符,源码量就急剧的增加了(已做简化)。 

  • 我们可以发现含有 __block 修饰的变量变成了 __Block_byref_val_0 结构体。[1] 
  • __Block_byref_val_0 结构体实例的成员变量__forwarding持有该实例自身的指针。 
  • Block转化而来的的 __main_block_func_0 结构体实例持有指向 __block变量的 __Block_byref_val_0 结构体实例的指针。 __Block_byref_val_0 结构体实例的成员变量 __forwarding 持有指向该实例自身的指针。通过成员变量 __forwarding 访问成员变量val的地址,从而可以修改自动变量。 
  • 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。 

主要作用:1.block对外部变量是只读的,要变成可读可写,就需要加上__block

2.将栈中的block复制到堆上一份,从而避免了循环引用这个情况

6.Block循环引用

在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了 __strong 修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下: typedef void (^blk_t)(void);

@interface MyObject : NSObject
@PRoperty (nonatomic, strong) id obj;
@property (nonatomic, strong) blk_t blk;
@end
@implementation MyObject
- (id)init
{
  self = [super init];
  _blk = ^{NSLog(@"obj = %@",_obj);};
  return self;
}
@end

通过编译器我们可以看到造成了循环引用,即Block语法内部使用了 _obj 变量,是因为 _obj 变量实际上截获了self。对编译器来说, _obj 变量只不过是对象的成员变量罢了。  

解决的方法便是便是通过 __weak 修饰符来修饰会被Block捕获的变量: 

id __weak obj = _obj;

_blk = ^{NSLog(@"obj = %@",obj);};

还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:

#define WS(weakSelf)  __weak __typeof(&*self)weakSelf = self;

调用 WS(ws) 之后 ws 就变成为了 __weak 修饰符修饰的 self 了.

7.关于Block的应用:

block  属性,一般用  copy  修饰;

7.1.如果没有对block进行copy操作,block就存储于栈空间

7.2.  如果对 block 进行 copy 操作, block 就存储于堆空间 --- 强引用

7.3.如果block存储于栈空间,不会对block内部所用到的对象产生强引用

7.4.如果block存储于堆空间,就会对block内部所用到的对象产生强引用

注意 :

7.4.1  :由于使用了  copy  修饰,如果  block  中调用了  block  属性的对象,就会造成循环引用

为了避免循环引用,需要对对象进行若引用修饰:

 ICKPerson *p = [[ICKPerson alloc] init];
 // 1、修饰方法1
     //    __unsafe_unretained typeof(p) weakP = p;
 // 2、修饰方法2
     __block typeof(p) weakP = p;
     p.testBlock = ^{
         [weakP run];
     };

7.4.2  、关于  block  中变量的值:

7.4.2.1    如果变量没有通过  __block  修饰,那么  block  中的变量本质是值捕获,在创建 block  的同时,是将变量的值传入到  b  lock  中  ,  无论什么时候调用,变量的值就是最初传进去的值

  int age = 10;
  void (^block)() = ^{ // 值捕获
    NSLog(@"age=%d", age);// 打印是10;
  };
  age = 20;
  block();

7.4.2.2    如果变量通过  __block  修饰,那么  block  中的变量实际传递的是变量的地址,在创建  block  的同时,是将变量的地址传入到  b  lock  中  ,  在调用block的时候,其变量的值是当时变量的值(通过地址(指针)获取到)。

 __block int age = 1;
  void (^block)() = ^{ // 值捕获
         NSLog(@"age=%d", age);// 打印是20;
   };
     age = 20;
     block();

7.4.2.3  、关于  block  的内部实现:

创建  block  的时候,内部是创建了对应的函数;

在调用  block  的时候,是调用了之前封装的函数。

7.4.2.4.3.1  、在头文件中(向其他文件中传递数据的文件)定义一个  block  :是否带参数,根据需求确定

 @class ICKAddViewController,ICKContact;
 typedef void(^ICKAddViewControllerBlock)(ICKContact *contact);
 @interface ICKAddViewController : UIViewController
 @property (nonatomic, strong) ICKAddViewControllerBlock contactBlock;
 @end

7.4.2.4.3.2  、在获取数据后,跳转页面之前,调用  block  ,将数据传递过去

 - (IBAction)addcontact {
     ICKContact *contact = [ICKContact contactWithName:self.nameFiled.text andPhone:self.phoneFiled.text];
     // 调用block
     if (self.contactBlock) {
         self.contactBlock(contact);
     }
     [self.navigationController popViewControllerAnimated:YES];
 }

7.4.2.4.3.3  、在获取(保存、利用)数据的文件中(拿到获取数据的对象的时候)调用其 block  属性,保存  block  代码段(实现特定功能的代码)

  // 跳转控制器时数据传递
  - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
      ICKAddViewController *addVc = segue.destinationViewController;
      // 声明block
      addVc.contactBlock = ^(ICKContact *contact){
          [self.contacts addObject:contact];
  
          // 存储数据
          NSString *cache = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
          NSString *path = [cache stringByAppendingString:@"contacts.data"];
         [NSKeyedArchiver archiveRootObject:self.contacts toFile:path];
         [self.tableView reloadData];
     };
 }

8.Block的存储类型

block的存储形态有三种:_NSConcretStackBlock(栈)、_NSConcretGlobalBlock(全局)、_NSConcretMallocBlock(堆)

要点一:当block在函数内部,且定义的时候就使用了函数内部的变量,那么这个  block是存储在栈上的。 

要点二:当block定义在函数体外面,或者定义在函数体内部且当时函数执行的时候,block体中并没有需要使用函数内部的局部变量时,也就是block在函数执行的时候只是静静地待在一边定义了一下而不使用函数体的内容,那么block将会被编译器存储为全局block。 

要点三:全局block存储在堆中,对全局block使用copy操作会返回原函数指针;而对栈中的block使用copy操作,会产生两个不同的block地址,也就是两个匿名函数的入口地址。 

要点四:ARC机制优化会将stack的block,转为heap的block进行调用。

9.Block存储域

__block 变量转换成了 __block 变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示: 


名称实质
Block 栈上Block的结构体实例  
__block变量 栈上__block变量的结构体实例  

前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:


  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

他们分别对应的存储区域如下所示:


设置对象的存储区域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区域)
_NSConcreteMallocBlock


定义Block时期内存区域分配在栈中,其Block类型为 __NSConcreteStackBlock 类对象. 

那么 _NSConcreteMallocBlock 类型的对象由何而来?这里肯定有人还在疑惑为什么 __block 变量转化而来的结构体为生成指向自身的 __forwarding 指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。 


行不行先来段代码看看:


void (^blk)();
if (/* some condition */) {
    blk = ^{ NSLog(@"Block A"); };
}else {
    blk = ^{ NSLog(@"Block B"); };
}
blk();

看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。


解决上面问题的方法就是使用 copy 方法,将block拷贝到堆中。拷贝完之后就是接下来要将的 _NSConcreteMallocBlock 类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。 


还有一种类型是 _NSConcreteGlobalBlock 类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快: 


void (^blk)() = {
  NSLog("This is a global block");
}
void main() {
}

10.__block变量存储域

如果Block配置在栈中,则在Block中使用的 __block 变量也分配在栈中。当Block被复制到堆中时, __block 变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的 __block 变量也就被释放了。下面来看堆和栈上 __block 混用的例子 


__block int val = 0;

void (^blk) (void) = [^{val++;} copy];

++val;

blk();

NSLog(@"val:%d",val);

执行结果为:


val: 2


在Block中和在Block外修改 __block 变量完全等效,它是怎么实现的呢? 

是因为执行 copy 方法之后,Block被复制到堆中,其内部捕获的 __block 变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的 __block 变量val会将原本指向自身的 __forwarding 指针指向复制到堆中的 __block 变量val的地址。这样堆中的 __block 变量被修改之后就等同于栈上的block被修改。 


通过该功能,无论是在Block的语法中、Block语法外使用 __block 变量,还是 __block 变量配置在栈上还是堆上,都可以顺利地访问一个 __block 变量。 

11.截获对象

先来看一段Blcok截获可变数组对象的例子:


blk blk;
{
    NSMutableArray *array = [[NSMutableArray alloc] init];
    blk = ^(id obj){
  [array addObject:obj];
  NSLog(@"arrayCount = %lu",(unsigned long)array.count);
    };
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

执行该段代码的结果为


arrayCount = 1;
arrayCount = 2;
arrayCount = 3;

从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。


解决方法是调用 copy 方法,形式如下: 


blk = [^(id obj){
   [array addObject:obj];

   NSLog(@"arrayCount = %lu",(unsigned long)array.count);
} copy];


http://www.cocoachina.com/ios/20150109/10891.html
http://my.oschina.net/leejan97/blog/268536
http://my.oschina.net/u/1432769/blog/390401