從iOS4開始,蘋果引入了這個C語言的擴充功能「Blocks」,在一些特定的場景下也是一把利刃。我前面一篇博客中初步介紹了Blocks這個東西,主要是語法的介紹(《iOS中Blocks的介紹》)。
我曾經看見了老外的一個系列的Blocks介紹,頗有深度(A look inside blocks:Episode 1,A look inside blocks:Episode 2, A look inside blocks:Episode 3),裏面深刻到彙編的層次對Blocks的實現進行了分析。不過若是象我這樣對於彙編不熟悉的人確定也是不少的,理解起來十分痛苦,因而就想到從ObjC自己對Blocks進行的處理裏面來入手分析,看看對於Blocks都悄悄作了什麼。
2.環境
很簡單,就是Xcode啦。使用的編譯器是CLang,主要是利用了-rewrite-objc這個參數,把源文件轉換成中間文件。這樣就揭開了面紗的一角。我使用的clang編譯器版本是:
Apple clang version 4.0 (tags/Apple/clang-421.0.60) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin12.5.0
Thread model: posix
轉成中間文件的命令是:clang -rewrite-objc 源文件
3. 例子1
- #include <stdio.h>
-
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", val);};
-
- val = 4;
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
代碼的運行結果是:
in Block():val=2
in main():val=4, val1=5
這裏咱們能夠看到Block對於自動變量的「快照」功能。因爲轉成中間文件以後發現長了不少,變成了100多行(不一樣的clang版本轉換出來的文件還不一樣,不過實現部分代碼仍是同樣的),下面的代碼是相關部分的節選,主要說明蘋果是如何實現的。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
-
- int main(int, const charchar **);
-
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- int val;
- __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
- static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
- int val = __cself->val;
- printf("in Block():val=%d\n", val);}
-
- static struct __main_block_desc_0 {
- unsigned long reserved;
- unsigned long Block_size;
- } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, val);
-
- val = 4;
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
中間文件確實看起來複雜了很多,不過仍是有脈絡可循。
看main函數的內容,裏面有個函數指針blk,這個就是指向Block的指針,因此難怪Block變量的聲明和函數指針如此相像(就是把*換成^),編譯器轉換後就是同一個東西啊。
咱們看blk這個函數指針,就是__main_block_impl_0這個結構體變量的指針,這個結構體變量此時已經存在,而後用__main_block_func_0等幾個變量賦初值。咱們能夠看到__main_block_impl_0這個struct中有個val這個項,而且在這裏也賦值了,這就是給變量照的「快照」,因爲這個變量在這裏被記錄了,因此不管外面的val變量如何變化,Block運行時使用的值就始終是「快照」的值了。同時咱們也注意到__main_block_impl_0這個struct中沒有val1這個項,因此說明若是Block中不用到的自動變量是不會自動加入到結構體中的。
Block的運行就是運行__main_block_impl_0這個struct中的FuncPtr這個指針,這個在前面初始化的時候已經被賦值成__main_block_func_0了,因此這裏也就是運行這個函數,並把本身的指針傳入。這裏咱們的實現很是簡單,就是一句printf語句。
4.例子2
- #include <stdio.h>
-
- int main(int argc, const charchar * argv[])
- {
- int __block val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", ++val);};
-
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
這個例子把自動變量val聲明成了__block變量,這樣語法上Block不是對val進行「快照」,而是會直接使用val變量,同時在Block內部val可讀可寫,再也不是隻讀的了。
運行結果以下:
in Block():val=3
in main(): val=3, val1=5
一樣展開成中間文件來看蘋果的實現。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
-
- int main(int, const charchar **);
- struct __Block_byref_val_0 {
- voidvoid *__isa;
- __Block_byref_val_0 *__forwarding;
- int __flags;
- int __size;
- int val;
- };
-
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- __Block_byref_val_0 *val;
- __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val;
- printf("in Block():val=%d\n", ++(val->__forwarding->val));}
- static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8
-
- static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8
-
- static struct __main_block_desc_0 {
- unsigned long reserved;
- unsigned long 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, const charchar * argv[])
- {
- __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 2};
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, (struct __Block_byref_val_0 *)&val, 570425344);
-
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", (val.__forwarding->val), val1);
- return 0;
- }
中間文件又變長了一些,除去咱們已經瞭解的部分,咱們能夠看到自動變量val再也不是直接加入到__main_block_impl_0裏面,而是又變成了一個__Block_byref_val_0的struct結構體的指針。
main函數裏面對於val的賦值已經變成了對這樣一個數據結構的賦值,第一句上就把2賦給了__Block_byref_val_0裏面的val項,而後在blk這個指針初始化的時候,把__Block_byref_val_0的結構體變量指針傳入__main_block_impl_0。此後全部對於自動變量val的操做都變成對val.__forwarding->val的操做。這樣就解決了Block內外變量同時變化的問題(在操做同一塊內存)。
這裏還看見__Block_byref_val_0裏面有個__forwarding項,這個項是指向自身的一根指針。在blk指針初始化的時候咱們把這個指針的值傳入了__main_block_impl_0。
在__main_block_desc_0裏面,多出了兩個函數指針,分別用於copy和dispose,這兩個函數這裏也是自動生成的。
5.總結
綜合前面的例子來看,Block的實現仍是藉助了C語言的函數指針來實現了,對於普通的自動變量,在Block聲明時會快照內容存儲;對於__block變量,則是生成一個數據結構來存儲,而後取代全部訪問這個變量的地方。
事實上,由於Block運行時徹底可能自動變量的生命週期已經結束,因此Block對於內存的管理是很複雜的,會把內容從棧上copy到堆上(這也是copy和dispose函數的做用)。因此Block雖然威力巨大,但使用時也須要遵循必定的規則。
iOS中Block介紹(二)內存管理與其餘特性
咱們在前一章介紹了block的用法,而正確使用block必需要求正確理解block的內存管理問題。這一章,咱們只陳述結果而不追尋緣由,咱們將在下一章深刻其緣由。
AD:2014WOT全球軟件技術峯會北京站 課程視頻發佈
1、block放在哪裏
咱們針對不一樣狀況來討論block的存放位置:
1.棧和堆
如下狀況中的block位於堆中:
- void foo()
- {
- __block int i = 1024;
- int j = 1;
- void (^blk)(void);
- void (^blkInHeap)(void);
- blk = ^{ printf("%d, %d\n", i, j);};
- blkInHeap = Block_copy(blk);
- }
-
- - (void)fooBar
- {
- _oi = 1;
- OBJ1* oj = self;
- void (^oblk)(void) = ^{ printf("%d\n", oj.oi);};
- void (^oblkInHeap)(void) = [oblk copy];
- }
2.全局區
如下狀況中的block位於全局區:
- static int(^maxIntBlock)(int, int) = ^(int a, int b){return a>b?a:b;};
- - (void)fooBar
- {
- int(^maxIntBlockCopied)(int, int) =[maxIntBlock copy];
- }
- void foo()
- {
- int(^maxIntBlockCopied)(int, int) = Block_copy(maxIntBlock);
- }
須要注意的是,這裏複製事後的block依舊位於全局區,實際上,複製操做是直接返回了原block對象。
2、block引用的變量在哪裏
1.全局區
全局區的變量存儲位置與block無關:
- static int gVar = 0;
- void foo()
- {
- static int stackVar = 0;
- }
注意:static變量是不容許添加__block標記的
2.堆棧
![](http://static.javashuo.com/static/loading.gif)
此時,你可能會問,當函數foo返回後,棧上的j已經回收,那麼blkInHeap怎麼能繼續使用它?這是由於沒有__block標記的變量,會被當作實參傳入block的底層實現函數中,當block中的代碼被執行時,j已經不是原來的j了,所謂物是人非就是這樣吧~
另外,若是使用到變量j的全部block都沒有被複制至heap,那麼這個變量j也不會被複制至heap。
所以,即便將j++這一句放到blk()這句以前,這段代碼執行後,控制檯打印結果也是:1024, 1。而不是1024, 2
3、其餘特性
1.複製的行爲
對block調用複製,有如下幾種狀況:
1.對全局區的block調用copy,會返回原指針,而且這期間不處理任何東西(至少目前的內部實現是這樣);
2.對棧上的block調用copy,每次會返回新複製到堆上的block的指針,同時,全部__block變量都會被複制至堆一份(屢次拷貝,只會生成一份)。
3.對已經位於heap上的block,再次調用copy,只會增長block的引用計數。
爲何咱們不討論retian的行爲?緣由是並無Block_retain()這樣的函數,並且objc裏面的retain消息發送給block對象後,其內部實現是什麼都不作。
2.objc類中的block複製
objc類實例方法中的block若是被複制至heap,那麼當前實例會被增長引用計數,當這個block被釋放時,此實例會被減小引用計數。
但若是這個block沒有使用當前實例的任何成員,那麼當前實例不會被增長引用計數。這也是很天然的道理,我既然沒有用到這個instance的任何東西,那麼我幹嗎要retian它?
咱們要注意的一點是,我看到網上有不少人說block引發了實例與block之間的循環引用(retain-cycle),而且給出解決方案:不直接使用self而先將self賦值給一個臨時變量,而後再使用這個臨時變量。
可是,你們注意,咱們必定要爲這個臨時變量增長__block標記(多謝第三篇文章回帖網友的提醒)。
這一章咱們以結果導向的方式來講明瞭各類狀況下,block的內存問題,下一章,我將剖析運行時庫的源碼,從根源闡述block的行爲。也就是過程導向的方式了。
//Block
// '^'脫字符
//函數指針
int (*p)(int, int) = sum;
//調用函數
sum(3, 4);
p(2 ,4);
//聲明一個Block變量
/*1*///int:返回值 ,後面兩個int:參數類型
int (^block1) (int, int) = nil;
//block 變量存儲的值,是block的實現部分
//實現部分返回值類型常省略
//參數變量名不能省略
//以';'結尾
block1 = ^(int a, int b){
return a + b;
};
//使用block1
int sum = block1(2, 4);
NSLog(@"%d",sum);
/*2*//////////////////////////////////
void (^block2)() = ^(){
NSLog(@"無參數 無返回值的 block2(); ");
};
block2();//調用
//3,求兩個數最大值 block
int (^maxValue) (int x,int y) = ^(int a,int b){
return a > b ? a : b;
};
int max = maxValue(2,3);//調用
NSLog(@"最大值:%d",max);
//4, 將字符串轉成數字的block
int (^numberFromString)(NSString *) = ^(NSString *str){
return [str intValue];
};
NSLog(@"%d",numberFromString(@"123"));//回調了 ^numberFromString 這個block
//5, 給可變數組排序的block
//5,1
NSComparator sortBlock = ^(id string1, id string2)
{
return [string1 compare:string2];
};
NSMutableArray *stringArray = [NSMutableArray arrayWithObjects:@"a", @"c", @"b", @"d",nil];
[stringArray sortedArrayUsingComparator:sortBlock];
NSLog(@"sortArray:%@", stringArray);
//5,2
[stringArray sortUsingComparator:^NSComparisonResult(id obj1,id obj2){
if ([obj1 intValue] > [obj2 intValue]) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
NSLog(@"--%@",stringArray);
//6, 取別名
typedef int (^SumBlock) (int,int);
SumBlock sumBlock1 = ^(int a,int b){
return a + b;
};
NSLog(@"%d",sumBlock1(7,8));
//7, 求階乘 的block
int (^jiecheng)(int) = ^(int n){
int m = 1;
for (int i = 1; i <= n; i++) {
m *= i;
}
return m;
};
NSLog(@"階乘:%d",jiecheng(3));
//8, block內部能夠訪問外部局部變量,可是不能修改,若要修改需加__block 修飾
__block int count = 2;
void (^sayHi)() = ^(){
count = 3;
NSLog(@"count 被修改爲=%d",count);
};
sayHi();
//9,