前言
本篇文章精講iOS開發中使用Block時一定要注意內存管理問題,很容易造成循環引用。本篇文章的目標是幫助大家快速掌握使用block的技巧。
我相信大家都覺得使用block給開發帶來了多大的便利,但是有很多開發者對block內存管理掌握得不夠好,導致經常出現循環引用的問題。對於新手來說,出現循環引用時,是很難去查找的,因此通過Leaks不一定能檢測出來,更重要的還是要靠自己的分析來推斷出來。
聲景一:Controller之間block傳值
現在,我們聲明兩個控制器類,一個叫ViewController,另一個叫HYBAController。其中,ViewController有一個按鈕,點擊時會push到HYBAController下。
先看HYBAController:
1
2
3
4
5
|
// 公開了一個方法 - (instancetype)initWithCallback:(HYBCallbackBlock)callback; // 非公開的屬性,這裏放出來只是告訴大家,HYBAController會對這個屬性強引用 @property (nonatomic, copy) HYBCallbackBlock callbackBlock; |
下面分幾種小場景來看看循環引用問題:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@interface ViewController () // 引用按鈕只是爲了測試 @property (nonatomic, strong) UIButton *button; // 只是爲了測試內存問題,引用之。在開發中,有很多時候我們是 // 需要引用另一個控制器的,因此這裏模擬之。 @property (nonatomic, strong) HYBAController *vc; @end // 點擊button時 - (void)goToNext { HYBAController *vc = [[HYBAController alloc] initWithCallback:^{ [self.button setTitleColor:[UIColor greenColor] forState:UIControlStateNormal]; }]; self.vc = vc; [self.navigationController pushViewController:vc animated:YES]; } |
現在看ViewController這裏,這裏在block的地方形成了循環引用,因此vc屬性得不到釋放。分析其形成循環引用的原因(如下圖):
可以簡單說,這裏形成了兩個環:
-
ViewController->強引用了屬性vc->強引用了callback->強引用了ViewController
-
ViewController->強引用了屬性vc->強引用了callback->強引用了ViewController的屬性button
對於此這問題,我們要解決內存循環引用問題,可以這麼解:
不聲明vc屬性或者將vc屬性聲明爲weak引用的類型,在callback回調處,將self.button改成weakSelf.button,也就是讓callback這個block對viewcontroller改成弱引用。如就是改成如下,內存就可以正常釋放了:
1
2
3
4
5
6
7
8
|
- (void)goToNext { __weak __typeof(self) weakSelf = self; HYBAController *vc = [[HYBAController alloc] initWithCallback:^{ [weakSelf.button setTitleColor:[UIColor greenColor] forState:UIControlStateNormal]; }]; // self.vc = vc; [self.navigationController pushViewController:vc animated:YES]; } |
筆者嘗試過使用Leaks檢測內存泄露,但是全是通過,一個綠色的勾,讓你以爲內存處理得很好了,實際上內存並得不到釋放。
針對這種場景,給大家提點建議:
-
在控制器的生命週期viewDidAppear裏打印日誌:
1
2
3
4
5
|
- (void)viewDidAppear:(BOOL)animated { [ super viewDidAppear:animated]; NSLog(@ "進入控制器:%@" , [[self class] description]); } |
-
在控制器的生命週期dealloc裏打印日誌:
1
2
3
|
- (void)dealloc { NSLog(@ "控制器被dealloc: %@" , [[self class] description]); } |
這樣的話,只要日誌沒有打印出來,說明內存得不到釋放,就需要學會分析內存引用問題了。
場景二:Controller與View之間Block傳值
我們先定義一個view,用於與Controller交互。當點擊view的按鈕時,就會通過block回調給controller,也就反饋到控制器了,並將對應的數據傳給控制器以記錄:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
typedef void(^HYBFeedbackBlock)(id model); @interface HYBAView : UIView - (instancetype)initWithBlock:(HYBFeedbackBlock)block; @end @interface HYBAView () @property (nonatomic, copy) HYBFeedbackBlock block; @end @implementation HYBAView - (void)dealloc { NSLog(@ "dealloc: %@" , [[self class] description]); } - (instancetype)initWithBlock:(HYBFeedbackBlock)block { if (self = [ super init]) { self.block = block; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [button setTitle:@ "反饋給controller" forState:UIControlStateNormal]; button.frame = CGRectMake(50, 200, 200, 45); button.backgroundColor = [UIColor redColor]; [button setTitleColor:[UIColor yellowColor] forState:UIControlStateNormal]; [button addTarget:self action:@selector(feedback) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:button]; } return self; } - (void)feedback { if (self.block) { // 傳模型回去,這裏沒有數據,假設傳nil self.block(nil); } } @end |
接下來看HYBAController,增加了兩個屬性,在viewDidLoad時,創建了aView屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@interface HYBAController() @property (nonatomic, copy) HYBCallbackBlock callbackBlock; @property (nonatomic, strong) HYBAView *aView; @property (nonatomic, strong) id currentModel; @end @implementation HYBAController - (instancetype)initWithCallback:(HYBCallbackBlock)callback { if (self = [ super init]) { self.callbackBlock = callback; } return self; } - (void)viewDidLoad { [ super viewDidLoad]; self.title = @ "HYBAController" ; self.view.backgroundColor = [UIColor whiteColor]; self.aView = [[HYBAView alloc] initWithBlock:^(id model) { // 假設要更新model self.currentModel = model; }]; // 假設佔滿全屏 self.aView.frame = self.view.bounds; [self.view addSubview:self.aView]; self.aView.backgroundColor = [UIColor whiteColor]; } - (void)viewDidAppear:(BOOL)animated { [ super viewDidAppear:animated]; NSLog(@ "進入控制器:%@" , [[self class] description]); } - (void)dealloc { NSLog(@ "控制器被dealloc: %@" , [[self class] description]); } @end |
關於上一場景所講的循環引用已經解決了,因此我們這一小節的重點就放在controller與view的引用問題上就可以了。
分析:如下圖所示:
所形成的環有:
-
vc->aView->block->vc(self)
-
vc->aView->block->vc.currentModel
解決的辦法可以是:在創建aView時,block內對currentModel的引用改成弱引用:
1
2
3
4
5
|
__weak __typeof(self) weakSelf = self; self.aView = [[HYBAView alloc] initWithBlock:^(id model) { // 假設要更新model weakSelf.currentModel = model; }]; |
我見過很多類似這樣的代碼,直接使用成員變量,而不是屬性,然後他們以爲這樣就不會引用self,也就是控制器,從而不形成環:
1
2
3
4
|
self.aView = [[HYBAView alloc] initWithBlock:^(id model) { // 假設要更新model _currentModel = model; }]; |
這是錯誤的理解,當我們引用了_currentModel時,它是控制器的成員變量,因此也就引用了控制器。要解決此問題,也是要改成弱引用:
1
2
3
4
5
|
__block __weak __typeof(_currentModel) weakModel = _currentModel; self.aView = [[HYBAView alloc] initWithBlock:^(id model) { // 假設要更新model weakModel = model; }]; |
這裏還要加上__block哦!
模擬循環引用
假設下面如此寫代碼,是否出現內存得不到釋放問題?(其中,controller屬性都是強引用聲明的)
@autoreleasepool { A *aVC = [[A alloc] init]; B *bVC = [[B allcok] init]; aVC.controller = bVC; bVC.controller = aVC; }
分析:
aVC->強引用了bVC->強引用了aVC,因此形成了一個環,導致內存得不到釋放。
寫在最後
本篇文章就講這麼多吧,寫本篇文章的目的是教大家如何分析內存是否形成環,只要懂得了如何去分析內存是否循環引用了,那麼在開發時一定會特別注意內存管理問題,而且查找內存相關的問題的bug時,也比較輕鬆。