• Block


    1.Block简介

    Block使用场景,可以在两个界面的传值,也可以对代码封装作为参数的传递等。用过GCD就知道Block的精妙之处。

    Block是一种比较特殊的数据类型。它可以保存一段代码,在合适的时候取出来调用。

    2.Block 底层实现的原理

    用Sublime Text建立一个Objective-C的文件,保存命名为BlockOne:

    然后写入下面的代码:

     1 #import <Foundation/Foundation.h>
     2 
     3         int main(int argc, const char * argv[])
     4          {
     5              @autoreleasepool 
     6              {
     7                 void (^huanggang)() = ^{printf("Hello World");};
     8 
     9                 huanggang();
    10             }
    11             return 0;
    12          }

    选中这个文件BlockOne,双击它,选中“显示简介”-->"名称与扩展名"-->修改为“BlockOne.m”-->打开终端-->输入 cd desktop -->输入 

    clang -rewrite-objc BlockOne.m;

    桌面会多出一个文件夹:

    用 Xcode打开它:

    定义完block之后,其实是创建了一个函数,在创建结构体的时候把函数的指针一起传给了block,所以之后可以拿出来调用。

    看值捕获的的问题:

     1 #import <Foundation/Foundation.h>
     2 
     3 int main(int argc, const char * argv[])
     4 {
     5     @autoreleasepool
     6     {
     7 
     8         int a = 10;
     9         void (^huanggang)() = ^{printf("a= %d",a);};
    10         
    11         huanggang();
    12     }
    13     return 0;
    14 }

     

    这是一个函数的实现,对应Block中的 {} 内的内容,这些内容当做了 C 语言函数来处理,函数参数中的 _cself 相当于 Objective-C 中的 self。

    Desc : 描述了 Block 大小、版本信息;

    1 impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock

    __main_block_impl_0即为main()函数栈上的Block结构体,其中的__block_impl结构体声明如下:

    1 struct __block_impl
    2  {
    3   void *isa;//指明对象的Class
    4   int Flags;
    5   int Reserved;
    6   void *FuncPtr;
    7 };

    __block_impl结构体,即为Block的结构体,可理解为Block的类结构。
    再看下main()函数翻译的内容:

    1 int main() {
    2     int count = 10;
    3     void (* huanggang)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    4 
    5     ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)huanggang);
    6 }

    去除掉复杂的类型转化,可简写为:

    1 int main() {
    2     int count = 10;
    3     sturct __main_block_impl_0 *huanggang = &__main_block_impl_0(__main_block_func_0,         //函数指针
    4                                                            &__main_block_desc_0_DATA)); //Block大小、版本等信息
    5 
    6     (*huanggang->FuncPtr)(huanggang);   //调用FuncPtr指向的函数,并将blk自己作为参数传入
    7 }

    由此,可以看出,Block也是Objective-C中的对象

    定义block的时候,变量a的值就传递到了block结构体中,仅仅是值传递,所以在block中修改a是不会影响到外面的a变量的。

    并不是直接传递a的值了,而是把a的地址传过去了,所以在block内部便可以修改到外面的变量了。

    根据isa指针,block一共有3种类型的block
    _NSConcreteGlobalBlock 全局静态
    _NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
    _NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁
    而ARC和MRC中,还略有不同

    观察上节代码中__main_block_impl_0结构体(main栈上Block的结构体)的构造函数可以看到,栈上的变量 a 以参数的形式传入到了这个构造函数中,此处即为变量的自动截获
    因此可以这样理解:__block_impl结构体已经可以代表Block类了,但在栈上又声明了__main_block_impl_0结构体,对__block_impl进行封装后才来表示栈上的Block类,就是为了获取Block中使用到的栈上声明的变量(栈上没在Block中使用的变量不会被捕获),变量被保存在Block的结构体实例中。
    所以在blk()执行之前,栈上简单数据类型的count无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。但这个变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。

    2.1 Block的存储域

    根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。

     2.1.2 全局区

    1 void (^blk)(void) = ^{
    2     NSLog(@"Global Block");
    3 };
    4 
    5 int main() {
    6     blk();
    7     NSLog(@"%@",[blk class]);    //打印:__NSGlobalBlock__
    8 }

     2.1.2 堆区

     1 int main() {
     2     void (^blk)(void) = ^{//没有截获自动变量的Block
     3         NSLog(@"Stack Block");
     4     };
     5     blk();
     6     NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
     7 
     8     int i = 1;
     9     void (^captureBlk)(void) = ^{//截获自动变量i的Block
    10         NSLog(@"Capture:%d", i);
    11     };
    12     captureBlk();
    13     NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
    14 }

    可以看到截获了自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。但捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock。

    2.12  Block复制

    配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。
    复制到堆上的Block,将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:
    1 impl.isa = &_NSConcreteMallocBlock;

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

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

    其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。
    上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是NSMallocBlock_类,就是因为这个Block对象赋值给了_strong修饰的变量captureBlk(strong是ARC下对象的默认修饰符)。
    因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是__NSStackBlock:

    1 int count = 0;
    2     blk_t blk = ^(){
    3         NSLog(@"In Stack:%d", count);
    4     };
    5 
    6     NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
    7     NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
    8     NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
    9     NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

    2.13 使用__block

    Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。
    自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。
    __block保证了栈上和Block内(通常在堆上)可以访问和修改“同一个变量”,__block是如何实现这一功能的?

    __block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。

     
    2.14 Block 的循环引用

    Block的循环引用原理和解决方法大家都比较熟悉,此处将结合上文的介绍,介绍一种不常用的解决Block循环引用的方法和一种借助Block参数解决该问题的方法。
    Block循环引用原因:一个对象A有Block类型的属性,从而持有这个Block,如果Block的代码块中使用到这个对象A,或者仅仅是用用到A对象的属性,会使Block也持有A对象,导致两者互相持有,不能在作用域结束后正常释放。
    解决原理:对象A照常持有Block,但Block不能强引用持有对象A以打破循环。
    解决方法
    方法一: 对Block内要使用的对象A使用_*_weak*进行修饰,Block对对象A弱引用打破循环。

    有三种常用形式:

    • 使用__weak ClassName
    1 __block XXViewController* weakSelf = self;
    2     self.blk = ^{
    3         NSLog(@"In Block : %@",weakSelf);
    4     };
    • 使用__weak typeof(self)
    __weak typeof(self) weakSelf = self;
        self.blk = ^{
            NSLog(@"In Block : %@",weakSelf);
        };
    • Reactive Cocoa中的@weakify和@strongify
    1 @weakify(self);
    2     self.blk = ^{
    3         @strongify(self);
    4         NSLog(@"In Block : %@",self);
    5     };

    方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。

     
    未修正前:
    1  __block XXController *blkSelf = self;
    2     self.blk = ^{
    3         NSLog(@"In Block : %@",blkSelf);
    4     };

    注意上述代码仍存在内存泄露,因为:

    • XXController对象持有Block对象blk
    • blk对象持有__block变量blkSelf
    • __block变量blkSelf持有XXController对象

    修正后:

    1 __block XXController *blkSelf = self;
    2     self.blk = ^{
    3         NSLog(@"In Block : %@",blkSelf);
    4         blkSelf = nil;//不能省略
    5     };
    6 
    7     self.blk();//该block必须执行一次,否则还是内存泄露

    在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:

    • XXController对象持有Block对象blk
    • blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
    • __block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环

    第二种使用__block打破循环的方法,优点是:

    • 可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
    • 不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题

    缺点也明显:

    • 必须手动保证__block变量最后设置为nil
    • block必须执行一次,否则__block不为nil循环应用仍存在

    因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。
    方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。

    即原来使用__weak的写法

    __weak typeof(self) weakSelf = self;
        self.blk = ^{
            __strong typeof(self) strongSelf = weakSelf;
            NSLog(@"Use Property:%@", strongSelf.name);
            //……
        };
        self.blk();

     改为Block传参写法后:

    1  self.blk = ^(UIViewController *vc) {
    2         NSLog(@"Use Property:%@", vc.name);
    3     };
    4     self.blk(self);

    优点:

    • 简化了两行代码,更优雅
    • 更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环

    3.Block的修饰

    ARC情况下

    3.1、如果用copy修饰Block,该Block就会存储在堆空间。则会对Block的内部对象进行强引用,导致循环引用。内存无法释放。

    解决方法:

    新建一个指针(__weak typeof(Target) weakTarget = Target )指向Block代码块里的对象,然后用weakTarget进行操作。就可以解决循环引用问题。

    3.2、如果用weak修饰Block,该Block就会存放在栈空间。不会出现循环引用问题。

    MRC情况下

    用copy修饰后,如果要在Block内部使用对象,则需要进行(__block typeof(Target) blockTarget = Target )处理。在Block里面用blockTarget进行操作。

    typedef int (^SumP)(int,int);//用typedef定义一个block类型
     1 //利用typedef定义block来创建一个block变量
     2 
     3    SumP sumblock1 = ^(int a,int b){
     4 
     5        return a - b;
     6 
     7     };
     8 
     9    int d = sumblock1(10,5);
    10 
    11     NSLog(@"d = %d",d);
    12 
    13  

     4.Block的几种格式

    4.1、Block的定义格式

    返回值类型(^block变量名)(形参列表) = ^(形参列表)

    {

    };

    调用Block保存的代码

    block变量名(实参);

    默认情况下,Block内部不能修改外面的局部变量
    Block
    内部可以修改使用__block修饰的局部变量

    4.2、Block的模式

    Block简单用法举例

    4.2.1.无参数无返回值的Block

    参数列表为空可以省略为:

    1 ^ {
    2       NSLog(@"No Parameter");
    3   };

    最减模式语法为:

    ^ {    表达式  }

    具体一点的有:

     1 /**
     2 
     3  *  无参数无返回值的Block
     4 
     5  */
     6 
     7 -(void)func1{
     8 
     9     /**
    10 
    11      *  void :就是无返回值
    12 
    13      *  emptyBlock:就是该block的名字
    14 
    15      *  ():这里相当于放参数。由于这里是无参数,所以就什么都不写
    16 
    17      */
    18 
    19     void (^emptyBlock)() = ^(){
    20 
    21         NSLog(@"无参数,无返回值的Block");
    22 
    23     };
    24 
    25     emptyBlock();
    26 
    27 }
    28 
    29  

    4.2.2.有参数无返回值的Block

     1 /**
     2 
     3      *  调用这个block进行两个参数相加
     4 
     5      *
     6 
     7      *  @param int 参数A
     8 
     9      *  @param int 参数B
    10 
    11      *
    12 
    13      *  @return 无返回值
    14 
    15      */
    16 
    17     void (^sumBlock)(int ,int ) = ^(int a,int b){
    18 
    19         NSLog(@"%d + %d = %d",a,b,a+b);
    20 
    21     };
    22 
    23     /**
    24 
    25      *  调用这个sumBlock的Block,得到的结果是20
    26 
    27      */
    28 
    29     sumBlock(10,10);

    4.2.3.有参数有返回值的Block

    最简单的:

    1 ^ int (int count) 
    2 {
    3         return count + 1;
    4  };

    其中,可省略部分有:

    • 返回类型,例:
    1 ^ (int count) 
    2 {
    3       return count + 1;
    4   };
     1 /**
     2 
     3      *  有参数有返回值
     4 
     5      *
     6 
     7      *  @param NSString 字符串1
     8 
     9      *  @param NSString 字符串2
    10 
    11      *
    12 
    13      *  @return 返回拼接好的字符串3
    14 
    15      */    
    16 
    17     NSString* (^logBlock)(NSString *,NSString *) = ^(NSString * str1,NSString *str2){
    18 
    19         return [NSString stringWithFormat:@"%@%@",str1,str2];
    20 
    21     };
    22 
    23     //调用logBlock,输出的是 我是Block
    24 
    25     NSLog(@"%@", logBlock(@“我是",@"Block"));
    26 
    27  

    声明了一个变量名为blk的Block:

    1     int (^blk)(int) = ^(int count)
    2     {
    3         return count +1;
    4     };

    当Block类型变量作为函数的参数时,写作:

    1 - (void)func:(int (^)(int))blk
    2 {
    3     NSLog(@"param:%d",blk(4));
    4 }

    在 - (void) viewDidLoad中调用

    1     [self func:^int(int A) {
    2         NSLog(@"alksjga;lg");
    3         return A +=3;;
    4         
    5     }];

    打印结果为:

    2017-07-27 03:23:06.227 Block_Demo[14689:780926] alksjga;lg
    2017-07-27 03:23:09.537 Block_Demo[14689:780926] param:7
    

    typedef可简写:

    1 typedef int (^blk_k) (int);
    2 
    3 
    4 - (void)funcOne:(blk_k)blk
    5 {
    6     NSLog(@"param:%d",blk(5));
    7 }

    在viewDidLoad中调用:

    1 [self funcOne:^int(int a) {
    2         return  a += 2;
    3     }];

    打印结果为:

    2017-07-27 03:29:09.593 Block_Demo[14689:780926] param:7
    

    4.3、Block结合typedef使用

    自己定义一个Block类型,用定义的类型去创建Block,更加简单便捷。

    这里举例一个Block回调修改上一下界面的背景颜色。

    ViewController1 控制器1,ViewController2 控制器2

    控制器1跳转到控制器2,然后在控制器2触发事件回调修改控制器1的背景颜色为红色。

    Demo

    ViewController2的实现

     1 #import <UIKit/UIKit.h>
     2 
     3 @interfaceViewController2 : UIViewController
     4 
     5 /**
     6 
     7  *  定义了一个changeColor的Block。这个changeColor必须带一个参数,这个参数的类型必须为id类型的
     8 
     9  *  无返回值
    10 
    11  *  @param id
    12 
    13  */
    14 
    15 typedefvoid(^changeColor)(id);
    16 
    17 /**
    18 
    19  *  用上面定义的changeColor声明一个Block,声明的这个Block必须遵守声明的要求。
    20 
    21  */
    22 
    23 @property (nonatomic, copy) changeColor backgroundColor;
    24 
    25 @end
    26 
    27 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    28 
    29     //声明一个颜色
    30 
    31     UIColor *color = [UIColor redColor];
    32 
    33     //用刚刚声明的那个Block去回调修改上一界面的背景色
    34 
    35     self.backgroundColor(color);
    36 
    37 }
    38 
    39  

    ViewController1的实现

     1 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
     2 
     3     ViewController2 *vc =[[ViewController2 alloc]init];
     4 
     5     // 回调修改颜色
     6 
     7     vc.backgroundColor = ^(UIColor *color){
     8 
     9         self.view.backgroundColor = color;
    10 
    11     };
    12 
    13     [self.navigationController pushViewController:vc animated:YES];
    14 
    15 }

    5.Block 截值

    5.1截获自动变量值

    Block表达式可截获所使用的自动变量的值。
    截获:保存自动变量的瞬间值。
    因为是“瞬间值”,所以声明Block之后,即便在Block外修改自动变量的值,也不会对Block内截获的自动变量值产生影响。
    例如:

    1 int i = 10;
    2     void (^blk)(void) = ^{
    3         NSLog(@"In block, i = %d", i);
    4     };
    5     i = 20;//Block外修改变量i,也不影响Block内的自动变量
    6     blk();//i修改为20后才执行,打印: In block, i = 10
    7     NSLog(@"i = %d", i);//打印:i = 20

    5.2 __block说明符号

    自动变量截获的值为Block声明时刻的瞬间值,保存后就不能改写该值,如需对自动变量进行重新赋值,需要在变量声明前附加__block说明符,这时该变量称为__block变量。
    例如:

     1 __block int i = 10;//i为__block变量,可在block中重新赋值
     2     void (^blk)(void) = ^{
     3         NSLog(@"In block, i = %d", i);
     4     };
     5     i = 20;
     6     blk();//打印: In block, i = 20
     7     NSLog(@"i = %d", i);//打印:i = 20
     8 

    5.3 自动变量值为一个对象情况

    当自动变量为一个类的对象,且没有使用__block修饰时,虽然不可以在Block内对该变量进行重新赋值,但可以修改该对象的属性。
    如果该对象是个Mutable的对象,例如NSMutableArray,则还可以在Block内对NSMutableArray进行元素的增删:

    1 NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];
    2     NSLog(@"Array Count:%ld", array.count);//打印Array Count:2
    3     void (^blk)(void) = ^{
    4         [array removeObjectAtIndex:0];//Ok
    5         //array = [NSNSMutableArray new];//没有__block修饰,编译失败!
    6     };
    7     blk();
    8     NSLog(@"Array Count:%ld", array.count);//打印Array Count:1

    参考:Block用法和实现原理

  • 相关阅读:
    在浏览器地址栏按回车、F5、Ctrl+F5刷新网页的区别
    RESTful 的总结
    Mvc项目部署IIS报错:没有为请求的URL配置默认文档,并且没有在服务器设置目录浏览
    Ajax的请求方式几传参的区别
    响应式布局中的CSS相对量
    理解 ES6 语法中 yield* 关键字的作用
    理解 ES6 语法中 yield 关键字的返回值
    配置IIS Express以便通过IP地址访问调试的网站
    在IntelliJ IDEA 13中配置OpenCV的Java开发环境
    iOS UITableView获取cell的indexPath及cell内部按钮点击事件处理
  • 原文地址:https://www.cnblogs.com/EchoHG/p/7007975.html
Copyright © 2020-2023  润新知