重識Objective-C:Block底層實(shí)現(xiàn)

本文主要整理了Objective-C的Block實(shí)現(xiàn)方式。
iOS 其他相關(guān)博文鏈接iOS-day-by-day

目錄

  • 1.Objective-C與其他語言中對比
  • 2.Block模式
  • 3.Block底層實(shí)現(xiàn)

一. Objective-C和其他語言對比

在其他許多編程語言中,也存在Block被稱為閉包(Closure)、lambda計(jì)算等。

程序語言 Block的名稱
C+ Blocks Block
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
C++ Lambda
Javascript Anonymous function

二.Block常用術(shù)語

1.Block語法結(jié)構(gòu)

Block是帶有自動(dòng)變量值得匿名函數(shù)。

Block語法.jpg

例如:

// 1
^ int (int count) { return count + 1;}
// 2
^ (int count) {printf("Block");}
// 3
^{printf("Block");}

Block字面值的寫法:

^ (double firstValue, double secondValue) {
     return firstValue *secondValue;
}

上面寫法省略了返回值的類型,可以顯示的指出返回值類型。通過使用typedef可以聲明“blk_t”類型變量

 typedef double (^blk_t)(double, double);
    blk_t blk = ^(double firstValue, double secondValue){
        return firstValue * secondValue;
    };
    NSLog(@"%f", blk(3, 4));

Block也是一個(gè)Objective-C對象,可以用于賦值,當(dāng)參數(shù)傳遞,也可以放在集合中。

2.截獲自動(dòng)變量值

int val = 10;
void (^blk_t) (void) = ^ {
     NSLog(@"val = %d", val);
};
val = 2;
blk_t(); // val = 10

執(zhí)行結(jié)果,不是val = 2,而是val = 10。捕獲的是執(zhí)行Block語法時(shí)的的瞬間值。這就是自動(dòng)變量值的捕獲。

3.__block說明符

自動(dòng)變量值截獲只能保存執(zhí)行Block語法瞬間值。保存后就不能修改該值。否則,會產(chǎn)生編譯錯(cuò)誤。如果想要修改截獲的自動(dòng)變量,需要使用__block修飾。

__block int val = 10;
void (^blk_t) (void) = ^ {
      val = 11;
     NSLog(@"val = %d", val );
};
blk_t(); // val = 11

使用附有__block說明符的自動(dòng)變量可在Block中賦值,該變量稱為_block變量。

三.Block底層實(shí)現(xiàn)

1. Block的實(shí)質(zhì)

Block是“帶有自動(dòng)變量值得匿名函數(shù)”。但Block究竟是什么呢?接下來我們一起來分析。

為了研究編譯器是如何實(shí)現(xiàn)block的,需要使用clang命令,可以將Objective-C的源碼改寫成C++源代碼,命令如下:

clang -rewrite-objc xxx.c

新建一個(gè)main.c的源文件:

int main(int argc, const char * argv[]) {
    void (^blk) (void) = ^ {
         NSLog(@"Hello, Block!");
    };
    blk();  
    return 0;
}

在文件所在的文件夾,使用命令:clang -rewrite-objc main.m,在目錄中得到一個(gè)名為main.cpp的文件,區(qū)區(qū)七八行代碼,轉(zhuǎn)換后變成10萬行,其中關(guān)鍵代碼:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d3_9wynvv910yz9wv20fw56jz0h0000gn_T_main_dd5e8c_mi_0);
        }
//附加信息,如Block大小
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// main
int main(int argc, const char * argv[]) {
        void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

接下來我們分析一下,這幾行關(guān)鍵代碼是什么意思?
int main()中開始。簡化一下發(fā)現(xiàn):

// block
void (*blk)(void) = __main_block_impl_0(_main_block_func_0, &_mian_block_desc_0_DATA);
//調(diào)用
(((*blk)->FuncPtr)blk);

第一條語句中出現(xiàn)的__main_block_impl_0(),__main_block_func_0__mian_block_desc_0_DATA是什么意思?別急,我們一條條來分析。
__main_block_impl_0()實(shí)際上是__main_block_impl_0結(jié)構(gòu)體的構(gòu)造函數(shù)。結(jié)構(gòu)體聲明如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

第一個(gè)成員變量是impl。先看看__block_impl結(jié)構(gòu)體的聲明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

從這里我們可能會想到某種標(biāo)記,預(yù)留區(qū)域和函數(shù)指針。具體內(nèi)容我們會在后面詳細(xì)講解。第二變量是Desc指針, __main_block_desc_0聲明如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

預(yù)留區(qū)域和Block的大小。接下來是構(gòu)造函數(shù):

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

通過前面調(diào)用構(gòu)造函數(shù),可以簡化為:

isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

其中:

isa = &__NSConcreteStackBlock;

將Block指針賦給Block的結(jié)構(gòu)體成員變量isa。其實(shí),Block就是Objective-C對象。想要理解上句代碼的意思,需要理解Objective-C類和對象的實(shí)質(zhì)。

id 類型

typedef struct objc_object *id;
typedef struct objc_class *Class;

id為objc_object結(jié)構(gòu)體的指針類型。Class 為objc_class結(jié)構(gòu)體的指針類型。objc_class結(jié)構(gòu)體在objc4/runtime.h中聲明:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

兩者很相似,objc_object結(jié)構(gòu)體和objc_class結(jié)構(gòu)體歸根結(jié)底實(shí)在各個(gè)對象和類的實(shí)現(xiàn)中使用的最基本的結(jié)構(gòu)體。
下面通過簡單的MyObject來看一下。

@implementation MyObject
{
    int val0;
    int val1;
}
@end

轉(zhuǎn)換為C++源代碼,MyObject結(jié)構(gòu)體

struct MyObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int val0;
    int val1;
};

實(shí)例變量val0和val1被直接聲明為結(jié)構(gòu)體的成員。上述中,結(jié)構(gòu)體都是基于objc_class結(jié)構(gòu)體的class_t結(jié)構(gòu)體。class_t聲明如下:

struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

該結(jié)構(gòu)體實(shí)例中,持有聲明的成員變量、方法的名稱、方法的實(shí)現(xiàn)、屬性以及父類的指針。回到

isa = &__NSConcreteStackBlock;

__NSConcreteStackBlock相當(dāng)于class_t結(jié)構(gòu)體實(shí)例,關(guān)于該類的信息都存放在__NSConcreteStackBlock中。

2. 截獲自動(dòng)變量

int main(int argc, const char * argv[]) {
    
    int val = 10;
    void (^blk) (void) = ^ {
        printf("%d", val);
    };
    blk();
    return 0;
}

使用clang命令轉(zhuǎn)為C++代碼,看看和之前的有什么區(qū)別。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val; // <== val 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

        printf("%d", val);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {

    int val = 10;
    void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

對比之前的代碼發(fā)現(xiàn),Block語法表達(dá)式中使用的自動(dòng)變量被作為成員變量追加到__main_block_impl_0結(jié)構(gòu)體中。并且類型完全相同,另外注意,未使用的變量并不會追加。

__main_block_impl_0結(jié)構(gòu)體實(shí)例初始化:

isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
val = 10;

這時(shí),在__main_block_impl_0結(jié)構(gòu)體實(shí)例中,自動(dòng)變量值被截獲。
總的來說,所謂“截獲自動(dòng)變量值”意味著在執(zhí)行Block語法時(shí),Block語法表達(dá)式所使用的自動(dòng)變量值被保存到Block的結(jié)構(gòu)體實(shí)例中。

3.__block說明符

在Block中修改捕獲的外部變量,需要使用__block修飾符(__block存儲域類說明符)。在C中存儲域類:typedef、extern、static、auto、register。__block與他們的作用相同,用來指定將變量值設(shè)置到哪個(gè)存儲域中。
下面看一個(gè)例子:

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        __block int val = 10;
        void (^blk) (void) = ^{
            val = 11;
        };
        blk();
    }
    return 0;
}

使用clang命令,轉(zhuǎn)為C++源碼如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref  <=======注意
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

            (val->__forwarding->val) = 11;
        }
// <====== 相當(dāng)于retain
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
// <====== 相當(dāng)于release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

        void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
 
    return 0;
}

上面是含有__block修飾符的代碼。對比一下和之前的差別。

 __block int val = 10;

__block修飾的變量變成結(jié)構(gòu)體

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding; // 
 int __flags;
 int __size;
 int val; // <=== val 作為結(jié)構(gòu)體的成員變量
};

變量val被作為結(jié)構(gòu)體的成員變量。_forwarding指針什么作用?后面在分析。

那變量賦值的代碼呢?

^{val = 11;}

如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

            (val->__forwarding->val) = 11;
        }

變量的復(fù)制

  • 對于block外部變量的引用,block默認(rèn)是將其復(fù)制到其數(shù)據(jù)結(jié)構(gòu)中來實(shí)現(xiàn)訪問的。
  • 對于__block修飾的外部變量引用,block是復(fù)制其引用地址來實(shí)現(xiàn)訪問的。

另外,可以參考《招聘一個(gè)靠譜的iOS (下)13、14題》

ARC,MRC 對Block類型的影響

在ARC開啟的情況下,將只會有NSConcreteGlobalBlock和NSConcreteMallocBlock類型的block。原本NSConreteStackBlock被NSConcreteMallocBlock類型替代。

在Block中,如果只使用全局或靜態(tài)變量或者不是用外部變量,那么Block塊的代碼會存儲在全局區(qū)。
在ARC中

  • 如果使用外部變量,Block塊的代碼會存儲在堆區(qū)。

在MRC中

  • 如果使用外部變量,Block塊的代碼會存儲在棧區(qū)。

Block默認(rèn)情況下不能修改外部變量,只能讀取外部變量。
在ARC中

  • 外部變量在堆中,這個(gè)變量在Block塊內(nèi)與在Block塊外地址相同。
  • 外部變量在棧中,這個(gè)變量會被copy到為Block代碼塊所分配的堆中。

在MRC中

  • 外部變量在堆中,這個(gè)變量在Block塊內(nèi)與在Block塊外地址相同。
  • 外部變量在棧中,這個(gè)變量會被copy到為Block代碼塊所分配的棧中

如果需要修改外部變量,需要在外部變量前聲明__block。
在ARC中

  • 外部變量存在堆中,這個(gè)變量在Block塊內(nèi)與Block塊外地址相同。
  • 外部變量存在棧中,這個(gè)變量會被轉(zhuǎn)移到堆中,不是復(fù)制。

在MRC中

  • 外部變量存在堆中,這個(gè)變量在Block塊內(nèi)與Block塊外地址相同。
  • 外部變量存在棧中,這個(gè)變量在Block塊內(nèi)與Block塊外地址相同。

關(guān)于Block的面試題

  1. 使用block時(shí)什么情況會發(fā)生引用循環(huán),如何解決?
  2. 在block內(nèi)如何修改block外部變量?
  3. 使用系統(tǒng)的某些block api(如UIView的block版本寫動(dòng)畫時(shí)),是否也考慮引用循環(huán)問題?

參考鏈接

談Objective-C block的實(shí)現(xiàn)
Block教程系列
對Objective-C中Block的追探
iOS中block實(shí)現(xiàn)的探究
Block 編程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評論 2 374

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