解析OC對象間的通信方式-Block篇

  • 什么是Block

OC作為C語言的超集钩杰,將面向過程的C語言擴展成了一門動態(tài)的面向?qū)ο笳Z言隧哮,其中Block就是OC對C語言中的函數(shù)指針幽勒、結(jié)構(gòu)體進行擴展而成的新的特色語法,block本質(zhì)是一個代碼塊焊虏,你也可以把block理解成能夠作為OC對象進行傳遞的匿名函數(shù)淡喜,并且是可以直接定義在其他函數(shù)內(nèi)部并共享該函數(shù)內(nèi)所有變量的匿名函數(shù)。

  • 如何使用Block

既然block是OC的對象诵闭,那么我將通過用OC的NSString對象進行類比的方式幫助你更好的了解它×锻牛現(xiàn)有如下代碼:


#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *myString = @"這是一個字符串對象";
    [self stringTest:myString];
}

- (void)stringTest:(NSString *)pString{
    NSLog(@"%@",pString);
}

上述代碼很簡單,把string對象傳遞給stringTest方法疏尿,在該方法中對該對象進行了打印操作∥林ィ現(xiàn)在,我們依樣畫葫蘆褥琐,用同樣的形式傳遞一個block對象锌俱,類似的,block的形參和實參的聲明如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^myBlock)(int, int) = ^void (int a, int b) {
        NSLog(@"計算結(jié)果=%d",a+b);
    };
    [self sumFunction:myBlock];
    
}


- (void)sumFunction:(void(^)(int a,int b))block{
    
    block(2,3);
    
}

第一次接觸block的新手看到以上代碼或許會感到費勁敌呈,我們一步一步來解析這個語法稍顯"別扭"的block贸宏。

void (^myBlock)(int, int) = ^void (int a, int b) {
    NSLog(@"計算結(jié)果=%d",a+b);
};

這是Block的完整定義造寝,等號左邊從左往右看,該block的返回類型為void吭练、變量名叫myBlock诫龙、^符號用于申明myBlock是一個block類型的變量、block入?yún)閮蓚€int型變量线脚,等號右邊則為myBlock的具體內(nèi)部實現(xiàn)赐稽,用{}將實現(xiàn)代碼包裹起來』虢模看到這里你是否覺得block和函數(shù)越發(fā)類似,有返回值類型晰绎,有入?yún)⒃⒙洹H绻闶煜語音,你會發(fā)現(xiàn)等號左邊的申明方式和C語言中的函數(shù)指針非常相似荞下,僅把*變成了^而已伶选,當然myBlock變量實際上是一個結(jié)構(gòu)體,而非單獨一個指針尖昏,等號右邊實際上則是一個沒有函數(shù)名的匿名函數(shù)仰税,將一個匿名函數(shù)的實現(xiàn)賦值給myBlock結(jié)構(gòu)體中的一個指針,就構(gòu)成了這樣一個完整的block型變量抽诉。而myBlock變量根據(jù)其作用域不同決定了其可以在對象內(nèi)部陨簇,甚至對象間進行傳遞。在實際開發(fā)中迹淌,我們常常會將myString申明為一個屬性河绽,以供本類中其他方法讀寫,現(xiàn)在唉窃,我們同樣將myBlock申明為一個屬性耙饰,通過申明屬性的方式,可以讓代碼看起來更加清晰明了纹份,申明方式如下:

//用typedef將MyBlock自定成一個類型名
typedef void(^MyBlock)(int a,int b);

#import "ViewController.h"

@interface ViewController ()
//block創(chuàng)建在棧區(qū)苟跪,使用copy修飾
@property(nonatomic,copy)MyBlock myBlock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myBlock = ^void (int a, int b) {
        NSLog(@"計算結(jié)果=%d",a+b);
    };
    [self sumFunction:self.myBlock];
    
}


- (void)sumFunction:(MyBlock)pBlock{
    
    pBlock(2,3);
    
}

@end

通過這種方式申明block,比第一種方式更清晰明了蔓涧。需要注意的是件已,使用typedef重命名時,(^MyBlock)中的MyBlock被抽象了一種自定義類型名而不再是變量名蠢笋,self.myBlock中的myBlock才是被作為變量進行傳遞拨齐。在此筆者希望讀者都能使用typedef的方式申明Block,這樣不論是形參又或?qū)崊⒌纳昝髯蚰寄苁褂媚阕约喝〉腗yBlock類型名來直接創(chuàng)建對象瞻惋。這種方式更接近于我們平時的代碼習慣厦滤,現(xiàn)在仔細觀察以上代碼,我們不難發(fā)現(xiàn)viewDidLoad方法和sumFunction方法之間歼狼,進行了一次簡單的"通信"掏导,我們先在viewDidLoad方法中創(chuàng)建了self.myBlock變量,即在viewDidLoad方法中內(nèi)聯(lián)了一個匿名函數(shù)羽峰,我們知道self.myBlock的內(nèi)部實現(xiàn)趟咆,但我們暫時還不想要執(zhí)行這個self.myBlock對象內(nèi)的實現(xiàn)代碼,直到代碼執(zhí)行到sumFunction方法梅屉,在sumFunction方法內(nèi)才又反向調(diào)用了這個Block值纱。嗯,看上去很不錯坯汤,但實際好像并沒有什么用處虐唠。事實上,在對象內(nèi)部方法之間使用block通信的確有些多此一舉惰聂,實際開發(fā)中也很少用到疆偿。因為你完全可以把self.myBlock的實現(xiàn)重新定義成新的方法,進行兩次正向調(diào)用搓幌。其實block的真正用武之地確實并不在此杆故,接下來,請閱讀如下較復雜的常見場景:

在UIController的viewDidLoad方法中溉愁,我們初始化頁面的同時還需要異步的從接口獲取頁面數(shù)據(jù)從而完成對view的渲染处铛,假設(shè)你的UIController已經(jīng)十分臃腫,你不希望UIController再負責網(wǎng)絡(luò)請求的邏輯叉钥,于是你寫了一個URLRequestManager類來專門負責網(wǎng)絡(luò)請求業(yè)務罢缸。當你需要發(fā)起URL請求時,只需要實例化這個manager投队,由他發(fā)起請求即可枫疆,Controller并不關(guān)心manager的內(nèi)部實現(xiàn)代碼,也不關(guān)心何時完成請求敷鸦,只需要在請求成功或者失敗時的結(jié)果告訴控制器即可息楔,控制器會在拿到數(shù)據(jù)后將數(shù)據(jù)賦值給view,完成界面的最終顯示扒披。

對于上述需求值依,我們就可以通過block來達到目的。代碼如下:

#import "ViewController.h"
#import "URLRequestManager.h"
@interface ViewController ()


@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    //創(chuàng)建頁面
    [self initSubViews];
    
    //數(shù)據(jù)請求
    [self requestData];
    
}

- (void)initSubViews{
    //view創(chuàng)建代碼實現(xiàn)
}

- (void)requestData{
    //發(fā)起請求
    [URLRequestManager requestWithUrl:@“url地址”
                           parameters:nil
                          backHandler:^(BOOL isSucessful, NSError *error, NSData *data) {
        
                              if (isSucessful) {
                                  //拿到數(shù)據(jù)
                                  NSLog(@"請求成功");
                                  NSLog(@"%@",data);
                                  //可以在這里進行界面賦值
                              }else{
                                  //彈出錯誤提示
                                  NSLog(@"%@",error);
                              }
                              
                          }];
}
@end

URLRequestManager提供了一個網(wǎng)絡(luò)請求方法碟案,URLRequestManager申明和實現(xiàn)如下:

#import <Foundation/Foundation.h>
typedef void(^RequestBackHandler)(BOOL isSucessful, NSError *error,NSData *data);

@interface URLRequestManager : NSObject

/**
 發(fā)起網(wǎng)絡(luò)請求
 
 @param url             請求地址
 @param parameters      請求體
 @param handler         回調(diào)Block
 */
+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(requestBackHandler)handler;

@end

#import "URLRequestManager.h"

@implementation URLRequestManager

+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(RequestBackHandler)handler{
    
    //延時兩秒調(diào)用block愿险,模擬網(wǎng)絡(luò)請求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        //執(zhí)行block回調(diào)
        handler(YES,nil,nil);
        
    });
    
    
}

@end

上述代碼中,我們就利用block完成了一次對象間的通信价说,代碼簡單的還原了異步網(wǎng)絡(luò)請求的需求辆亏,控制器的requestData方法中調(diào)用了manager的類方法發(fā)起網(wǎng)絡(luò)請求风秤,并且傳入了請求所需的參數(shù)以及定義好的block對象,實際使用中扮叨,我們不關(guān)心類方法的內(nèi)部實現(xiàn)缤弦,只要在其完成請求后再執(zhí)行調(diào)用我們早已經(jīng)定義好的block對象即可。如果你熟悉代理模式彻磁,會發(fā)現(xiàn)其實這兩者之間相似但又有細微差別碍沐,兩者主要都用于對象的回調(diào),但block更注重結(jié)果的傳輸衷蜓,代碼更清晰簡練累提,delegate更偏向過程信息的傳輸,代碼更規(guī)范嚴謹恍箭。

  • Block的內(nèi)部構(gòu)造

前面提到刻恭,block對象實際上是一個結(jié)構(gòu)體而非簡單的函數(shù)指針,現(xiàn)在我們就具體來探索一下Block的神秘本質(zhì)扯夭。block的數(shù)據(jù)結(jié)構(gòu)定義如下


4.Block內(nèi)部結(jié)構(gòu)

對應的結(jié)構(gòu)體定義如下:

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實例實際上由以下6部分構(gòu)成:

isa指針:指向該block類型的類的指針,每個Objective-C對象鞍匾,都有一個isa
指針交洗,指向?qū)ο蟮念悾鳦lass里也有個isa的指針, 指向meteClass(元類)橡淑。元類保存了類方法的列表构拳。元類也有isa指針,它的isa指針最終指向的是一個根元類(root meteClass)。根元類的isa指針指向本身梁棠。
flags:按bit位表示一些block的附加信息置森,比如判斷block類型、判斷block引用計數(shù)符糊、判斷block是否需要執(zhí)行輔助函數(shù)等凫海。
reserved:保留變量,我的理解是表示block內(nèi)部的變量數(shù)男娄。
invoke:函數(shù)指針行贪,指向block的實現(xiàn)代碼地址。
descriptor:指向結(jié)構(gòu)體的指針模闲,block的附加描述信息建瘫,比如保留變量數(shù)、block的大小尸折、copy和dispose輔助函數(shù)的函數(shù)指針啰脚,copy函數(shù)為當block執(zhí)行copy操作或者當block從棧上拷貝到堆上時調(diào)用,dispose函數(shù)則是block在堆上釋放時調(diào)用实夹。
variables:block內(nèi)部捕獲的對象橄浓,如void (^blk)(void) = ^{print(fmt,val)}粒梦;此時,variables中則為fmt和val這兩個變量

由于篇幅有限贮配,筆者不再對block的各個部分做具體介紹谍倦。

  • Block的類型

根據(jù)block的本身的存儲位置,block有三種類型泪勒,分別如下:

NSGlobalBlock: 類似函數(shù)昼蛀,位于text段;
NSStackBlock : 位于棧內(nèi)存圆存,僅在函數(shù)作用域內(nèi)有效叼旋;
NSMallocBlock: 位于堆內(nèi)存。

block的類型并非我們創(chuàng)建block時手動指定的沦辙,而是編譯器根據(jù)block捕獲的外部變量的不同而自動確定的夫植。以下三個例子分別對應三種類型的block。

{  
    float (^myBlock)(float, float) = ^(float a, float b){  
          NSLog(@"heheda");
    };  
   
    NSLog(@"block is %@", myBlock); 
    //block is <__NSGlobalBlock__: 0x47d0>  
}  
{  
  
    NSString *str = @"heheda";
    NSLog(@"block is %@", ^{  
        NSLog(@"%@", str);   
    });  
    //block is <__NSStackBlock__: 0xbfffdac0>  
}
{
    NSString *str = @"heheda";
    void (^TestBlock)(void) = ^{  
        NSLog(@"%@", str);  
    };  
    NSLog(@"block is %@", TestBlock);  
    //block is <__NSStackBlock__: 0x75425a0>   MRC
    //block is <__NSMallocBlock__: 0x75425a0>   ARC
   
} 

分析以上三個打印結(jié)果油讯,我們得出以下結(jié)論:

  1. 如果block沒有捕獲任何外部變量详民,該block所需要的全部信息都能在編譯期確定。該block是全局存在的陌兑,相當于函數(shù)沈跨。
  2. 如果block捕獲了自動變量,block存在于棧區(qū)兔综,copy操作可以使其存儲于堆區(qū)饿凛。
  3. 在ARC下,賦值的同時編譯器會幫我們進行copy操作软驰,無需手動涧窒。
  • Block注意事項

1、若要修改捕獲到的自動變量锭亏,用__block修飾該變量纠吴。

示例:

typedef void(^MyBlock)(void);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int x = 3;
    MyBlock  myBlock = ^{
        x+=1;
    };
    myBlock();
    NSLog(@"%d",x);
}

運行以上代碼編譯器會報錯,并告訴你要將變量x添加__block修飾符贰镣。其實這個錯誤原因很容易理解呜象,學習C語言的時候我們知道,向某個函數(shù)傳入變量的值碑隆,實際上只是將該變量的值賦值給該函數(shù)內(nèi)的形參恭陡,函數(shù)內(nèi)部并不能修改這個變量本身,若要修改該變量上煤,應傳入其地址休玩。block同樣可以通過這種方式達到目的,不同之處在于x是直接被“捕獲”而不是作為參數(shù)傳入,代碼如下:

typedef void(^MyBlock)(void);

- (void)viewDidLoad {
   [super viewDidLoad];
   
   int x = 3;
   int *p = &x;
   MyBlock  myBlock = ^{
       *p +=1;
   };
   myBlock();
   NSLog(@"%d",x);
}

然而這樣的代碼明顯不是我們想要的拴疤,因此OC為我們提供了__block修飾符永部,所以上述代碼可以用以下代碼代替:

typedef void(^MyBlock)(void);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block int x = 3;
    MyBlock  myBlock = ^{
        x +=1;
    };
    myBlock();
    NSLog(@"%d",x);
}

將x用__block修飾后,指針p指向x的操作便交由block內(nèi)部去實現(xiàn)呐矾,此外苔埋,x在存儲方式也發(fā)上了變化,由原本的棧區(qū)改為了堆區(qū)蜒犯。而對于全局變量和靜態(tài)變量组橄,我們則可以直接在block內(nèi)部修改其值。

block內(nèi)部還可以訪問類的實例變量和self變量罚随,且block會按照屬性的修飾語義進行引用玉工。這就引出了我們需要特別注意的問題,即循環(huán)引用淘菩。

2遵班、如果塊所捕獲的對象直接或間接地保留了塊本身,那么就要當心循環(huán)引用問題潮改。

示例:

#import "ViewController.h"

typedef void(^MyBlock)(void);

@interface ViewController ()

@property(nonatomic,strong)NSString *myStr;
@property(nonatomic,copy)MyBlock myBlock;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myBlock = ^{
        self.myStr = @"123";
    };
    
}

@end

運行上述代碼狭郑,系統(tǒng)會有如下警告

Capturing 'self' strongly in this block is likely to lead to a retain cycle

如果你熟悉OC內(nèi)存管理機制,你應該知道這是self和myBlock兩個對象相互引用汇在,從而導致了內(nèi)存無法正確釋放愿阐。為了避免循環(huán)引用,可以將代碼如下修改(MRC下將__weak替換為__block):

__weak __typeof__(self) weakSelf = self;
self.myBlock = ^{
    weakSelf.myStr = @"123";    
};

上述情況系統(tǒng)很容易能夠檢測出趾疚,故我們可以排查修改,但有時候會遇到情況較為復雜的情況以蕴,編譯器未必能發(fā)現(xiàn)糙麦,而循環(huán)引用導致的crash難以追蹤,一旦出現(xiàn)非常頭疼丛肮。所以在使用block的時候赡磅,希望讀者們能夠多多注意這方面的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宝与,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子习劫,更是在濱河造成了極大的恐慌咆瘟,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诽里,死亡現(xiàn)場離奇詭異袒餐,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門灸眼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卧檐,“玉大人,你說我怎么就攤上這事焰宣∶骨簦” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵匕积,是天一觀的道長盈罐。 經(jīng)常有香客問我闸天,道長暖呕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任苞氮,我火速辦了婚禮湾揽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笼吟。我一直安慰自己库物,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布贷帮。 她就那樣靜靜地躺著戚揭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撵枢。 梳的紋絲不亂的頭發(fā)上民晒,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音锄禽,去河邊找鬼潜必。 笑死,一個胖子當著我的面吹牛沃但,可吹牛的內(nèi)容都是我干的磁滚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼宵晚,長吁一口氣:“原來是場噩夢啊……” “哼垂攘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淤刃,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤晒他,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钝凶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仪芒,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡唁影,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了掂名。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片据沈。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饺蔑,靈堂內(nèi)的尸體忽然破棺而出锌介,到底是詐尸還是另有隱情,我是刑警寧澤猾警,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布孔祸,位于F島的核電站,受9級特大地震影響发皿,放射性物質(zhì)發(fā)生泄漏崔慧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一穴墅、第九天 我趴在偏房一處隱蔽的房頂上張望惶室。 院中可真熱鬧,春花似錦玄货、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽可柿。三九已至,卻和暖如春丙者,著一層夾襖步出監(jiān)牢的瞬間趾痘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留卵贱,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓键俱,卻偏偏與公主長得像兰绣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子编振,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Block使用場景,可以在兩個界面的傳值臀玄,也可以對代碼封裝作為參數(shù)的傳遞等瓢阴。用過GCD就知道Block的精妙之處健无。...
    Coder_JMicheal閱讀 720評論 2 1
  • iOS代碼塊Block 概述 代碼塊Block是蘋果在iOS4開始引入的對C語言的擴展,用來實現(xiàn)匿名函數(shù)的特性,B...
    smile刺客閱讀 2,333評論 2 26
  • 前言 Blocks是C語言的擴充功能累贤,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,759評論 0 23
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,686評論 0 9
  • 一硼被、Objective-C發(fā)展史 Objective-C從1983年誕生渗磅,已經(jīng)走過了30多年的歷程。隨著時間的推移...
    沒事蹦蹦閱讀 5,819評論 12 34