OC中Block原理解析

原創(chuàng)文章轉(zhuǎn)載請注明出處,謝謝


這段時(shí)間重新回顧了一下Block的知識,由于只要講原理方面的知識,所以關(guān)于Block的用法就不做介紹了,不清楚的同學(xué)請自行補(bǔ)充。

Block介紹

Block是C語言的擴(kuò)充,即帶有自動(dòng)變量的匿名函數(shù),它和普通變量一樣,可以作為自動(dòng)變量,函數(shù)參數(shù),靜態(tài)變量,全局變量。


Block中截獲自動(dòng)變量值

首先我們要理解自動(dòng)變量是什么?我們觀察下面一段代碼,一個(gè)非常簡單的Block,打印了val的變量。

int val = 1;
void (^blk)(void) = ^{
    printf("%d\n", val);
};
val = 2;
blk();

/*output*/
1

在上面的源碼中,Block表達(dá)式使用了之前聲明的自動(dòng)變量val,雖然我們在之后調(diào)用block之前有修改val的值,但是打印出來的結(jié)果還是之前的舊值,關(guān)于這點(diǎn)大家應(yīng)該都知道,但是至于原因是為什么,其實(shí)就是Block保存了val的瞬間值,具體說明我們需要通過了解Block的實(shí)現(xiàn)原理才清楚。


Block的定義

通過將代碼轉(zhuǎn)換成C++的源碼我們可以發(fā)現(xiàn)Block的定義。

int main(int argc, const char * argv[]) {
    int val = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    val = 2;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我們發(fā)現(xiàn)轉(zhuǎn)換后的代碼主要包括以下幾個(gè)結(jié)構(gòu)體:

// __main_block_impl_0就是block的定義,它也是由幾個(gè)變量和構(gòu)造函數(shù)組成的。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val; // 自動(dòng)變量備份
  __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;
  }
};
struct __block_impl {
  void *isa;     // 地址指向
  int Flags;     // 某些標(biāo)志
  int Reserved;  // 關(guān)于版本升級所需的區(qū)域
  void *FuncPtr; //具體block函數(shù)的實(shí)現(xiàn)
};
static struct __main_block_desc_0 {
  size_t reserved;   // 關(guān)于版本升級所需的區(qū)域
  size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// 該函數(shù)的參數(shù)__cself相當(dāng)于C++實(shí)例方法中的this,也就是OC中的self,指向Block自身;
// 通過讀取自身保存的val,最后輸出結(jié)果
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  printf("%d\n", val);
}

通過上面的結(jié)構(gòu)我們可以發(fā)現(xiàn)Block的具體結(jié)構(gòu),我們會過頭來繼續(xù)看之前的自動(dòng)截獲變量的值不會變的問題,我們來看一下轉(zhuǎn)換后的函數(shù)調(diào)用。

int val = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

我們的block其實(shí)是傳入了val的值并不是指針,block中備份了一個(gè)新的值,所以當(dāng)val的值再之后再被修改的時(shí)候,block中的值不會改變。

所以Block的結(jié)構(gòu)其實(shí)是總結(jié)就是下圖 :

1.pic.jpg

Block中改寫自動(dòng)變量值

好,如果我們想改變自動(dòng)變量的值呢?腦補(bǔ)一下是不是可以用以下幾種方式來改變:

  • 靜態(tài)局部變量
  • 靜態(tài)全局變量
  • 全局變量

因?yàn)殪o態(tài)變量都是存在數(shù)據(jù)段,并不是棧里,數(shù)據(jù)并不會離開作用域而銷毀,所以我們來驗(yàn)證一下,定義如下三個(gè)變量:

static int static_global_val = 1;
int global_val = 2;

int main(int argc, const char * argv[]) {
    static int static_val = 3;
    void (^blk)(void) = ^{
        static_global_val++;
        global_val++;
        static_val++;
        printf("%d\n", static_global_val);
        printf("%d\n", global_val);
        printf("%d\n", static_val);
    };
    blk();
    return 0;
}
/*output*/
2
3
4

果然采用這種方式是可以改變自動(dòng)變量的值的,我們來看一下具體的block結(jié)構(gòu)有什么變化:

static int static_global_val = 1;
int global_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_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 *static_val = __cself->static_val; // bound by copy
  static_global_val++;
  global_val++;
  (*static_val)++;
  printf("%d\n", static_global_val);
  printf("%d\n", global_val);
  printf("%d\n", (*static_val));
}

int main(int argc, const char * argv[]) {
    static int static_val = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我們發(fā)現(xiàn)對于全局變量和全局靜態(tài)變量來講沒有什么改變,它們?nèi)匀皇侨肿兞浚珜τ陟o態(tài)局部變量來說,它則是傳入了靜態(tài)局部變量的指針,block通過保存靜態(tài)變量的指針,來在block中修改自動(dòng)變量的值。

那我們是不是就可以通過靜態(tài)局部變量這種方式在block中修改變量的值呢?答案是最好不要,因?yàn)檫@種情況下block的代碼會存放在數(shù)據(jù)段中,詳細(xì)的說明我們下面再說,主要會講到MRC和ARC在block的區(qū)別,這里主要是為了引出我們的__block關(guān)鍵字。


Block中的__block說明符

關(guān)于__block說明符大家肯定不會陌生了,它的出現(xiàn)允許我們能在block中修改自動(dòng)變量的值,只要我們在定義變量的時(shí)候在前面加上block說明符就可以了,那么它的原理是什么呢?我們還是通過一段代碼來看一下結(jié)果:

 __block int val = 1;
 void (^blk)(void) = ^{
     val++;
     printf("%d\n", val);
 };
 blk();
 
 /*output*/
 2

將上面的源碼轉(zhuǎn)化成C++,我們就可以發(fā)現(xiàn)這個(gè)時(shí)候的Block結(jié)構(gòu)和之前不使用__block有很明顯的區(qū)別了。

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)++;
  printf("%d\n", (val->__forwarding->val));
}
    
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*/);
 }

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), 1};
        
    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;
}

我們仔細(xì)回過頭來看,其實(shí)這個(gè)結(jié)構(gòu)無非多了一個(gè)結(jié)構(gòu)__Block_byref_val_0,它正是我們聲明的val;

// 構(gòu)造函數(shù)
{
 (void*)0,
 (__Block_byref_val_0 *)&val, 
 0, 
 sizeof(__Block_byref_val_0), 
 1
}

// _block結(jié)構(gòu)
struct __Block_byref_val_0 {
  void *__isa;   // 變量存放的區(qū)域
  __Block_byref_val_0 *__forwarding;  //指向自身
 int __flags; // 標(biāo)志位
 int __size; //  結(jié)構(gòu)大小
 int val;    // 變量的實(shí)際值
};

于是我們可以明白為什么聲明__block的變量可以在block中修改自動(dòng)變量了,因?yàn)樽兞勘旧硎且粋€(gè)結(jié)構(gòu)體了,我們存放指針的方式就可以修改實(shí)際的值了。

所以帶有__block說明符的Block的結(jié)構(gòu)其實(shí)是總結(jié)就是下圖 :

2.pic.jpg

好,到現(xiàn)在為止我們還有很多的問題沒有解決,讓我們回過頭來梳理一下:

  • 為什么__block其實(shí)只是定義了一個(gè)結(jié)構(gòu)體,我們只是通過指針來修改它的值,那么為什么不直接使用一個(gè)變量的指針來修改值就好了,這樣不是更佳簡單?
  • 為什么我們不推薦靜態(tài)局部變量的形式來修改我們的自動(dòng)變量呢?
  • __Block_byref_val_0結(jié)構(gòu)中的forwarding是用來干什么的呢?
  • 還有就是我們__block_impl中的isa指針,是指向什么?NSConcreteStackBlock又是什么?
  • 當(dāng)我們使用了__block以后,在main_block_desc_0中就多了main_block_copy_0和main_block_dispose_0兩個(gè)函數(shù),是用來干什么的?

接下來的一部分就來講關(guān)于這些問題的解釋。


Block的存儲域

首先要說明的就是我們的變量,它們存放的地方可以是棧區(qū),數(shù)據(jù)段,以及堆區(qū),所以Block其實(shí)也一樣,它也可以存放在這些地方,主要有以下幾種:

  • _NSConcreteStackBlock // 存儲在棧上
  • _NSConcreteGlobalBlock // 存儲在數(shù)據(jù)段
  • _NSConcreteMallocBlock // 存儲在堆上

我們最早的一段代碼,isa指向的就是存放在棧上,但是并不是所有的Block都是存放在棧上的。

存放在數(shù)據(jù)段(全局區(qū))的Block情況

1 使用了靜態(tài)或者全局變量的時(shí)候,block實(shí)際上是存放在全局區(qū)的,我們來驗(yàn)證一下:

// 靜態(tài)變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

static int val = 1;
void (^blk)(void) = ^{
    val++;
    printf("%d\n", val);
};
blk();

// 全局變量也是一樣,所以不做展示

但是這里有一點(diǎn)值得注意的是,通過clang轉(zhuǎn)換出來的C++代碼,好想一直都是顯示_NSConcreteStackBlock,這個(gè)其實(shí)是有問題的,可能是編譯器的優(yōu)化什么的,這個(gè)我們不做考慮。所以我們并不推薦使用局部靜態(tài)變量來修改自動(dòng)變量的值,因?yàn)樗鼤盐覀兊腷lock全部存放在全局區(qū)中,這顯然是不太好的。

2 Block語法的表達(dá)式中不使用截獲的自動(dòng)變量,也就是不使用外部變量,block也是存放在全局區(qū)的。

// 不使用外部變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

void (^blk)(void) = ^{
    printf("Hello\n");
};
blk();

好,以上兩種方式,其實(shí)我們的block并不會存放在棧區(qū),而是存放在全局區(qū),也就是數(shù)據(jù)段里面。

存放在堆區(qū)的Block情況

為什么我們有時(shí)候要把block存放在堆上,而不在棧上呢?默認(rèn)我們都是存放在棧上的,但是這有一個(gè)問題,設(shè)置在棧上的Block一但作用域結(jié)束就會被自動(dòng)釋放,由于__block變量也是在棧上,同樣在作用域結(jié)束的同時(shí),變量同樣會被釋放。

所以這個(gè)時(shí)候我們就需要將block從棧上復(fù)制到堆上來解決這個(gè)問題。那么什么情況下我們需要這么做呢?其實(shí)講這部分的內(nèi)容是需要區(qū)分環(huán)境來說明的,也就是在ARC和MRC這兩種環(huán)境下是有區(qū)別的。首先我們來看如下的一段代碼來說明這個(gè)問題:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return b;
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 1606416289
 po b
 <__NSStackBlock__: 0x7fff5fbff7a0>
 
 /*ARC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

我們發(fā)現(xiàn)MRC和ARC下的結(jié)果是不一樣的,明顯ARC下是對的,MRC下的堆棧已經(jīng)不對了,但是為什么會發(fā)生這種情況呢?其實(shí)就是我上面講的__block變量在離開作用域的時(shí)候已經(jīng)被釋放了,所以這個(gè)時(shí)候的val已經(jīng)是一個(gè)野指針了,結(jié)果當(dāng)然不對。那么為什么ARC結(jié)果就是對的呢?

原因就在于ARC情況下,編譯器主動(dòng)的把棧上的block做了一個(gè)備份到堆上。所以我們還可以正確的訪問變量。
那么我們之前的__forwarding為什么會指向自身就可以理解了,其實(shí)這個(gè)時(shí)候棧上的forwarding其實(shí)是去指向堆中的forwarding,而堆中的forwarding指向的還是自己,所以這樣就能保證我們訪問的就是同一個(gè)變量,就如下圖所示:

3.pic_hd.jpg

那我們我們?nèi)绾问謩?dòng)的去把block從棧上備份到堆上呢?答案就是使用copy。我們把原來在MRC下執(zhí)行的代碼做修改:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return [b copy];
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

這個(gè)時(shí)候我們發(fā)現(xiàn)block已經(jīng)備份到堆上去了。

在ARC下,通常講Block作為返回值的時(shí)候,編譯器會自動(dòng)加上copy,也就是自動(dòng)生成復(fù)制到堆上的代碼。但是也有些例外的情況,連ARC也無法判斷,這個(gè)時(shí)候ARC出于保守估計(jì)是不會主動(dòng)加上copy的,因?yàn)閺腷lock從棧上復(fù)制到堆上是非常吃CPU的,如果Block在棧上就已經(jīng)做夠使用了,那么我們其實(shí)就不需要將它拷貝到堆上了。

主要有以下幾種情況編譯器是不能判斷的,所以不會加上copy:

  • 向方法或者函數(shù)的參數(shù)中傳遞Block的時(shí)候。(需要我們自己手動(dòng)使用copy)
  • Cocoa框架的方法且方法中含有usingBlock等時(shí)。(不需要我們手動(dòng)copy)
  • GCD的API中傳遞Block時(shí)。(不需要我們手動(dòng)copy)

但是這里我強(qiáng)烈的懷疑第一種情況在xcode7中可能已經(jīng)被優(yōu)化了,我們看下面這個(gè)經(jīng)典的例子:

id getBlockArray() {
    int val = 10;
    return [NSArray arrayWithObjects:^{NSLog(@"a %d", val);},
                                     ^{NSLog(@"b %d", val);},
                                     nil];
}

id datas = getBlockArray();
blk a = (blk)[datas objectAtIndex:0];
a();

理論上getBlockArray獲取到的Block無論在MRC下還是ARC下都應(yīng)該是在棧區(qū),這段代碼應(yīng)該是不對的,網(wǎng)上也有人說是不對的,但是它們都是使用了xcode5,我在xcode7下,在ARC的情況下竟然可以運(yùn)行,而且結(jié)果正確,所以我懷疑現(xiàn)在已經(jīng)沒問題了。不過我們還是要記住,哪些情況是需要我們主動(dòng)把block從棧區(qū)備份到堆區(qū)的才可以。

最后總結(jié)一下什么時(shí)候棧上的block會復(fù)制到堆上呢?(ARC下)

  • 調(diào)用Block的copy函數(shù)的時(shí)候
  • Block作為函數(shù)值返回的時(shí)候
  • 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時(shí)候
  • 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí)

還有就是除了以下幾種情況外,我們都應(yīng)該主動(dòng)調(diào)用Block的copy方法,其實(shí)有點(diǎn)重復(fù)。

  • Block作為函數(shù)值返回的時(shí)候
  • 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時(shí)候
  • 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí)

總結(jié)

感覺這次講的有些亂了,我并不是從MRC時(shí)代開始寫OC的,所以直接從ARC開始讓我不知道很多以前MRC時(shí)代的東西,這點(diǎn)讓我感覺Apple做的其實(shí)不好,ARC對于開發(fā)雖然方便了,但是也容易讓開發(fā)人員變的不了解很多東西的原理了。

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

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