【iOS重學】Block底層原理(二)

寫在前面

在上一篇文章【iOS重學】Block底層原理(一)中我們主要講了Block的基本使用、底層原理、對變量的捕獲機制以及Block的幾種類型,本文是第二篇,主要內(nèi)容包含:

  • __block修飾符的基本使用
  • __block修飾的變量在Block內(nèi)部的底層結(jié)構(gòu)
  • __block的內(nèi)存管理
  • 循環(huán)引用

__Block的基本使用

如果想在Block內(nèi)部修改auto變量的值,我們一般是無法直接修改的,會報如下錯誤:

1.png

__block修飾符就是用來解決Block內(nèi)部無法修改auto變量值的問題。

__block不能用來修飾全局變量、static變量。

__block修飾的變量底層結(jié)構(gòu)探究

__block int age = 10;
void(^Block)(void) = ^{
    NSLog(@"age is %d",age);
};
Block();

如上,使用__block修飾的變量在Block內(nèi)部之后的底層結(jié)構(gòu)是什么樣的呢?跟之前對比有什么不一樣。
不使用__block修飾符,Block底層結(jié)構(gòu)如下:

2.png

使用__block修飾符,Block底層結(jié)構(gòu)如下:
3.png

4.png

我們發(fā)現(xiàn)底層結(jié)構(gòu)確實發(fā)生了變化:被__block修飾的變量會被包裝成一個__Block_byref_age_0的對象,這個對象的結(jié)構(gòu)里面有個int age,具體如下:

struct __Block_byref_age_0 {
  void *__isa;
  __Block_byref_age_0 *__forwarding; // 是指向自己的一個指針
  int __flags;
  int __size;
  int age;
};

問題

NSMutableArray *tempArr = [NSMutableArray array];
void(^Block)(void) =  ^(){
[tempArr addObject:@"1"];
};
Block();

以上代碼結(jié)果是否會報錯?
不會,[tempArr addObject:@"1"]只是在使用tempArr指針并沒有修改tempArr。

__block int age = 10;
NSLog(@"1---%p",&age);
void(^Block)(void) = ^{
    NSLog(@"age is %d",age);
    NSLog(@"2---%p",&age);
};
Block();
NSLog(@"3---%p",&age);

我們打印age的地址看一下:

2022-12-08 14:34:52.006645+0800 BlockDemo[8781:6613854] 1---0x7ff7bfeff2d8
2022-12-08 14:34:52.007272+0800 BlockDemo[8781:6613854] age is 10
2022-12-08 14:34:52.007357+0800 BlockDemo[8781:6613854] 2---0x100b478a8
2022-12-08 14:34:52.007395+0800 BlockDemo[8781:6613854] 3---0x100b478a8

從打印結(jié)果我們看到1和2、3的age的地址值不一樣,我們可以根據(jù)上面的底層結(jié)構(gòu)探索來解釋一下為什么?

struct __Block_byref_age_0 {
 void *__isa;
 struct __Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  struct __Block_byref_age_0 *age;
};

__block int age = 10;
NSLog(@"1---%p",&age);
void(^Block)(void) = ^{
    NSLog(@"age is %d",age);
    NSLog(@"2---%p",&age);
};

struct __main_block_impl_0 *implBlock = (__bridge struct __main_block_impl_0  *)Block;

我們把Block轉(zhuǎn)為__main_block_impl_0的結(jié)構(gòu)體來分析一下:

5.png

Block內(nèi)部的__Block_byref_age_0結(jié)構(gòu)體地址值是:0x100e1c7e0,而我們打印age的地址值是:0x100e1c7f8,兩個不一樣,說明打印的age的地址值不是__Block_byref_age_0結(jié)構(gòu)體age的值,接著往下分析:

// 地址值:0x100e1c7e0
struct __Block_byref_age_0 {
 void *__isa; // 8byte __isa地址值:0x100e1c7e0
 struct __Block_byref_age_0 *__forwarding; // 8byte __forwarding地址值:0x100e1c7e8
 int __flags;// 4byte __flags地址值:0x100e1c7d2
 int __size; // 4byte __size地址值:0x100e1c7d6
 int age;// 4byte age地址值:0x100e1c7d8
};

通過上面的分析我們看到:我們打印的age的地址值其實是__Block_byref_age_0結(jié)構(gòu)體中age的地址值。

__block的內(nèi)存管理

在上面我們分析的是基本數(shù)據(jù)類型用__block來修飾,我們接下來看一下更復(fù)雜的情況:__block用來修飾對象類型。

__block NSObject *object = [[NSObject alloc] init];
void(^Block)(void) = ^{
    NSLog(@"object is %@",object);
};
Block();

底層結(jié)構(gòu)如下:

6.png

這里有兩點值得我們注意一下:
1的位置多了兩個函數(shù)copydispose,這點我們在上一篇文章講到過因為Block捕獲的變量是對象類型,所以會有這兩個函數(shù),這里我們就不贅述了,除此之外我們發(fā)現(xiàn)__Block_byref_object_0這個結(jié)構(gòu)體里面也多了兩個函數(shù):__Block_byref_id_object_copy__Block_byref_id_object_dispose,里面具體實現(xiàn)如下:

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

內(nèi)部也是調(diào)用的_Block_object_assign_Block_object_dispose,和我們之前講的是一樣的。
并且我們看到__Block_byref_object_0結(jié)構(gòu)體里面會對object這個對象有一個強引用。
下面我們來總結(jié)一下:
1、當Block在棧上時,并不會對__block修飾的變量產(chǎn)生強引用。
2、當Block被copy到堆上時,會調(diào)用Block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用__Block_object_assign函數(shù),__Block_object_assign函數(shù)會對__block修飾的變量形成強引用。

7.png

8.png

3、當Block從堆中移除時,會調(diào)用Block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會調(diào)用__Block_object_dispose函數(shù),__Block_object_dispose會對__block修飾的變量進行一次release操作。
9.png

10.png

Block的循環(huán)引用

// Person 類
@interface Person : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, copy) void(^PersonBlock)(void);

@end

@implementation Person

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

@end

// main函數(shù)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.PersonBlock = ^{
            NSLog(@"person's age is %d",person.age);
        };
        person.PersonBlock();
    }
    NSLog(@"--------");
    return 0;
}

打印結(jié)果:

2022-12-08 16:07:45.878556+0800 BlockDemo[9541:6681377] person's age is 10
2022-12-08 16:07:45.879146+0800 BlockDemo[9541:6681377] --------

發(fā)現(xiàn)person對象并沒有被釋放還存在內(nèi)存里面,這就是我們常說的循環(huán)引用(內(nèi)存泄漏)。
下圖表示了上面對象之間的持有關(guān)系:

11.png

如何解決循環(huán)引用?
其實就是把2和3其中一個變成弱引用即可,那么到底2和3誰變成弱引用更合適呢,3是Person對象有一個PersonBlock這個屬性,我們希望當這個Person對象還在的時候隨時能訪問到PersonBlock,所以3應(yīng)該是個強引用,我們把2換成弱引用即可。

  • 使用__weak,__unsafe_unretain
  • 使用_block,但是必須調(diào)用block
// 方式一
Person *person = [[Person alloc] init];
__weak typeof(person) weakPerson = person;
person.age = 10;
person.PersonBlock = ^{
    NSLog(@"person's age is %d",weakPerson.age);
};
person.PersonBlock();

// 方式二
Person *person = [[Person alloc] init];
__unsafe_unretained typeof(person) weakPerson = person;
person.age = 10;
person.PersonBlock = ^{
    NSLog(@"person's age is %d",weakPerson.age);
};
person.PersonBlock();

// 方式三
__block Person *person = [[Person alloc] init];
person.age = 10;
person.PersonBlock = ^{
    NSLog(@"person's age is %d",person.age);
    person = nil;
};
person.PersonBlock();

我們來分析一下__block修飾的變量的內(nèi)存問題:

__block Person *person = [[Person alloc] init];
person.age = 10;
person.PersonBlock = ^{
    NSLog(@"person's age is %d",person.age);
};
person.PersonBlock();

用一張圖來表示他們之間的引用關(guān)系:


12.png

如何解決循環(huán)引用?

Person *person = [[Person alloc] init];
__block __weak typeof(person) weakPerson = person;
person.age = 10;
person.PersonBlock = ^{
    NSLog(@"person's age is %d",weakPerson.age);
};
person.PersonBlock();

底層結(jié)構(gòu)如下:


13.png

這樣我們就可以解決Block帶來的一些循環(huán)引用的問題啦。

寫在最后

關(guān)于Block的底層原理在這里就全部結(jié)束了,如有錯誤請多多指教。

?著作權(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)容