Objective-C的Block

作者: qhd | 来源:发表于2017-03-24 17:28 被阅读1023次

    声明:本文是读了<Objective-C高级编程>做的笔记,以及结合本人写的例子总结的Block知识。

    目录

    <a name="a1"></a>Block入门

    <a name="a1-1"></a>什么是Block

    Block是带有自动变量值的匿名函数。

    <a name="a1-2"></a>如何定义一个Block

    跟定义一个函数是差不多的,只是不需要写名字。
    完整的语法:^ 返回值类型 参数列表 表达式

    ^int (int count){return count + 1;}
    

    若没有返回值,可以省略返回值类型:^ 参数列表 表达式

    ^ (int count){return count + 1;}
    

    若不使用参数,参数列表也可省略。以下为不适用参数的Block语法:

    ^void (void){printf("hello world");}
    

    该代码可以省略为如下形式:

    ^{printf("hello world");}
    

    <a name="a1-3"></a>如何声明一个Block类型的变量

    返回值类型 (^变量名称) 参数列表

    int (^blk)(int);
    

    其实这就跟C语言中的函数指针很相似:

    int func(int count) {
        return count + 1; 
    }
    
    int (*funcptr)(int) = &func;
    

    Block作为一个方法的参数时,声明的写法有点不一样:

    + (void)querDataWithCallBack:(void(^)(id))callBack;
    

    <a name="a1-4"></a>把一个Block赋值给Block类型变量

    就是声明写在左边,定义写在右边,中间一个等号。有了以上两个解析,很容易就可以写出:

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

    也可以这样写:

    int (^blk)(int);
    blk = ^int (int count) {
        return count + 1;
    };
    
    int (^blk1)(int) = blk;
    
    int (^blk2)(int);
    blk2 = blk1;
    

    <a name="a1-5"></a>用typedef为Block类型定义一个简单的别名

    typedef int (^qhdBlock)(int);
    
    qhdBlock a = ^ int (int count) {
        return count + 1;
    };
    
    

    <a name="a1-6"></a>调用Block

    就像调用C语言函数一样

    int (^blk)(int) = ^int (int count) {
        return count + 1;
    }; 
    blk(1); //调用
    

    <a name="a1-7"></a>用Block作为回调

    - (void)querNetworkDataWithCallBack:(void(^)(id))callBack {
        id result = nil;
        
        //这里从网络中获取数据,给result赋值
        //通常较耗时,需要开子线程
        
        if (callBack) {
            callBack(result);
        }
    }
    
    - (void)test {
        [self querNetworkDataWithCallBack:^(id data) {
            //使用网络返回的数据
            //NSLog(@"%@", data);
        }];
    }
    

    <a name="a1-8"></a>用Block实现策略模式

    typedef int (^calculateBlock)(int,int);
    
    - (int)calculateBlock:(calculateBlock)type num1:(int)num1 num2:(int)num2 {
        return type(num1, num2);
    }
    
    - (void)test {
        calculateBlock add = ^(int a1, int a2) { return a1 + a1; };
        calculateBlock subtract = ^(int a1, int a2) { return a1 - a1; };
        calculateBlock multiply = ^(int a1, int a2) { return a1 * a1; };
        calculateBlock divide = ^(int a1, int a2) { return a1 / a1; };
        
        NSLog(@"4+5=%d",[self calculateBlock:add num1:4 num2:5]);
        NSLog(@"4-5=%d",[self calculateBlock:subtract num1:4 num2:5]);
        NSLog(@"4x5=%d",[self calculateBlock:multiply num1:4 num2:5]);
        NSLog(@"4/5=%d",[self calculateBlock:divide num1:4 num2:5]);
    }
    

    <a name="a1-9"></a>截取自动变量

    (这也是Block和函数指针的区别,因为Block的定义可以嵌套在方法里,所以能够截取方法里的自动变量)
    举个例子:

    - (void)test {
        int val = 10;
        NSString *str = @"hello";
        void (^blk)() = ^ {
            NSLog(@"%@:%d", str, val);
        };
        val = 50;
        str = @"good";
        blk();
    }
    

    这里运行的结果是:hello:10
    而不是预期的:good:50
    说明了在Block中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写自动变量的值也不影响Block执行时自动变量的值。
    这就是自动变量值的截获

    <a name="a2"></a>一些用法疑问

    <a name="a2-1"></a>在MRC下,Block在声明property时为什么要用copy

    因为Block是在栈上的,不是在堆上的,超出作用域就会被自动释放。因此需要用copy,而不是用retain。如果是在ARC下,copy和strong都行。

    以下例子,在Block有读取外部变量的情况下,用retain修饰,不在有效作用域内执行block会报EXC_BAD_ACCESS错误,即野指针异常问题,为什么会出现此错误,因为Block的作用域范围是在viewDidLoad内,超出作用域Block就会被释放,在viewDidAppear时已经超出了作用域。解决此问题的方法是把retain换成copy。

    @interface ViewController ()
    
    @property (retain, nonatomic) void(^myBlock)();
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        int a = 1;
        void (^tempBlock)() = ^() {
            NSLog(@"a:%d", a);
        };
        [self setMyBlock:tempBlock];
        NSLog(@"finish set");//在这断点,观察myBlock的地址
    }
    
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];//在这断点,再观察myBlock的地址,已经变了
        
        self.myBlock(); 
        NSLog(@"finish run");
    }
    
    @end
    

    <a name="a3"></a>__block标识符的用法

    <a name="a3-1"></a>截获基本数据类型的变量

    假如我们想在Block里面改变在Block以外的变量值,看例子:

    int a = 0;
    void (^blk)(void) = ^{ a = 1;}; //这里会报错:Variable is not assignable (missing __block type specifier)
    blk();
    printf("a = %d", a);
    

    看报错提示,我们给int a附加__block说明符,就能实现Block内赋值:

    __block int a = 0;
    void (^blk)(void) = ^{ a = 1;};
    blk();
    printf("a = %d", a);
    

    该代码的执行结果:a = 1

    <a name="a3-2"></a>截获对象类型的变量

    在截获Objective-C对象时:

    NSMutableArray *m = [NSMutableArray array];
    void (^blk)(void) = ^{
        [m addObject:@"abc"];
    };
    blk();
    NSLog(@"m[0] = %@", m[0]);
    

    这是没有问题的,因为m就是一个对象指针,所以Block是截获了指针,在Block里面对指针所指向的内容进行修改是可以的。

    但是给m赋值会怎样:

    NSMutableArray *m = [NSMutableArray array];
    void (^blk)(void) = ^{
        m = [NSMutableArray array];//这里会报错:Variable is not assignable (missing __block type specifier)
    };
    

    这是会产生编译错误。

    这种情况,需要给截获的自动变量附加__block说明符。

    __block NSMutableArray *m = [NSMutableArray array];
    void (^blk)(void) = ^{
        m = [NSMutableArray array];
    };
    

    <a name="a4"></a>Block实质

    <a name="a4-1"></a>初探Block转换出的C代码

    新建一个block.m文件,这个例子有两个Block,这样比《Objective-C高级编程》中的例子更好说明哪些是公共的,有参数是怎样的。

    #import <Foundation/Foundation.h>
    
    int main(int argc, char * argv[]) {
        
        void (^myBlock)(void) = ^{
            printf("hello world\n");
        };
        myBlock();
        
        int a = 10;
        BOOL (^yourBlock)(int) = ^(int b){
            int sum = a + b;
            printf("%d + %d = %d\n", a, b, sum);
            return YES;
        };
        yourBlock(5);
        
        return 0;
    }
    

    运行命令:clang -rewrite-objc block.m 生成文件blcok.cpp,通过"-rewrite-objc"选项就能将含有Block语法的源代码变换为C++的源码,其实只是用到了struct结构,其本质是C语言代码。变换后的代码如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    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)};
    
    
    struct __main_block_impl_1 {
        struct __block_impl impl;
        struct __main_block_desc_1* Desc;
        int a;
        __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    
    static BOOL __main_block_func_1(struct __main_block_impl_1 *__cself, int b) {
        int a = __cself->a; // bound by copy
        
        int sum = a + b;
        printf("%d + %d = %d\n", a, b, sum);
        return ((bool)1);
    }
    
    static struct __main_block_desc_1 {
        size_t reserved;
        size_t Block_size;
    } __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
    
    
    int main(int argc, char * argv[]) {
        
        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
        
        int a = 10;
        BOOL (*yourBlock)(int) = ((BOOL (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, a));
        ((BOOL (*)(__block_impl *, int))((__block_impl *)yourBlock)->FuncPtr)((__block_impl *)yourBlock, 5);
        
        return 0;
    }
    

    以上,我故意把第一个结构体与下面的代码分开,来表达__block_impl是默认自带的结构体。
    第一眼看起来很多代码,写惯OC的可能看不惯C的写法,其实不太复杂,就是结构体的声明与构造,还有一些类型转换。

    我们定义了第一个Block(即myBlock),就生成了:

    • __main_block_impl_0(myBlock的结构体)
    • __main_block_func_0(myBlock的执行函数)
    • __main_block_desc_0(myBlock的描述)

    第二个Block(即yourBlock)则生成了:

    • __main_block_impl_1(yourBlock的结构体)
    • __main_block_func_1(yourBlock的执行函数)
    • __main_block_desc_1(yourBlock的描述)

    可见Block对应的结构体或函数的命名是跟顺序相关的,并且每新定义1个Block就至少产生2个结构和1个函数。

    我们先看__main_block_impl_x(x是指序号)里面有什么,直接用代码跟注释的方式:

    struct __main_block_impl_x {
        struct __block_impl impl;         //这是Block的公共结构体
        struct __main_block_desc_x* Desc; //这是Block的描述,指向__main_block_desc_x结构体
        int a;                            //这里就是截获的自动变量
        ...                               //如果截获了多个自动变量,这里会有多个
        //这是自身这个结构体的构造函数
        __main_block_impl_x(void *fp, struct __main_block_desc_x *desc, int _a, int flags=0) : a(_a) {//这里_a参数会赋值给a变量
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    

    我们再看__block_impl是什么,继续以代码加注释的方式:

    struct __block_impl {
      void *isa;     //isa有点类似于class的isa,有三种值:&_NSConcreteStackBlock/&_NSConcreteGlobalBlock/&_NSConcreteMallocBlock
      int Flags;     
      int Reserved;
      void *FuncPtr; //函数指针
    };
    

    然后我们可以看回__main_block_impl_x的构造函数里的实现,给isa赋了&_NSConcreteStackBlock,给FuncPtr赋了第一个参数fp,给Desc赋了第二个参数desc,给a赋了第三个参数_a

    我们再看__main_block_desc_x的结构体,继续以代码加注释的方式:

    static struct __main_block_desc_x {
        size_t reserved;   
        size_t Block_size;  //Block的大小
    } __main_block_desc_x_DATA = { 0, sizeof(struct __main_block_impl_x)};//这里定义了一个变量__main_block_desc_x_DATA
    

    我们再看Block的执行函数,第一个参数是当前函数所属于的Block,第二个参数开始就是Block的参数了。

    static BOOL __main_block_func_x(struct __main_block_impl_x *__cself, int b)
    

    在main函数里,我们把类型转换去掉就清晰多了:

    int main(int argc, char * argv[]) {
    
        void (*myBlock)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
        (myBlock->FuncPtr)(myBlock);
        
        int a = 10;
        BOOL (*yourBlock)(int) = (&__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, a));
        (yourBlock->FuncPtr)(yourBlock, 5);
    
        return 0;
    }
    

    在main函数里,先初始化Block,再调用Block。第二个Block,在初始化时会传入a变量的值,调用Block时传入5。
    至此为止,定义的两个Block算简单,转出来的C代码都比较清晰。

    <a name="a4-2"></a>带__block标识符转出来的C代码

    下面看看带有__block标识符的变量转成C代码是怎样的:

    #import <Foundation/Foundation.h>
    
    int main(int argc, char * argv[]) {
        
        __block int a = 1;
        void (^aBlock)(void) = ^{
            a = 2;
        };
        aBlock();
        
        return 0;
    }
    

    运行"clang -rewrite-objc"后:

    struct __Block_byref_a_0 {
        void *__isa;
        __Block_byref_a_0 *__forwarding;
        int __flags;
        int __size;
        int a;
    };
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        __Block_byref_a_0 *a; // by ref
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
        (a->__forwarding->a) = 2;
    }
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*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, char * argv[]) {
    
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
        void (*aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
    
        return 0;
    }
    

    对比之前的例子,我们发现多了一个结构体__Block_byref_a_0和两个函数__main_block_copy_0__main_block_dispose_0

    我们看结构体__Block_byref_a_0,里面有一个跟原自动变量的类型和命名都一样的成员变量a,又有一个指向自身类型的指针__forwarding,这不是链表型的设计吗。后来一看main函数在创建__Block_byref_a_0类型的a变量时,把__forwarding指向了自己,再看创建__main_block_impl_0实例时传递的参数值时&a,即a地址。再看Block的执行函数__main_block_func_0,原来的a=2;变成了(a->__forwarding->a) = 2;,这就是为什么在Block能够改变外部变量的原因,是传递了指针,不是简单地传值。

    <a name="a4-3"></a>存储域

    学习过C语言的应该知道,一个程序的内存分为:

    • 代码区(.text区)
    • 数据区(.data区)

    Block的类对应的存储域对应如下:

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

    到目前为止以上的例子都是_NSConcreteStackBlock类,那什么时候是其他类型呢,看以下例子,当全局变量的地方有Block语法时,Block就会是_NSConcreteGlobalBlock类对象

    void (^myBlock)(void) = ^{printf("hello");};
    
    int main(int argc, char * argv[]) {
        return 0;
    }
    

    那么_NSConcreteMallocBlock类何时会使用?当在栈区的Block超出了其所属的作用域,该Block就会被废弃。如果我们想在超出其作用域时使用它,那么我们就需要把Block复制到堆上,在堆上的Block其类型就是_NSConcreteMallocBlock

    以下我们来看个例子:

    @interface ViewController ()
    @property (copy, nonatomic) void(^myBlock)();
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        int a = 1;
        void (^tempBlock)() = ^() {
            NSLog(@"a:%d", a);
        };
        [self setMyBlock:tempBlock];
        NSLog(@"finish set");
    }
    
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        self.myBlock();
        NSLog(@"finish run");
    }
    
    @end
    

    如果用Xcode断点调试会发现tempBlock的__isa__NSStackBlock__,而myBlock的__isa__NSMallocBlock__,对Block进行复制会把其从栈复制到堆。在ARC时以上代码把property的修饰符改为strong也是会对Block进行复制的,但用assign是不会复制的。在MRC时property的修饰符设置copy才会复制,retain并不会复制。

    总结,如果我们想要长久持有Block,对Block进行复制,要用如下修饰符:

    编译环境 修饰符
    MRC copy
    ARC copy,strong

    另外对在不同区域的Block进行复制的效果如下:

    Block的类 源所在的存储域 复制效果
    _NSConcreteStackBlock 从栈复制到堆
    _NSConcreteGlobalBlock 数据区域 什么都不做
    _NSConcreteMallocBlock 引用计数增加

    <a name="a4-4"></a>复制Block后,__block变量的存储域变化

    当把Block从栈复制到堆后,__block变量也会复制到堆。复制到堆后,怎么保持两边的值一致呢?这时我们回顾一下结构体成员变量__forwarding,在复制前,栈上的__block变量的__forwarding指向了自己,在复制后,栈上的__block变量的__forwarding指向了堆上的__block变量,而堆上的__block变量的__forwarding仍然是指向自己,前面看到读写变量实际是读取__forwarding指向的结构体下的成员变量,这样就相当于,两边的变量都是用了同一个值。

    <a name="a4-5"></a>循环引用问题

    @interface User : NSObject
    @property (copy, nonatomic) void (^myBlock)();
    @end
    
    @implementation User
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            [self setMyBlock:^{
                NSLog(@"%@", self);
            }];
        }
        return self;
    }
    
    @end
    

    以上代码,当创建User类的对象时,对象持有成员变量_myBlock_myBlock持有了所使用的self(即对象本身),这就是循环引用,会造成内存泄漏。

    可以这样来避免循环引用:

            __weak User *ws = self;
            [self setMyBlock:^{
                NSLog(@"%@", ws);
            }];
    

    当然用__unsafe_unretained也是可以的,但是__weak可以防止野指针的问题。这是比较简单的循环,因为只有两个对象互相引用,在项目中可能有更多的对象组成了循环,所以写代码时要清晰地知道会不会有强引用。

    注意在MRC时,retain并不会复制Block,推荐使用copy来持有Block。

    另外在MRC时,__block说明符被用来避免Block中的循环引用。这是由于Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若没有__block说明符,则被retain。

    在ARC时,__block的作用只是能否改变外部变量。所以要注意__block说明符在MRC和ARC是有很大区别的。当然我们现在的项目代码都使用ARC来写项目了。

    附上我常用的定义weakSelf,strongSelf的宏:

    #define WEAK_OBJ(obj, name)     __weak __typeof(obj) name = obj
    
    #define STRONG_OBJ(obj, name)   __strong __typeof(obj) name = obj
    
    #define WEAK_SELF(name)         WEAK_OBJ(self, name)
    

    相关文章

      网友评论

      本文标题:Objective-C的Block

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