1.相關概念
在這篇筆記開始之前,我們需要對以下概念有所了解。
1.1 操作系統中的棧和堆
注:這裡所說的堆和棧與數據結構中的堆和棧不是一回事。
我們先來看看一個由C/C++/OBJC編譯的程序占用內存分布的結構:
棧區(stack):由系統自動分配,一般存放函數參數值、局部變量的值等。由編譯器自動創建與釋放。其操作方式類似於數據結構中的棧,即後進先出、先進後出的原則。
例如:在函數中申明一個局部變量int b;系統自動在棧中為b開辟空間。
堆區(heap):一般由程序員申請並指明大小,最終也由程序員釋放。如果程序員不釋放,程序結束時可能會由OS回收。對於堆區的管理是采用鏈表式管理的,操作系統有一個記錄空閒內存地址的鏈表,當接收到程序分配內存的申請時,操作系統就會遍歷該鏈表,遍歷到一個記錄的內存地址大於申請內存的鏈表節點,並將該節點從該鏈表中刪除,然後將該節點記錄的內存地址分配給程序。
例如:在C中malloc函數
代碼如下:
char p1;
p1 = (char )malloc(10);
但是p1本身是在棧中的。
鏈表:是一種常見的基礎數據結構,一般分為單向鏈表、雙向鏈表、循環鏈表。以下為單向鏈表的結構圖:
單向鏈表是鏈表中最簡單的一種,它包含兩個區域,一個信息域和一個指針域。信息域保存或顯示關於節點的信息,指針域儲存下一個節點的地址。
上述的空閒內存地址鏈表的信息域保存的就是空閒內存的地址。
全局區/靜態區:顧名思義,全局變量和靜態變量存儲在這個區域。只不過初始化的全局變量和靜態變量存儲在一塊,未初始化的全局變量和靜態變量存儲在一塊。程序結束後由系統釋放。
文字常量區:這個區域主要存儲字符串常量。程序結束後由系統釋放。
程序代碼區:這個區域主要存放函數體的二進制代碼。
下面舉一個前輩寫的例子:
代碼如下:
//main.cpp
int a = 0; // 全局初始化區
char *p1; // 全局未初始化區
main {
int b; // 棧
char s[] = "abc"; // 棧
char *p2; // 棧
char *p3 = "123456"; // 123456\0在常量區,p3在棧上
static int c =0; // 全局靜態初始化區
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); // 分配得來的10和20字節的區域就在堆區
strcpy(p1, "123456"); // 123456\0在常量區,這個函數的作用是將"123456" 這串字符串復制一份放在p1申請的10個字節的堆區域中。
// p3指向的"123456"與這裡的"123456"可能會被編譯器優化成一個地址。
}
strcpy函數
原型聲明:extern char *strcpy(char* dest, const char *src);
功能:把從src地址開始且含有NULL結束符的字符串復制到以dest開始的地址空間。
1.2 結構體(Struct)
在C語言中,結構體(struct)指的是一種數據結構。結構體可以被聲明為變量、指針或數組等,用以實現較復雜的數據結構。結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的類型,成員一般用名字訪問。
我們來看看結構體的定義:
代碼如下:
struct tag { member-list } variable-list;
在一般情況下,tag,member-list,variable-list這三部分至少要出現兩個。以下為示例:
代碼如下:
// 該結構體擁有3個成員,整型的a,字符型的b,雙精度型的c
// 並且為該結構體聲明了一個變量s1
// 該結構體沒有標明其標簽
struct{
int a;
char b;
double c;
} s1;
// 該結構體擁有同樣的三個成員
// 並且該結構體標明了標簽EXAMPLE
// 該結構體沒有聲明變量
struct EXAMPLE{
int a;
char b;
double c;
};
//用EXAMPLE標簽的結構體,另外聲明了變量t1、t2、t3
struct EXAMPLE t1, t2[20], *t3;
以上就是簡單結構體的代碼示例。結構體的成員可以包含其他結構體,也可以包含指向自己結構體類型的指針。結構體的變量也可以是指針。
下面我們來看看結構體成員的訪問。結構體成員依據結構體變量類型的不同,一般有2種訪問方式,一種為直接訪問,一種為間接訪問。直接訪問應用於普通的結構體變量,間接訪問應用於指向結構體變量的指針。直接訪問使用結構體變量名.成員名,間接訪問使用(*結構體指針名).成員名或者使用結構體指針名->成員名。相同的成員名稱依靠不同的變量前綴區分。
代碼如下:
struct EXAMPLE{
int a;
char b;
};
//聲明結構體變量s1和指向結構體變量的指針s2
struct EXAMPLE s1, *s2;
//給變量s1和s2的成員賦值,注意s1.a和s2->a並不是同一成員
s1.a = 5;
s1.b = 6;
s2->a = 3;
s2->b = 4;
最後我們來看看結構體成員存儲。在內存中,編譯器按照成員列表順序分別為每個結構體成員分配內存。如果想確認結構體占多少存儲空間,則使用關鍵字sizeof,如果想得知結構體的某個特定成員在結構體的位置,則使用offsetof宏(定義於stddef.h)。
代碼如下:
struct EXAMPLE{
int a;
char b;
};
//獲得EXAMPLE類型結構體所占內存大小
int size_example = sizeof( struct EXAMPLE );
//獲得成員b相對於EXAMPLE儲存地址的偏移量
int offset_b = offsetof( struct EXAMPLE, b );
1.3 閉包(Closure)
閉包就是一個函數,或者一個指向函數的指針,加上這個函數執行的非局部變量。
說的通俗一點,就是閉包允許一個函數訪問聲明該函數運行上下文中的變量,甚至可以訪問不同運行上文中的變量。
我們用腳本語言來看一下:
代碼如下:
function funA(callback){
alert(callback());
}
function funB(){
var str = "Hello World"; // 函數funB的局部變量,函數funA的非局部變量
funA(
function(){
return str;
}
);
}
通過上面的代碼我們可以看出,按常規思維來說,變量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的定義:
代碼如下:
myBlock = ^( int paramA )
{
return [ NSString stringWithFormat: @"Passed number: %i", paramA ];
};
上面的代碼中,將一個函數體賦值給了myBlock變量,其接收一個名為paramA的參數,返回一個NSString對象。
注意:一定不要忘記block後面的分號。
定義好block後,就可以像使用標准函數一樣使用它了:
代碼如下:
myBlock(7);
由於block數據類型的語法會降低整個代碼的閱讀性,所以常使用typedef來定義block類型。例如,下面的代碼創建了GetPersonEducationInfo和GetPersonFamilyInfo兩個新類型,這樣我們就可以在下面的方法中使用更加有語義的數據類型。
代碼如下:
// Person.h
#import // Define a new type for the block
typedef NSString * (^GetPersonEducationInfo)(NSString *);
typedef NSString * (^GetPersonFamilyInfo)(NSString *);
@interface Person : NSObject
- (NSString *)getPersonInfoWithEducation:(GetPersonEducationInfo)educationInfo
andFamily:(GetPersonFamilyInfo)familyInfo;
@end
我們用一張大師文章裡的圖來總結一下block的結構:
2.2 將block作為參數傳遞
代碼如下:
// .h
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock;
// .m
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock
{
NSLog(@"Block returned: %@", myBlock(7) );
}
由於Objective-C是強制類型語言,所以作為函數參數的block也必須要指定返回值的類型,以及相關參數類型。
2.3 閉包性
上文說過,block實際是Objc對閉包的實現。
我們來看看下面代碼:
代碼如下:
#import void logBlock( int ( ^ theBlock )( void ) )
{
NSLog( @"Closure var X: %i", theBlock() );
}
int main( void )
{
NSAutoreleasePool * pool;
int ( ^ myBlock )( void );
int x;
pool = [ [ NSAutoreleasePool alloc ] init ];
x = 42;
myBlock = ^( void )
{
return x;
};
logBlock( myBlock );
[ pool release ];
return EXIT_SUCCESS;
}
上面的代碼在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的值時,會發生什麼:
代碼如下:
myBlock = ^( void )
{
x++;
return x;
};
編譯器會報錯,表明在block中變量x是只讀的。
有時候確實需要在block中處理變量,怎麼辦?別著急,我們可以用__block關鍵字來聲明變量,這樣就可以在block中修改變量了。
基於之前的代碼,給x變量添加__block關鍵字,如下:
代碼如下:
__block int x;
對於用__block修飾的外部變量引用,block是復制其引用地址來實現訪問的,如下圖:
3.編譯器中的block
3.1 block的數據結構定義
我們通過大師文章中的一張圖來說明:
上圖這個結構是在棧中的結構,我們來看看對應的結構體定義:
代碼如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
從上面代碼看出,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。例如:
代碼如下:
#include int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
_NSConcreteStackBlock:保存在棧中的block,當函數返回時被銷毀。例如:
#include int main()
{
char a = 'A';
^{ printf("%c\n",a); } ();
return 0;
}
_NSConcreteMallocBlock:保存在堆中的block,當引用計數為0時被銷毀。該類型的block都是由_NSConcreteStackBlock類型的block從棧中復制到堆中形成的。例如下面代碼中,在exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中就被復制到了堆中,成為_NSConcreteMallocBlock類型的block:
代碼如下:
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
總結一下:
3.3 編譯器如何編譯
我們通過一個簡單的示例來說明:
代碼如下:
#import typedef void(^BlockA)(void);
__attribute__((noinline))
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
BlockA block = ^{
// Empty block
};
runBlockA(block);
}
上面的代碼定義了一個名為BlockA的block類型,該block在函數doBlockA中實現,並將其作為函數runBlockA的參數,最後在函數doBlockA中調用函數runBloackA。
注意:如果block的創建和調用都在一個函數裡面,那麼優化器(optimiser)可能會對代碼做優化處理,從而導致我們看不到編譯器中的一些操作,所以用__attribute__((noinline))給函數runBlockA添加noinline,這樣優化器就不會在doBlockA函數中對runBlockA的調用做內聯優化處理。
我們來看看編譯器做的工作內容:
代碼如下:
#import __attribute__((noinline))
void runBlockA(struct Block_layout *block) {
block->invoke();
}
void block_invoke(struct Block_layout *block) {
// Empty block function
}
void doBlockA() {
struct Block_descriptor descriptor;
descriptor->reserved = 0;
descriptor->size = 20;
descriptor->copy = NULL;
descriptor->dispose = NULL;
struct Block_layout block;
block->isa = _NSConcreteGlobalBlock;
block->flags = 1342177280;
block->reserved = 0;
block->invoke = block_invoke;
block->descriptor = descriptor;
runBlockA(&block);
}
上面的代碼結合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.下面來看幾個具體的運行示例:
4.1參數是NSString*的代碼塊
代碼如下:
void (^printBlock)(NSString *x);
printBlock = ^(NSString* str)
{
NSLog(@"print:%@", str);
};
printBlock(@"hello world!");
運行結果是:print:hello world!
4.2代碼用在字符串數組排序
代碼如下:
NSArray *stringArray = [NSArray arrayWithObjects:@"abc 1", @"abc 21", @"abc 12",@"abc 13",@"abc 05",nil];
NSComparator sortBlock = ^(id string1, id string2)
{
return [string1 compare:string2];
};
NSArray *sortArray = [stringArray sortedArrayUsingComparator:sortBlock];
NSLog(@"sortArray:%@", sortArray);
運行結果:
sortArray:( "abc 05", "abc 1", "abc 12", "abc 13", "abc 21" )
4.3代碼塊的遞歸調用
代碼塊想要遞歸調用,代碼塊變量必須是全局變量或者是靜態變量,這樣在程序啟動的時候代碼塊變量就初始化了,可以遞歸調用
代碼如下:
static void (^ const blocks)(int) = ^(int i)
{
if (i > 0) {
NSLog(@"num:%d", i);
blocks(i - 1);
}
};
blocks(3);
運行打印結果:
num:3 num:2 num:1
4.4在代碼塊中使用局部變量和全局變量
在代碼塊中可以使用和改變全局變量
代碼如下:
int global = 1000;
int main(int argc, const char * argv[])
{
@autoreleasepool {
void(^block)(void) = ^(void)
{
global++;
NSLog(@"global:%d", global);
};
block();
NSLog(@"global:%d", global);
}
return 0;
}
運行打印結果:
global:1001 global:1001
而局部變量可以使用,但是不能改變。
代碼如下:
int local = 500;
void(^block)(void) = ^(void)
{
// local++;
NSLog(@"local:%d", local);
};
block();
NSLog(@"local:%d", local);
在代碼塊中改變局部變量編譯不通過。怎麼在代碼塊中改變局部變量呢?在局部變量前面加上關鍵字:__block
代碼如下:
__block int local = 500;
void(^block)(void) = ^(void)
{
local++;
NSLog(@"local:%d", local);
};
block();
NSLog(@"local:%d", local);
運行結果:
local:501 local:501