Block 是iOS在4.0之後新增的程式語法,在iOS SDK 4.0之後,block應用幾乎無處不在。
在其他語言中也有類似的概念稱做閉包(closure),比如object C的好兄弟swift 中閉包(swift 閉包詳解)的使用跟 OC的block一樣重要。總的來說:
Block是C語言的
Block是一個數據類型
Block 是一個提前準備好的代碼,在需要的時候執行
1. block作用:
Block用來封裝一段代碼,可以在任何時候執行;
- Block可以作爲函數參數或者函數的返回值,而其本身又可以帶輸入參數或返回值。
- 蘋果官方建議儘量多用block。在多線程、異步任務 、集合遍歷、集合排序、動畫轉場用的很多
在新的iOS API中block被大量用來取代傳統的delegate和callback,而新的API會大量使用block主要是基於以下兩個原因:
A. 可以直接在block代碼塊中寫等會要接着執行的代碼,直接把block變成函數的參數傳入函數中,這是新API最常使用block的地方。
B. 可以存取局部變量,在傳統的callback操作時,若想要存取局部變量得將變量封裝成結構體才能使用,而block則是可以很方便地直接存取局部變量。
2. Block的定義:
定義時,把block當成數據類型
特點:
1. 類型比函數定義多了一個 ^
2. 設置數值,有一個 ^,內容是 {} 括起的一段代碼
(1)基本定義方式
/*
*1.最簡單的定義方式:
*格式:void (^myBlock)() = ^ { // 代碼實現; }
*/
void (^myBlock)() = ^ {
NSLog(@"hello");
};
// 執行時,把block當成函數
myBlock();
/*
*2.定義帶參數的block:
*格式:void (^block名稱)(參數列表) = ^ (參數列表) { // 代碼實現; }
*/
void (^sumBlock)(int, int) = ^ (int x, int y) {
NSLog(@"%d", x + y);
};
sumBlock(10, 20);
/*
*3.定義帶返回值的block
*格式:返回類型 (^block名稱)(參數列表) = ^ 返回類型 (參數列表) { // 代碼實現; }
*/
int (^sumBlock2)(int, int) = ^ int (int a, int b) {
return a + b;
};
NSLog(@"%d", sumBlock2(4, 8));
(2) block 指針
Block Pointer是這樣定義的:
回傳值 (^名字) (參數列);
//聲明一個名字爲square的Block Pointer,其所指向的Block有一個int輸入和int輸出
int (^square)(int);
//block 指針square的內容
square = ^(int a){ return a*a ; };
//調用方法,感覺是是不是很像function的用法?
int result = square(5);
NSLog(@"%d", result);
(3) 用typedef先聲明類型,再定義變量進行賦值
typedef int (^MySum)(int,int);
MySum sum = ^(int a,int b)
{
return a + b;
};
(4) block 訪問外部變量
但是block使用有個特點,Block可以訪問局部變量,但是不能修改:
int sum = 10;
int (^MyBlock)(int) = ^(int num)
{
sum++;//編譯報錯
return num * sum;
};
如果要修改就要加關鍵字 __block (下面詳細說明):
__block int sum =10;
(5) block 與函數指針
下面比較下函數指針與block異同:
定義函數指針
int (*myFn)();
調用函數指針(*myFn)(10, 20);
定義Block
int (^MyBlocks)(int,int);
調用BlocksMyBlocks(10, 20);
3. block訪問外部變量
block 訪問外部變量有幾個特點必須知道:
- block內部可以訪問外部變量;
- 默認情況下block內部不能
修改
外面的局部變量; - 給局部變量加上關鍵字
_block
,這個局部變量就可以在block內部修改;
block中可以訪問外部變量。但是
不能修改它
,否則編譯錯誤
。但是可以改變全局變量、靜態變量(static)、全局靜態變量。
上面的特點是有原因滴:
A.
爲何不讓修改變量
:這個是編譯器決定的。理論上當然可以修改變量了,只不過block捕獲的是外部變量的副本,名字一樣。爲了不給開發者迷惑,乾脆不讓賦值。道理有點像:函數參數,要用指針,不然傳遞的是副本(大家想起那個經典的兩個數調換值的問題了吧)。B.
可以修改靜態變量的值
。靜態變量屬於類的,不是某一個變量。所以block內部不用調用cself指針。所以block可以調用。
(1) __block存儲類型
通過__block存儲類型修飾符, 變量在block中可被修改。__block存儲跟register、auto和static存儲類型相似(但是之間互斥),用於局部變量。__block變量存儲在堆區
,因此,這個block使用的外部變量,將會在棧結束被留下來。
從優化角度考慮,block存儲在棧上,如果block被拷貝(通過Block_copy或者copy),變量被拷貝到堆
。因此__block變量的地址就會改變。
__block變量還有兩個限制,他們不能是可變數組(NSMutableArray),不能是結構體(structure)。
__block 變量的內部實現要複雜許多,__block 變量其實是一個結構體對象,拷貝的是指向該結構體對象的指針
(2) block訪問外部變量
上面已經說過,默認block 訪問的外部變量是隻讀屬性的,若要對外部變量進行讀寫,需要在定義外部變量時加一個 __block, 示例如下:
//示例1:block訪問外部變量
void demoBlock1()
{
int x = 10;
NSLog(@"定義前 %p", &x);// 局部變量在棧區
// 在定義block的時候,如果引用了外部變量,默認是把外部變量當做是常量編碼到block當中,並且把外部變量copy到堆中,外部變量值爲定義block時變量的數值
// 如果後續再修改x的值,默認不會影響block內部的數值變化!
// 在默認情況下,不允許block內部修改外部變量的數值!因爲會破壞代碼的可讀性,不易於維護!
void(^myBlock)() = ^ {
NSLog(@"%d", x);
NSLog(@"in block %p", &x); // 堆中的地址
};
//輸出是10,因爲block copy了一份x到堆中
NSLog(@"定義後 %p", &x); // 棧區
x = 20;
myBlock();
}
//示例2:在block中修改外部變量
void demoBlock2()
{
// 使用 __block,說明不在關心x數值的具體變化
__block int x = 10;
NSLog(@"定義前 %p", &x); // 棧區
// !定義block時,如果引用了外部使用__block的變量,在block定義之後, block外部的x和block內部的x指向了同一個值,內存地址相同
void (^myBlock)() = ^ {
x = 80;
NSLog(@"in block %p", &x); // 堆區
};
NSLog(@"定義後 %p", &x); // 堆區
myBlock();
NSLog(@"%d", x);
//打印x的值爲8,且地址在堆區中
}
下面的例子就有點難度了,讓我們看下block對指針變量的訪問
//例子3:block對指針變量的訪問
void demoBlock3()
{
// !指針記錄的是地址
NSMutableString *strM = [NSMutableString stringWithString:@"zhangsan"];
//strM是指針,其在堆中存儲的是zhangsan這個string在內存中的的地址值
//&strM是指針strM在堆中的地址
NSLog(@"定義前 %p %p", strM, &strM);
void (^myBlock)() = ^ {
/*首先調用block會對strM(指針)進行一份copy,這份copy會在堆中創建
另一個指針,這個指針存儲的值同strM,都是zhangsan的地址,
即新copy的指針指向的內容沒有變
*/
// 注意下面的操作是修改strM指針指向的內容
[strM setString:@"lisi"];
NSLog(@"inblock %p %p", strM, &strM);
//輸出:strM沒有變,因爲存儲的都是zhangsan的地址,&strM爲堆中新地址
/*
*這句代碼是修改指針strM,因爲strM copy過來後是隻讀的,所以同例子2編譯會報錯,需要在定義strM時加__block
strM = [NSMutableString stringWithString:@"wangwu"];
NSLog(@"inblock %p %p", strM, &strM);
*/
};
//大家想想使用__block輸出會是什麼呢
NSLog(@"定義後 %p %p", strM, &strM);
myBlock();
NSLog(@"%@", strM);
}
上面的例子搞定了,來讓我們看下各種類型的變量與block之間的互動:
//示例4:各種類型的變量和block之間的互動
extern NSInteger CounterGlobal;
static NSInteger CounterStatic;
NSInteger localCounter = 42 ;
__block char localCharacter;
void (^aBlock)( void ) = ^( void )
{
++ CounterGlobal ; //可以存取。
++ CounterStatic ; //可以存取。
CounterGlobal = localCounter; //localCounter在block 建立時就不可變了。
localCharacter = 'a' ; //設定外面定義的localCharacter 變數。
};
++localCounter; //不會影響的block 中的值。
localCharacter = 'b' ;
aBlock(); //執行block 的內容。
//執行完後,localCharachter 會變成'a'
(3) block 引用成員變量
OC對象,不同於基本類型
,Block會引起對象的引用計數變化。若我們在block中引用到oc的對象,則對象的引用計數器會加1, 不過在對象前 加__block修飾,則參考計數不變。
- 若直接存取實例變量(instance variable),self的參考計數將被加1。
- 若透過變量存取實例變量的值,則變量的參考計數將被加1。
- 在對象前加 __block 則參考計數不會自動加1。
//例子1:定義一個變量間接給block調用,成員變量引用計數不變
dispatch_async (queue, ^{
// 因爲直接存取實例變量instanceVariable ,所以self 的retain count 會加1
doSomethingWithObject (instanceVariable);
});
//通過
id localVaribale = instanceVariable;
dispatch_async (queue, ^{
//localVariable 是存取值,所以這時只有localVariable 的retain count 加1
//self 的 return count 並不會增加。
doSomethingWithObject (localVaribale);
});
上面只是簡單演示下block引用成員變量,下面我們研究下block引用成員變量時出現的一個經典問題:循環引用
。
在block內部使用成員變量,如下:
@interface ViewController : UIViewController
{
NSString *_string;
}
@end
在block創建中:
_block = ^(){
NSLog(@"string %@", self.string);
};
上面代碼中block是會對內部的成員變量進行一次retain, 即self會被retain一次。
對於block 使用 成員變量self.string來說,block內部是直接強引用self的。也就是block持有了self,在這裏bock又作爲self的一個成員被持有,就會導致
循環引用和內存泄露
。
修改方案很簡單:
新建一個
__block scope
的局部變量,並把self賦值給它,而在block內部則使用這個局部變量來進行取值,上面說過:__block標記的變量是不會被自動retain的。
__block ViewController *controller = self;
_block = ^(){
NSLog(@"string %@", controller.string);
};
4. block 基本使用
當block定義完成後,我們除了可以像使用一般函數的方式來直接調用它以外,還可以有其他妙用,這些靈活的應用纔是block最爲強大的地方。
(1) block 作爲函數參數
我們可以像使用一般函數使用參數的方式將block以函數參數的型式傳入函數中,在這種情況下,大多數我們使用block的方式將不會傾向定義一個block,而是直接以內嵌的方式
來將block傳入,這也是目前新版SDK中主流的做法
下面的例子中,block本身就是函數參數的一部分
char *myCharacters[ 3 ] = { "TomJohn" , "George" , "Charles Condomine" };
qsort_b (myCharacters, 3 , sizeof ( char *), ^( const void *l, const void *r)
{
//需要類型強轉下
char *left = *( char **)l;
char *right = *( char **)r;
return strncmp (left, right, 1 );
} // 這裏是block 的終點
);
// 最後的結果爲:{"Charles Condomine", "George", "TomJohn"}
(2) Block當作方法的參數
// 所有的資料
NSArray *array = [ NSArray arrayWithObjects : @"A" , @"B" , @"C" , @"A" , @"B" , @"Z" , @"G" , @"are" , @" Q" ,nil ];
// 我們只要這個集合內的資料
NSSet *filterSet = [ NSSet setWithObjects : @"A" , @"B" , @"Z" , @"Q" , nil ];
BOOL (^test)( id obj, NSUInteger idx, BOOL *stop);
test = ^ ( id obj, NSUInteger idx, BOOL *stop) {
// 只對前5 筆資料做檢查
if (idx < 5 )
{
if ([filterSet containsObject : obj])
{
return YES ;
}
}
return NO ;
};
NSIndexSet *indexes = [array indexesOfObjectsPassingTest :test];
NSLog ( @"indexes: %@" , indexes);
// 結果:indexes: <NSIndexSet: 0x6101ff0>[number of indexes: 4 (in 2 ranges), indexes: (0-1 3-4)]
// 前5筆資料中,有4筆符合條件,它們的索引值分別是0-1, 3-4
(3)OC方法中block實例
A. sortedArrayUsingComparator:
//這裏面block代碼塊直接內嵌作爲方法的參數
NSArray *sortedArray = [array sortedArrayUsingComparator: ^(id obj1, id obj2) {
//左邊大於右邊,降序
if ([obj1 integerValue] > [obj2 integerValue])
{
return (NSComparisonResult)NSOrderedDescending;
}
//右邊大於左邊,升序
if ([obj1 integerValue] < [obj2 integerValue])
{
return (NSComparisonResult)NSOrderedAscending;
}
//相同
return (NSComparisonResult)NSOrderedSame;
}];
B. enumerateObjectsUsingBlock
通常enumerateObjectsUsingBlock: 和 (for(… in …)在效率上基本一致,有時會快些。主要是因爲它們都是基於 NSFastEnumeration
實現的。快速迭代在處理的過程中需要多一次轉換,當然也會消耗掉一些時間. 基於Block的迭代可以達到本機存儲一樣快的遍歷集合. 對於字典同樣適用。
注意”enumerateObjectsUsingBlock” 修改局部變量時, 你需要聲明局部變量爲 __block 類型.
enumerateObjectsWithOptions:usingBlock: 支持併發迭代或反向迭代,併發迭代時效率也非常高.
對於字典而言, enumerateObjectsWithOptions:usingBlock 也是唯一的方式可以併發實現恢復Key-Value值.
示例代碼:
//定義一個可變數組
NSMutableArray *test = [NSMutableArray array];
//向數組中添加元素
for (int i= 0; i < 10000; i++)
{
[test addObject:@"i"];
}
//迭代數組輸出
[test enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"%@",obj);
}];
5. block內存管理
(1)堆(Stack)和棧(Heap)
heap和stack是內存管理的兩個重要概念。這裏指的是內存的分配區域。
stack的空間由操作系統進⾏行分配。
在現代操作系統中,一個線程會分配⼀個stack. 當一個函數被調用,一個stack frame(棧幀)就會被壓到stack裏。裏包含這個函數涉及的參數,局部變量,返回地址等相關信息。當函數返回後,這個棧幀就會被銷燬。⽽這一切都是自動的,由系統幫我們進行分配與銷燬。對於程序員是透明的,我們不需要手動調度。
.heap的空間需要手動分配。 heap與動態內存分配相關,內存可以隨時在堆中分配和銷燬。我們需要明確請求內存分配與內存銷燬。 簡單來說,就
是malloc與free.
(2)Objective-C中的Stack和Heap
首先所有的Objective-C對象都是分配在heap的。 在OC經典的內存分配與初始化:
NSObject *obj = [[NSObject alloc] init];
一個對象在alloc的時候,就在Heap分配了內存空間。 stack對象通常有速度的優勢,⽽且不會發生內存泄露問題。那麼爲什麼OC的對象都是分配在heap的呢? 原因在於:
- stack對象的⽣生命週期所導致的問題。例如一旦函數返回,則所在的stack frame就會被摧毀。那麼此時返回的對象也 會一併摧毀。這個時候我們去retain這個對象是無效的。因爲整個stack frame都已經被摧毀了。簡單⽽言就是stack 對象的⽣命週期不適合Objective-C的引用計數內存管理⽅方法。
.- stack對象不夠靈活,不具備足夠的擴展性。創建時⻓度已經是固定的,⽽stack對象的擁有者也就是所在的stack frame
我們知道block 在使用@property定義時,官方建議我們使⽤用copy修飾符
// 定義一個塊代碼的屬性,block屬性需要用 copy
@property (nonatomic, copy) void (^completion)(NSString *text);
雖然在ARC時代已經不需要再顯式聲明瞭,使用strong是沒有問題的,但是仍然建 議我們使⽤copy以顯示相關拷貝⾏爲。
(3)爲什麼要使用copy?!
其實Objective-C是有它的Stack object的。那就是block。
在Objective-C語⾔言中,⼀一共有3種類型的block:
- _NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量。
- _NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷燬。
- _NSConcreteMallocBlock 保存在堆中的block,當引⽤用計數爲0時會被銷燬。
這⾥我們主要基於內存管理
的角度對它們進行分類。
NSConcreteGlobalBlock,這種不捕捉外界變量的block是不需要內存管理的,這種block不存在於Heap或是Stack⽽是作爲代碼片段存在,類似於C函數。
NSConcreteStackBlock,需要涉及到外界變量的block在創建的時候是在stack上⾯分配空間的,也就是⼀旦所在函數返回,執行彈棧,則會被摧毀。這就導致內存管理的問題,如果我們希望保存這個block或者是返回它,如果沒有做進⼀步的copy處理,則必然會出現問題。
舉個栗子,在手動管理引⽤計數時,如果在exampleD_getBlock方法返回block 時沒有執行[[block copy] autorelease]
的操作,則方法執行完畢後,block就會被銷燬, 返回block是無效的。
//定義了一個block
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%c\n", d);
};
}
void exampleD()
{
exampleD_getBlock();
}
NSConcreteMallocBlock,因此爲了解決block作爲Stack object的這個問題,我們最終需要把它拷⻉到堆上來。
拷貝到堆後,block的⽣命週期就與⼀般的OC對象⼀樣了,我們通過引用計數來對其進行內存管理。
現在我們知道爲麼麼要Copy了吧-_-
block在創建時是stack對象,如果我們需要在離開當前函數仍能夠使用我們創建的block。我們就需要把它 拷⻉到堆上以便進行以引用計數爲基礎的內存管理。
在ARC模式下,系統幫助我們完成了copy的⼯作。在ARC下,即使你聲明的修飾符是strong,實際上效果是與聲明爲copy一樣的。 因此在ARC情況下,創建的block仍然是NSConcreteStackBlock類型,只不過當block被引用或返回時
,ARC幫助我們完成了copy和內存管理的工作。
總結
在ARC下,我們可以將block看做⼀一個正常的OC對象,與其他對象的內存管理沒什麼不同。MRC下要使用 Block_copy()和 Block_release 來管理內存。
(4)再來一個栗子
上面講到ARC下, block在被引用或返回時類型會由NSConcreteStackBlock轉換爲 NSConcreteHeapBlock,那在MRC環境下該怎麼辦呢。
block在創建的時候,它的內存是分配在棧(stack)上,而不是在堆(heap)上。
我們在viewDidLoad中創建一個_block:
- (void)viewDidLoad
{
[superviewDidLoad];
int number = 1;
_block = ^(){
NSLog(@"number %d", number);
};
}
並且在一個按鈕的事件中調用了這個block:
- (IBAction)testDidClick:(id)sender {
_block();
}
此時如果按了按鈕之後就會導致程序崩潰,解決這個問題的方法很簡單
在創建完block的時候需要調用
Block_copy
函數。它會把block從棧上移動到堆上,那麼就可以在其他地方使用這個block了。Block_copy
實際上是一個宏,如下:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
使用後,使用 Block_release,從堆中釋放掉
修改代碼如下:
_block = ^(){
NSLog(@"number %d", number);
};
_block = Block_copy(_block);
同理,特別需要注意的地方就是在把block放到集合類當中去的時候,如果直接把生成的block放入到集合類中,是無法在其他地方使用block,必須要對block進行copy。示例如下:
[array addObject:[[^{ NSLog(@"hello!"); } copy] autorelease]];
Q:爲什麼不使用簡單的copy方法 而是 Blockcopy呢?
因爲blcok是複雜的匿名函數,簡單的copy在有些時候不能實現準確的copy,詳細就要看各自的C源碼了
6. 視圖控制器反向傳值
使用Block的地方很多,其中傳值只是其中的一小部分,下面介紹Block在兩個界面之間的傳值:
先說一下思想:
首先,創建兩個視圖控制器,在第一個視圖控制器中創建一個UILabel和一個UIButton,其中UILabel是爲了顯示第二個視圖控制器傳過來的字符串,UIButton是爲了push到第二個界面。
第二個界面的只有一個UITextField,是爲了輸入文字,當輸入文字,並且返回第一個界面的時候,當第二個視圖將要消失的時候,就將第二個界面上TextFiled中的文字傳給第一個界面,並且顯示在UILabel上。
其實核心代碼就幾行代碼:
在第二個視圖控制器的.h文件中定義聲明Block屬性
typedef void (^ReturnTextBlock)(NSString *showText);
@interface TextFieldViewController : UIViewController
@property (nonatomic, copy) ReturnTextBlock returnTextBlock;
- (void)returnText:(ReturnTextBlock)block;
@end
第一行代碼是爲要聲明的Block重新定義了一個名字
ReturnTextBlock
這樣,下面在使用的時候就會很方便。
第三行是定義的一個Block屬性
第四行是一個在第一個界面傳進來一個Block語句塊的函數,不用也可以,不過加上會減少代碼的書寫量
實現第二個視圖控制器的方法
- (void)returnText:(ReturnTextBlock)block {
self.returnTextBlock = block;
}
- (void)viewWillDisappear:(BOOL)animated {
if (self.returnTextBlock != nil) {
self.returnTextBlock(self.inputTF.text);
}
}
其中inputTF是視圖中的UITextField。
第一個方法就是定義的那個方法,把傳進來的Block語句塊保存到本類的實例變量returnTextBlock(.h中定義的屬性)中,然後尋找一個時機調用,而這個時機就是上面說到的,當視圖將要消失的時候,需要重寫:
- (void)viewWillDisappear:(BOOL)animated;
方法。
在第一個視圖中獲得第二個視圖控制器,並且用第二個視圖控制器來調用定義的屬性
如下:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
TextFieldViewController *tfVC = segue.destinationViewController;
[tfVC returnText:^(NSString *showText) {
self.showLabel.text = showText;
}];
}
可以看到代碼中的註釋,系統告訴我們可以用
[segue destinationViewController]
來獲得新的視圖控制器,也就是我們說的第二個視圖控制器。
這時候上面(第一步中)定義的那個方法起作用了,如果你寫一個[tfVC return Text按回車 ,系統會自動提示出來一個:
tfVC returnText:<#^(NSString *showText)block#>
的東西,我們只要在焦點上回車,就可以快速創建一個代碼塊了,大家可以試試。這在寫代碼的時候是非常方便的。
面試題:
- __block什麼時候用
當需要在block 中修改外部變量時使用,當需要訪問內部成員變量時。
2.在block裏面, 對數組執行添加操作, 這個數組需要聲明成 __block嗎?
當然不需要,因爲數組可以理解爲指針,在block中對數組進行添加操作,只是改變了指針指向的值,而沒有修改外部數組地址,詳細參見block訪問成員變量示例3
3.在block裏面, 對NSInteger進行修改, 這個NSInteger是否需要聲明成__blcok
必須需要,NSInteger -> typedef long NSInteger; 這貨披着OC的外衣,其實就是一個基本類型,基本類型在沒有static 等的保護下,當然需要__block
悄悄告訴你哦,block在iOS的面試中是非常重要的,如果你能把上面講解的內容理解了,那麼就仰天長嘯出門去了。
參考
- http://www.cocoachina.com/ios/20120514/4247.html
使用block開發遇到的問題 http://blog.csdn.net/hherima/article/details/3858610
這篇block 博客系列從C源碼的角度詳細分析了blcok原理- http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/
巧叔的談Objective-C Block的實現