程序员

Objective-C 块(Block)的使用与理解

2016-03-17  本文已影响927人  yahtzee_

简书上的所有内容都可以在我的个人博客上找到(打个广告😅)


块(Block)是在 iOS 开发中很常用的一个工具,它可以使我们代码的业务逻辑更加的紧凑。本文会分两部分来讲块,第一部分是块的基础知识,第二部分是对块的本质的一些理解。

块的基础知识


块的声明

块的声明类似于函数指针,只不过把 * 换成了 ^ 。声明时最前面是返回类型,中间是块名,最后是参数类型。

// returnType (^blockName) (parameters)
int (^someBlock) (int, int);

块的实现

// ^returnType(parameters){/*code*/};
^int(int a, int b) {
    return a+b;
};

其中返回值是可以省略的,它可以自动判断,如果参数也是空的话我们可以简化成这样:

^{
    return 100;
};

块的使用

块的使用就像使用 C 函数一样

someBlock(1,2);

用 typedef 来简化块的声明

typedef int (^CYAddBlock) (int, int);
CYAddBlock addBlock = ^(int a, int b) {
    return a+b;
};
addBlock(1,2);

块能捕获在它声明范围里的所有变量

但是不能再块中修改捕获到的变量,如果我们尝试修改编译器会报错,无法通过编译。

int count = 0;
int (^someBlock) () = ^{
    return count;
};
int result = someBlock();   // result = 0

用 __block 修饰变量,可以在块中修改变量的值

__block int count = 0;
int (^someBlock) () = ^{
    return ++count;
};
int result = someBlock();   // result = 1

其实块在捕获变量时只是拷贝了一份变量的值,而用 __block 修饰后,拷贝的是变量的地址,所以我们就可以在块中修改变量了。这就和 C 语言中函数的参数是类似的道理。

小心产生循环引用

在一个类中,块能直接访问并修改实例变量的值,但是一定要注意不管是通过 self 来访问属性还是直接通过 _instanceVariable 来访问都会捕获 self。因为在直接访问 _instanceVariable 等效于这样:

self->_instanceVariable;

而一旦捕获了self, 我们就一定要注意循环引用导致的内存泄露了。我们在 CYClass 中声明了一个块和一个属性:

typedef void (^CYBlock) ();

@interface CYClass : NSObject
@property (nonatomic, copy)NSString *aString;
@property (nonatomic, copy)CYBlock aBlock;
@end

然后重写它的 init 和 dealloc 方法:

@implementation CYClass
- (instancetype)init
{
    if (self = [super init]) {
        _aString = @"Hello Cyrus";
        _aBlock = ^{
            NSLog(@"%@", _aString);
        };
    }
    return self;
}

- (void)dealloc {
    NSLog(@"CYClass deinit");
}
@end

然后我们实例化一个对象,紧接着就把它设为 nil 会发现 dealloc 方法并没有被调用,也就是说发生了内存泄露。

CYClass *c = [CYClass new];
c = nil;

就像这样:

我们可以通过 __weak 来打破这个循环,我们修改一下初始化方法:

- (instancetype)init
{
    if (self = [super init]) {
        _aString = @"Hello Cyrus";
        __weak typeof(self) weakSelf = self;
        _aBlock = ^{
            __strong typeof(self) strongSelf = weakSelf;
            NSLog(@"%@", strongSelf.aString);
        };
    }
    return self;
}

在执行之前的代码就会发现这个对象成功的销毁了。

2016-03-12 16:30:24.995 BlockExample[4777:111670] CYClass deinit

块的本质


块的结构

我们可以在这里找到 Block 的定义:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

看到 Block_layout 结构体里的第一个变量是一个 isa 指针, 我们应该就能猜到 Block 也是一个对象。然后再看第四个变量是一个叫 invoke 的函数指针,它指向的就是 Block 具体实现的函数了,这个函数至少要接受一个 void* 类型的参数,这个参数就是块本身。再下面的 descriptor 指向的就是 Block_descriptor 结构体,里面就是一些 Block 的信息。而在这些变量下面存放的就是 Block 捕获的那些变量

块的类型

在 OC 中一共有3种块,分别是:

我们在 MRC 的环境下运行下面的代码:

void (^globalBlock)()  = ^{ NSLog(@"global block"); };
NSLog(@"%@", globalBlock);
        
NSString *str1 = @"stack block";
void (^stackBlcok)() = ^{ NSLog(@"%@", str1); };
NSLog(@"%@", stackBlcok);
    
NSString *str2 = @"malloc block";
void (^mallockBlock)() = [^{ NSLog(@"%@", str2); } copy];
NSLog(@"%@", mallockBlock);

打印结果:

2016-03-13 16:23:50.676 BlockExample[1760:28902] <__NSGlobalBlock__: 0x1000042b0>
2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSStackBlock__: 0x7fff5fbff790>
2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSMallocBlock__: 0x100303fa0>

结果和我们预期的结果是一样的。我们在换成 ARC 的环境下运行一次:

2016-03-13 16:26:05.767 BlockExample[1805:29822] <__NSGlobalBlock__: 0x1000052f0>
2016-03-13 16:26:05.768 BlockExample[1805:29822] <__NSMallocBlock__: 0x100400340>
2016-03-13 16:26:05.769 BlockExample[1805:29822] <__NSMallocBlock__: 0x1004001b0>

我们发现原来的栈块也变成了堆块。这是因为在把一个快赋值给一个strong对象时 ARC 会自动帮我们执行一次copy。如果我们直接这样,那么还是会打印出 __NSStackBlock :

NSString *str1 = @"stack block";
NSLog(@"%@", ^{ NSLog(@"%@", str1); });
// 打印结果
2016-03-13 16:30:06.180 BlockExample[1881:32260] <__NSStackBlock__: 0x7fff5fbff798>
上一篇 下一篇

猜你喜欢

热点阅读