ios編程之回調機制詳解: ios
————————————————c++
函數/方法/block塊一系列概念:程序員
函數在大部分高級語言中都是個重要的概念,函數實現就是對一段代碼的封裝,咱們通常會爲了完成某一個業務功能或編程邏輯而須要組織數行代碼,而這數行代碼還有可能被使用屢次,因此將它們封裝成一個函數,每一次的執行咱們稱之爲調函數或函數調用。編程
在C程序中,咱們知道程序是從main函數開始執行,執行完main函數就退出程序,其實咱們程序員不多去跟蹤整個程序的執行流,一個程序(一段二進制代碼)如何從加載到運行咱們本身的代碼,這其中的過程咱們不多涉及,做爲應用層的程序員也確實不必去理解這些幕後過程,但我仍是要說明一點:C程序的main函數是由咱們程序員實現,由C的幕後去調用這個main函數,因此咱們要實現這個函數。 api
在面程對象的語法中,有了類和對象的概念,一段代碼可能歸屬於某個類或類的對象, 即該段代碼只有某個類或類的對象纔有調用權限,那麼這段代碼被封裝成類的方法或類的實例方法,方法和函數這兩個名稱其實不必區別,在c++中,方法也叫類的成員函數,在Objective-C中,方法的寫法和函數也只有書寫形式上的區別。既然有了方法,那麼執行這個方法的代碼也叫調方法或方法調用。數組
在Objective-C的語法中又多了一個概念叫block, 譯爲代碼塊, 它也是對一段代碼的封裝,只是它相對函數或方法有更炫酷的功能(能夠不經過傳參便可訪問塊外的變量),若是想執行這個代碼塊,就只需像調函數同樣來調block。多線程
————————————————框架
回調的概念:異步
有三個角色(A,B,C),它們都可覺得函數/方法/block塊這一。在A的內部去調B, 但在調B時將C做爲參數傳入B的內部, 在B的內部會去調C ,這樣一來,A沒有直接調用C,而是經過角色B來調C ,這個過程叫回調,其中的C被稱爲回調函數/回調方法/回調block函數
————————————————
回調的分類:
隨着角色C的種類不一樣,回調也分爲: 函數回調,方法回調和block回調
————————————————
函數回調:
函數回調(角色C是函數)代碼示例一:
// 函數回調: 只要角色C是函數(無論A和B是方法仍是block),這個過程叫 函數回調
// 角色C: 是函數, 是一個回調函數
int getSum(int a, int b){
return a+b;
}
// 角色B:是函數
float getAvg(int a, int b, int (*sumFunc)(int,int)){
float sum = sumFunc(a,b);
return sum/2;
}
// 角色A: 是函數
int main(int argc, const char * argv[]) {
int x = 5;
int y = 6;
// 在A中調B的時候將C傳入B的內部
float avg = getAvg(x, y, getSum );
NSLog(@"avg=%f", avg);
return 0;
}
解釋:
A : main函數
B : getAvg函數
C : getSum函數
main內部調了getAvg,在調getAvg時將getSum做爲參數傳入getAvg的內部(以指針變量sumFunc保
存了傳入的函數的地址,在內部調sumFunc從而達到了調用getSum函數的目的),上述是函數回調,只要C
是函數,咱們這個過程叫 函數回調,無論A和B是函數/方法/block
注意: 角色B須要爲角色C安排一個函數指針類型的形參
回調過程的傳參通道:
在上邊示例代碼中,角色C須要兩個參數,而角色B也經過形參傳進來了兩個數據正好爲角色C所用,因此
咱們說:若是角色B爲了給C提供實參而定義了形參變量,咱們將這個形參變量稱做這個回調過程當中
的傳參通道 注意: 通常在C語言中,角色B只需用一個指針類型的形參便可提供給C任何數量的實參,若是是多個數據,
能夠將多個數據封裝成一個結構體,將結構體的地址傳入便可,在oc語言中,角色B也只需傳一個id類
型的對象便可,若是角色C須要多個對象,能夠將多個對象加入到一個容器類對象(數組或字典)中,將
這個容器對象傳入到B內部便可。
函數回調代碼示例二:
// 函數回調二: 角色B爲方法,角色C仍是函數, 角色A爲函數
int function(int a){
printf("a=%d",a);
return a;
}
@interface Person : NSObject
// 角色B: 方法的聲明
-(void)workUsingFunction:(int (*)(int)) func;
@end
@implementation Person
// 角色B: 方法的實現
-(void)workUsingFunction:(int (*)(int))func{
func( 888 );
}
@end
// 角色A: 函數
int main(int argc, const char * argv[]) {
Person* p = [Person new];
// 調 B, 傳入 C
[p workUsingFunction:function ];
return 0;
}
注意: 由於角色B是方法,OC中的方法的書寫形式和函數有區別,形參的類型必須用圓括號括起來,因此上邊
的形參類型寫法是: (int (*)(int)) func 。 其中,func爲形參變量名,(int (*)(int))爲形參類型。若是
理解這種格式有困難,可使用typedef的用法, 例如: typedef int (*cFunc_ptr)(int); 則cFunc_ptr就
表明 int (*)(int) 這種函數指針類型,那麼角色B可寫成: -(void)workUsingFunction:(cFunc_ptr)func;
————————————————
方法回調:
方法回調(角色C是方法)示例代碼:
@interface Person : NSObject
// 角色C的接口,是方法
-(void)eat:(id)food;
@end
@implementation Person
// 角色C的實現
-(void)eat:(id)food{
NSLog(@"Person正在吃%@",food);
}
@end
// 角色B 是函數
void BFunction(id target, SEL selector, id object){
[target performSelector:selector withObject:object];
}
// 角色A 是函數
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person* p = [Person new];
BFunction(p, @selector(eat:), @"米飯");
}
return 0;
}
注意: 由於角色C是方法,而方法歸屬於類的對象,而只有類的對象纔有對C的調用權限,因此要想在B的內部
完成對C的回調,必需同時將C所屬的對象傳入到B的內部,再讓對象執行 performSelector: …來完成對
C的調用, 方法回調過程有兩個特色與函數回調不同,第一,在函數回調中,在角色B中直接用B的形參(
是一個函數指針類型的形參,待C以實參傳入B時,這個形參就表明角色C)來調用便可完成對C的回調。但在
方法回調中,須要用對象來調performSelector: selector …來完成對角色C的調用。第二,傳參通道必須
傳oc對象類型
———————————————
block回調:
block回調(角色C是block塊)示例代碼一:
@interface Person : NSObject
// 角色B的接口 是方法
-(void)doBlock:(void (^)(void))block;
@end
@implementation Person
// 角色B的實現
-(void)doBlock:(void (^)(void))block{
// 執行block
block();
}
@end
// 角色A 是函數
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 角色C: 一個block代碼塊
void (^cBlock)(void) = ^(){
NSLog(@"此處是角色C的實現代碼");
};
Person* p = [ Person new];
[p doBlock:cBlock];
}
return 0;
}
解釋:
上邊的示例中,角色C是一個不須要參數的block,因此角色B不須要爲它開闢傳參通道
注意:
因爲block的特色,代碼的形式變幻無窮,例如: [p doBlock:^{ NSLog(@"此處是角色C的實現代碼");}];
block回調(角色C是block塊)示例代碼一:
@interface Person : NSObject
// 角色B的接口 是方法
-(void)doBlock:(void (^)(id))block withObject:(id)object;
@end
@implementation Person
// 角色B的實現
-(void)doBlock:(void (^)(id))block withObject:(id)object{
// 執行block
block(object);
}
@end
// 角色A 是函數
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 角色C: 一個block代碼塊
void (^cBlock)(id object) = ^(id object){
NSLog(@"此處是角色C的實現代碼,參數=%@",object);
};
Person* p = [ Person new];
[p doBlock:cBlock withObject:@"肖在"];
}
return 0;
}
注意: 上邊的示例中,角色C須要參數,因此角色B須要爲它開闢一個傳參通道
———————————————
回調過程當中的異步處理:
在基於C的unix系統api接口中,建立一個新線程,將一段代碼放到這個新線程中去執行,這個過程原本也是
屬於一個異步回調的過程。那麼在ios中的異步處理被多個框架封裝,例如:NSThread, GCD的異步,NSQueue
等, 多線程異步回調花樣繁多,但咱們仍是從上邊的ABC三者的回調過程當中來理解異步。
同步回調:
若是角色C被A傳入B以後,C和B在同一個線程中執行,那麼這個過程叫同步回調,同步回調的特
點是C沒有返回時B也不可能返回,即在B中阻塞等待C的返回。
異步回調:
若是角色C被A傳入B以後, C又被安排在另外一個線程中去執行,即B和C不在同一個線程中執行,那
麼這個過程叫異步回調,異步回調的特色是角色B不會等待C的返回。即角色B會很快執行完畢,但角色
C在B返回時可能還在執行
異步回調的示例代碼:
@interface Person : NSObject
-(void)eat:(id)food;
@end
@implementation Person
//角色C:
-(void)eat:(id)food{
sleep(3); //休眠三秒
NSLog(@"Person正在吃:%@",food);
}
@end
// 角色B:
void BFunction(id target, SEL selector, id object){
// 這是NSObject的一個分類提供的異步執行接口
[target performSelectorInBackground:selector withObject:object];
}
// 角色A:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person* p = [ Person new];
BFunction(p, @selector(eat:), @"水果");
// 注意: 在回調eat:時,在eat:內部會休眠三秒,而異步回調是把eat:放到
// 一個新線程中去運行,角色B會很快返回,即不等待C的返回,因此在
// BFunction這個函數返回後,main函數也要退出了,那麼整個程序退
// 出了, 那麼在運行角色C的線程也會被迫退出(即還沒執行完eat:就
// 會被退出),因此咱們要阻止程序過快退出,能夠用NSRunLoop,便可
// 以讓主線程(即執行main函數的線程)休眠更長的時間等待便可
sleep(10);
}
return 0;
}
注意: 異步方法有不少種,不要拘泥於某一種,本文只是作最簡單的舉例
最強大的異步應該是GCD中的異步,能指定延時時間,或等待其它任務
完成以後再回調角色C,功能異常強大,並且GCD的性能也很高效,它是
從內核級別去優化多線程操做。
———————————————
方法回調過程當中的對象內存管理:
在方法回調過程當中,由於須要傳入方法所屬的對象,若是全部回調過程都是同步回調,
那麼不須要考慮傳入的這個對象的內存管理,正由於有了異步回調,那麼有可能出現角
色C還沒開始執行,C所屬的對象y就已經被釋放掉了,那麼再對一個被釋放的對象(野指針)
調方法就會發生內存錯誤或完不成對C的調用,因此在異步回調過程當中,對傳入的對象
有必要作強引用。在執行完C以後,又有必要對該對象作一次減計數。
———————————————
Objective-C中的定時器回調及其傳參通道:
在ios中,定時器能夠在指定的時間回調對象的方法,同時能夠以某個時間間隔重複回
調對象的方法,此時定時器不能確保傳入的對象什麼時候會被釋放掉,因此ios中的定時器
在啓動時就將對象強引用,直到這個定時器對象關閉後纔將對象計數減1,因此在定時
器回調的應用當中,要注意定時器的關閉,不然會引發內存泄漏。
另外須要注意的一點: 定時器回調時的角色C定義時的參數必須是NSTimer類的對象,
而真正須要傳進來的參數就是NSTimer對象的userInfo屬性,但在角色B傳參時正常傳便可。
定時器回調的示例代碼:
@interface Person : NSObject
-(void)eat:(id)food;
@end
@implementation Person
// 角色C: 注意此處的形參必須是NSTimer,用於定時器回調
-(void)eat:(NSTimer*)timer{
// 實際的傳參通道在timer的userInfo屬性中,若是須要多個參數,那麼該屬性
// 能夠是容器
id food = timer.userInfo;
NSLog(@"Person對象在吃:%@",food);
}
@end
// 角色A
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person* p = [Person new];
// 角色B: 是定時器NSTimer實現的一個類方法,參數傳入到userInfo
[NSTimer scheduledTimerWithTimeInterval:1 target:p selector:@selector(eat:) userInfo:@"海蔘" repeats:YES];
// 注意,定時器也是異步處理,並且定時器是以事件形式觸發執行,因此必須
// 何定主線程不退出,並且還要能接收定時器事件,因此須要開啓NSRunLoop循
// 環,關於NSRunLoop和定時器若是不理解,能夠去查閱相關文檔
[[NSRunLoop currentRunLoop]run];
}
return 0;
}
注意: 1, 定時器處理必需啓動NSRunLoop循環,
2, 傳參通道
3, 定時器啓動後p在定時器內部會被強引用
4, 什麼時候關閉定時器要根據具體業務場景決定,關閉定時器便可取消對p的一次引用計數
—————完畢——————[做者: 肖在 ]——[公司: 北京千鋒互聯科技有限公司]——[日期:2016年4月24日]———