探尋Block的本質(6)—— __block的深刻分析

Block傳送門🦋🦋🦋

探尋Block的本質(1)—— 基本認識markdown

探尋Block的本質(2)—— 底層結構iphone

探尋Block的本質(3)—— 基礎類型的變量捕獲函數

探尋Block的本質(4)—— Block的類型oop

探尋Block的本質(5)—— 對象類型的變量捕獲post

__block的使用場景

你們應該都知道,若是想在block內部修改從外部捕獲的auto變量的值,能夠在該auto變量定義的時候,加上關鍵字__block。代碼案例以下學習

#import <Foundation/Foundation.h>

typedef void(^CLBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int a = 10;
        int b = 30;
        CLBlock myblock = ^{
            a = 20;
            NSLog(@"%d",b);
        };
        
        myblock();
        
        NSLog(@"myblock執行完以後,a = %d",a);
    }
    
    return 0;
}

*********************運行結果*********************
2019-09-04 19:41:51.709406+0800 Block學習[29867:3904669] 30
2019-09-04 19:41:51.709706+0800 Block學習[29867:3904669] myblock執行完以後,a = 20
複製代碼

__block只能夠用來做用於auto變量,它的目的就是爲了可以讓auto變量可以在block內部內修改。而全局變量和static變量原本就能夠從block內部進行修改,所以__block對它們來講沒有意義,因此__block被規定只能用於修飾auto變量,這一點應該不難理解。spa

__block的本質

老套路,咱們先經過終端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp來看一下__block以及block在底層張什麼樣子。首先看看block的底層結構.net

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp,
                      struct __main_block_desc_0 *desc,
                      int _b,
                      __Block_byref_a_0 *_a,
                      int flags=0) : b(_b), a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

爲了比較,我特地加了一個int b做爲對比,順便回顧一下,基本類型的auto變量被block捕獲的時候,就是經過值拷貝的形式把值賦給block內部相對應的基本類型變量。而案例裏面的__block int a = 10,咱們能夠看到在底層,系統是把int a包裝到了一個叫__Block_byref_a_0的對象裏面。這個對象的結構以下設計

struct __Block_byref_a_0 {
  void *__isa;//有isa,是一個對象
__Block_byref_a_0 *__forwarding;//指向自身類型對象的指針
 int __flags;//不用關心
 int __size;//本身所佔大小
 int a;//被封裝的 基本數據類型變量
};
複製代碼

看看在main函數中__Block_byref_a_0被賦了什麼值3d

//__block int a = 10;
__Block_byref_a_0 a = {(void*)0,
                             &a,
                              0,
      sizeof(__Block_byref_a_0),
                             10
        				};

複製代碼

image

圖中能夠看出來,10被存儲到了block內部__Block_byref_a_0對象的成員變量int a上。__Block_byref_a_0對象裏面的成員變量__forwarding實際上指向了__Block_byref_a_0對象自身。 咱們來看block內的代碼對於a的賦值是如何操做的

image

爲何用a->__forwarding->a,而不是a->a直接拿到int a,經過__forwarding轉一圈有什麼用意?這個等會解答。

這樣__block的底層實現就說完了。

__block的細節

上面,咱們知道了經過 __block int a = 10定義以後,這個a底層是一個__Block_byref_a_0對象,數值10存放在這個對象內部的成員變量int a上面。可是咱們在寫代碼的時候,能夠直接經過__Block_byref_a_0對象a來賦值,那麼在block定義初始化結束,完成變量捕獲以後,oc代碼中再次經過a訪問到的究竟是什麼呢?例以下面

image

咱們先來看一份代碼案例

**********************testVC.m**********************
#import "testVC.h"

@implementation testVC typedef void(^CLBlock)(void);

struct __Block_byref_a_0 {
    void *__isa;
    struct __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

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;
    struct __Block_byref_a_0 *a; // by ref
    
};




 struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
     void *copy;
     void *dispose;
 };



- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    [self blockTest];
    
}

- (void)blockTest {
    __block int a = 10;
    
    CLBlock myblock = ^{
        a = 20;
        NSLog(@"此時在在myblock內部的oc代碼裏直接經過a訪問的內存空間是:%p",&a);
    };
    
    struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)myblock;
    NSLog(@"myBlock通過初始化,完成變量捕獲以後,其內部的[__Block_byref_a_0 *] a = %p",tmpBlock->a);
    NSLog(@"此時在在myblock外部的oc代碼裏直接經過a訪問的內存空間是:%p",&a);
    
    myblock();
}

@end

*************************運行結果************************
2019-09-04 21:28:07.189104+0800 BT[30733:3968805] myBlock捕獲完變量以後,[__Block_byref_a_0 *] a = 0x7ffeede8f840
2019-09-04 21:28:07.189221+0800 BT[30733:3968805] 此時在在block外部的oc代碼裏直接經過a訪問的內存空間是:0x7ffeede8f858
2019-09-04 21:28:07.189296+0800 BT[30733:3968805] 此時在在block內部的oc代碼裏直接經過a訪問的內存空間是:0x7ffeede8f858
複製代碼

從打印咱們看到,myblock內部的[__Block_byref_a_0 *] a指向的地址是0x7ffeede8f840,以後咱們在任意地方經過a訪問的內存地址是0x7ffeede8f858,十六進制下它們地址相差了0x18,也就是十進制下的24個字節。

image

從示意圖能夠看出,經過[__Block_byref_a_0 *] a的地址往高地址走24個字節,正好是它內部封裝的那個int a。也就是說咱們在oc代碼裏面完成了myblock的初始化以及 __block變量的捕獲以後,只能經過a訪問到被封裝在 __ Block_byref_a_0 * 內部的這個int a的內存空間。

蘋果這麼作的意圖我猜想是想向開發者隱藏__ Block_byref_a_0 *的存在,但願開發者把__block int a就當成一個普通的int a來看待。蘋果嗎,老是這麼小家子氣,能夠理解。(此處純屬自我發揮,還待大牛給出正解:)

__block的內存管理

咱們知道,若是block捕獲一個基礎類型的auto變量,是不用考慮內存管理的。可是__block的本質做用,是將所修飾的對象包裝成一個__ Block_byref_xx_x *,而後進行捕獲,而__ Block_byref_xx_x *本質上也是一個對象,所以確定須要處理它的內存管理問題。

咱們已經知道,若是一個block位於棧空間上,那麼是不須要考慮被它所捕獲的對象類型的auto變量的內存管理問題的。所謂的內存管理,是針對建立在堆空間上的oc對象而言的,由於咱們做爲開發者,只可以管理堆上的空間。棧空間的內存是由系統管理的,不用咱們操心。

關於內存管理問題這裏,咱們所討論的問題須要考慮三個關鍵因素:__block__weak對象變量基本類型變量,他們合法的組合有以下幾種:

  1. 基本類型變量
  2. 對象變量
  3. __weak + 對象變量
  4. __block + 基本類型變量
  5. __block + 對象變量
  6. __block + __weak + 對象變量

我在【對象類型的auto變量捕獲】這一篇裏面詳細分析了一個對象類型的auto變量被block捕獲時的內存管理過程,上面的一、二、3這三種場景已經獲得了說明。下面咱們來分析一下四、五、6這三種場景。

4 --- __block + 基本類型變量

首先代碼上一份

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            __block int a = 10;
            myblock = ^ {
                a = 20;
            };
        }
        
        myblock();
    }
    return 0;
}
複製代碼

編譯以後block相關的底層結構以下

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref 
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
		{
		_Block_object_assign(
							 (void*)&dst->a, 
							 (void*)src->a, 
							 8/*BLOCK_FIELD_IS_BYREF*/♥️♥️♥️♥️♥️♥️
							);
		}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
		{
		_Block_object_dispose(
								(void*)src->a, 
								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*);
}
複製代碼

咱們知道__block int a = 10;這句代碼的做用,是將int a包裝在struct __Block_byref_a_0內部,這樣block實際上捕獲的是這個struct __Block_byref_a_0,它能夠被看成一個對象來看待,因此內存管理上面,最終仍然是經過_Block_object_assign_Block_object_dispose這兩個函數來處理,可是能夠看到這兩個函數的最後一個參數是8(對於對象類型的捕獲,傳遞的參數是3),這個參數代表了即將要處理的是一個struct __Block_byref_a_0,由於它是沒有__weak__strong標記的,因此處理方式很簡單,就是copy到堆上的時候,同時須要進行retain,dispose的時候同時須要進行release



5 --- __block + 對象變量

上代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            CLPerson *person = [[CLPerson alloc] init];
            __block CLPerson *blockPerson = person;
            myblock = ^ {
                blockPerson.age = 10;
            };
        }
        
        myblock();
    }
    return 0;
}
複製代碼

編譯以後, __block CLPerson *blockPerson的底層結構以下

struct __Block_byref_blockPerson_0 {
  void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 CLPerson *__strong blockPerson;
};
複製代碼

從這個結構能夠看出兩點變化:

  • 相比較基本類型變量,對象類型的變量被__block修飾後,底層所生成的__Block_byref_xxx_x結構體裏面多了兩個函數指針,__Block_byref_id_object_copy__Block_byref_id_object_dispose
  • 對象類型的變量被封裝到__Block_byref_xxx_x內部之後,默認是被__strong修飾的。

上面發現的兩個新函數指針__Block_byref_id_object_copy__Block_byref_id_object_dispose就是當__Block_byref_xxx_x被拷貝到堆空間的時候,以及將要被系統釋放的時候調用的。咱們能夠在main函數裏面找到它們的最終賦值,分別是__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131,它們的定義以下

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
複製代碼

咱們考到,其實最終仍是調用了_Block_object_assign_Block_object_assign這兩個函數,二從參數能夠看出,它們所要處理的對象就是__Block_byref_id_object_dispose內部所封裝的對象類型變量,也就是咱們代碼中的CLPerson *blockPerson,由於默認blockPerson是被__strong修飾的,因此接下來對於blockPerson的內存管理方式就和咱們以前所分析過的是同樣的。



6 --- __block + __weak + 對象變量

上代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            CLPerson *person = [[CLPerson alloc] init];
            __block __weak CLPerson *weakBlockPerson = person;
            myblock = ^ {
                weakBlockPerson.age = 10;
            };
        }
        
        myblock();
    }
    return 0;
}
複製代碼

這裏就直接給出編譯以後的底層結構struct __Block_byref_xxx_x來進行對比

struct __Block_byref_weakBlockPerson_0 {
  void *__isa;
__Block_byref_weakBlockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 CLPerson *__weak weakBlockPerson;
};
複製代碼

由於咱們顯式地給對象變量加上了__weak,所以struct __Block_byref_xxx_x內部封裝的就是一個指向對象的弱指針CLPerson *__weak weakBlockPerson。根據上面的分析,最後一樣進入到_Block_object_assign_Block_object_assign這兩個函數進行處理,處理方式再也不贅述。

最後在經過圖例在梳理一下

__block + 基本類型變量

__block + 對象類型變量

__block + __weak + 對象類型變量

最後再來解決那個咱們中篇遺留的問題:__forwarding的做用 從上圖能夠很清晰的看出,當__Block_byref_xxx_x(假設爲A)從棧空間被拷貝到堆空間(假設堆上的那一份爲B)的時候,棧上A__forwarding指針會被指向堆空間上的B,而B自己的__forwarding仍然指向B本身,由於在底層訪問__Block_byref_xxx_x所封裝的目標變量,是經過__Block_byref_xxx_x->__forwarding->目標變量,這樣,不管咱們訪問入口對象__Block_byref_xxx_x是在棧上仍是在堆上,都能保證最終訪問到的目標變量是堆空間上的那一份。這樣的設計就正好契合了堆空間上的__Block_byref_xxx_x對象存在的目的。

到此,關於__block的底層實現就分析到這裏。

Block傳送門🦋🦋🦋

探尋Block的本質(1)—— 基本認識

探尋Block的本質(2)—— 底層結構

探尋Block的本質(3)—— 基礎類型的變量捕獲

探尋Block的本質(4)—— Block的類型

探尋Block的本質(5)—— 對象類型的變量捕獲

相關文章
相關標籤/搜索