本文要將block的如下機制,並配合具體代碼詳細描述:ios
block 與 外部變量api
block 的存儲域:棧塊、堆塊、全局塊app
定義框架
塊與函數相似,只不過是直接定義在另外一個函數裏,和定義它的那個函數共享同一個範圍內的東西。函數
訪問外部變量spa
堆塊內部,棧是紅燈區,堆是綠燈區。設計
根據塊的存儲位置,可將塊分爲全局塊、棧塊、堆塊。這裏先主要針對堆塊講解。指針
Block不容許修改外部變量的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於「函數」的範疇,變量進入block,實際就是已經改變了做用域。在幾個做用域之間進行切換時,若是不加上這樣的限制,變量的可維護性將大大下降。又好比我想在block內聲明瞭一個與外部同名的變量,此時是容許呢仍是不容許呢?只有加上了這樣的限制,這樣的情景才能實現。因而棧區變成了紅燈區,堆區變成了綠燈區。code
幾種演算orm
block調用 基本數據類型
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
{
NSLog(@
"\n--------------------block調用 基本數據類型---------------------\n"
);
int
a =
10
;
NSLog(@
"block定義前a地址=%p"
, &a);
void
(^aBlock)() = ^(){
NSLog(@
"block定義內部a地址=%p"
, &a);
};
NSLog(@
"block定義後a地址=%p"
, &a);
aBlock();
}
/*
結果:
block定義前a地址=0x7fff5bdcea8c
block定義後a地址=0x7fff5bdcea8c
block定義內部a地址=0x7fa87150b850
*/
/*
流程:
1. block定義前:a在棧區
2. block定義內部:裏面的a是根據外面的a拷貝到堆中的,不是一個a
3. block定義後:a在棧區
*/
{
NSLog(@
"\n--------------------block調用 __block修飾的基本數據類型---------------------\n"
);
__block
int
b =
10
;
NSLog(@
"block定義前b地址=%p"
, &b);
void
(^bBlock)() = ^(){
b =
20
;
NSLog(@
"block定義內部b地址=%p"
, &b);
};
NSLog(@
"block定義後b地址=%p"
, &b);
NSLog(@
"調用block前 b=%d"
, b);
bBlock();
NSLog(@
"調用block後 b=%d"
, b);
}
/*
結果:
block定義前b地址=0x7fff5bdcea50
block定義後b地址=0x7fa873b016d8
調用block前 b=10
block定義內部b地址=0x7fa873b016d8
調用block後 b=20
*/
/*
流程:
1. 聲明 b 爲 __block (__block 所起到的做用就是隻要觀察到該變量被 block 所持有,就將「外部變量」在棧中的內存地址放到了堆中。)
2. block定義前:b在棧中。
3. block定義內部: 將外面的b拷貝到堆中,而且使外面的b和裏面的b是一個。
4. block定義後:外面的b和裏面的b是一個。
5. block調用前:b的值還未被修改。
6. block調用後:b的值在block內部被修改。
*/
{
NSLog(@
"\n--------------------block調用 指針---------------------\n"
);
NSString *c = @
"ccc"
;
NSLog(@
"block定義前:c=%@, c指向的地址=%p, c自己的地址=%p"
, c, c, &c);
void
(^cBlock)() = ^{
NSLog(@
"block定義內部:c=%@, c指向的地址=%p, c自己的地址=%p"
, c, c, &c);
};
NSLog(@
"block定義後:c=%@, c指向的地址=%p, c自己的地址=%p"
, c, c, &c);
cBlock();
NSLog(@
"block調用後:c=%@, c指向的地址=%p, c自己的地址=%p"
, c, c, &c);
}
/*
c指針自己在block定義中和外面不是一個,可是c指向的地址一直保持不變。
1. block定義前:c指向的地址在堆中, c指針自己的地址在棧中。
2. block定義內部:c指向的地址在堆中, c指針自己的地址在堆中(c指針自己和外面的不是一個,可是指向的地址和外面指向的地址是同樣的)。
3. block定義後:c不變,c指向的地址在堆中, c指針自己的地址在棧中。
4. block調用後:c不變,c指向的地址在堆中, c指針自己的地址在棧中。
*/
{
NSLog(@
"\n--------------------block調用 指針並修改值---------------------\n"
);
NSMutableString *d = [NSMutableString stringWithFormat:@
"ddd"
];
NSLog(@
"block定義前:d=%@, d指向的地址=%p, d自己的地址=%p"
, d, d, &d);
void
(^dBlock)() = ^{
NSLog(@
"block定義內部:d=%@, d指向的地址=%p, d自己的地址=%p"
, d, d, &d);
d.string = @
"dddddd"
;
};
NSLog(@
"block定義後:d=%@, d指向的地址=%p, d自己的地址=%p"
, d, d, &d);
dBlock();
NSLog(@
"block調用後:d=%@, d指向的地址=%p, d自己的地址=%p"
, d, d, &d);
}
/*
d指針自己在block定義中和外面不是一個,可是d指向的地址一直保持不變。
在block調用後,d指向的堆中存儲的值發生了變化。
*/
{
NSLog(@
"\n--------------------block調用 __block修飾的指針---------------------\n"
);
__block NSMutableString *e = [NSMutableString stringWithFormat:@
"eee"
];
NSLog(@
"block定義前:e=%@, e指向的地址=%p, e自己的地址=%p"
, e, e, &e);
void
(^eBlock)() = ^{
NSLog(@
"block定義內部:e=%@, e指向的地址=%p, e自己的地址=%p"
, e, e, &e);
e = [NSMutableString stringWithFormat:@
"new-eeeeee"
];
};
NSLog(@
"block定義後:e=%@, e指向的地址=%p, e自己的地址=%p"
, e, e, &e);
eBlock();
NSLog(@
"block調用後:e=%@, e指向的地址=%p, e自己的地址=%p"
, e, e, &e);
}
/*
從block定義內部使用__block修飾的e指針開始,e指針自己的地址由棧中改變到堆中,即便出了block,也在堆中。
在block調用後,e在block內部從新指向一個新對象,e指向的堆中的地址發生了變化。
*/
{
NSLog(@
"\n--------------------block調用 retain cycle---------------------\n"
);
View *v = [[View alloc] init];
v.tag =
1
;
v.frame = CGRectMake(
100
,
100
,
100
,
100
);
[self.view addSubview:v];
//self->view->v
void
(^block)() = ^{
v.backgroundColor = [UIColor orangeColor];
//定義內部:block->v
};
v.block = block;
//v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
3
* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//預計3秒後釋放v對象。
[v removeFromSuperview];
});
}
/*
結果:
不會輸出 dealloc.
*/
/*
流程:
1. self->view->v
2. block定義內部:block->v 由於block定義裏面調用了v
3. v->block
結論:
引發循環引用的是block->v->block,切斷其中一個線便可解決循環引用,跟self->view->v這根線無關
*/
{
NSLog(@
"\n--------------------block調用self---------------------\n"
);
View *v = [[View alloc] init];
v.tag =
2
;
v.frame = CGRectMake(
100
,
220
,
100
,
100
);
[self.view addSubview:v];
//self->view->v
void
(^block)() = ^{
self.view.backgroundColor = [UIColor redColor];
//定義內部:block->self
_count ++;
//調用self的實例變量,也會讓block強引用self。
};
v.block = block;
//v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
3
* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//預計3秒後釋放self這個對象。
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = nil;
});
}
/*
結果:
不會輸出 dealloc.
*/
/*
流程:
1. self->view->v
2. v->block
3. block->self 由於block定義裏面調用了self
結論:
在block內引用實例變量,該實例變量會被block強引用。
引發循環引用的是self->view->v->block->self,切斷一個線便可解決循環引用。
*/
|
棧塊、堆塊、全局塊
塊自己也是對象,由isa指針、塊對象正常運轉所需的信息、捕獲到的變量組成。
根據Block建立的位置不一樣,Block有三種類型,建立的Block對象分別會存儲到棧、堆、全局數據區域。
block_storage.png
上面講了塊會把它所捕獲的全部變量都拷貝一份,這些拷貝放在 descriptor 變量後面,捕獲了多少個變量,就要佔據多少內存空間。請注意,拷貝的並非對象自己,而是指向這些對象的指針變量。
1. 在全局數據區的Block對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
NSLog(@
"\n--------------------block的存儲域 全局塊---------------------\n"
);
void
(^blk)(
void
) = ^{
NSLog(@
"Global Block"
);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
結果:輸出 __NSGlobalBlock__
*/
/*
結論:
全局塊:這種塊不會捕捉任何狀態(外部的變量),運行時也無須有狀態來參與。塊所使用的整個內存區域,在編譯期就已經肯定。
全局塊通常聲明在全局做用域中。但注意有種特殊狀況,在函數棧上建立的block,若是沒有捕捉外部變量,block的實例仍是會被設置在程序的全局數據區,而非棧上。
*/
|
2. 在堆上建立的Block對象
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
|
{
NSLog(@
"\n--------------------block的存儲域 堆塊---------------------\n"
);
int
i =
1
;
void
(^blk)(
void
) = ^{
NSLog(@
"Malloc Block, %d"
, i);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
結果:輸出 __NSMallocBlock__
*/
/*
結論:
堆塊:解決塊在棧上會被覆寫的問題,能夠給塊對象發送copy消息將它拷貝到堆上。複製到堆上後,塊就成了帶引用計數的對象了。
在ARC中,如下幾種狀況棧上的Block會自動複製到堆上:
- 調用Block的copy方法
- 將Block做爲函數返回值時(MRC時此條無效,需手動調用copy)
- 將Block賦值給__strong修飾的變量時(MRC時此條無效)
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時
上述代碼就是在ARC中,block賦值給__strong修飾的變量,而且捕獲了外部變量,block就會自動複製到堆上。
*/
|
3. 在棧上建立的Block對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
NSLog(@
"\n--------------------block的存儲域 棧塊---------------------\n"
);
int
i =
1
;
__weak
void
(^blk)(
void
) = ^{
NSLog(@
"Stack Block, %d"
, i);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
結果:輸出 __NSStackBlock__
*/
/*
結論:
棧塊:塊所佔內存區域分配在棧中,編譯器有可能把分配給塊的內存覆寫掉。
在ARC中,除了上面四種狀況,而且不在global上,block是在棧中。
*/
|
內存泄漏
堆塊訪問外部變量時會拷貝一份指針到堆中,至關於強引用了指針所指的值。若是該對象又直接或間接引用了塊,就出現了循環引用。
解決方法:要麼在捕獲時使用__weak解除引用,要麼在執行完後置nil解除引用(使用後置nil的方式,若是未執行,則仍會內存泄漏)。
注意:使用__block並不能解決循環引用問題。
優缺點
優勢:
捕獲外部變量
下降代碼分散程度
缺點:
循環引用引發內存泄露
總結
在block內部,棧是紅燈區,堆是綠燈區。
在block內部使用的是將外部變量的拷貝到堆中的(基本數據類型直接拷貝一份到堆中,對象類型只將在棧中的指針拷貝到堆中而且指針所指向的地址不變。)
__block修飾符的做用:是將block中用到的變量,拷貝到堆中,而且外部的變量自己地址也改變到堆中。
循環引用:分析實際的引用關係,block中直接引用self也不必定會形成循環引用。
__block不能解決循環引用,須要在block執行尾部將變量設置成nil(但問題不少,好比block永遠不執行,外面變量變了裏面也變,裏面變了外面也變等問題)
__weak能夠解決循環引用,block在捕獲weakObj時,會對weakObj指向的對象進行弱引用。
使用__weak時,可在block開始用局部__strong變量持有,以避免block執行期間對象被釋放。
塊的存儲域:全局塊、棧塊、堆塊
全局塊不引用外部變量,因此不用考慮。
堆塊引用的外部變量,不是原始的外部變量,是拷貝到堆中的副本。
棧塊自己就在棧中,引用外部變量不會拷貝到堆中。
參考