美文网首页
iOS block的用法和原理实现

iOS block的用法和原理实现

作者: 153037c65b0c | 来源:发表于2020-01-09 17:11 被阅读0次

    1.block的语法

    1.1 标准声明和定义

     返回类型(^block名称)(参数类型) = ^返回类型(变量类型 变量名称){实现}
    

    直接定义block时,可以省略定义时的返回类型,即

    返回类型(^block名称)(参数类型) = ^(变量类型 变量名称){实现}
    

    若参数类型为void,可省略写成

    返回类型(^block名称)(void) = ^{实现}
    

    匿名block:block定义时,等号右边的即为匿名block

    1.2 typedef简化block的声明

    typedef 返回类型(^block名称)(参数类型);
    

    2. block中变量的访问

    2.1 block访问局部变量

        int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"num的值为%d", num);
        };
        num = 15;
        myBlock();
    

    输出结果为10,若在block中修改num的值,则会报错。
    把上述代码转为c++代码,转换步骤:

    在终端cd当文件所在目录,在终端输入命令 clang -rewrite-objc 文件名
    

    转换前的代码

    @implementation WYYBlock
    - (void)test{
        int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"num的值为%d", num);
        };
        num = 15;
        myBlock();
    }
    @end
    

    转换后的代码与block相关的如下:

    struct __WYYBlock__test_block_impl_0 {
      struct __block_impl impl;
      struct __WYYBlock__test_block_desc_0* Desc;
      int num;
      __WYYBlock__test_block_impl_0(void *fp, struct __WYYBlock__test_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __WYYBlock__test_block_func_0(struct __WYYBlock__test_block_impl_0 *__cself) {
      int num = __cself->num; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_bh_3br_11pn5hxckj2qxzhfb1c80000gp_T_WYYBlock_00083b_mi_0, num);
        }
    
    static struct __WYYBlock__test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __WYYBlock__test_block_desc_0_DATA = { 0, sizeof(struct __WYYBlock__test_block_impl_0)};
    
    static void _I_WYYBlock_test(WYYBlock * self, SEL _cmd) {
        int num = 10;
    //声明定义block
        void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0((void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, num));
        num = 15;
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    

    定义声明block转换后的代码是下面的内容

    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
            (void*)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, num)
        );
    

    可以看出来,block变量实际上是一个指向结构体__WYYBlock__test_block_impl_0的指针,而结构体的第三个元素是局部变量num的值
    __WYYBlock__test_block_impl_0中,WYYBlock是该文件的类名,test是block所在方法的名称。
    结构体的第一个变量是(void*)__WYYBlock__test_block_func_0,在该方法中可以看到,在方法内部有一个参数num接受了外面的num
    基本数据类型会直接把值传递过来,所以num的值在block定义完时就已经固定,在定义之后无法在进行修改。

    2.2block中访问__block修饰的局部变量

    把OC代码修改为

    - (void)test{
        __block int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"num的值为%d", num);
        };
        num = 15;
        myBlock();
    }
    

    可以看到转换后,block的声明和定义变为

    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
      (void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344)
    );
    

    可以看到原来的第三个参数num,变成了*(__Block_byref_num_0 )&num,传入的是num的引用,所以此时定义block后改变num的值,调用block时,num的值会相应改变,也可以在block中改变num的值。
    同时,在结构体__WYYBlock__test_block_impl_0中第三个变量也由原来的 int num;变成了 __Block_byref_num_0 *num;
    总结:block在定义后,局部变量的值改变不会改变block中局部变量的值,并且在block中局部变量的值无法修改。因为在Block定义时便是将 "局部变量的值" 传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改并不会影响Block内部的值。如果要在block中修改局部变量的值,或者局部变量的值会随时修改,局部变量需要用__block修饰

    2.3block内访问全局变量

    把OC代码修改为

    int num = 10;
    - (void)test{
        void (^myBlock)(void) = ^{
            NSLog(@"num的值为%d", num);
        };
        num = 15;
        myBlock();
    }
    

    运行结果为15,同时在block中修改num的值也不会报错
    把当前代码编译成C++文件后可以发现,声明定义block时并没有再传入num的值或者指针

    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
    (void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA) );
    

    __WYYBlock__test_block_impl_0结构体中也没有了num,所以说明全局变量不会再传入block中,当调用block时需要使用全局变量时是从类的空间中获取的,所以调用block时获取到的值是num最后的值,在block中也能修改num的值。
    全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block变量所指向的结构体。

    2.4 block内访问静态变量

    把OC代码修改为

    - (void)test{
        static int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"num的值为%d", num);
        };
        num = 15;
        myBlock();
    }
    

    运行结果为15,同时在block中修改num的值也不会报错
    把当前代码编译成C++文件后可以发现,声明定义block时传入的是num的指针

    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
    (void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA,  &num));
    

    2.5block内访问类型为对象的局部变量

    有一个WYYPerson类,类中有一个int类型的age属性,OC代码如下

    WYYPerson *person = [[WYYPerson alloc] init];
        person.age = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"%d", person.age);
        };
        person.age = 18;
        myBlock();
    

    运行结果为18,因为局部对象的类型为对象时,对象传入的是引用
    编译成C++后的文件内容

    //block中的代码
    static void __WYYBlock__test_block_func_0(struct __WYYBlock__test_block_impl_0 *__cself) {
      WYYPerson *person = __cself->person; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_bh_3br_11pn5hxckj2qxzhfb1c80000gp_T_WYYBlock_90d3f3_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
        }
    //block的定义和实现
    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
    (void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, person, 570425344));
    

    从block的定义和实现可以知道,传入的是person对象的指针,从block中的代码可以知道,当获取person的值是通过给person对象发送消息获取age的值的,所以在block中也可以修改person的值
    把一个类改为在MRC模式下的步骤:
    Build phases -> Compile Sources -> 双击需要改成MRC模式的类会弹出输入框 -> 把-fno-objc-arc复制粘贴进去

    3.block的三种类型

    1.NSGlobalBlock:全局的静态 block,在block中不访问外部局部变量,可以访问外部全局变量和静态变量。此时为NSGlobalBlock。
    2.NSStackBlock :保存在栈中的 block,当函数返回时会被销毁。
    3._NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

    3.1 在MRC和ARC两种情况下,block内部不使用局部变量,在block中使用全局变量和静态变量,block是NSGlobalBlock

    在block中不访问外部局部变量,代码如下

      void (^myBlock)(void) = ^{
            NSLog(@"不访问外部局部变量");
        };
        myBlock();
        NSLog(@"%@", myBlock);
    

    打印结果为
    不访问外部局部变量
    <NSGlobalBlock: 0x100f182c0>
    在MRC模式下打印结果相同
    在MRC模式下在block内部访问静态变量,block也是NSGlobalBlock
    代码

    static int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"%d", num);
        };
        myBlock();
        NSLog(@"%@", myBlock);
    

    结果:<NSGlobalBlock: 0x1006142c0>
    在MRC模式下在block内部访问全局变量,block也是NSGlobalBlock
    代码:

    int num = 10;
    - (void)test{
        void (^myBlock)(void) = ^{
            NSLog(@"%d", num);
        };
        myBlock();
        NSLog(@"%@", myBlock);
    }
    

    结果:<NSGlobalBlock: 0x1045742c0>
    总结:
    在ARC下,block内部不使用局部变量,在block中使用全局变量和静态变量,block的类型都是NSGlobalBlock
    在MRC下,block内部不使用局部变量,在block中使用全局变量和静态变量,block的类型都是NSGlobalBlock

    3.2 在MRC情况下会存在栈block

    在ARC模式下没有NSStackBlock,必须在MRC下才能看到, 代码如下:

        int num = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"%d", num);
        };
        myBlock();
        NSLog(@"%@", myBlock);
    

    打印结果为:<NSStackBlock: 0x16ba85328>

    3.3 在MRC下分别用assign,retain,strong,weak,copy修饰block

    如果是全局block的话,不管用什么修饰词,仍然是全局block,此处不再做代码验证,我自己已经打印过结果,下面只验证在MRC下使用局部变量,本来是栈block
    用assign关键字修饰block,完整代码:

    @interface WYYBlock ()
    @property (nonatomic, assign) void (^myBlock)(void);
    
    @end
    
    @implementation WYYBlock
    
    - (void)test{
        int num = 10;
        self.myBlock = ^{
            NSLog(@"%d", num);
        };
        self.myBlock();
        NSLog(@"%@", self.myBlock);
    }
    - (void)dealloc{
        NSLog(@"WYYBlock的dealloc方法被调用");
        [super dealloc];
    }
    - (void)test2{
        NSLog(@"%@", self.myBlock);
    }
    @end
    

    在外面先调用test方法,再调用test2方法,在执行test2中的代码NSLog(@"%@", self.myBlock);,崩溃。说明在栈中的block出栈之后即会被销毁,此时block为栈block。
    用retain关键字修饰block

    @property (nonatomic, retain) void (^myBlock)(void);
    结果与用assign修饰时相同,仍然为栈block,在执行test2中的代码*NSLog(@"%@", self.myBlock);*,崩溃
    

    用weak关键字修饰block

    @property (nonatomic, weak) void (^myBlock)(void);
    结果与用assign修饰时相同,仍然为栈block,在获取myBlock时崩溃,造成了野指针
    

    用strong关键字修饰block

    @property (nonatomic, strong) void (^myBlock)(void);
    打印结果为
    <__NSMallocBlock__: 0x280588a20>
     <__NSMallocBlock__: 0x280588a20>
    此时block变成了堆block,不再随方法结束而销毁,所以可以第二次进行调用
    

    用copy关键字修饰block

    @property (nonatomic, copy) void (^myBlock)(void);
    结果与用strong修饰时相同,block变成了堆block
    

    由以上结果可以得到结果,在MRC模式下,当用copy和strong修饰时,block会被存放在堆中,不会因为所在方法结束而销毁block。

    3.4 在ARC下分别用assign,retain,strong,weak,copy修饰block

    如果是全局block的话,不管用什么修饰词,仍然是全局block,此处不再做代码验证,我自己已经打印过结果,下面只验证在ARC下使用局部变量,本来是堆block
    用assign关键字修饰block

    @property (nonatomic, assign) void (^myBlock)(void);
    此时会变成栈block,当在test2,再次调用block时,会崩溃,因为block已经被销毁
    

    用weak修饰时也会变成栈block,并且编译器会提示使用过后会销毁
    用retain,strong,copy修饰时都仍然是堆block。
    用retain修饰时会提示警告

    Retain'ed block property does not copy the block - use copy attribute instead
    提示,retain不会赋值block,推荐使用copy代替
    

    总结:
    1.block内部没有调用外部局部变量,或者调用了全局变量或者静态变量时,在MRC和ARC时都在全局区。
    2.在ARC下,block调用了外部局部变量,系统默认会对block进行copy操作,也在堆区。在MRC下block调用了外部局部变量,会在栈区,所以此时修饰block要使用strong或者copy,把block放在堆区,这样block才不会随时被销毁,造成调用的时候已经被销毁的情况。
    3.block造成循环引用的原因:block内部是一个结构体,会捕获外部传入的局部变量,保存在block的结构体中,如果在block中使用self,block又被self持有,此时会造成循环引用
    4.block用weakSelf时,为什么在block内部又要声明一个strongSelf?
    如果只用weakSelf打破循环引用的话,block是没有持有控制器的,当控制器被pop掉的时候,控制器就已经被销毁了,可能此时block被调用,可是控制器已经被销毁了,会引起崩溃,所以需要在block中在强引用self
    5.strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。

    __weak __typeof__(self) weakSelf = self; 
    __strong __typeof(self) strongSelf = weakSelf;  
    

    相关文章

      网友评论

          本文标题:iOS block的用法和原理实现

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