iOS进阶 -- Block基础探索

前言

对于iOS开发人员来说,block可以说是很熟悉的了。在平时的开发中,我们经常会使用到block作为回调,或作为一个属性,或用作方法的参数。当然,在面试中,block相关的问题,也会经常被问到,例如block的分类等。本篇我们就来进行一下block的基础探索,主要分为两个方面:

  • block有哪几种
  • block的循环引用问题

一、block的分类

block还有分类吗?答案当然是有的。为了验证这一点,我们新建一个iOS工程,在viewDidLoad方法中,写下如下代码:

int a = 10;

void (^myBlock)(void) = ^{
};

void (^testBlock)(void) = ^{
    NSLog(@"%d", a);
};

NSLog(@"%@", myBlock);
NSLog(@"%@", testBlock);
复制代码

分别打印两个block的结果如下:

Xnip2021-12-06_18-10-53.png

可以发现myBlocktestBlock虽然看起来是一样的,都是无参数无返回值的,但是却分别为两种不同类型__NSGlobalBlock____NSMallocBlock__,也就是全局block堆block

对比两者,其唯一的区别就在于testBlock引用了一个外部的局部变量a,那么将testBlock中的代码注释掉呢?其结果如下:

Xnip2021-12-06_18-16-44.png

结果表明只要不引用外部的局部变量,testBlock也是一个全局block。那么我们再试一下,testBlock引用一个全局变量或静态变量:


int globalA = 20; // 新增
static int b = 30; // 新增

int a = 10;
void (^myBlock)(void) = ^{
};

void (^testBlock)(void) = ^{
    NSLog(@"%d", a);
};

// 新增
void (^globalVarBlock)(void) = ^{
    NSLog(@"%d", globalA);
};

// 新增
void (^staticTestBlock)(void) = ^{
    NSLog(@"%d", b);
};

NSLog(@"%@", myBlock);
NSLog(@"%@", testBlock);
NSLog(@"%@", globalVarBlock); // 新增
NSLog(@"%@", staticTestBlock); //新增
复制代码

打印结果如下:

Xnip2021-12-06_18-21-54.png 结果显示,当引用全局变量和静态变量时,globalVarBlockstaticTestBlock依然是全局block。因此可以发现,block是否为全局block,在于其是否捕获外部局部变量

常说堆栈,既然有堆block,那么有没有栈block呢?当然有。在上面的demo中,再加一个例子进行测试:

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

void (^ __weak weakBlock)(void) = ^{
    NSLog(@"%d", a);
};

NSLog(@"%@", testBlock);
NSLog(@"%@", weakBlock);
复制代码

打印结果如下:

Xnip2021-12-06_18-40-34.png

结果显示,当加上__weak修饰后,weakBlock的类型就变成了__NSStackBlock__,也即栈block。

我们继续测试,将weakBlock拷贝给另一个block,代码如下:

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

void (^ __weak weakBlock)(void) = ^{
    NSLog(@"%d", a);
};

void (^ copyBlock)(void) = [weakBlock copy];

NSLog(@"%@", testBlock);
NSLog(@"%@", weakBlock);
NSLog(@"%@", copyBlock);
复制代码

结果如下所示: Xnip2021-12-06_18-45-12.png 很显然,将一个栈block拷贝给一个变量后,新的变量不再是栈block,而是一个堆block。可以发现,testBlock是因为强引用,copyBlock是因为copy,两者皆是堆block,也就是说堆block和栈block的区别为是否是强引用或copy

不过上面这个结果并不能信服,因为我们使用的都是局部变量。下面我们分别看下,block作为返回值和属性的情况。

1、作为方法返回值

首先新写一个方法,并且分别用__weak接收不用__weak接收,代码如下:

- (void (^)(void))getBlock {
    int a = 10;
    return ^{
        NSLog(@"%d", a);
    };;
}

void (^ returnBlock)(void) = [self getBlock];
void (^ __weak weakReturnBlock)(void) = [self getBlock];

NSLog(@"%@", returnBlock);
NSLog(@"%@", weakReturnBlock);
复制代码

打印结果如下:

Xnip2021-12-06_20-59-00.png

结果显示,不管是否使用__weak修饰,block作为方法参数返回值时,都会拷贝到堆区。

2、作为属性 新建三个block属性,分别使用copy、strong、weak修饰,然后赋值相同的block,代码如下:

@property (nonatomic, copy) void (^blockCopy)(void);
@property (nonatomic, strong) void (^blockStrong)(void);
@property (nonatomic, weak) void (^blockWeak)(void);

self.blockCopy = ^{
    NSLog(@"%d", a);
};

self.blockStrong = ^{
    NSLog(@"%d", a);
};

self.blockWeak = ^{
    NSLog(@"%d", a);
};

NSLog(@"%@", self.blockCopy);
NSLog(@"%@", self.blockStrong);
NSLog(@"%@", self.blockWeak);
复制代码

打印结果如下:

Xnip2021-12-06_21-07-45.png

结果也是均为堆block。

根据测试结果,可以总结如下:

  • block可以分为三种,全局block、堆block和栈block,分别对应__NSGlobalBlock____NSMallocBlock____NSStackBlock__三种类型
  • 不引用外部局部变量,或只引用静态或全局变量的为全局block
  • 使用外部局部变量的为堆block或栈block,两者区别在于:
    • 在函数内部使用__weak修饰的为栈block
    • 赋值给强引用或者手动copy的为堆block
    • 作为方法返回值或者属性的也为堆block

二、block与循环引用

在iOS中采用的是ARC的内存管理方式,其中很重要的一点就是引用计数方式,也就是说当一个对象被alloc、copy或者retain时,其引用计数就会加1,当一个对象的引用计数为0时,就表示该对象不再被引用,可以释放了。

如果这种引用关系是单向的,例如 A -> B -> C,那么就不会有问题。但是,在实际的开发中,经常会存在两个对象相互引用的情况,例如A<=>B或者 A -> B -> C -> A,像这样就构成了一个引用的闭环,即循环引用,如果这个环无法打破,其结果就是对象无法释放,造成内存泄漏。

说到循环引用,很多人第一个想到的就是block,而提起block,也会自然想到循环引用。可见两者之间的联系十分紧密。事实上,block使用不当确实会造成循环引用,但是OC中引起循环引用的除了block之外,还有其他的方式,例如代理。下面我们就一起探索下,引起循环引用的方式,以及如何解决循环引用问题。

2.1 如何检测循环引用

工欲善其事,必先利其器。探索循环引用之前,我们首先要知道如何检测循环引用,这里有两种方法可以供我们使用。

  • 1、第一种也是最为熟知的一种,就是在类中重写dealloc方法,该方法是OC类的析构方法,当对象被释放前,会调用该方法,如果对象没有被释放则不会调用。这一方式大家非常熟悉,本篇就不再演示。

    • 这种方式可以很准确的检测出对象是否释放,一旦发现该方法没有调用,就可以考虑是否出现了循环引用。
    • 不过这种方式有一个局限性,就是我们需要提前知道是哪些类需要进行检测,但是如果我们想看下工程中是否出现了内存泄漏,就不是那么容易做到了。
  • 2、还有一种是利用XCode的性能检测工具Instruments中的Allocations工具来检测,通过该工具启动工程,就可以检测到运行时对象的开辟和释放情况,而且很方便的一点是我们不需要知道,。本文的demo使用的是XCode 13.1Instruments的打开方式是左上XCode菜单 -> Open Developer Tool -> Instruments,打开后如下图所示:

Xnip2021-12-09_14-45-15.png

按照步骤1和步骤2选择好后,点击Choose,进入下一界面:

Xnip2021-12-09_15-01-15.png

如图所示,进入LeakViewController页面两次,第二次进入没有退出的情况下,内存中应该只有一个对象,但是结果显示的是两次。这说明LeakViewControllerLeakShowTool在第一次退出时没有被释放,可以检查是否发生了循环引用(事实上确实有循环引用,为了测试专门写的)。

2.2 代理为何不使用strong

在平时用到代理时,我们都是使用weak进行修饰,但是为什么呢?如果不使用weak,换成strong或者copy会怎样呢?我们可以做实验看下(实际上,上一节中用到的就是这个例子),将一个代理的属性修饰符改为strong,代码如下:

// LeakShowTool部分
@protocol LeakProtocol <NSObject>

- (void)show;

@end

@interface LeakShowTool : NSObject

@property (nonatomic, strong) id<LeakProtocol> delegate;

@end

// LeakViewController部分
@interface LeakViewController () <LeakProtocol>

@property (nonatomic, strong) LeakShowTool *tool;

@end

@implementation LeakViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.tool = [[LeakShowTool alloc] init];
    self.tool.delegate = self;
}

- (void)show {
    NSLog(@"show");
}
@end
复制代码

该实验的结果,如图所示: Xnip2021-12-09_16-01-04.png 当使用strong修饰代理属性时,立马也产生了循环引用,可见循环引用并不是block的专属。

此时发生循环引用的原因如下图:

Xnip2021-12-09_15-35-56.png

如果改成weak,则是下面的情况:

Xnip2021-12-09_15-50-49.png

2.3 block造成循环引用的原因

在上一小节中展示了使用代理造成循环引用的情况,本小节回归到block上来,继续探索下block造成循环引用原因,首先将代码改成block的形式,如下:

@interface ViewController ()

@property (nonatomic, assign) int result;

@property (nonatomic, strong) void(^block)(void);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.block = ^{
        NSLog(@"value = %d", self.result);
    };
}

@end
复制代码

这段代码不用运行也知道是会循环引用的,为了查看原因,我们先看下在底层,block是以一种怎样的形式存在的。

通过clang编译ViewController.m,其命令如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

编译后得到一个ViewController.cpp文件,打开如下:

Xnip2021-12-09_18-39-03.png

由于代码太多,这里只截图ViewDidLoadblock相关的代码。在底层ViewDidLoad变成了_I_ViewController_viewDidLoad,如图的最下面所示,在调用setBlock:时,传入了一个参数,该参数是__ViewController__viewDidLoad_block_impl_0类型,该类型其实是一个结构体,如图最上方。

在这个结构体中,可以看到一个成员ViewController *self;,该成员的赋值可以在__ViewController__viewDidLoad_block_impl_0的构造函数中找到,就是外部传入的self

结合上面的OC代码,最终的情况是self持有了block,block也持有了self,两者相互等待释放,形成了循环引用,如下图所示:

Xnip2021-12-09_18-52-47.png

三、解决block循环引用的方式

上一节主要了解了造成循环引用的原因,本章节继续探索如何解决block的循环引用问题。解决循环引用问题,关键的一点是将对象间的引用闭环打破,当其中某一个环能够被销毁时,环上的其他对象也可以得以释放,从而解决问题。根据这一点,可以通过以下几种方式来解决循环引用,下面分别来看下。

3.1 _ _weak解决循环引用

首先在LeakViewController中写下如下代码:

@interface LeakViewController ()

@property (nonatomic, assign) int result;

@property (nonatomic, strong) void(^block)(void);

@end

@implementation LeakViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"进入%@页面", [**self** class]);
    self.block = ^{
        NSLog(@"value = %d", self.result);
    };
}

- (void)dealloc {
    NSLog(@"%@--%s", [self class], __FUNCTION__);
}

@end
复制代码

上诉代码肯定会造成循环引用,打印结果如下:

Xnip2021-12-10_15-44-28.png

可以发现,退出页面后页面本该销毁,结果并没有走dealloc方法。此时将代码改成如下所示:

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"进入%@页面", [self class]);
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"value = %d", weakSelf.result);
    };
}
复制代码

执行结果如下:

Xnip2021-12-10_15-55-02.png

很显然循环引用解决了,其原理如下:

Xnip2021-12-10_16-38-05.png

不过上述代码存在一个问题,如果加上了延迟执行,则weakSelf会提前释放,代码如下:

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    NSLog(@"进入%@页面", [self class]);
    self.result = 2;
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            NSLog(@"value = %d", weakSelf.result);
        });
    };

    self.block();
}
复制代码

执行结果如下:

Xnip2021-12-10_16-59-44.png

后面两次是进入页面马上退出后的结果,很显然此时weakSelf已经为nil,所以打印结果value = 0。正常情况下,这样写没有太大问题,但是如果在退出页面时,确实有延迟执行的需求,那么这种写法就会存在不可预知的风险。因此,如果有这种需求,可以将代码改成如下所示:

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    NSLog(@"进入%@页面", [self class]);
    self.result = 2;
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            NSLog(@"value = %d", strongSelf.result);
        });
    };

    self.block();
}
复制代码

在block内部strongSelfweakSelf做一次强引用,weakSelf可以正常释放,strongSelf因为是block内部的局部变量,所以当block释放时,strongSelf也会被释放。打印结果如下:

Xnip2021-12-10_17-21-16.png

3.2 临时变量和参数

上面__weak和__strong的方式中,strongSelf实际上只是一个局部变量,通过这种方式避免了self的释放后代码块中的代码执行异常。借助这种思想,我们可以通过使用局部变量的方式来打破循环引用。

在OC中,可以通过__block来实现,代码如下:

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    NSLog(@"进入%@页面", [self class]);
    self.result = 2;
    __block LeakViewController *vc = self;
    self.block = ^{
        NSLog(@"value = %d", vc.result);
        vc = nil;
    };

    self.block();
}
复制代码

执行结果如下:

Xnip2021-12-10_18-00-43.png

这种方式也可以打破循环引用的闭环,而且不用担心延迟执行时的提前释放问题,只要在代码块只想结束时将vc这一临时变量置为nil

不过这种实现方式并不优雅,每次都要手动将vc置为nil,增加了操作成本,并且难保不会忘记,一旦忘记还是会造成循环引用。所以,可以进行如下改动:

@property (nonatomic, strong) void(^block)(LeakViewController *vc); // 将block的定义修改下,将self作为一个参数传入


- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    NSLog(@"进入%@页面", [self class]);
    self.result = 2;
    self.block = ^(LeakViewController *vc){
        NSLog(@"value = %d", vc.result);
    };

    self.block(self);
}
复制代码

因为vc是block的一个参数,当代码块执行完毕后,vc会自动被置为nil,从而打破了循环引用。

总结

本篇探索了block的基础,包含了block的分类以及循环引用问题,总结下来,有以下几点:

  • block总体上分为三种__NSGlobalBlock____NSMallocBlock____NSStackBlock__
    • 不引用外部的局部变量的为 __NSGlobalBlock__,否则为__NSMallocBlock____NSStackBlock__
    • __NSMallocBlock____NSStackBlock__的区别在于是否是强引用或者copy,如果是则为__NSMallocBlock__,否则为__NSStackBlock__
  • block使用不当,会造成循环引用,即对象的引用形成了一个闭环,最终相互等待,谁也无法释放
  • 解决block循环引用的方式有 __weak和__strong临时变量和参数

以上为对于block的基础总结,其中对于block如何拷贝self和其他变量,本篇只是简略的提及,后续会继续探索,期待继续关注,对于本篇中表述不当的地方,也欢迎大家指正。

猜你喜欢

转载自juejin.im/post/7040018320229302280