標籤:
1.相關概念
在這篇筆記開始之前,我們需要對以下概念有所瞭解。
1.1 作業系統中的棧和堆
註:這裡所說的堆和棧與資料結構中的堆和棧不是一回事。
我們先來看看一個由C/C++/OBJC編譯的程式佔用記憶體分布的結構:
棧區(stack):由系統自動分配,一般存放函數參數值、局部變數的值等。由編譯器自動建立與釋放。其操作方式類似於資料結構中的棧,即後進先出、先進後出的原則。
例如:在函數中申明一個局部變數int b;系統自動在棧中為b開闢空間。
堆區(heap):一般由程式員申請並指明大小,最終也由程式員釋放。如果程式員不釋放,程式結束時可能會由OS回收。對於堆區的管理是採用鏈表式管理的,作業系統有一個記錄空閑記憶體位址的鏈表,當接收到程式分配記憶體的申請時,作業系統就會遍曆該鏈表,遍曆到一個記錄的記憶體位址大於申請記憶體的鏈表節點,並將該節點從該鏈表中刪除,然後將該節點記錄的記憶體位址分配給程式。
例如:在C中malloc函數
1 char p1; 2 p1 = (char )malloc(10);
但是p1本身是在棧中的。
鏈表:是一種常見的基礎資料結構,一般分為單向鏈表、雙向鏈表、迴圈鏈表。以下為單向鏈表的結構圖:
單向鏈表是鏈表中最簡單的一種,它包含兩個地區,一個資訊域和一個指標域。資訊域儲存或顯示關於節點的資訊,指標域儲存下一個節點的地址。
上述的空閑記憶體位址鏈表的資訊域儲存的就是空閑記憶體的地址。
全域區/靜態區:顧名思義,全域變數和靜態變數儲存在這個地區。只不過初始化的全域變數和靜態變數儲存在一塊,未初始化的全域變數和靜態變數儲存在一塊。程式結束後由系統釋放。
文字常量區:這個地區主要儲存字串常量。程式結束後由系統釋放。
程式碼區:這個地區主要存放函數體的二進位代碼。
下面舉一個前輩寫的例子:
1 //main.cpp 2 int a = 0; // 全域初始化區 3 char *p1; // 全域未初始化區 4 main { 5 int b; // 棧 6 char s[] = "abc"; // 棧 7 char *p2; // 棧 8 char *p3 = "123456"; // 123456\0在常量區,p3在棧上 9 static int c =0; // 全域靜態初始化區10 p1 = (char *)malloc(10);11 p2 = (char *)malloc(20); // 分配得來的10和20位元組的地區就在堆區12 strcpy(p1, "123456"); // 123456\0在常量區,這個函數的作用是將"123456" 這串字串複製一份放在p1申請的10個位元組的堆地區中。13 // p3指向的"123456"與這裡的"123456"可能會被編譯器最佳化成一個地址。14 }
strcpy函數
原型聲明:extern char *strcpy(char* dest, const char *src);
功能:把從src地址開始且含有NULL結束符的字串複製到以dest開始的地址空間。
1.2 結構體(Struct)
在C語言中,結構體(struct)指的是一種資料結構。結構體可以被聲明為變數、指標或數組等,用以實現較複雜的資料結構。結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的類型,成員一般用名字訪問。
我們來看看結構體的定義:
struct tag { member-list } variable-list;
struct:結構體關鍵字。
tag:結構體標籤。
member-list:結構體成員列表。
variable-list:為結構體聲明的變數列表。
在一般情況下,tag,member-list,variable-list這三部分至少要出現兩個。以下為樣本:
1 // 該結構體擁有3個成員,整型的a,字元型的b,雙精確度型的c 2 // 並且為該結構體聲明了一個變數s1 3 // 該結構體沒有標明其標籤 4 struct{ 5 int a; 6 char b; 7 double c; 8 } s1; 9 // 該結構體擁有同樣的三個成員10 // 並且該結構體標明了標籤EXAMPLE11 // 該結構體沒有聲明變數12 struct EXAMPLE{13 int a;14 char b;15 double c;16 };17 //用EXAMPLE標籤的結構體,另外聲明了變數t1、t2、t318 struct EXAMPLE t1, t2[20], *t3;
以上就是簡單結構體的程式碼範例。結構體的成員可以包含其他結構體,也可以包含指向自己結構體類型的指標。結構體的變數也可以是指標。
下面我們來看看結構體成員的訪問。結構體成員依據結構體變數類型的不同,一般有2種訪問方式,一種為直接存取,一種為間接訪問。直接存取應用於普通的結構體變數,間接訪問應用於指向結構體變數的指標。直接存取使用結構體變數名.成員名,間接訪問使用(*結構體指標名).成員名或者使用結構體指標名->成員名。相同的成員名稱依靠不同的變數首碼區分。
1 struct EXAMPLE{ 2 int a; 3 char b; 4 }; 5 //聲明結構體變數s1和指向結構體變數的指標s2 6 struct EXAMPLE s1, *s2; 7 //給變數s1和s2的成員賦值,注意s1.a和s2->a並不是同一成員 8 s1.a = 5; 9 s1.b = 6;10 s2->a = 3;11 s2->b = 4;
最後我們來看看結構體成員儲存。在記憶體中,編譯器按照成員列表順序分別為每個結構體成員分配記憶體。如果想確認結構體佔多少儲存空間,則使用關鍵字sizeof,如果想得知結構體的某個特定成員在結構體的位置,則使用offsetof宏(定義於stddef.h)。
1 struct EXAMPLE{2 int a;3 char b;4 };5 //獲得EXAMPLE類型結構體所佔記憶體大小6 int size_example = sizeof( struct EXAMPLE );7 //獲得成員b相對於EXAMPLE儲存地址的位移量8 int offset_b = offsetof( struct EXAMPLE, b );
1.3 閉包(Closure)
閉包就是一個函數,或者一個指向函數的指標,加上這個函數執行的非局部變數。
說的通俗一點,就是閉包允許一個函數訪問聲明該函數運行上下文中的變數,甚至可以訪問不同運行上文中的變數。
我們用指令碼語言來看一下:
1 function funA(callback){ 2 alert(callback()); 3 } 4 function funB(){ 5 var str = "Hello World"; // 函數funB的局部變數,函數funA的非局部變數 6 funA( 7 function(){ 8 return str; 9 }10 );11 }
通過上面的代碼我們可以看出,按常規思維來說,變數str是函數funB的局部變數,範圍只在函數funB中,函數funA是無法訪問到str的。但是上述程式碼範例中函數funA中的callback可以訪問到str,這是為什麼呢,因為閉包性。
2.blcok基礎知識
block實際上就是Objective-C語言對閉包的實現。
2.1 block的原型及定義
我們來看看block的原型:
NSString * ( ^ myBlock )( int );
上面的代碼聲明了一個block(^)原型,名字叫做myBlock,包含一個int型的參數,傳回值為NSString類型的指標。
下面來看看block的定義:
1 myBlock = ^( int paramA )2 {3 return [ NSString stringWithFormat: @"Passed number: %i", paramA ];4 };
上面的代碼中,將一個函數體賦值給了myBlock變數,其接收一個名為paramA的參數,返回一個NSString對象。
注意:一定不要忘記block後面的分號。
定義好block後,就可以像使用標準函數一樣使用它了:
myBlock(7);
由於block資料類型的文法會降低整個代碼的閱讀性,所以常使用typedef來定義block類型。例如,下面的代碼建立了GetPersonEducationInfo和GetPersonFamilyInfo兩個新類型,這樣我們就可以在下面的方法中使用更加有語義的資料類型。
1 // Person.h2 #import // Define a new type for the block3 typedef NSString * (^GetPersonEducationInfo)(NSString *);4 typedef NSString * (^GetPersonFamilyInfo)(NSString *);5 @interface Person : NSObject6 - (NSString *)getPersonInfoWithEducation:(GetPersonEducationInfo)educationInfo7 andFamily:(GetPersonFamilyInfo)familyInfo;8 @end
我們用一張大師文章裡的圖來總結一下block的結構:
2.2 將block作為參數傳遞
1 // .h2 -(void) testBlock:( NSString * ( ^ )( int ) )myBlock;3 // .m4 -(void) testBlock:( NSString * ( ^ )( int ) )myBlock5 {6 NSLog(@"Block returned: %@", myBlock(7) );7 }
由於Objective-C是強制類型語言,所以作為函數參數的block也必須要指定傳回值的類型,以及相關參數類型。
2.3 閉包性
上文說過,block實際是Objc對閉包的實現。
我們來看看下面代碼:
1 #import void logBlock( int ( ^ theBlock )( void ) ) 2 { 3 NSLog( @"Closure var X: %i", theBlock() ); 4 } 5 int main( void ) 6 { 7 NSAutoreleasePool * pool; 8 int ( ^ myBlock )( void ); 9 int x;10 pool = [ [ NSAutoreleasePool alloc ] init ];11 x = 42;12 myBlock = ^( void )13 {14 return x;15 };16 logBlock( myBlock );17 [ pool release ];18 return EXIT_SUCCESS;19 }
上面的代碼在main函數中聲明了一個整型,並賦值42,另外還聲明了一個block,該block會將42返回。然後將block傳遞給logBlock函數,該函數會顯示出返回的值42。即使是在函數logBlock中執行block,而block又聲明在main函數中,但是block仍然可以訪問到x變數,並將這個值返回。
注意:block同樣可以訪問全域變數,即使是static。
2.4 block中變數的複製與修改
對於block外的變數引用,block預設是將其複製到其資料結構中來實現訪問的,如:
通過block進行閉包的變數是const的。也就是說不能在block中直接修改這些變數。來看看當block試著增加x的值時,會發生什麼:
1 myBlock = ^( void )2 {3 x++;4 return x;5 };
編譯器會報錯,表明在block中變數x是唯讀。
有時候確實需要在block中處理變數,怎麼辦?別著急,我們可以用__block關鍵字來聲明變數,這樣就可以在block中修改變數了。
基於之前的代碼,給x變數添加__block關鍵字,如下:
__block int x;
對於用__block修飾的外部變數引用,block是複製其引用地址來實現訪問的,如:
3.編譯器中的block
3.1 block的資料結構定義
我們通過大師文章中的一張圖來說明:
這個結構是在棧中的結構,我們來看看對應的結構體定義:
1 struct Block_descriptor { 2 unsigned long int reserved; 3 unsigned long int size; 4 void (*copy)(void *dst, void *src); 5 void (*dispose)(void *); 6 }; 7 struct Block_layout { 8 void *isa; 9 int flags;10 int reserved;11 void (*invoke)(void *, ...);12 struct Block_descriptor *descriptor;13 /* Imported variables. */14 };
從上面代碼看出,Block_layout就是對block結構體的定義:
isa指標:指向表明該block類型的類。
flags:按bit位表示一些block的附加資訊,比如判斷block類型、判斷block引用計數、判斷block是否需要執行輔助函數等。
reserved:保留變數,我的理解是表示block內部的變數數。
invoke:函數指標,指向具體的block實現的函數調用地址。
descriptor:block的附加描述資訊,比如保留變數數、block的大小、進行copy或dispose的輔助函數指標。
variables:因為block有閉包性,所以可以訪問block外部的局部變數。這些variables就是複製到結構體中的外部局部變數或變數的地址。
3.2 block的類型
block有幾種不同的類型,每種類型都有對應的類,上述中isa指標就是指向這個類。這裡列出常見的三種類型:
_NSConcreteGlobalBlock:全域的靜態block,不會訪問任何外部變數,不會涉及到任何拷貝,比如一個空的block。例如:
1 #include int main()2 {3 ^{ printf("Hello, World!\n"); } ();4 return 0;5 }
_NSConcreteStackBlock:儲存在棧中的block,當函數返回時被銷毀。例如:
1 #include int main()2 {3 char a = ‘A‘;4 ^{ printf("%c\n",a); } ();5 return 0;6 }
_NSConcreteMallocBlock:儲存在堆中的block,當引用計數為0時被銷毀。該類型的block都是由_NSConcreteStackBlock類型的block從棧中複製到堆中形成的。例如下面代碼中,在exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中就被複製到了堆中,成為_NSConcreteMallocBlock類型的block:
1 void exampleB_addBlockToArray(NSMutableArray *array) { 2 char b = ‘B‘; 3 [array addObject:^{ 4 printf("%c\n", b); 5 }]; 6 } 7 void exampleB() { 8 NSMutableArray *array = [NSMutableArray array]; 9 exampleB_addBlockToArray(array);10 void (^block)() = [array objectAtIndex:0];11 block();12 }
總結一下:
_NSConcreteGlobalBlock類型的block要麼是空block,要麼是不訪問任何外部變數的block。它既不在棧中,也不在堆中,我理解為它可能在記憶體的全域區。
_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變數,並且該block只且只有有一次執行,因為棧中的空間是可重複使用的,所以當棧中的block執行一次之後就被清除出棧了,所以無法多次使用。
_NSConcreteMallocBlock類型的block有閉包行為,並且該block需要被多次執行。當需要多次執行時,就會把該block從棧中複製到堆中,供以多次執行。
3.3 編譯器如何編譯
我們通過一個簡單的樣本來說明:
1 #import typedef void(^BlockA)(void); 2 __attribute__((noinline)) 3 void runBlockA(BlockA block) { 4 block(); 5 } 6 void doBlockA() { 7 BlockA block = ^{ 8 // Empty block 9 };10 runBlockA(block);11 }
上面的代碼定義了一個名為BlockA的block類型,該block在函數doBlockA中實現,並將其作為函數runBlockA的參數,最後在函數doBlockA中調用函數runBloackA。
注意:如果block的建立和調用都在一個函數裡面,那麼最佳化器(optimiser)可能會對代碼做最佳化處理,從而導致我們看不到編譯器中的一些操作,所以用__attribute__((noinline))給函數runBlockA添加noinline,這樣最佳化器就不會在doBlockA函數中對runBlockA的調用做內聯最佳化處理。
我們來看看編譯器做的工作內容:
1 #import __attribute__((noinline)) 2 void runBlockA(struct Block_layout *block) { 3 block->invoke(); 4 } 5 void block_invoke(struct Block_layout *block) { 6 // Empty block function 7 } 8 void doBlockA() { 9 struct Block_descriptor descriptor;10 descriptor->reserved = 0;11 descriptor->size = 20;12 descriptor->copy = NULL;13 descriptor->dispose = NULL;14 struct Block_layout block;15 block->isa = _NSConcreteGlobalBlock;16 block->flags = 1342177280;17 block->reserved = 0;18 block->invoke = block_invoke;19 block->descriptor = descriptor;20 runBlockA(&block);21 }
上面的代碼結合block的資料結構定義,我們能很容易得理解編譯器內部對block的工作內容。
3.4 copy()和dispose()
上文中提到,如果我們想要在以後繼續使用某個block,就必須要對該block進行拷貝操作,即從棧空間複製到堆空間。所以拷貝操作就需要調用Block_copy()函數,block的descriptor中有一個copy()輔助函數,該函數在Block_copy()中執行,用於當block需要拷貝對象的時候,拷貝輔助函數會retain住已經拷貝的對象。
既然有有copy那麼就應該有release,與Block_copy()對應的函數是Block_release(),它的作用不言而喻,就是釋放我們不需要再使用的block,block的descriptor中有一個dispose()輔助函數,該函數在Block_release()中執行,負責做和copy()輔助函數相反的操作,例如釋放掉所有在block中拷貝的變數等。
4.總結
以上內容是我學習各大師的文章後對自己學習情況的一個記錄,其中有部分文字和程式碼範例是來自大師的文章,還有一些自己的理解,如有錯誤還請大家勘誤
Objective-C中的Block[轉]