Objective-C的Block

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

目录

Block入门

什么是Block

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

如何定义一个Block

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

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

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

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

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

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

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

^{printf("hello world");}

如何声明一个Block类型的变量

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

int (^blk)(int);

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

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

int (*funcptr)(int) = &func;

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

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

把一个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;

用typedef为Block类型定义一个简单的别名

typedef int (^qhdBlock)(int);

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

调用Block

就像调用C语言函数一样

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

用Block作为回调

- (void)querNetworkDataWithCallBack:(void(^)(id))callBack {
    id result = nil;
    
    //这里从网络中获取数据,给result赋值
    //通常较耗时,需要开子线程
    
    if (callBack) {
        callBack(result);
    }
}

- (void)test {
    [self querNetworkDataWithCallBack:^(id data) {
        //使用网络返回的数据
        //NSLog(@"%@", data);
    }];
}

用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]);
}

截取自动变量

(这也是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执行时自动变量的值。
这就是自动变量值的截获

一些用法疑问

在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

__block标识符的用法

截获基本数据类型的变量

假如我们想在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

截获对象类型的变量

在截获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];
};

Block实质

初探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代码都比较清晰。

带__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能够改变外部变量的原因,是传递了指针,不是简单地传值。

存储域

学习过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进行复制,要用如下修饰符:

编译环境修饰符
MRCcopy
ARCcopy,strong

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

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

复制Block后,__block变量的存储域变化

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

循环引用问题

@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)
    原文作者:qhd
    原文地址: https://www.jianshu.com/p/eb13952769e2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞