那些關(guān)于iOS Blocks的坑(一)


  • 很多時候,我們都只關(guān)心怎么把一些語法用得非常熟練,記住所有的坑點,比如說Blocks,什么循環(huán)引用的坑,屬性聲明用copy...等等。但是我們總是懶于去深究,為什么會有這些問題。源碼面前,沒有秘密。今天就一起來分析下關(guān)于Blocks的底層。

這個專題主要介紹Blocks的底層實現(xiàn),分為以下幾個部分,將通過多篇博文一一闡述。
1.Blocks的本質(zhì)
2.Blocks為什么截取自動變量值
3.__block說明符
4.關(guān)于Blocks的存儲
5.關(guān)于__block變量的存儲
6.Blocks的循環(huán)引用問題


Blocks的本質(zhì)

  • Blocks究竟是什么?我們先用clang(LLVM編譯器)將我們的OC代碼轉(zhuǎn)化C++源代碼,所謂源碼面前,了無秘密。
//block.m
int main() {
    int count = 10;
    void (^blk)(void) = ^{
        printf("%d\n", count);
    };
    blk();
    return 0;
}

<strong>clang -rewrite-objc 源代碼文件名</strong>

#define BLOCK_IMPL
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;
    int count;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    printf("%d\\n", count);
}

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 count = 10;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

以上便是那份OC代碼block.m文件編譯出來摘取的關(guān)鍵代碼。幾行代碼瞬間變成了幾十行代碼??粗車樔耍鋵嵅浑y分析。

先來看main函數(shù)中的調(diào)用聲明和調(diào)用block的代碼,轉(zhuǎn)化成了什么。
int main() {
    int count = 10;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

  • int count = 10; 局部變量count,賦值為10;
  • void (*blk)(void), 這個東西有C語言函數(shù)指針基礎(chǔ)的童鞋肯定一眼就認(rèn)出來。其實void (^blk)(void) 就是轉(zhuǎn)化為指針名字為blk的函數(shù)指針。我們對block賦值,實則是對該函數(shù)指針賦值。
  • 其次我們對void (*blk)(void)函數(shù)指針進(jìn)行賦值,賦值對象為結(jié)構(gòu)體struct __main_block_impl_0,<strong>通過它的構(gòu)造方法對相應(yīng)的參數(shù)進(jìn)行賦值。</strong>
    • impl.isa = &_NSConcreteStackBlock;isa指針其實是指向其父類class_t的地址,后面會有討論。
    • impl.FuncPtr = fp; 這一句很關(guān)鍵,該FuncPtr是指向調(diào)用方法的指針,也就是我們執(zhí)行block表達(dá)式時,去調(diào)用的方法,這里傳的參數(shù)是方法__main_block_func_0;
  • 最后一句代碼就是通過調(diào)用__block_impl指針中的FuncPtr指向的方法去執(zhí)行block;
__cself參數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    printf("%d\\n", count);
}
  • 執(zhí)行blk()調(diào)用的函數(shù)中,參數(shù)__cself類型便是上面我們分析的這個結(jié)構(gòu)體,大家肯定一下子就明白了,這是參數(shù)相當(dāng)于self, 在C++中相當(dāng)于this指針。即是當(dāng)前類的對象。
  • <strong>__main_block_impl_0</strong>結(jié)構(gòu)體中包含以下這些成員變量。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int count;
}
  • <strong>struct __block_impl</strong>

結(jié)構(gòu)體中的第一個參數(shù)所對應(yīng)的結(jié)構(gòu)體 struct __block_impl,根據(jù)名稱可以聯(lián)想到某些標(biāo)志,今后版本升級所需的區(qū)域,以及函數(shù)指針。

struct __block_impl {
    void *isa;  
    int Flags;        //標(biāo)志
    int Reserved;     //今后版本升級所需的區(qū)域
    void *FuncPtr;    //函數(shù)指針
};
  • <strong>struct __main_block_desc_0* Desc</strong>
static struct __main_block_desc_0 {
    size_t reserved;      //今后版本升級所需的區(qū)域
    size_t Block_size;    //Block的大小
} 
  • <strong>__main_block_impl_0結(jié)構(gòu)體中最后一個成員變量count</strong>
    不難發(fā)現(xiàn),這是個類成員變量,是存在于block內(nèi)部的。這也是為什么block會截取局部變量的原因,它是從外部定義的變量count中,通過值傳遞拷貝到block內(nèi)部的。所以也就導(dǎo)致,在外部修改了count值,是沒辦法在block中也對應(yīng)修改的原因。接下來會詳細(xì)介紹。
__main_block_func_0函數(shù)
int count = __cself->count; // bound by copy
printf("%d\\n", count);
  • 到這里大家應(yīng)該豁然開朗了,這里打印的count值,是通過__cself指針獲取到block中的那個類成員變量count。以上便是這樣一個block的底層代碼分析。

Blocks為什么截取自動變量值

  • 從上面的分析中,我們知道,count變量在block定義的時候,便作為形式參數(shù),通過__main_block_impl_0構(gòu)造函數(shù)進(jìn)行了值拷貝。
何為值拷貝,何為地址拷貝?
  • 所謂值拷貝,<strong>就是重新開辟一塊內(nèi)存將值拷貝存進(jìn)這塊新的內(nèi)存中。它和被拷貝的值所在的地址是不同的。</strong>
  • 所謂地址拷貝,<strong>就是開辟一個指針的內(nèi)存,將指針的指向賦值為被拷貝的值所在的那塊內(nèi)存。</strong>
  • 通過值拷貝,無法通過修改被拷貝的值進(jìn)而修改拷貝的那個值,這也造成了所謂的<strong>Blocks截取自動變量值</strong>的效果。而通過地址拷貝可以做到,這也是接下來__block將要做的事情。

__block關(guān)鍵字

  • 前面一個例子中,如果我們在Block中進(jìn)行局部變量的修改,那么編譯器就不樂意了。
//block.m
int main() {
    int count = 10;
    void (^blk)(void) = ^{
        count = 11;//編譯錯誤
    };
    blk();
    return 0;
}
  • 產(chǎn)生以下編譯錯誤
error: variable is not assignable (missing __block type specifier)
        count = 11;
  • 顯然,編譯器是拒絕這么做的,并告訴我們 count變量缺少 <__block類型說明符 >(missing __block type specifier)
解決方案
  • 對于這個問題,有兩種解決方案。
    • 第一種就是編譯器提示我們的,對count加入<strong>__block聲明</strong>
    • 第二種則是將count聲明為<strong>靜態(tài)變量,靜態(tài)全局變量,或者全局變量。</strong>

  • 直接看__block聲明之后,編譯器為我們做了些什么。
//block.m
int main() {
   __block int count = 10; 
   void (^blk)(void) = ^{ 
      count = 11;
   };
   blk();
   return 0;
}

clang -rewrite-obj 編譯文件

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__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_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 11;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->count, 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() {

    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
        (void*)0,
        (__Block_byref_count_0 *)&count, 
        0, 
        sizeof(__Block_byref_count_0), 
        10
    };
    
    void (*blk)(void) = ((void (*)())&__main_block_impl_0(
    (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
  • 每次編譯出來是不是都感覺壓力山大,哈哈。慢慢剖析一下通過__block說明符編譯出來的源碼。
  • 先來看看__block變量count是怎么轉(zhuǎn)化過來的。
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
        (void*)0,
        (__Block_byref_count_0 *)&count, 
        0, 
        sizeof(__Block_byref_count_0), 
        10
    };

  • 通過前面的介紹,我們可以看到,這個過程又轉(zhuǎn)化成了我們相對比較熟悉的結(jié)構(gòu)體了。__block變量如同Blocks一樣變成了__Block_byref_count_0結(jié)構(gòu)體類型的自動變量,即在棧上生成的__Block_byref_count_0的結(jié)構(gòu)體實例。
__Block_byref_count_0
struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

相比起我們一開始編譯出來沒有使用__block說明符聲明的代碼中,多出了我們不太熟悉的一個指針

__forwarding    
//持有指向該實例自身的指針。而我們之所以能在block中修改外部變量的原因就在于此。
//原理就是,通過修改成員變量__forwarding訪問成員變量count。(成員變量count是該實例自身持有的變量,它相當(dāng)于block中的原自動變量)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 11;
}
// 通過以上這段代碼,大家就知道了,__block說明符就是通過修改__forwarding指針中的count變量從而達(dá)到修改外部變量的效果,多數(shù)時候我們可以推理出,能夠修改一個地方達(dá)到修改其他地方的這種場景,無異于對同一塊內(nèi)存上的內(nèi)容進(jìn)行了修改,而能夠達(dá)到這種效果的,便是指針的操作。

至于靜態(tài)變量,靜態(tài)全局變量和全局變量也能達(dá)到這個效果的辦法,請大家自己編譯源碼查看下,其實原理也是通過修改指針來達(dá)到這個效果的。而這些變量的值是存在于__main_block_impl_0結(jié)構(gòu)體中的

關(guān)于Blocks的存儲

  • 從上面編譯出來的代碼中,我們前面提到的isa指針,在初始化Block中,impl.isa = &_NSConcreteStackBlock;所謂結(jié)構(gòu)體類型的自動變量,即棧上生成的該結(jié)構(gòu)體的實例。

  • 以上的Block的類轉(zhuǎn)化為_NSConcreteStackBlock,雖然并沒有出現(xiàn)轉(zhuǎn)化過后對應(yīng)的源代碼,但還有幾個與之類似的類.

    • _NSConcreteStackBlock //將該類的對象Block設(shè)置在棧上
    • _NSConcreteGlobalBlock //與全局變量一樣,設(shè)置在數(shù)據(jù)區(qū)域(.data區(qū))中
    • _NSConcreteMallocBlock //設(shè)置在malloc函數(shù)分配的內(nèi)存塊中(即堆)
分析如何創(chuàng)建對應(yīng)存儲區(qū)的Block
  • _NSConcreteStackBlock : 通常情況下,在方法內(nèi)部手動聲明定義的Block,均為棧上分配的。
  • _NSConcreteGlobalBlock : 將Block聲明在函數(shù)外部,作為全局變量的Block則是分配在數(shù)據(jù)區(qū)域。
  • _NSConcreteMallocBlock :
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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