iOS底層原理總結(jié) - 探尋block本質(zhì)(二)

本篇主要是對(duì)小碼哥底層視頻學(xué)習(xí)的總結(jié)。方便日后復(fù)習(xí)。
上一篇《iOS底層原理總結(jié) - 探尋block本質(zhì)(一)》:
http://m.itdecent.cn/p/deb04ce08d1a

本篇學(xué)習(xí)總結(jié):

  • blcok對(duì)對(duì)象變量的捕獲
  • block內(nèi)修改變量的值
  • __block內(nèi)存管理
  • 循環(huán)引用問(wèn)題即解決方式

好了,帶著問(wèn)題,我們一一開(kāi)始閱讀吧 ??

一.blcok對(duì)對(duì)象變量的捕獲

上節(jié)中我們討論了block可以捕獲局部變量,包括基本數(shù)據(jù)類型和對(duì)象,那么當(dāng)在block中訪問(wèn)對(duì)象時(shí),什么時(shí)候銷毀呢?
還是先上代碼吧

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"------block內(nèi)部%d",person.age);
            };
        } // ARC環(huán)境下執(zhí)行完畢,person沒(méi)有被釋放
        NSLog(@"--------");
    } // ARC環(huán)境下person 釋放
    return 0;
}

//打印結(jié)果如下:
--------
person delloc

大括號(hào)執(zhí)行完畢之后,person對(duì)象依然沒(méi)有被釋放,上一篇文章中提到過(guò),person為auto局部變量,傳入block的變量同樣為person,即block有一個(gè)強(qiáng)引用指針指向person,所以block不被銷毀的話,person對(duì)象也不會(huì)被銷毀。

查看一下c++文件


強(qiáng)指針引用.png

如果將ARC環(huán)境改為MRC,又會(huì)是怎么樣的結(jié)果呢?

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"------block內(nèi)部%d",person.age);
            };
           [person release];//MRC下需要手動(dòng)釋放對(duì)象空間
        } //當(dāng)block還未被釋放時(shí),person對(duì)象已經(jīng)被釋放了
        NSLog(@"--------");
    } 
    return 0;

//打印結(jié)果如下:
person delloc
--------
}

block調(diào)用copy操作之后,person不會(huì)被釋放。

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = [^{
                NSLog(@"------block內(nèi)部%d",person.age);
            } copy];
            [person release];
        }
        NSLog(@"--------");
    } /
    return 0;
}

//打印結(jié)果如下:
--------

在MRC環(huán)境下,只需要將棧空間的block進(jìn)行一次copy操作,就能將??臻g的block拷貝到堆上,person對(duì)象不會(huì)被釋放,說(shuō)明拷貝到堆上的block對(duì)person進(jìn)行一次retain操作,以保證person不會(huì)被銷毀。堆空間的block自己銷毀之后也會(huì)對(duì)person對(duì)象進(jìn)行一個(gè)release操作。

總結(jié)如下:

??臻g上的block不會(huì)對(duì)對(duì)象進(jìn)行強(qiáng)引用,堆空間的block有能力持有外部調(diào)用的對(duì)象,即對(duì)對(duì)象進(jìn)行強(qiáng)引用或者去除強(qiáng)引用的操作。

__weak
__weak添加之后,person在作用域執(zhí)行完畢后就被銷毀了

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            __weak Person *weakPerson = person;
            
            block = ^{
                NSLog(@"------block內(nèi)部%d",weakPerson.age);
            };
            
        }
        NSLog(@"--------");
    }
    return 0;
}

//打印結(jié)果如下:
person delloc
--------

我們轉(zhuǎn)化為c++代碼看一下差別
上述的代碼含有__weak修飾變量符,如果還用之前的命令行會(huì)報(bào)錯(cuò),我們需要告知編譯器使用ARC環(huán)境以及版本號(hào),添加說(shuō)明

-fobjc-arc -fobjc-runtime=ios-8.0.0

完成命令行如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

查看c++代碼

__weak修飾變量.png

總結(jié)一下:

1.__weak/__strong 這類修飾符只能修飾對(duì)象,不可修飾基本數(shù)據(jù)類型
2.MRC環(huán)境下不管是__weak/__strong修飾的對(duì)象,棧上的block都不會(huì)對(duì)對(duì)象進(jìn)行強(qiáng)引用,局部對(duì)象一出作用域就會(huì)自動(dòng)被釋放。
3.ARC環(huán)境下堆上的block默認(rèn)對(duì)對(duì)象時(shí)強(qiáng)引用,只有等block內(nèi)存釋放的時(shí)候才會(huì)釋放引用對(duì)象。__weak修飾的對(duì)象,堆上的block對(duì)其是弱引用,局部對(duì)象一出作用域就會(huì)自動(dòng)被釋放。

__main_block_copy_0 和__main_block_dispose_0
當(dāng)block中捕獲對(duì)象類型的變量時(shí),我們發(fā)現(xiàn)block結(jié)構(gòu)體中的描述結(jié)構(gòu)體__main_block_desc_0中多了兩個(gè)參數(shù)copydispose函數(shù),查看源碼:

__main_block_copy_0、__main_block_dispose_0函數(shù).png

copydispose 函數(shù)中傳入的都是__main_block_impl_0結(jié)構(gòu)體本身。

這里說(shuō)的copy就是__main_block_copy_0函數(shù),__main_block_copy_0函數(shù)內(nèi)部調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)自動(dòng)根據(jù)__main_block_impl_0結(jié)構(gòu)體內(nèi)部的person對(duì)象是什么類型的指針,對(duì)person對(duì)象產(chǎn)生強(qiáng)引用或者弱引用。可以理解為_Block_object_assign函數(shù)內(nèi)部會(huì)對(duì)person進(jìn)行引用計(jì)數(shù)器的操作,如果__main_block_impl_0結(jié)構(gòu)體內(nèi)person指針是__strong類型,則為強(qiáng)引用,引用計(jì)數(shù)+1,如果__main_block_impl_0結(jié)構(gòu)體內(nèi)person指針是__weak類型,則為弱引用,引用計(jì)數(shù)不變。

這里的dispose函數(shù)指的是_Block_object_dispose函數(shù),當(dāng)block從堆上移除時(shí)就會(huì)自動(dòng)調(diào)用__main_block_dispose_0函數(shù),__main_block_dispose_0函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose會(huì)對(duì)person對(duì)象做釋放操作,類似于release,也就是斷開(kāi)對(duì)person對(duì)象的引用
person究竟是否被釋放還取決于person對(duì)象自己的引用計(jì)數(shù)。

總結(jié)一下:

1.一旦block中捕獲的變量為對(duì)象類型,block結(jié)構(gòu)體中的__main_block_desc_0會(huì)多出兩個(gè)參數(shù)copydispose。因?yàn)樵L問(wèn)的是個(gè)對(duì)象,block希望擁有這個(gè)對(duì)象,就需要對(duì)對(duì)象進(jìn)行引用,也就是進(jìn)行內(nèi)存管理的操作,比如說(shuō)對(duì)象進(jìn)行retain操作,當(dāng)block從堆上移除時(shí)調(diào)用dispose函數(shù)。copy跟dispose方法內(nèi)部調(diào)用看上面總結(jié)。
2.當(dāng)block內(nèi)部訪問(wèn)了對(duì)象類型變量時(shí),如果block在棧上,block內(nèi)部不會(huì)對(duì)person產(chǎn)生強(qiáng)引用,不論block結(jié)構(gòu)體內(nèi)部的變量是__strong修飾還是__weak修飾,都不會(huì)對(duì)變量產(chǎn)生強(qiáng)引用。
3.如果block被拷貝到堆上,copy函數(shù)會(huì)調(diào)用_Block_object_assign函數(shù),根據(jù)變量的修飾符(__strong,__weak,unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)應(yīng)用還是弱引用。
4.如果block從堆上移除,dispose函數(shù)會(huì)調(diào)用_Block_object_dispose函數(shù),自動(dòng)釋放引用的變量。

問(wèn)題
1.下列代碼person何時(shí)銷毀

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",person);
    });
    NSLog(@"touchBegin----------End");
}
打印結(jié)果.png

上文提到過(guò)ARC環(huán)境中,block作為GCD API的方法參數(shù)時(shí)會(huì)自動(dòng)進(jìn)行copy操作,因此block在堆空間,并且使用強(qiáng)引用訪問(wèn)person對(duì)象,因此block內(nèi)部copy函數(shù)會(huì)對(duì)person進(jìn)行強(qiáng)引用。當(dāng)block執(zhí)行完畢需要被銷毀時(shí),調(diào)用dispose函數(shù)釋放對(duì)person對(duì)象的引用,person沒(méi)有強(qiáng)指針指向時(shí)才會(huì)被銷毀

2.下列代碼person何時(shí)銷毀?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", weakP);
    });
    NSLog(@"touchBegin----------End");
}
打印內(nèi)容.png

block中對(duì)weakP為__weak弱引用,因此block內(nèi)部copy函數(shù)會(huì)對(duì)person同樣進(jìn)行弱引用,當(dāng)大括號(hào)執(zhí)行完畢時(shí),person對(duì)象沒(méi)有強(qiáng)指針引用就會(huì)被釋放。因此block塊執(zhí)行的時(shí)候打印null

3.下列person對(duì)象何時(shí)釋放呢?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSLog(@"weakP ----- %@", weakP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"person ----- %@",person);
        });
    });
    NSLog(@"touchBegin----------End");
}
打印結(jié)果.png

block內(nèi)部對(duì)person對(duì)象先進(jìn)行弱引用,再進(jìn)行強(qiáng)引用,當(dāng)?shù)谝粋€(gè)GCD函數(shù)執(zhí)行完畢后,person對(duì)象被block弱引用著,不會(huì)對(duì)引用計(jì)數(shù)造成變化,第二個(gè)GCD函數(shù)對(duì)person對(duì)象是強(qiáng)引用,只能等第二個(gè)block執(zhí)行完畢之后才可釋放person對(duì)象,因此第二個(gè)block執(zhí)行完畢后會(huì)打印兩次person對(duì)象。

4.下面這種情況person對(duì)象什么時(shí)候釋放呢?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *waekP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSLog(@"person ----- %@",person);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"weakP ----- %@",waekP);
        });
    });
    NSLog(@"touchBegin----------End");
}
打印結(jié)果.png

block內(nèi)部對(duì)person對(duì)象先進(jìn)行強(qiáng)引用,再進(jìn)行弱引用,當(dāng)?shù)谝粋€(gè)GCD函數(shù)執(zhí)行之前,person對(duì)象被block強(qiáng)引用著,第一個(gè)GCD函數(shù)執(zhí)行完畢后,隨著block釋放而釋放,第二個(gè)GCD函數(shù)沒(méi)有之前person對(duì)象已經(jīng)釋放了,因此第二個(gè)block塊執(zhí)行的時(shí)候打印null。

二.block內(nèi)修改變量的值

還是先上代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        Block block = ^ {
            // age = 20; // 無(wú)法修改
            NSLog(@"%d",age);
        };
        block();
    }
    return 0;
}

默認(rèn)情況block內(nèi)部不能修改捕獲的變量數(shù)值,通過(guò)之前對(duì)源碼的分析可以知道。age 是在main函數(shù)內(nèi)部聲明的,說(shuō)明age的內(nèi)存存在??臻g,但是block內(nèi)部的代碼在__main_block_func_0函數(shù)內(nèi)部。__main_block_func_0函數(shù)內(nèi)部無(wú)法訪問(wèn)age變量的內(nèi)存空間,兩個(gè)函數(shù)的??臻g不一樣,__main_block_func_0內(nèi)部拿到的age是block結(jié)構(gòu)體內(nèi)部的age,因?yàn)闊o(wú)法在__main_block_func_0函數(shù)內(nèi)部去修改main函數(shù)內(nèi)部的變量。

如果我們想要在block內(nèi)部修改局部變量的數(shù)值,可以采用以下兩種方式
1.局部變量使用static修飾

前文提到過(guò)static修飾的局部變量被block捕獲后轉(zhuǎn)化成指針類型的變量,在__main_block_func_0函數(shù)內(nèi)部可以拿到age變量的內(nèi)存地址,因?yàn)榫涂梢栽赽lock內(nèi)部修改age的值。

2.__block修飾局部變量

__block用于解決block內(nèi)部不能修改auto變量值的問(wèn)題,__block不能修飾靜態(tài)變量(static)和全局變量。

__block int age = 10;

編譯器會(huì)將__block修飾的變量包裝成一個(gè)對(duì)象,查看其底層c++源碼。

_block修飾的變量源碼.png

解釋一下上面的代碼

首先被__block修飾的age變量聲明變?yōu)槊麨?code>age的__Block_byref_age_0的結(jié)構(gòu)體,也就是說(shuō)加上__block修飾的話捕獲到的block內(nèi)的變量為__Block_byref_age_0類型的結(jié)構(gòu)體。

通過(guò)下圖查看__Block_byref_age_0結(jié)構(gòu)體內(nèi)存儲(chǔ)哪些元素。


__Block_byref_age_0賦值.png

isa指針__Block_byref_age_0結(jié)構(gòu)體也有一個(gè)isa指針,說(shuō)明__Block_byref_age_0的本質(zhì)也是一個(gè)對(duì)象。
__forwarding__forwarding__Block_byref_age_0 結(jié)構(gòu)體類型的,并且__Block_byref_age_0存儲(chǔ)的值為(__Block_byref_age_0 *)&age,即結(jié)構(gòu)體自己的內(nèi)存地址。
** __flags**:默認(rèn)傳入0。
__sizesizeof(__Block_byref_age_0)__Block_byref_age_0所占用的空間。
age:真正存儲(chǔ)變量的地方,這里存儲(chǔ)局部變量為10
接著將__Block_byref_age_0結(jié)構(gòu)體中的變量存入__main_block_impl_0結(jié)構(gòu)體中,并賦值給__Block_byref_age_0 *age;

__Block_byref_age_0 *age賦值.png

之后調(diào)用block,首先取出__main_block_impl_0中的age,通過(guò)age結(jié)構(gòu)體拿到__forwarding指針,上面提到過(guò)__forwarding中保存的就是__Block_byref_age_0結(jié)構(gòu)體本身,這里也就是age(__Block_byref_age_0),在通過(guò)__forwarding拿到結(jié)構(gòu)體中的age(10)變量并修改其值。
后續(xù)NSLog中使用age時(shí)也通過(guò)同樣的方式獲取age的值。

修改結(jié)構(gòu)體內(nèi)的age值.png

到此為止,__block修飾的局部變量在block內(nèi)部可以進(jìn)行修改,__block將變量包裝成對(duì)象,然后在把age結(jié)構(gòu)體封裝到main_block_impl結(jié)構(gòu)體里面,block內(nèi)部存儲(chǔ)的變量為結(jié)構(gòu)體指針,也就可以通過(guò)指針找到內(nèi)存地址進(jìn)而修改變量的值。

用圖總結(jié)一下:


__block修飾變量.png

3.__block修飾對(duì)象類型
那么如果變量本身就是對(duì)象類型呢?通過(guò)以下代碼生成c++源碼查看。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        NSLog(@"%@",person);
        Block block = ^{
            person = [[Person alloc] init];
            NSLog(@"%@",person);
        };
        block();
    }
    return 0;
}

通過(guò)源碼查看,將對(duì)象包裝在一個(gè)新的結(jié)構(gòu)體中,結(jié)構(gòu)體內(nèi)部會(huì)有一個(gè)person對(duì)象,不一樣的地方是結(jié)構(gòu)體內(nèi)部添加了內(nèi)存管理的兩個(gè)函數(shù)__Block_byref_id_object_copy__Block_byref_id_object_dispose

__block修飾對(duì)象類型源碼.png

__Block_byref_id_object_copy__Block_byref_id_object_dispose函數(shù)的調(diào)用時(shí)機(jī)及作用在__block內(nèi)存管理部分詳細(xì)分析。
問(wèn)題
1.以下代碼是否可以正確執(zhí)行?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [NSMutableArray array];
        Block block = ^{
            [array addObject: @"5"];
            [array addObject: @"5"];
            NSLog(@"%@",array);
        };
        block();
    }
    return 0;
}

上述代碼可以正常執(zhí)行,因?yàn)樵赽lock塊中僅僅中使用了array的內(nèi)存地址,往內(nèi)存地址中添加內(nèi)容,并沒(méi)有修改array的內(nèi)存地址,因此array不需要使用__block修飾也可以正確編譯。上面說(shuō)的是在block內(nèi)部可以修改__block修飾的變量值。
因?yàn)楫?dāng)僅僅是使用局部變量的內(nèi)存地址,而不是修改的時(shí)候,類似上述的代碼,盡量不要添加__block,因?yàn)樘砑觃_block修飾符之后,系統(tǒng)會(huì)自動(dòng)創(chuàng)建相應(yīng)的結(jié)構(gòu)體,占用不必要的內(nèi)存空間。

2.上面提到的__block修飾的age變量在編譯時(shí)被封裝為結(jié)構(gòu)體,那么當(dāng)在外部使用age變量的時(shí)候,使用的是__Block_byref_age_0結(jié)構(gòu)體還是__Block_byref_age_0結(jié)構(gòu)體中的age變量呢?
為了驗(yàn)證上述問(wèn)題,還是先上代碼
同樣使用自定義結(jié)構(gòu)體的方式來(lái)查看其內(nèi)部結(jié)構(gòu)

typedef void (^Block)(void);

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

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

struct __Block_byref_age_0 {
    void *__isa;
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_age_0 *age; // by ref
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        Block block = ^{
            age = 20;
            NSLog(@"age is %d",age);
        };
        block();
        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        NSLog(@"%p",&age);
    }
    return 0;
}

打印斷點(diǎn)查看結(jié)構(gòu)體內(nèi)部結(jié)構(gòu)


_Block_byref_age_0結(jié)構(gòu)體.png

通過(guò)查看main_block_impl結(jié)構(gòu)體其中的內(nèi)容,找到age結(jié)構(gòu)體,其中重點(diǎn)觀察兩個(gè)元素:

1.__forwarding中存儲(chǔ)的地址確實(shí)是age結(jié)構(gòu)體變量自己的地址
2.age中存儲(chǔ)著修改后的20
上面也提到過(guò),在block中使用或者修改age的時(shí)候都是通過(guò)結(jié)構(gòu)體__Block_byref_age_0找到__forwarding在找到變量age的。
apple為了隱藏__Block_byref_age_0結(jié)構(gòu)體的實(shí)現(xiàn),打印age變量的地址發(fā)現(xiàn)其實(shí)是__Block_byref_age_0結(jié)構(gòu)體age變量的地址。

age的內(nèi)存地址推算.png

通過(guò)上圖的計(jì)算可以發(fā)現(xiàn)打印age的地址同__Block_byref_age_0結(jié)構(gòu)體內(nèi)的age值的地址相同,也就是說(shuō)外面使用的age,代表的就是結(jié)構(gòu)體內(nèi)的age值,所以直接拿來(lái)用的age就是之前聲明的int age。
總結(jié)一下__block修飾對(duì)象類型的底層實(shí)現(xiàn):

__block修飾對(duì)象.png

三.__block內(nèi)存管理

上文提到當(dāng)block中捕獲的對(duì)象類型的變量時(shí),block中的__main_block_desc_0結(jié)構(gòu)體內(nèi)部自動(dòng)添加copydispose函數(shù)對(duì)捕獲的變量進(jìn)行內(nèi)存管理。
那么同樣的當(dāng)block內(nèi)部捕獲__block修飾的對(duì)象類型的變量時(shí),__Block_byref_person_0結(jié)構(gòu)體內(nèi)部也會(huì)自動(dòng)添加__Block_byref_id_object_copy__Block_byref_id_object_dispose對(duì)被__block包裝成結(jié)構(gòu)體的對(duì)象進(jìn)行內(nèi)存管理。

當(dāng)block內(nèi)存在棧上時(shí),并不會(huì)對(duì)__block修飾變量產(chǎn)生內(nèi)存管理(基本數(shù)據(jù)類型,對(duì)象類型)。當(dāng)block被copy到堆上時(shí)會(huì)調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)對(duì)__block變量形成強(qiáng)弱引用。

首先看一張圖看一下block復(fù)制到堆上的內(nèi)存變化


__block copy內(nèi)存管理.png

當(dāng)block被copy到堆上時(shí),block內(nèi)部引用的__block變量也會(huì)被復(fù)制到堆上,并且持有變量,如果block復(fù)制到堆上的同時(shí), __block變量已經(jīng)存在堆上了,則不會(huì)復(fù)制。
當(dāng)block從堆上移除的話,就會(huì)調(diào)用dispose函數(shù),也就是__main_block_dispose_0函數(shù),__main_block_dispose_0函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),會(huì)自動(dòng)釋放引用的__block變量。

__block 釋放內(nèi)存管理.png

block內(nèi)部決定什么時(shí)候?qū)⒆兞繌?fù)制到堆上,什么時(shí)候?qū)ψ兞孔鰬?yīng)用計(jì)數(shù)的操作。
__block修飾的變量在block結(jié)構(gòu)體中一直都是強(qiáng)引用,而其他類型的是由傳入的對(duì)象指針類型決定。
還是上代碼

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int number = 20;
        __block int age = 10;
        
        NSObject *object = [[NSObject alloc] init];
        __weak NSObject *weakObj = object;
        
        Person *p = [[Person alloc] init];
        __block Person *person = p;
        __block __weak Person *weakPerson = p;
        
        Block block = ^ {
            NSLog(@"%d",number); // 局部變量
            NSLog(@"%d",age); // __block修飾的局部變量
            NSLog(@"%p",object); // 對(duì)象類型的局部變量
            NSLog(@"%p",weakObj); // __weak修飾的對(duì)象類型的局部變量
            NSLog(@"%p",person); // __block修飾的對(duì)象類型的局部變量
            NSLog(@"%p",weakPerson); // __block,__weak修飾的對(duì)象類型的局部變量
        };
        block();
    }
    return 0;
}

上述__main_block_impl_0結(jié)構(gòu)體中看出,沒(méi)有使用__block修飾的變量(object和weakObj)則根據(jù)他們本身被block捕獲的指針類型對(duì)他們進(jìn)行強(qiáng)引用或者弱引用,而一旦使用__block修飾的變量,__main_block_impl_0結(jié)構(gòu)體內(nèi)一律使用強(qiáng)指針引用生成的結(jié)構(gòu)體。
接著我們來(lái)看__block修飾的變量生成的結(jié)構(gòu)體有什么不同

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

struct __Block_byref_person_1 {
  void *__isa;
__Block_byref_person_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__strong person;
};

struct __Block_byref_weakPerson_2 {
  void *__isa;
__Block_byref_weakPerson_2 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__weak weakPerson;
};

如上面分析那樣,__block修飾對(duì)象類型的變量生成的結(jié)構(gòu)體內(nèi)部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose兩個(gè)函數(shù),用于對(duì)對(duì)象類型的變量進(jìn)行內(nèi)存管理的操作,所以__Block_byref_weakPerson_2對(duì)weakPerson就是弱引用,__Block_byref_person_1對(duì)person是強(qiáng)引用。

我們來(lái)看一下copy函數(shù)內(nèi)部調(diào)用

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
}

__main_block_copy_0函數(shù)中會(huì)根據(jù)變量是強(qiáng)弱指針以及有沒(méi)有被__block修飾做出不同的處理,強(qiáng)指針在block內(nèi)部產(chǎn)生強(qiáng)引用,弱指針在block內(nèi)部產(chǎn)生弱引用。被__block修飾的變量最后的參數(shù)傳入的是8,沒(méi)有被__block修飾的變量最后的參數(shù)傳入的是3。
當(dāng)block從堆上中移除時(shí)通過(guò)dispose函數(shù)來(lái)釋放他們。

static void __main_block_dispose_0(struct __main_block_impl_0*src)
 {
    _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
    
}

__forwarding指針

上面提到過(guò)__forwarding指針指向的是結(jié)構(gòu)體自己,當(dāng)使用變量的時(shí)候,通過(guò)結(jié)構(gòu)體找到__forwarding指針,在通過(guò)__forwarding指針找到相應(yīng)的變量,這樣設(shè)計(jì)的目的是為了方便內(nèi)存管理,通過(guò)上面對(duì)__block變量的內(nèi)存管理分析我們知道,block被復(fù)制到堆上時(shí),會(huì)將block中引用的變量也復(fù)制到堆中。

我們重回到源碼中,當(dāng)在block中修改__block修飾的變量時(shí)。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
        }

通過(guò)源碼可以知道,當(dāng)修改__block修飾的變量時(shí),是根據(jù)變量生成的結(jié)構(gòu)體__Block_byref_age_0找到__forwarding指針,__forwarding指針指向的結(jié)構(gòu)體是自己,所以根據(jù)__forwarding可以找到結(jié)構(gòu)體中的age變量,從而進(jìn)行修改。

當(dāng)block在棧中時(shí),__Block_byref_age_0結(jié)構(gòu)體中的__forwarding指針指向結(jié)構(gòu)體自己。
而當(dāng)block被復(fù)制到堆上時(shí),棧中的__Block_byref_age_0結(jié)構(gòu)體也會(huì)被復(fù)制到堆中一份,而此時(shí)棧中的__Block_byref_age_0結(jié)構(gòu)體中的__forwarding指針指向的就是堆中的__Block_byref_age_0結(jié)構(gòu)體,堆中的__Block_byref_age_0結(jié)構(gòu)體內(nèi)的__forwarding指針依然指向自己。

此時(shí)當(dāng)對(duì)age進(jìn)行修改時(shí)

// 棧中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding獲取堆中的age結(jié)構(gòu)體
// age->__forwarding->age 修改堆中age結(jié)構(gòu)體的age變量
(age->__forwarding->age) = 20;

通過(guò)__forwarding指針巧妙的將修改的變量賦值給堆中的__Block_byref_age_0中。
我們通過(guò)一張圖展示__forwarding指針的作用

__forwarding指針.png

因此block內(nèi)部拿到的變量實(shí)際就是在堆上的,當(dāng)block進(jìn)行copy被復(fù)制到堆上時(shí),_Block_object_assign函數(shù)內(nèi)做的這一系列操作。
被__block修飾的對(duì)象類型的內(nèi)存管理
使用一下代碼,生成c++代碼查看內(nèi)部實(shí)現(xiàn)

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        Block block = ^ {
            NSLog(@"%p", person);
        };
        block();
    }
    return 0;
}
// __Block_byref_person_0結(jié)構(gòu)體聲明

__attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
    (void*)0,
    (__Block_byref_person_0 *)&person,
    33554432,
    sizeof(__Block_byref_person_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    
    ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
};

之前提到過(guò)__block修飾的對(duì)象類型生成的結(jié)構(gòu)體中新增加兩個(gè)函數(shù)void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);。這兩個(gè)函數(shù)為_(kāi)_block修飾的對(duì)象提供了內(nèi)存管理的操作。
可以看出為void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);賦值的分別為__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。找到這兩個(gè)函數(shù)。

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

上述源碼中可以發(fā)現(xiàn)__Block_byref_id_object_copy_131函數(shù)中同樣調(diào)用了_Block_object_assign函數(shù),而_Block_object_assign函數(shù)內(nèi)部拿到dst指針即block對(duì)象自己的地址加上40個(gè)自己,并且_Block_object_assign最后傳入的參數(shù)時(shí)131,同block直接對(duì)對(duì)象進(jìn)行內(nèi)存管理傳入的參數(shù)3.8都不同,可以猜想_Block_object_assign內(nèi)部根據(jù)傳入的參數(shù)不同進(jìn)行不同的操作。
通過(guò)對(duì)上面__Block_byref_person_0結(jié)構(gòu)體占用空間的計(jì)算發(fā)現(xiàn)__Block_byref_person_0結(jié)構(gòu)體占用的空間是48個(gè)字節(jié),而加40恰好指向的就是person指針。
也就是說(shuō)copy函數(shù)會(huì)將person地址傳入_Block_object_assign函數(shù),_Block_object_assign中對(duì)person對(duì)象進(jìn)行強(qiáng)引用或者弱引用。

強(qiáng)引用示意圖.png

如果使用__weak修飾變量查看一下其中的源碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        __block __weak Person *weakPerson = person;
        Block block = ^ {
            NSLog(@"%p", weakPerson);
        };
        block();
    }
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakPerson_0 *weakPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0中沒(méi)有任何變化,__main_block_impl_0對(duì)weakPerson依然是強(qiáng)引用,但是__Block_byref_weakPerson_0中對(duì)weakPerson變?yōu)榱?code>__weak指針。

struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__weak weakPerson;
};

也就是說(shuō)無(wú)論如何block內(nèi)部中對(duì)__block修飾變量生成的結(jié)構(gòu)體都是強(qiáng)引用,結(jié)構(gòu)體內(nèi)部對(duì)外部變量的引用取決于傳入block內(nèi)部的變量是強(qiáng)引用還是弱引用

弱引用示意圖.png

MRC環(huán)境下,盡管調(diào)用了copy操作,__block結(jié)構(gòu)體不會(huì)對(duì)person產(chǎn)生強(qiáng)引用,依然是弱引用。

MRC環(huán)境?。?!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        Block block = [^ {
            NSLog(@"%p", person);
        } copy];
        [person release];
        block();
        [block release];
    }
    return 0;
}

上述代碼person會(huì)先釋放

block的copy[50480:8737001] -[Person dealloc]
block的copy[50480:8737001] 0x100669a50

當(dāng)block從堆中移除的時(shí)候。會(huì)調(diào)用dispose函數(shù),block塊中去除對(duì)__Block_byref_person_0 *person;的引用,__Block_byref_person_0結(jié)構(gòu)體中也會(huì)調(diào)用dispose操作去除對(duì)Person *person;的引用。以保證結(jié)構(gòu)體和結(jié)構(gòu)體內(nèi)部的對(duì)象可以正常釋放

__block內(nèi)存管理示意圖.png
四.循環(huán)引用問(wèn)題

循環(huán)引用導(dǎo)致內(nèi)存泄漏。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"%d",person.age);
        };
    }
    NSLog(@"大括號(hào)結(jié)束啦");
    return 0;
}

//打印結(jié)果如下:
大括號(hào)結(jié)束啦

可以發(fā)現(xiàn)大括號(hào)結(jié)束之后,person依然沒(méi)有被釋放,產(chǎn)生了循環(huán)引用。
通過(guò)一張圖看一下他們之間的內(nèi)存結(jié)構(gòu)


產(chǎn)生循環(huán)引用示意圖.png

上圖中可以發(fā)現(xiàn),Person對(duì)象和block對(duì)象相互之間產(chǎn)生了強(qiáng)引用,導(dǎo)致雙方都不會(huì)被釋放,進(jìn)而造成內(nèi)存泄漏。
解決循環(huán)引用問(wèn)題 - ARC

首先為了能隨時(shí)執(zhí)行block,我們希望person對(duì)block強(qiáng)引用,而block內(nèi)部對(duì)person的引用為弱引用最好。
使用__weak__unsafe_unretained修飾符可以解決循環(huán)引用的問(wèn)題
我們上面也提到過(guò)__weak會(huì)使block內(nèi)部將指針變?yōu)槿踔羔?,block對(duì)person對(duì)象為弱指針的話,也就不會(huì)出現(xiàn)互相引用而導(dǎo)致不會(huì)被釋放了。

使用`__weak`和`__unsafe_unretained`修飾 .png

__weak和__unsafe_unretained的區(qū)別

__weak不會(huì)產(chǎn)生強(qiáng)引用,指向的對(duì)象銷毀時(shí),會(huì)自動(dòng)將指針置為nil,因此一般通過(guò)__weak來(lái)解決問(wèn)題。
__unsafe_unretained不會(huì)引起強(qiáng)引用,指向的對(duì)象銷毀時(shí),指針存儲(chǔ)的地址值不變,當(dāng)再次通過(guò)指針獲取對(duì)象時(shí),訪問(wèn)的時(shí)壞內(nèi)存,所以是不安全的。
使用__block也可以解決循環(huán)引用的問(wèn)題。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"%d",person.age);
            person = nil;
        };
        person.block();
    }
    NSLog(@"大括號(hào)結(jié)束啦");
    return 0;
}

上述代碼之間的相互引用可以使用下圖表示:


使用__block也可以解決循環(huán)引用.png

上面我們提到過(guò),在block內(nèi)部使用變量使用的其實(shí)是__block修飾的變量生成的結(jié)構(gòu)體__Block_byref_person_0內(nèi)部的person對(duì)象,那么當(dāng)person對(duì)象置為nil也就斷開(kāi)了結(jié)構(gòu)體對(duì)person的強(qiáng)引用,那么三角的循環(huán)引用就會(huì)自動(dòng)斷開(kāi),該釋放的時(shí)候也就釋放了。但是有弊端,必須執(zhí)行block,并且在block內(nèi)部將person對(duì)象置為nil。也就說(shuō)在block執(zhí)行之前代碼是因?yàn)檠h(huán)引用導(dǎo)致內(nèi)存泄漏的。
用一張圖總結(jié)吧,方便記憶

ARC解決循環(huán)引用問(wèn)題.png

解決循環(huán)引用問(wèn)題 - MRC

使用__unsafe_unretained解決,在MRC環(huán)境下不支持使用__weak,使用原理同ARC環(huán)境下相同,這里不在多說(shuō)。
使用__block也能解決循環(huán)引用的問(wèn)題,因?yàn)樯衔?code>__block內(nèi)存管理中提到過(guò),MRC環(huán)境下,盡管調(diào)用了copy操作,__block結(jié)構(gòu)體不會(huì)對(duì)person產(chǎn)生強(qiáng)引用,依然是弱引用。因此同樣可以解決循環(huán)引用的問(wèn)題。

用一張圖總結(jié)吧,方便記憶


MRC解決循環(huán)引用問(wèn)題.png

__strong和__weak

__weak typeof(self) weakSelf = self;
person.block = ^{
    __strong typeof(weakSelf) myself = weakSelf;
    NSLog(@"age is %d", myself->_age);
};

有時(shí)候?yàn)榱吮苊鈈lock內(nèi)部的對(duì)象在調(diào)用時(shí)被銷毀,所以在block內(nèi)部重新使用__strong修飾self變量。

到底為止,block的底層知識(shí)記錄完畢,看一下面試題

面試題:

  • 1.block原理是什么,本質(zhì)是什么

block就是一個(gè)封裝函數(shù)以及函數(shù)調(diào)用環(huán)境的oc對(duì)象

  • 2.__block的作用是什么,有什么使用注意點(diǎn)

__block可以解決block內(nèi)部無(wú)法修改auto變量值的問(wèn)題,因?yàn)榫幾g器會(huì)將__block變量包裝成一個(gè)對(duì)象。
使用注意點(diǎn):注意內(nèi)存管理,MRC下

  • 3.block的屬性修飾詞為啥時(shí)copy 使用注意點(diǎn)

方便對(duì)block進(jìn)行內(nèi)存管理
注意點(diǎn):循環(huán)引用,以及循環(huán)引用的解決方式

  • 4.block在修改NSMutablearray的指針變量時(shí),還需要添加__block修飾符嗎?

能不添加就不添加,因?yàn)閎lock內(nèi)部只是獲取指針變量指向的對(duì)象數(shù)據(jù),并不是改變指針變量存儲(chǔ)的數(shù)據(jù)

本篇學(xué)習(xí)先記錄到此,感謝閱讀,如有錯(cuò)誤,不吝賜教。

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

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

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