Objective-C基礎之四(深入理解Block)

Andres工坊發表於2020-01-06

什麼是Block

Block其實就是一個程式碼塊,通常被稱為“閉包”,它封裝了函式呼叫以及函式呼叫環境,以便在合適的時機進行呼叫,在OC中,Block其實就是一個OC物件,它可以當做引數傳遞。

Block的結構如下:

Objective-C基礎之四(深入理解Block)

Block的本質

無外部變數訪問時Block的底層結構

  • 首先,建立一個Demo,在main.m中加入如下程式碼:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^test)(void) = ^{
            NSLog(@"Block");
        };
        test();
    }
    return 0;
}
複製程式碼
  • 然後通過xcrun指令將main.m檔案轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
複製程式碼
  • 檢視生成的main.cpp檔案,首先看main函式,轉換成C++之後,結構如下,此處去除了多餘的強轉操作,方便閱讀
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        //Block的定義
        void(*test)(void) = &__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA
                                                  );
        //block的呼叫
        test->FuncPtr(test);
    }
    return 0;
}
複製程式碼
  • Block在編譯完成之後,轉換成了__main_block_impl_0型別的結構體,它的內部結構如下
struct __main_block_impl_0 {
  //存放了block的一些基本資訊,包括isa,函式地址等等
  struct __block_impl impl; 
  //存放block的一些描述資訊
  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;
  }
};
複製程式碼

由於結構體__block_impl是直接存放在__main_block_impl_0結構體的內部,所以__main_block_impl_0結構體也可以轉換成如下形式

struct __block_impl {
  void *isa;    //isa指標,可以看出Block其實就是一個OC物件
  int Flags;    //標識,預設為0
  int Reserved; //保留欄位
  void *FuncPtr;//函式記憶體地址
};

struct __main_block_impl_0 {
  void *isa;    
  int Flags;    
  int Reserved; 
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
};
複製程式碼

block將我們所要呼叫的程式碼封裝成了函式__main_block_func_0,並且將函式__main_block_func_0的記憶體地址儲存在到void *FuncPtr中,具體函式如下

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //此處就是呼叫的NSLog
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_4f0065_mi_0);
}
複製程式碼

結構體__main_block_desc_0中則儲存了block所佔用記憶體大小等描述資訊

static struct __main_block_desc_0 {
  size_t reserved;      //保留欄位
  size_t Block_size;    //__main_block_impl_0結構體所佔記憶體大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製程式碼
  • 由此可以看出block底層其實就是一個OC物件,因為它內部擁有isa指標。同時block將內部所要執行的程式碼封裝成了函式,並且將函式記憶體地址儲存到結構體當中,以便在合適的時機進行呼叫

訪問外部變數時Block的底層結構

我們在使用Block的過程中,可以在Block內部訪問外部的變數,包含區域性變數、靜態變數(相當於私有的全域性變數)、全域性變數等等。現在就通過一個Demo來看一下block底層是如何訪問外部變數的。

  • 首先建立Demo,在main.m檔案中新增如下程式碼
//定義全域性變數c
int c = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //區域性變數a
        int a = 10;
        //靜態變數b
        static int b = 20;
        void(^test)(void) = ^{
            NSLog(@"Block - %d, %d, %d", a, b, c);
        };
        test();
    }
    return 0;
}
複製程式碼
  • 將main.m轉換成C++程式碼後,再次檢視main函式,結果如下
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;
        static int b = 20;
        void(*test)(void) = (&__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA,
                                                  a,
                                                  &b));
        test->FuncPtr(test);
    }
    return 0;
}
複製程式碼

可以看出,此時__main_block_impl_0結構體中多了兩個引數,分別是區域性變數a的值,靜態變數b的指標,也就是它的記憶體地址。

  • 檢視__main_block_impl_0結構體的記憶體結構
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

發現,在__main_block_impl_0結構體中多了兩個成員變數,一個是int a,一個是int *b

  • 當通過test->FuncPtr(test)執行block時,會通過結構體中的FuncPtr找到函式__main_block_func_0的地址進行呼叫,檢視__main_block_func_0函式如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int *b = __cself->b; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_064cd6_mi_0, a, (*b), c);
}
複製程式碼

__main_block_func_0函式中,訪問區域性變數a和靜態變數b時都是通過傳遞過來的__main_block_impl_0結構體拿到對應的成員變數進行訪問,但是全域性變數c並沒有存放在結構體中,而是直接進行訪問。

  • 由此我們就可以得出結論,block中有變數捕獲的機制
    • 當訪問區域性變數的時候,會將區域性變數的值捕獲到block中,存放在一個同名的成員變數中。
    • 當訪問靜態變數時,會將靜態變數的地址捕獲到block中,存放在一個同名的成員變數中。
    • 當訪問全域性變數時,因為全域性變數是一直存在,不會銷燬,所以在block中直接訪問全域性變數,不需要進行捕獲

此處需要注意的是,其實在OC中有個預設的關鍵字auto,在我們建立區域性變數的時候,會預設在區域性變數前加上auto關鍵字進行修飾,例如上文中的int a,其實就相當於auto int a。auto關鍵字的含義就是它所修飾的變數會自動釋放,也表示著它所修飾的變數會存放到棧空間,系統會自動對其進行釋放。

block總結

block底層結構總結

block在編譯完成之後會轉換成結構體進行儲存,結構體中的成員變數如下,其中在成員變數descriptor指向的結構體中,多了兩個函式指標分別為copydispose,這兩個函式和block內部物件的記憶體管理有關,後面會具體說明。

Objective-C基礎之四(深入理解Block)

block變數捕獲總結

block使用變數捕獲機制來保證在block內部能夠正常的訪問外部變數。

Objective-C基礎之四(深入理解Block)

  • 當block訪問的是auto型別的區域性變數時,會將區域性變數捕獲到block內部的結構體中,並且是直接捕獲變數的值。
  • 當block訪問的是static型別的靜態變數時,會將靜態變數捕獲到block內部的結構體中,並且捕獲的是靜態變數的地址。
  • 當block訪問的是全域性變數時,不會進行捕獲,直接進行訪問。

block的型別

block的三種型別

在OC當中block其實擁有三種型別,可以通過class或者isa指標來檢視block具體的型別

  • 首先在main.m中新增以下示例程式碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //第一種型別NSGlobalBlock
        NSLog(@"%@",[^{
            NSLog(@"NSGlobalBlock");
        } class]);
        
        //第二種型別NSStackBlock
        int a = 10;
        NSLog(@"%@",[^{
            NSLog(@"%d", a);
        } class]);
        
        //第三種型別NSMallocBlock - 1
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
        NSLog(@"%@",[test2 class]);
        
        //第三種型別NSMallocBlock - 2
        NSLog(@"%@",[[^{
            NSLog(@"%d", a);
        } copy] class]);
    }
    return 0;
}
複製程式碼

執行結果如下:

Objective-C基礎之四(深入理解Block)

  • 執行後可以發現,block可以有三種型別,分別是NSGlobalBlockNSStackBlockNSMallocBlock,這三種型別分別存放在.data區、棧區堆區。對應的結構圖如下

Objective-C基礎之四(深入理解Block)

圖中的block型別和上文中列印出來的block型別對應關係如下

class方法返回型別 isa指向型別
NSGlobalBlock _NSConcreteGlobalBlock
NSStackBlock _NSConcreteStackBlock
NSMallocBlock _NSConcreteMallocBlock

但是不管它是哪種block型別,最終都是繼承自NSBlock型別,而NSBlock繼承自NSObject,所以這也說明了block本身就是一個物件。

如何區分block型別

在上述示例中,提到了四種生成不同型別的block的方法,分別如下:

  1. 沒有訪問區域性變數的block,並且沒有強指標指向block,則此block為NSGlobalBlock
  2. 訪問了區域性變數的block,但是沒有強指標指向block,則此block為NSStackBlock
  3. 訪問了區域性變數的block,並且有強指標指向block,則此block為NSMallocBlock
  4. NSStackBlock型別的block,執行了copy操作之後,生成的block為NSMallocBlock

其實第三點和第四點生成的都是NSMallocBlock,由此我們就可以得到下面的結論

block的型別 block執行的操作
NSGlobalBlock 沒有訪問auto型別的變數
NSStackBlock 訪問了auto型別的變數
NSMallocBlock __NSStackBlock__型別的block執行了copy操作

block的copy操作

block執行copy操作後的記憶體變化

NSGlobalBlockNSStackBlockNSMallocBlock三種型別的block分別存放在了資料區、棧區和堆區。將三種型別的block分別進行copy操作之後,產生的結果如下:

  • NSGlobalBlock的block進行copy操作,什麼也不會發生,生成的還是NSGlobalBlock型別的block
  • NSStackBlock型別的block進行操作,會將block從棧上覆制一份到堆中,生成NSMallocBlock型別的block
  • NSMallocBlock型別的block進行copy操作,此block的引用計數會加1

結構圖如下

Objective-C基礎之四(深入理解Block)

ARC環境下哪些操作會自動進行copy操作?

在上述示例中,NSStackBlock型別的block,執行了copy操作之後,生成的block為NSMallocBlock,其實不止這一種方式生成NSMallocBlock,以下是OC中在ARC環境下自動觸發copy操作的幾種情況:

  1. block作為返回值時,會自動進行copy
typedef void(^block)(void);
block test(){
    return ^{
        NSLog(@"NSMallocBlock");
    };
}
複製程式碼
  1. 使用__strong型別的指標指向block時,會執行copy操作
void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
複製程式碼
  1. block作為Cocoa API中含有usingBlock的方法的引數時,會執行copy操作
NSArray *arr = @[@"1",@"2"];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
     NSLog(@"NSMallocBlock");
}];
複製程式碼
  1. block作為GCD方法的引數時會執行copy操作
dispatch_async(dispatch_get_main_queue(), ^{
     NSLog(@"NSMallocBlock");
});
複製程式碼

我們平常在使用block作為屬性的時候,都會使用copy修飾符來修飾,其實內部就是對block進行了一次copy操作,將block拷貝到堆上,以便我們手動管理block的記憶體

block訪問物件型別

訪問物件型別的auto變數時,block的底層結構

上文中Block訪問的外部變數都是基本資料型別,所以不涉及到記憶體管理,如果在block中訪問外部物件時,block內部又是什麼樣的結構呢?

  • 首先在main.m中加入以下示例程式碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //預設物件
        NSObject *obj1 = [[NSObject alloc] init];
        void(^test1)(void) = ^{
            NSLog(@"NSMallocBlock - %@", obj1);
        };
        test1();
        
        //使用__weak指標修飾物件
        NSObject *obj2 = [[NSObject alloc] init];
        __weak typeof(obj2) weakObj = obj2;
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %@", weakObj);
        };
        test2();
    }
    return 0;
}
複製程式碼
  • 使用如下指令將main.m檔案轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製程式碼

此處由於使用了__weak關鍵字來修飾物件,涉及到runtime,所有需要指定runtime的版本。

  • 轉換成main.cpp檔案後,檢視block的底層結構為
//直接訪問外部物件的block內部結構
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //生成strong型別的指標
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//訪問__weak修飾符修飾的外部物件的block內部結構
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  //自動生成weak型別的指標
  NSObject *__weak weakObj;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

複製程式碼

這時發現,如果直接在block中訪問外部的auto型別的物件,預設是在block結構體中生成一個strong型別的指標指向外部物件,如結構體__main_block_impl_0。如果在block中訪問了__weak修飾符修飾的外部物件,那麼在它的內部會生成一個weak型別的指標指向外部物件,如結構體__main_block_impl_1

__main_block_impl_0的建構函式中,obj(_obj)就代表著,以後建構函式傳過來的_obj引數會自動賦值給結構體中的成員變數obj。

  • 由於__main_block_desc_0__main_block_desc_1結構相同,所以以下只以__main_block_desc_0為例,檢視__main_block_desc_0結構體,會發現它內部新增加了兩個函式指標,如下
static struct __main_block_desc_0 {
  size_t reserved;  //保留欄位
  size_t Block_size; //整個block所佔記憶體空間
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);   //copy函式
  void (*dispose)(struct __main_block_impl_0*); //dispose函式
} __main_block_desc_0_DATA = { 0,
                               sizeof(struct __main_block_impl_0),
                               __main_block_copy_0,
                               __main_block_dispose_0};
複製程式碼

新增加了copy和dispose兩個函式指標,對應著函式__main_block_copy_0__main_block_dispose_0,如下

//copy指標指向的函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
//dispose指標指向的函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

複製程式碼

之前說過,block封裝了函式呼叫和函式呼叫環境,這也就意味這如果它引用了外部的物件,就需要對外部物件進行記憶體管理操作。__main_block_copy_0函式內部會呼叫_Block_object_assign函式,它的主要作用是根據外部引用的物件的修飾符來進行相應的操作,如果外部物件是使用__strong來修飾,那麼_Block_object_assign函式會對此物件進行一次類似retain的操作,使得外部物件的引用計數+1。

__main_block_dispose_0函式內部會呼叫_Block_object_dispose函式,它的作用就是在block內部函式執行完成之後對block內部引用的外部物件進行一次release操作。

總結

Block在棧上

如果block在棧上,那麼在block中訪問物件型別的auto變數時,是不會對auto變數產生強引用的。這個需要在MRC情況下進行測試,將Xcode中Build Settings下的Automatic Reference Counting設定成NO,表明當前使用MRC環境。

  • 首先建立XLPerson類,重寫dealloc方法,方便測試
@implementation XLPerson

- (void)dealloc{
    [super dealloc];
    NSLog(@"%s", __func__);
}

@end
複製程式碼
  • 在main.m中增加如下測試程式碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //建立block
        TestBlock block;
        {
            XLPerson *person = [[XLPerson alloc] init];
            block = ^{
                NSLog(@"block --- %p", &person);
            };
            NSLog(@"%@", [block class]);
            [person release];
        }
        NSLog(@"block執行前");
        block();
        [block release];
        NSLog(@"block執行後");
        
    }
    return 0;
}
複製程式碼
  • 執行程式,得到如下的列印資訊

Objective-C基礎之四(深入理解Block)

可以發現,在MRC環境下,即使是有強指標指向block,系統也不會對block進行預設的copy操作,所以當前的block型別依舊為NSStackBlock型別。而且,在block執行之前,XLPerson就已經釋放了,說明在棧上的block並沒有對person物件進行強引用。

block被copy堆上

  • 首先,如果block被copy到了堆上,在訪問auto修飾的物件變數時,內部會自動呼叫copy函式,它內部會呼叫_Block_object_assign函式,_Block_object_assign函式會根據auto變數的修飾符(__strong、__weak、__unsafe_unretained)做出相應的處理,如果是__strong修飾,則內部對外部的物件形成強引用,如果是__weak或者__unsafe_unretained,則會形成弱引用
  • 如果block執行完成,被系統從堆中移除時,會呼叫dispose函式,它內部呼叫_Block_object_dispose函式,_Block_object_dispose函式會自動釋放引用的auto變數,也就是對引用的auto變數進行一次release操作。
  • copy和dispose函式呼叫時機如下
函式 呼叫時機
copy 棧上的block被複制到堆上
dispose 堆上的block被釋放時

__block

__block的作用

使用block時,如果block中訪問到了外部被auto修飾的變數,我們經常使用到__block來修飾外部變數,它的主要作用就是能夠讓我們在block內部來修改外部變數的值,當然,block只能用來修飾auto變數,不能用來修飾全域性變數和靜態變數。

  • 首先來建立Demo,檢視原始碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block XLPerson *person = [[XLPerson alloc] init];
        __block int a = 10;
        TestBlock block = ^{
            person = nil;
            a = 20;
            NSLog(@"block -- a:%d, person:%@",a,person);
        };
        block();
        NSLog(@"block呼叫後,a:%d, person:%@",a,person);
    }
    return 0;
}
複製程式碼
  • 通過以下指令轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製程式碼
  • 首先檢視block結構體的原始碼,發現block內部多了兩個指標,__Block_byref_person_0型別的指標person和__Block_byref_a_1型別的指標a
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
複製程式碼
  • 再檢視main函式中,區域性變數和block的建立方式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        //封裝person物件
       __Block_byref_person_0 person = {
            0,      //
            &person,
            33554432,
            sizeof(__Block_byref_person_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            objc_msgSend(objc_msgSend(objc_getClass("XLPerson"), sel_registerName("alloc")), sel_registerName("init"))};
            
        //封裝變數a
        __Block_byref_a_1 a = {
            0,
            (__Block_byref_a_1 *)&a,
            0,
            sizeof(__Block_byref_a_1),
            10
        };
        
        //建立block
        TestBlock block = (&__main_block_impl_0(
                                                __main_block_func_0,
                                                &__main_block_desc_0_DATA,
                                                &person,
                                                &a,
                                                570425344));
        block->FuncPtr(block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_115560_mi_1,(a.__forwarding->a),(person.__forwarding->person));
    }
    return 0;
}
複製程式碼

__block修飾物件型別auto變數

通過__block修飾的person物件在編譯後被封裝成了__Block_byref_person_0型別的結構體,內部有多個成員變數,如下

#將person物件封裝成結構體__Block_byref_person_0
struct __Block_byref_person_0 {
  void *__isa;                                      //isa指標
__Block_byref_person_0 *__forwarding;               //forwarding指標
 int __flags;                                       //標識位
 int __size;                                        //結構體所佔記憶體大小
 void (*__Block_byref_id_object_copy)(void*, void*);//函式指標指向copy函式
 void (*__Block_byref_id_object_dispose)(void*);    //函式指標指向dispose函式
 XLPerson *__strong person;                         //強引用XLPerson的例項物件
};


//__Block_byref_person_0結構體的建立與賦值
__Block_byref_person_0 person = {
    0,                                  //對應isa指標,傳0
    &person,                            //對應forwarding指標,將結構體自身的地址傳給了forwarding指標
    33554432,                           //對應flags
    sizeof(__Block_byref_person_0),     //當前結構體所需記憶體大小
    __Block_byref_id_object_copy_131,   //copy函式
    __Block_byref_id_object_dispose_131,//dispose函式
    objc_msgSend(objc_msgSend(objc_getClass("XLPerson"),    
                              sel_registerName("alloc")), 
                              sel_registerName("init"))     //通過objc_msgSend建立XLPerson物件,並且將物件的指標傳入結構體中
};
複製程式碼

可以明顯看出,在結構體__Block_byref_person_0中,存在如下成員變數

  • isa指標,此處賦值為0,同時也能說明此結構體也是一個OC物件
  • forwarding指標,指向結構體自身的記憶體地址
  • flags,標誌位,此處傳33554432
  • size,結構體大小,通過sizeof(__Block_byref_person_0)獲得,此處可以簡單計算出結構體所需記憶體大小為48個位元組
  • __Block_byref_id_object_copy,copy函式,因為在結構體中引用到了person物件,所以呼叫此方法來根據person指標的引用型別決定是否對person物件進行retain操作,此處person物件是使用__strong來修飾,所以copy函式的作用就是對person物件進行一次retain操作,引用計數+1。
  • __Block_byref_id_object_dispose_131,dispose函式,在結構體從記憶體中移除的時候,會呼叫dispose函式,對person物件進行一次release操作,引用計數-1
  • person指標,因為我們外部建立的是XLPerson的例項物件,所以結構體內部直接儲存了person指標來指向我們建立的XLPerson物件。

前文提到過,因為block封裝了函式呼叫環境,所以一旦它內部引用了外部的auto物件,就需要對外部物件的記憶體進行管理,所以才有了copy函式和dispose函式。此處也一樣,因為使用__block修飾的XLPerson物件的指標存放在了結構體內部,所以需要使用copy函式和dispose函式來管理物件的記憶體。

__block修飾基本資料型別auto變數

如果使用__block來修飾基本資料型別的auto變數,就會將變數封裝成__Block_byref_a_1型別的結構體,內部結構如下

#將變數a封裝成結構體__Block_byref_a_1
struct __Block_byref_a_1 {
  void *__isa;                  //isa指標
__Block_byref_a_1 *__forwarding;//forwarding指標
 int __flags;                   //標識位
 int __size;                    //結構體大小
 int a;                         //變數a
};

//封裝變數a
__Block_byref_a_1 a = {
    0,                          //isa,傳0
    (__Block_byref_a_1 *)&a,    //傳入當前結構體a的地址
    0,                          //flags
    sizeof(__Block_byref_a_1),  //結構體的大小
    10                          //外部變數a的值
};
複製程式碼

相對於__block修飾auto物件,如果修飾基本資料型別,則結構體中少了copy函式和dispose函式,因為基本資料型別不需要進行記憶體管理,所以不需要呼叫這兩個函式。

  • isa指標,此處傳0
  • forwarding指標,指向結構體自身的記憶體地址
  • flags,此處傳0
  • size,結構體的大小,使用sizeof(__Block_byref_a_1)來獲取,此處結構體佔用記憶體大小為32個位元組(結構體本身需要的記憶體大小為28個位元組)
  • a,儲存了外部變數a的值,此處為10

總結

  • __block可以用來解決在block內部無法修改auto變數值的問題。
  • __block只能用來修飾auto型別變數,無法用來修飾全域性變數、靜態變數等等
  • 使用__block修飾的auto變數,編譯器會將此變數封裝成一個結構體(其實也是一個物件),結構體內部有以下幾個成員變數
    • isa指標
    • forwarding指標,指向自身記憶體地址
    • flags
    • size,結構體的大小
    • val(使用的外部變數,如果是基本資料型別,就是變數的值,如果是物件型別,就是指向物件的指標)
  • __block修飾基本資料型別的auto變數,例如__block int a,那麼封裝的結構體內部成員變數如上,如果是修飾物件型別的auto變數,如__block XLPerson *person,那麼生成的結構體中會多出copy和dispose兩個函式,用來管理person物件的記憶體。

__block的記憶體管理

當block訪問外部__block修飾的auto變數時,會將變數封裝成結構體,並且將結構體的地址值存放在block內部

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
複製程式碼

其中persona就是指向兩個__block結構體的指標,正因為在block中有引用到__Block_byref_person_0__Block_byref_a_1,那麼block就必須對這兩個結構體的記憶體進行管理,所以相應的在__main_block_desc_0中就生成了兩個函式copy和dispose,專門用來管理persona所指向的結構體(也是物件)的記憶體。如下

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*);  //copy函式
  void (*dispose)(struct __main_block_impl_0*);                            //dispose函式
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,            //copy函式
    __main_block_dispose_0          //dispose函式
};
複製程式碼

相應的copy函式和dispose函式如下

//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->person,src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

這裡和上文中說到的block中訪問外部物件的記憶體管理相同

  • 當block在棧上的時候,block內部並不會對__block變數產生強引用,在上文中已經使用demo驗證過
  • 當block被copy到堆上時,首先會呼叫copy函式,在copy函式內部會呼叫_Block_object_assign函式來對__block變數形成強引用。這裡和之前說到的block訪問外部auto物件有點不同,如果block訪問外部物件,_Block_object_assign會根據外部物件的修飾符是否是__strong還是__weak來決定是否對物件形成強引用,但是如果是訪問__block變數,block就一定會對__block變數形成強引用。

Objective-C基礎之四(深入理解Block)

當圖中的Block0被賦值到堆上時,會將他所引用的__block變數一起賦值到堆上,並且對堆上的__block變數產生強引用

Objective-C基礎之四(深入理解Block)

當圖中的Block1被複制到堆上時,因為之前__block變數已經被複制到了堆上,所以Block1只是對堆上的__block變數產生強引用。

  • 當block從堆中移除時,會呼叫block內部的dispose函式,dispose函式內部又會呼叫_Block_object_dispose函式來自動釋放引用的__block變數,相當於對__block變數執行一次release操作。

Objective-C基礎之四(深入理解Block)

當Block0和Block1都被廢棄時,Block0和Block1對__block變數的引用會被釋放,所以__block變數最終因為沒有持有者而被廢棄

__block中的__forwarding指標

__block修飾的auto變數所對應的結構體如下

Objective-C基礎之四(深入理解Block)

在結構體中有一個__forwarding指標指向自己,在後續訪問__block變數的時候也是通過__forwarding指標來進行訪問

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_1 *a = __cself->a; // bound by ref
  __Block_byref_person_0 *person = __cself->person; // bound by ref

  (a->__forwarding->a) = 20;        //通過__forwarding指標來拿到a進行修改
  (person->__forwarding->person) = __null; //通過__forwarding指標來拿到person進行修改
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_fbc4b7_mi_0,(a->__forwarding->a),(person->__forwarding->person));
}
複製程式碼

當block在棧上時,__block變數也存放在棧上,它內部的__forwarding指標指向它本身

Objective-C基礎之四(深入理解Block)

當block被複制到堆上之後,block所引用的__block變數也會被複制到堆上,這樣在棧上和堆上各存在一份__block變數,此時將棧上__block變數中的__forwarding指標指向堆上__block變數的地址,同時,堆上的__block變數中的__forwarding指標指向它本身,那麼此時,不管我們是訪問棧上__block變數中的屬性值還是堆上__block變數中的屬性值,都是通過__forwarding指標訪問到堆上的__block變數。

Objective-C基礎之四(深入理解Block)

__block修飾的物件型別記憶體管理總結

  • 當__block變數存放在棧上時,他內部不會對指向的物件產生強引用
  • 當block被copy到堆上時,它訪問的__block變數也會被copy到堆上
    • 會首先呼叫__block變數內部的copy函式,copy函式內部會呼叫_Block_object_assign函式
    • _Block_object_assign函式會更加所指向物件的修飾符(__strong、__weak、__unsafe_unretained)來做出相應的操作,如果是__strong修飾的物件,則會對它進行強引用
  • 如果block從堆上移除
    • 會呼叫__block變數內部的dispose函式,dispose函式內部會呼叫_Block_object_dispose函式
    • _Block_object_dispose函式會對__block變數內部引用的物件進行釋放操作,相當於執行一次release。

block訪問物件型別的auto變數和__block變數對比

相同點

當block存放在棧上是,對物件型別的auto變數和__block變數都不會產生強引用

不同點

當block被copy到堆上時

  • 訪問物件型別的auto變數時,block內部會呼叫copy函式,根據物件的修飾符(__strong、__weak、__unsafe_unretained)來決定是否對物件進行強引用,如果是__strong修飾的物件,則進行強引用。copy函式如下
//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
複製程式碼
  • 訪問__block變數時,block內部會直接呼叫copy函式,對__block變數進行強引用,copy函式如下
//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

當block從堆中移除

當block從堆中移除時,都會呼叫dispose函式來對引用的物件進行釋放

  • 引用物件型別的auto變數時呼叫的dispose函式
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

複製程式碼
  • 引用__block變數時呼叫的dispose函式
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

雖然呼叫的都是copy函式,但是傳遞的引數型別不同,訪問物件型別的auto變數時,傳遞的引數為3(BLOCK_FIELD_IS_OBJECT),訪問__block變數時,傳遞的引數為8(BLOCK_FIELD_IS_BYREF)

補充

block迴圈引用的問題

在使用block時,如果block作為一個物件的屬性,並且在block中也使用到了這個物件,則會產生迴圈引用,導致block和物件相互引用,無法釋放。Demo如下

typedef void(^TestBlock)(void);
@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, copy)TestBlock block;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XLPerson *person = [[XLPerson alloc] init];
        person.name = @"張三";
        person.block = ^{
            NSLog(@"%@",person.name);
        };
        person.block();
    }
    return 0;
}
複製程式碼

解決方式有兩種(此處主要講解ARC的情況下):

  • 使用__weak來修飾物件
__weak typeof(person) weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
複製程式碼
  • 使用__unsafe_unretained來修飾物件
__unsafe_unretained XLPerson *weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
複製程式碼

__weak和__unsafe_unretained的區別

__weak和__unsafe_unretained最終的效果都是能shi使block不對外部訪問的物件形成強引用,而是形成弱引用。也就是說外部物件的引用計數不會增加。但是__weak和__unsafe_unretained也有區別,__weak在物件被銷燬後會自動將weak指標置為nil,而__weak和__unsafe_unretained修飾的物件在被銷燬後,指標是不會被清空的,如果後續訪問到了這個指標,會報野指標的錯誤,因此在遇到迴圈引用的時候,優先使用__weak來解決。更多的關於__weak的內容會在後續文章中進行學習。

面試題

1、block的本質是什麼?

block其實就是封裝了函式呼叫與呼叫環境的OC物件,它的底層其實是一個結構體。

2、__block的作用是什麼?

在block中如果想要修改外部訪問的auto變數,就需要使用__block來修飾auto變數,它會將修飾的變數封裝成一個結構體,結構體內部存放著變數的值。如果__block修飾的是物件型別,那麼在結構體中會儲存著儲存物件記憶體地址的指標,同時在結構體中還多出兩個函式指標copy和dispose,用來管理物件的記憶體。

3、block作為屬性時為什麼要用copy來修飾?

在ARC中,block如果使用copy來修飾,會將block從棧上覆制到堆上,方便我們手動管理block的記憶體,如果不用copy來修飾的話,那麼block就會存在棧上,由系統自動釋放記憶體。

4、使用block會遇到什麼問題?怎麼解決?

在使用block過程中,會遇到迴圈引用的問題,解決方式就是使用__weak或者__unsafa_unretain來修飾外部引用的物件。優先使用__weak。

結束語

以上內容純屬個人理解,如果有什麼不對的地方歡迎留言指正。

一起學習,一起進步~~~

相關文章