Block 原理和內(nèi)存管理

Block是什么?

在oc中它是帶有^符號的匿名函數(shù),遵循BN范式: ^ 返回值類型 參數(shù)列表 表達式 (參數(shù)和返回值為空的時候可以省略)。

而在C中(OC編譯后的C)它會被編譯成一堆結(jié)構(gòu)體和幾個函數(shù)以及靜態(tài)變量。

而這幾個結(jié)構(gòu)體和函數(shù),正是Block實現(xiàn)的本質(zhì).

  1. Block 結(jié)構(gòu)體
  2. 表達式轉(zhuǎn)換成的函數(shù)
  3. 描述體
  4. Block主體結(jié)構(gòu)體
  5. Block的調(diào)用
// Block 結(jié)構(gòu)體
struct __block_imp1 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
}

// 描述體
static struct __main_block_desc_0 {
  unsigned long reserved;
  unsigned long Block_size;
} __main_block_desc_0_DATA = {
  0,
  sizeof(struct __main_block_imp1_0)
}

// Block主體結(jié)構(gòu)體
struct __main_block_imp1_0 {
  struct __block_imp1 imp1;
  struct __main_block_desc_0* Desc;
  
  __main_block_imp1_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    imp1.isa = &_NSConcreteStackBlock;
    imp1.Flags = flags;
    imp1.FuncPtr = fp;
    Desc = desc;
  }
}

// 表達式轉(zhuǎn)換成的函數(shù)
static void  __main_block_func_0(struct __main_block_imp1_0 *__cself) {
  printf("this is a block");
}

int main() {
  // Block的調(diào)用
  void (*blk)(void) = (void(*)(void))&__main_block_imp1_0((void*)__main_block_func_0,&__main_block_desc_0_DATA);
  ((void(*)(struct __block_imp1 *))((struct __block_imp1 *)blk)->FuncPtr)((struct __block_imp1 *)blk);
  return 0;
}

逐一來分析一下,這幾個結(jié)構(gòu)體的作用。

// Block 結(jié)構(gòu)體
struct __block_imp1 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
}

由于轉(zhuǎn)化后的Block類型結(jié)構(gòu)體的成員中有isa指針,不難得知它和某些OC的類有關系,而在struct __main_block_imp1_0的初始化函數(shù)中,isa被賦予了一個_NSConcreteStackBlock的類。這表明該Block屬于stack存在于棧中的類。

_NSConcreteStackBlock  存在于棧中
_NSConcreteMallocBlock 存在于堆中
_NSConcreteGlobalBlock 存在于.Data(程序的數(shù)據(jù)區(qū)域)中

?

// 描述體
static struct __main_block_desc_0 {
  unsigned long reserved;
  unsigned long Block_size;
} __main_block_desc_0_DATA = {
  0,
  sizeof(struct __main_block_imp1_0)
}

只是存儲了一些升級區(qū)域和結(jié)構(gòu)大小。

// Block主體結(jié)構(gòu)體
struct __main_block_imp1_0 {
  struct __block_imp1 imp1;
  struct __main_block_desc_0* Desc;
  
  __main_block_imp1_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    imp1.isa = &_NSConcreteStackBlock;
    imp1.Flags = flags;
    imp1.FuncPtr = fp;
    Desc = desc;
  }
}

__main_block_imp1_0結(jié)構(gòu)體從某種程度上封裝了前兩個結(jié)構(gòu)體,但它的實際意義不止于此。

// 表達式轉(zhuǎn)換成的函數(shù)
static void  __main_block_func_0(struct __main_block_imp1_0 *__cself) {
  printf("this is a block");
}

int main() {
  // Block的調(diào)用
  void (*blk)(void) = (void(*)(void))&__main_block_imp1_0((void*)__main_block_func_0,&__main_block_desc_0_DATA);
  ((void(*)(struct __block_imp1 *))((struct __block_imp1 *)blk)->FuncPtr)((struct __block_imp1 *)blk);
  return 0;
}

函數(shù)和調(diào)用很簡單,就是一些類型轉(zhuǎn)換容易讓人眼花繚亂,經(jīng)過刪繁就簡之后,就是簡單的函數(shù)聲明和調(diào)用而已。

需要注意的是,__main_block_func_0是以__main_block_imp1_0這個主體結(jié)構(gòu)體入?yún)⒌模@在變量捕捉的時候有很大作用。

變量捕捉

熟悉了Block的本質(zhì)之后,就是對于Block的運用了,Block為什么如此好用的原因之一就在于,我們可以做到一般函數(shù)不能做到的事情,例如:局部變量的捕捉。

int main() {
    int v = 1;
    void (^blk)(void) = ^{
        printf("this is a block %d",v);
    };
    blk();
    return 0;
}

可以看到,變量v能夠在表達式內(nèi)部獲取到。

然而當我們想要在內(nèi)部改變這個變量的值的時候,會得到一個編譯錯誤。

int main() {
    int v = 1;
    void (^blk)(void) = ^{
        printf("this is a block %d",v);
        v = 2; // error: Variable is not assignable (missing __block type specifier)
    };
    blk();
    return 0;
}

為什么?

既然錯誤發(fā)生在表達式內(nèi),我們就來分析一下編譯后的函數(shù)static void __main_block_func_0(struct __main_block_imp1_0 *__cself) 捕獲變量后,這個函數(shù)的實現(xiàn)變成了這樣:

// 表達式轉(zhuǎn)換成的函數(shù)
static void  __main_block_func_0(struct __main_block_imp1_0 *__cself) {
        int v = __cself->v;
    printf("this is a block %d",v);
}

入?yún)?code>__main_block_imp1_0多了個v的成員變量。

也就是說,編譯后,__main_block_imp1_0結(jié)構(gòu)體負責存儲捕獲的局部變量。

然而捕獲的變量也是Int類型的,并非指針,所以我們在__main_block_func_0中改變變量V的值是沒有任何意義的。編譯器很聰明的檢測到了這個操作。

那么我們加入__block之后呢?

int main() {
    __block int v = 1;
    void (^blk)(void) = ^{
        printf("this is a block %d",v);
        v = 2;
    };
    blk();
    return 0;
}

編譯器的報錯消失了。

意味著我們可以隨意在block內(nèi)改動v這個局部變量了。

那么內(nèi)部是怎么實現(xiàn)的呢?

還是看函數(shù)實現(xiàn):

// 表達式轉(zhuǎn)換成的函數(shù)
static void  __main_block_func_0(struct __main_block_imp1_0 *__cself) {
        __Block_byref_val_0 *val = __cself->val;
    printf("this is a block %d",(val->_forwarding->v));
    (val->_forwarding->v) = 2;
}

__main_block_imp1_0里存儲的成員V變了,不再是簡單的Int類型,而是變成了

__Block_byref_val_0 指針。

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

需要稍微分析一下,這幾個成員變量的意義。

找到它初始化的地方,在main函數(shù)內(nèi):

__Block_byref_val_0 v = (
  0,
  &v,
  0,
  sizeof(__Block_byref_val_0),
  1
)
  • isa 被賦值為空

  • _forwarding 指向了自己,

  • flags =0

  • size 傳入了大小

  • v 賦值成捕獲的變量的值

而原本的int變量v消失了,__Block_byref_val_0變量v取而代之,將此結(jié)構(gòu)體的指針賦值給

__main_block_imp1_0成員變量后,就可以實現(xiàn)對變量v的修改了。

這是__block的作用,可是在結(jié)構(gòu)體

__Block_byref_val_0初始化的時候還有一個有趣的地方,就是:

__Block_byref_val_0 *__forwarding;

__forwarding指針有什么意義呢?它指向了自己,并且在函數(shù)體內(nèi)修改存儲值時,也訪問了"自己"。為何要多此一舉?

Block的存儲域

再弄清__forwarding指針的作用之前,我們先要縷清楚這三種Block的區(qū)別以及應用的場景。

類型 內(nèi)存分布 場景
_NSConcreteStackBlock 存在于棧中 定義在函數(shù)內(nèi)部,未被強引用,且捕獲了外部變量。
_NSConcreteMallocBlock 存在于堆中 被強引用,由_NSConcreteStackBlock被拷貝到堆內(nèi)存。
_NSConcreteGlobalBlock 存在于.Data(程序的數(shù)據(jù)區(qū)域)中 定義在全局區(qū)域,或者未捕獲外部變量且定義在函數(shù)內(nèi)部的block

從內(nèi)存管理的角度看,_NSConcreteGlobalBlock類型的Block討論意義不大。

_NSConcreteStackBlock和 _NSConcreteMallocBlock 才是我們要討論的重點。

假定這樣一種情況(MRC下):

blk_t blk;
{
     NSAutoreleasePool *pool = [NSAutoreleasePool new];
    id array = [[[NSMutableArray alloc] init] autorelease];
    blk = ^( id obj){
        [array addObject:obj];
        NSLog(@"count is %d",[array count]);
    };
     [pool release];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
    

由于是MRC環(huán)境,加入NSAutoreleasePool來模擬array的釋放周期。

可以看到,在大括號以外調(diào)用block,肯定會崩潰,因為array已經(jīng)釋放了。并且在這種情況下,Block屬于_NSConcreteStackBlock類型。

我們稍稍改動一下:

blk_t blk;
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    id array = [[[NSMutableArray alloc] init] autorelease];
    blk = [^( id obj){
        [array addObject:obj];
        NSLog(@"count is %d",[array count]);
    } copy];
    [pool release];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

程序輸出正常了,array也被block正常捕獲。

而Block的類型經(jīng)過copy之后,變成了_NSConcreteMallocBlock,copy方法實際調(diào)用的是:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

_Block_copy 作用按照文檔上來說是將Block拷貝到堆內(nèi)存——這和我們的測試結(jié)果一樣。

隨著Block被復制到堆內(nèi)存之后,Block所持有的__block變量也會復制到堆內(nèi)存并且持有,要證明需要看編譯后的代碼:

static void __main_block_copy_0 (struct __main_block_imp1_0*dst ,struct __main_block_imp1_0*src){
  _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_imp1_0*scr){
  _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

這兩個方法是在Block被復制到堆內(nèi)存和從堆內(nèi)存釋放的時候調(diào)用的。

_Block_object_assign_Block_object_dispose分別是Block持有變量的賦值和釋放。

__forwarding指針:

這時候我們可以來解釋這個指針的作用了:

__block int v = 1;
void (^blk)(void) = [^{v++;} copy];
v++;
blk();
printf("num is  %d",v);

可以看到經(jīng)過copy后的變量v在外部仍然可以改變,試想如果沒有__forwarding指針存在的話,v經(jīng)過編譯后,結(jié)構(gòu)體仍然存在于棧上,此時改變v的值結(jié)果肯定不是我們想要的3。

__forwarding保證__Block的變量在賦值時使得位于棧上的結(jié)構(gòu)體內(nèi)的__forwarding指針指向堆內(nèi)的Block,從內(nèi)訪問同一個且正確的變量。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容