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

上一篇文章iOS底層原理總結(jié) - 探尋block的本質(zhì)(一)中已經(jīng)介紹過(guò)block的底層本質(zhì)實(shí)現(xiàn)以及了解了變量的捕獲,本文繼續(xù)探尋block的本質(zhì)。
Block-Demo
快速寫(xiě)block

block對(duì)對(duì)象變量的捕獲

block一般使用過(guò)程中都是對(duì)對(duì)象變量的捕獲,那么對(duì)象變量的捕獲同基本數(shù)據(jù)類型變量相同嗎?

查看一下代碼思考:當(dāng)在block中訪問(wèn)的為對(duì)象類型時(shí),對(duì)象什么時(shí)候會(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);
            };
        } // 執(zhí)行完畢,person沒(méi)有被釋放
        NSLog(@"--------");
    } // person 釋放
    return 0; 
}

大括號(hào)執(zhí)行完畢之后,person依然不會(huì)被釋放。
上一篇文章提到過(guò),personauto變量,傳入的block的變量同樣為person,
block有一個(gè)強(qiáng)引用引用person,所以block不被銷毀的話,peroson也不會(huì)銷毀。 查看源代碼確實(shí)如此:

image.png

將上述代碼轉(zhuǎn)移到MRC環(huán)境下,在MRC環(huán)境下即使block還在,person卻被釋放掉了。因?yàn)镸RC環(huán)境下block在??臻g,??臻g對(duì)外面的person不會(huì)進(jìn)行強(qiáng)引用。

//MRC環(huán)境下代碼
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];
        } // person被釋放
        NSLog(@"--------");
    }
    return 0;
}

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

block = [^{
   NSLog(@"------block內(nèi)部%d",person.age);
} copy];

上文中也提到過(guò),只需要對(duì)棧空間的block進(jìn)行一次copy操作,將棧空間的block拷貝到堆中,person就不會(huì)被釋放,說(shuō)明堆空間的block可能會(huì)對(duì)person進(jìn)行一次retain操作,以保證person不會(huì)被銷毀。堆空間的block自己銷毀之后也會(huì)對(duì)持有的對(duì)象進(jìn)行release操作。
也就是說(shuō):
??臻g上的block不會(huì)對(duì)對(duì)象強(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 *waekPerson = person;
            block = ^{
                NSLog(@"------block內(nèi)部%d",waekPerson.age);
            };
        }
        NSLog(@"--------");
    }
    return 0;
}

將代碼轉(zhuǎn)化為c++來(lái)看一下上述代碼之間的差別。 __weak修飾變量,需要告知編譯器使用ARC環(huán)境及版本號(hào)否則會(huì)報(bào)錯(cuò),添加說(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

image.png

__weak修飾的變量,在生成的__main_block_impl_0中也是使用__weak修飾。

__main_block_copy_0 和 __main_block_dispose_0

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

image.png

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

copy本質(zhì)就是__main_block_copy_0函數(shù),__main_block_copy_0函數(shù)內(nèi)部調(diào)用_Block_object_assign函數(shù),_Block_object_assign中傳入的是person對(duì)象的地址,person對(duì)象,以及8。

dispose本質(zhì)就是__main_block_dispose_0函數(shù),__main_block_dispose_0函數(shù)內(nèi)部調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)傳入的參數(shù)是person對(duì)象,以及8。

_Block_object_assign函數(shù)調(diào)用時(shí)機(jī)及作用

當(dāng)block進(jìn)行copy操作的時(shí)候就會(huì)自動(dòng)調(diào)用__main_block_desc_0內(nèi)部的__main_block_copy_0函數(shù),__main_block_copy_0函數(shù)內(nèi)部會(huì)調(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ì)person對(duì)象產(chǎn)生強(qiáng)引用或者弱引用??梢岳斫鉃?code>_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ù)不變。

_Block_object_dispose函數(shù)調(diào)用時(shí)機(jī)及作用

當(dāng)block從堆中移除時(shí)就會(huì)自動(dòng)調(diào)用__main_block_desc_0中的__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ì)對(duì)象進(jìn)行retarn操作,因此一旦block捕獲的變量是對(duì)象類型就會(huì)會(huì)自動(dòng)生成copydispose來(lái)對(duì)內(nèi)部引用的對(duì)象進(jìn)行內(nèi)存管理。
    1. 當(dāng)block內(nèi)部訪問(wèn)了對(duì)象類型的auto變量時(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)引用。
    1. 如果block被拷貝到堆上。copy函數(shù)會(huì)調(diào)用_Block_object_assign函數(shù),根據(jù)auto變量的修飾符(__strong,__weak,unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用或者弱引用
    1. 如果block從堆中移除,dispose函數(shù)會(huì)調(diào)用_Block_object_dispose函數(shù),自動(dòng)釋放引用的auto變量。

問(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");
}

打印內(nèi)容

image.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 *waekP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",waekP);
    });
    NSLog(@"touchBegin----------End");
}

打印內(nèi)容

image.png

答:block中對(duì)waekP__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. 通過(guò)示例代碼進(jìn)行總結(jié)。

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

打印內(nèi)容

image.png
- (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(@"person ----- %@",person);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"weakP ----- %@",weakP);
        });
    });
    NSLog(@"touchBegin----------End");
}

打印內(nèi)容

image.png

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不能修改外部的局部變量。通過(guò)之前對(duì)源碼的分析可以知道。

age是在main函數(shù)內(nèi)部聲明的,說(shuō)明age的內(nèi)存存在于main函數(shù)的??臻g內(nèi)部,
但是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ù)的棧空間不一樣,
__main_block_func_0內(nèi)部拿到的ageblock結(jié)構(gòu)體內(nèi)部的age,
因此無(wú)法在__main_block_func_0函數(shù)內(nèi)部去修改main函數(shù)內(nèi)部的變量。

方式一:age使用static修飾。(不推薦 ,這樣age會(huì)一直存在內(nèi)存中)

前文提到過(guò)static修飾的age變量傳遞到block內(nèi)部的是指針,在__main_block_func_0函數(shù)內(nèi)部就可以拿到age變量的內(nèi)存地址,因此就可以在block內(nèi)部修改age的值。

方式二:__block

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

__block int age = 10;

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

image.png

上述源碼中可以發(fā)現(xiàn)

首先被__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ǔ)哪些元素。

image.png

__isa指針__Block_byref_age_0中也有isa指針也就是說(shuō)__Block_byref_age_0本質(zhì)也一個(gè)對(duì)象。

__forwarding__forwarding__Block_byref_age_0結(jié)構(gòu)體類型的,并且__forwarding存儲(chǔ)的值為(__Block_byref_age_0 *)&age,即結(jié)構(gòu)體自己的內(nèi)存地址。

__flags :0

__sizesizeof(__Block_byref_age_0)__Block_byref_age_0所占用的內(nèi)存空間。

age :真正存儲(chǔ)變量的地方,這里存儲(chǔ)局部變量10。

接著將__Block_byref_age_0結(jié)構(gòu)體age存入__main_block_impl_0結(jié)構(gòu)體中,并賦值給__Block_byref_age_0 *age;

image.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的值。

image.png

為什么要通過(guò)__forwarding獲取age變量的值?

__forwarding是指向自己的指針。這樣的做法是為了方便內(nèi)存管理,之后內(nèi)存管理章節(jié)會(huì)詳細(xì)解釋。

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

__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

image.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)有修改arry的內(nèi)存地址,因此array不需要使用__block修飾也可以正確編譯。

因此當(dāng)僅僅是使用局部變量的內(nèi)存地址,而不是修改的時(shí)候,盡量不要添加__block,通過(guò)上述分析我們知道一旦添加了__block修飾符,系統(tǒng)會(huì)自動(dòng)創(chuàng)建相應(yīng)的結(jié)構(gòu)體,占用不必要的內(nèi)存空間。

2. 上面提到過(guò)__block修飾的age變量在編譯時(shí)會(huì)被封裝為結(jié)構(gòu)體,那么當(dāng)在外部使用age變量的時(shí)候,使用的是__Block_byref_age_0結(jié)構(gòu)體呢?還是__Block_byref_age_0結(jié)構(gòu)體內(nèi)的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)

image.png

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

  1. __forwarding其中存儲(chǔ)的地址確實(shí)是age結(jié)構(gòu)體變量自己的地址
  2. age中存儲(chǔ)這修改后的變量20。
image.png

上面也提到過(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)體內(nèi)age變量的地址。

image.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

__block內(nèi)存管理

上文提到當(dāng)block中捕獲對(duì)象類型的變量時(shí),block中的__main_block_desc_0結(jié)構(gòu)體內(nèi)部會(huì)自動(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)存管理。當(dāng)blcokcopy到堆上時(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)引用(相當(dāng)于retain)

首先通過(guò)一張圖看一下block復(fù)制到堆上時(shí)內(nèi)存變化

image.png

當(dāng)blockcopy到堆上時(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變量。

image.png

block內(nèi)部決定什么時(shí)候?qū)⒆兞繌?fù)制到堆中,什么時(shí)候?qū)ψ兞孔鲆糜?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;
}

將上述代碼轉(zhuǎn)化為c++代碼查看不同變量之間的區(qū)別

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  int number;
  NSObject *__strong object;
  NSObject *__weak weakObj;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_person_1 *person; // by ref
  __Block_byref_weakPerson_2 *weakPerson; // by ref

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

上述__main_block_impl_0結(jié)構(gòu)體中看出,沒(méi)有使用__block修飾的變量(object 和 weadObj)則根據(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ù),用來(lái)對(duì)對(duì)象類型的變量進(jìn)行內(nèi)存管理的操作。而結(jié)構(gòu)體對(duì)對(duì)象的引用類型,則取決于block捕獲的對(duì)象類型的變量。weakPerson是弱指針,所以__Block_byref_weakPerson_2對(duì)weakPerson就是弱引用,person是強(qiáng)指針,所以__Block_byref_person_1對(duì)person就是強(qiáng)引用。

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)體自己因此可以找到age變量進(jìn)行修改。

當(dāng)block在棧中時(shí),__Block_byref_age_0結(jié)構(gòu)體內(nèi)的__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指針的作用

image.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;
}

來(lái)到源碼查看__Block_byref_person_0結(jié)構(gòu)體及其聲明

__Block_byref_person_0結(jié)構(gòu)體

typedef void (*Block)(void);
struct __Block_byref_person_0 {
  void *__isa;  // 8 內(nèi)存空間
__Block_byref_person_0 *__forwarding; // 8
 int __flags; // 4
 int __size;  // 4
 void (*__Block_byref_id_object_copy)(void*, void*); // 8
 void (*__Block_byref_id_object_dispose)(void*); // 8
 Person *__strong person; // 8
};
// 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48 
// __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ù)為__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è)字節(jié)。并且_Block_object_assign最后傳入的參數(shù)是131,同block直接對(duì)對(duì)象進(jìn)行內(nèi)存管理傳入的參數(shù)3,8都不同??梢圆孪?code>_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)引用或者弱引用。

image.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)引用還是弱引用。

image.png

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

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ì)象可以正常釋放。

循環(huá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;
}

運(yùn)行代碼打印內(nèi)容

block的copy[55423:9158212] 大括號(hào)結(jié)束啦

可以發(fā)現(xiàn)大括號(hào)結(jié)束之后,person依然沒(méi)有被釋放,產(chǎn)生了循環(huán)引用。

通過(guò)一張圖看一下他們之間的內(nèi)存結(jié)構(gòu)

image.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對(duì)強(qiáng)引用,而block內(nèi)部對(duì)person的引用為弱引用最好。

使用__weak__unsafe_unretained修飾符可以解決循環(huán)引用的問(wèn)題

我們上面也提到過(guò)__weak會(huì)使block內(nèi)部將指針變?yōu)槿踔羔槨?code>block對(duì)person對(duì)象為弱指針的話,也就不會(huì)出現(xiàn)相互引用而導(dǎo)致不會(huì)被釋放了。

image.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ì)產(chǎn)生前引用,不安全,指向的對(duì)象銷毀時(shí),指針存儲(chǔ)的地址值不變。

使用__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;
}

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

image.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)引用就自動(dòng)斷開(kāi)。該釋放的時(shí)候就會(huì)釋放了。但是有弊端,必須執(zhí)行block,并且在block內(nèi)部將person對(duì)象置為nil。也就是說(shuō)在block執(zhí)行之前代碼是因?yàn)檠h(huán)引用導(dǎo)致內(nèi)存泄漏的。

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

使用__unsafe_unretained解決。在MRC環(huán)境下不支持使用__weak,使用原理同ARC環(huán)境下相同,這里不在贅述。

使用__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)題。

__strong__weak

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

block內(nèi)部重新使用__strong修飾self變量是為了在block內(nèi)部有一個(gè)強(qiáng)指針指向weakSelf避免在block調(diào)用的時(shí)候weakSelf已經(jīng)被銷毀。

參考文章:https://juejin.im/post/5b0d026bf265da090e3decb7

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