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

上一篇文章iOS底層原理總結 - 探尋block的本質(一)中已經介紹過block的底層本質實現以及瞭解了變量的捕獲,本文繼續探尋block的本質。ios

block對對象變量的捕獲

block通常使用過程當中都是對對象變量的捕獲,那麼對象變量的捕獲同基本數據類型變量相同嗎?c++

查看一下代碼思考:當在block中訪問的爲對象類型時,對象何時會銷燬?面試

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內部%d",person.age);
            };
        } // 執行完畢,person沒有被釋放
        NSLog(@"--------");
    } // person 釋放
    return 0; 
}
複製代碼

大括號執行完畢以後,person依然不會被釋放。上一篇文章提到過,personaotu變量,傳入的block的變量一樣爲person,即block有一個強引用引用person,因此block不被銷燬的話,peroson也不會銷燬。 查看源代碼確實如此安全

強指針引用

將上述代碼轉移到MRC環境下,在MRC環境下即便block還在,person卻被釋放掉了。由於MRC環境下block在棧空間,棧空間對外面的person不會進行強引用。bash

//MRC環境下代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            block = ^{
                NSLog(@"------block內部%d",person.age);
            };
            [person release];
        } // person被釋放
        NSLog(@"--------");
    }
    return 0;
}
複製代碼

block調用copy操做以後,person不會被釋放。app

block = [^{
   NSLog(@"------block內部%d",person.age);
} copy];
複製代碼

上文中也提到過,只須要對棧空間的block進行一次copy操做,將棧空間的block拷貝到堆中,person就不會被釋放,說明堆空間的block可能會對person進行一次retain操做,以保證person不會被銷燬。堆空間的block本身銷燬以後也會對持有的對象進行release操做。iphone

也就是說棧空間上的block不會對對象強引用,堆空間的block有能力持有外部調用的對象,即對對象進行強引用或去除強引用的操做。函數

__weak

__weak添加以後,person在做用域執行完畢以後就被銷燬了。post

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內部%d",waekPerson.age);
            };
        }
        NSLog(@"--------");
    }
    return 0;
}
複製代碼

將代碼轉化爲c++來看一下上述代碼之間的差異。 __weak修飾變量,須要告知編譯器使用ARC環境及版本號不然會報錯,添加說明-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

__weak修飾變量

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

__main_block_copy_0 和 __main_block_dispose_0

當block中捕獲對象類型的變量時,咱們發現block結構體__main_block_impl_0的描述結構體__main_block_desc_0中多了兩個參數copydispose函數,查看源碼:

__main_block_copy_0、__main_block_dispose_0函數

copydispose函數中傳入的都是__main_block_impl_0結構體自己。

copy本質就是__main_block_copy_0函數,__main_block_copy_0函數內部調用_Block_object_assign函數,_Block_object_assign中傳入的是person對象的地址,person對象,以及8。

dispose本質就是__main_block_dispose_0函數,__main_block_dispose_0函數內部調用_Block_object_dispose函數,_Block_object_dispose函數傳入的參數是person對象,以及8。

_Block_object_assign函數調用時機及做用

當block進行copy操做的時候就會自動調用__main_block_desc_0內部的__main_block_copy_0函數,__main_block_copy_0函數內部會調用_Block_object_assign函數。

_Block_object_assign函數會自動根據__main_block_impl_0結構體內部的person是什麼類型的指針,對person對象產生強引用或者弱引用。能夠理解爲_Block_object_assign函數內部會對person進行引用計數器的操做,若是__main_block_impl_0結構體內person指針是__strong類型,則爲強引用,引用計數+1,若是__main_block_impl_0結構體內person指針是__weak類型,則爲弱引用,引用計數不變。

_Block_object_dispose函數調用時機及做用

當block從堆中移除時就會自動調用__main_block_desc_0中的__main_block_dispose_0函數,__main_block_dispose_0函數內部會調用_Block_object_dispose函數。

_Block_object_dispose會對person對象作釋放操做,相似於release,也就是斷開對person對象的引用,而person到底是否被釋放仍是取決於person對象本身的引用計數。

總結

  1. 一旦block中捕獲的變量爲對象類型,block結構體中的__main_block_desc_0會出兩個參數copydispose。由於訪問的是個對象,block但願擁有這個對象,就須要對對象進行引用,也就是進行內存管理的操做。好比說對對象進行retarn操做,所以一旦block捕獲的變量是對象類型就會會自動生成copydispose來對內部引用的對象進行內存管理。

  2. 當block內部訪問了對象類型的auto變量時,若是block是在棧上,block內部不會對person產生強引用。不論block結構體內部的變量是__strong修飾仍是__weak修飾,都不會對變量產生強引用。

  3. 若是block被拷貝到堆上。copy函數會調用_Block_object_assign函數,根據auto變量的修飾符(__strong,__weak,unsafe_unretained)作出相應的操做,造成強引用或者弱引用

  4. 若是block從堆中移除,dispose函數會調用_Block_object_dispose函數,自動釋放引用的auto變量。

問題

1. 下列代碼person在什麼時候銷燬 ?

- (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");
}
複製代碼

打印內容

打印內容

答:上文提到過ARC環境中,block做爲GCD API的方法參數時會自動進行copy操做,所以block在堆空間,而且使用強引用訪問person對象,所以block內部copy函數會對person進行強引用。當block執行完畢須要被銷燬時,調用dispose函數釋放對person對象的引用,person沒有強指針指向時纔會被銷燬。

2. 下列代碼person在什麼時候銷燬 ?

- (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");
}
複製代碼

打印內容

打印內容

答:block中對waekP__weak弱引用,所以block內部copy函數會對person一樣進行弱引用,當大括號執行完畢時,person對象沒有強指針引用就會被釋放。所以block塊執行的時候打印null

3. 經過示例代碼進行總結。

- (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(@"weakP ----- %@",waekP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"person ----- %@",person);
        });
    });
    NSLog(@"touchBegin----------End");
}
複製代碼

打印內容

打印內容

- (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");
}
複製代碼

打印內容

打印內容

block內修改變量的值

本部分分析基於下面代碼。

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

複製代碼

默認狀況下block不能修改外部的局部變量。經過以前對源碼的分析能夠知道。

age是在main函數內部聲明的,說明age的內存存在於main函數的棧空間內部,可是block內部的代碼在__main_block_func_0函數內部。__main_block_func_0函數內部沒法訪問age變量的內存空間,兩個函數的棧空間不同,__main_block_func_0內部拿到的ageblock結構體內部的age,所以沒法在__main_block_func_0函數內部去修改main函數內部的變量。

方式一:age使用static修飾。

前文提到過static修飾的age變量傳遞到block內部的是指針,在__main_block_func_0函數內部就能夠拿到age變量的內存地址,所以就能夠在block內部修改age的值。

方式二:__block

__block用於解決block內部不能修改auto變量值的問題,__block不能修飾靜態變量(static) 和全局變量

__block int age = 10;
複製代碼

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

__block修飾的變量源碼

上述源碼中能夠發現

首先被__block修飾的age變量聲明變爲名爲age__Block_byref_age_0結構體,也就是說加上__block修飾的話捕獲到的block內的變量爲__Block_byref_age_0類型的結構體。

經過下圖查看__Block_byref_age_0結構體內存儲哪些元素。

__Block_byref_age_0賦值

__isa指針__Block_byref_age_0中也有isa指針也就是說__Block_byref_age_0本質也一個對象。

__forwarding__forwarding__Block_byref_age_0結構體類型的,而且__forwarding存儲的值爲(__Block_byref_age_0 *)&age,即結構體本身的內存地址。

__flags :0

__sizesizeof(__Block_byref_age_0)__Block_byref_age_0所佔用的內存空間。

age :真正存儲變量的地方,這裏存儲局部變量10。

接着將__Block_byref_age_0結構體age存入__main_block_impl_0結構體中,並賦值給__Block_byref_age_0 *age;

__Block_byref_age_0 *age賦值

以後調用block,首先取出__main_block_impl_0中的age,經過age結構體拿到__forwarding指針,上面提到過__forwarding中保存的就是__Block_byref_age_0結構體自己,這裏也就是age(__Block_byref_age_0),在經過__forwarding拿到結構體中的age(10)變量並修改其值。

後續NSLog中使用age時也經過一樣的方式獲取age的值。

修改結構體內的age值

爲何要經過__forwarding獲取age變量的值?

__forwarding是指向本身的指針。這樣的作法是爲了方便內存管理,以後內存管理章節會詳細解釋。

到此爲止,__block爲何能修改變量的值已經很清晰了。__block將變量包裝成對象,而後在把age封裝在結構體裏面,block內部存儲的變量爲結構體指針,也就能夠經過指針找到內存地址進而修改變量的值。

__block修飾對象類型

那麼若是變量自己就是對象類型呢?經過如下代碼生成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;
}
複製代碼

經過源碼查看,將對象包裝在一個新的結構體中。結構體內部會有一個person對象,不同的地方是結構體內部添加了內存管理的兩個函數__Block_byref_id_object_copy__Block_byref_id_object_dispose

__block修飾對象類型源碼

__Block_byref_id_object_copy__Block_byref_id_object_dispose函數的調用時機及做用在__block內存管理部分詳細分析。

問題

1. 如下代碼是否能夠正確執行

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;
}
複製代碼

答:能夠正確執行,由於在block塊中僅僅是使用了array的內存地址,往內存地址中添加內容,並無修改arry的內存地址,所以array不須要使用__block修飾也能夠正確編譯。

所以當僅僅是使用局部變量的內存地址,而不是修改的時候,儘可能不要添加__block,經過上述分析咱們知道一旦添加了__block修飾符,系統會自動建立相應的結構體,佔用沒必要要的內存空間。

2. 上面提到過__block修飾的age變量在編譯時會被封裝爲結構體,那麼當在外部使用age變量的時候,使用的是__Block_byref_age_0結構體呢?仍是__Block_byref_age_0結構體內的age變量呢?

爲了驗證上述問題 一樣使用自定義結構體的方式來查看其內部結構

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

複製代碼

打印斷點查看結構體內部結構

_Block_byref_age_0結構體

經過查看blockImpl結構體其中的內容,找到age結構體,其中重點觀察兩個元素:

  1. __forwarding其中存儲的地址確實是age結構體變量本身的地址
  2. age中存儲這修改後的變量20。

上面也提到過,在block中使用或修改age的時候都是經過結構體__Block_byref_age_0找到__forwarding在找到變量age的。

另外apple爲了隱藏__Block_byref_age_0結構體的實現,打印age變量的地址發現實際上是__Block_byref_age_0結構體內age變量的地址。

age的內存地址推算

經過上圖的計算能夠發現打印age的地址同__Block_byref_age_0結構體內age值的地址相同。也就是說外面使用的age,表明的就是結構體內的age值。因此直接拿來用的age就是以前聲明的int age

__block內存管理

上文提到當block中捕獲對象類型的變量時,block中的__main_block_desc_0結構體內部會自動添加copydispose函數對捕獲的變量進行內存管理。

那麼一樣的當block內部捕獲__block修飾的對象類型的變量時,__Block_byref_person_0結構體內部也會自動添加__Block_byref_id_object_copy__Block_byref_id_object_dispose對被__block包裝成結構體的對象進行內存管理。

block內存在棧上時,並不會對__block變量產生內存管理。當blcokcopy到堆上時 會調用block內部的copy函數,copy函數內部會調用_Block_object_assign函數,_Block_object_assign函數會對__block變量造成強引用(至關於retain)

首先經過一張圖看一下block複製到堆上時內存變化

__block copy內存管理

blockcopy到堆上時,block內部引用的__block變量也會被複制到堆上,而且持有變量,若是block複製到堆上的同時,__block變量已經存在堆上了,則不會複製。

當block從堆中移除的話,就會調用dispose函數,也就是__main_block_dispose_0函數,__main_block_dispose_0函數內部會調用_Block_object_dispose函數,會自動釋放引用的__block變量。

__block 釋放內存管理

block內部決定何時將變量複製到堆中,何時對變量作引用計數的操做。

__block修飾的變量在block結構體中一直都是強引用,而其餘類型的是由傳入的對象指針類型決定。

一段代碼更深刻的觀察一下。

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); // 對象類型的局部變量
            NSLog(@"%p",weakObj); // __weak修飾的對象類型的局部變量
            NSLog(@"%p",person); // __block修飾的對象類型的局部變量
            NSLog(@"%p",weakPerson); // __block,__weak修飾的對象類型的局部變量
        };
        block();
    }
    return 0;
}
複製代碼

將上述代碼轉化爲c++代碼查看不一樣變量之間的區別

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結構體中看出,沒有使用__block修飾的變量(object 和 weadObj)則根據他們自己被block捕獲的指針類型對他們進行強引用或弱引用,而一旦使用__block修飾的變量,__main_block_impl_0結構體內一概使用強指針引用生成的結構體。

接着咱們來看__block修飾的變量生成的結構體有什麼不一樣

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修飾對象類型的變量生成的結構體內部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose兩個函數,用來對對象類型的變量進行內存管理的操做。而結構體對對象的引用類型,則取決於block捕獲的對象類型的變量。weakPerson是弱指針,因此__Block_byref_weakPerson_2weakPerson就是弱引用,person是強指針,因此__Block_byref_person_1對person就是強引用。

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函數中會根據變量是強弱指針及有沒有被__block修飾作出不一樣的處理,強指針在block內部產生強引用,弱指針在block內部產生弱引用。被__block修飾的變量最後的參數傳入的是8,沒有被__block修飾的變量最後的參數傳入的是3。

當block從堆中移除時經過dispose函數來釋放他們。

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指針

上面提到過__forwarding指針指向的是結構體本身。當使用變量的時候,經過結構體找到__forwarding指針,在經過__forwarding指針找到相應的變量。這樣設計的目的是爲了方便內存管理。經過上面對__block變量的內存管理分析咱們知道,block被複制到堆上時,會將block中引用的變量也複製到堆中。

咱們重回到源碼中。當在block中修改__block修飾的變量時。

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));
        }
複製代碼

經過源碼能夠知道,當修改__block修飾的變量時,是根據變量生成的結構體這裏是__Block_byref_age_0找到其中__forwarding指針,__forwarding指針指向的是結構體本身所以能夠找到age變量進行修改。

當block在棧中時,__Block_byref_age_0結構體內的__forwarding指針指向結構體本身。

而當block被複制到堆中時,棧中的__Block_byref_age_0結構體也會被複制到堆中一份,而此時棧中的__Block_byref_age_0結構體中的__forwarding指針指向的就是堆中的__Block_byref_age_0結構體,堆中__Block_byref_age_0結構體內的__forwarding指針依然指向本身。

此時當對age進行修改時

// 棧中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding獲取堆中的age結構體
// age->__forwarding->age 修改堆中age結構體的age變量
(age->__forwarding->age) = 20;
複製代碼

經過__forwarding指針巧妙的將修改的變量賦值在堆中的__Block_byref_age_0中。

咱們經過一張圖展現__forwarding指針的做用

__forwarding指針

所以block內部拿到的變量實際就是在堆上的。當block進行copy被複制到堆上時,_Block_object_assign函數內作的這一系列操做。

被__block修飾的對象類型的內存管理

使用如下代碼,生成c++代碼查看內部實現

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結構體及其聲明

__Block_byref_person_0結構體

typedef void (*Block)(void);
struct __Block_byref_person_0 {
  void *__isa;  // 8 內存空間
__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結構體聲明

__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"))
};
複製代碼

以前提到過__block修飾的對象類型生成的結構體中新增長了兩個函數void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);。這兩個函數爲__block修飾的對象提供了內存管理的操做。

能夠看出爲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。找到這兩個函數

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_byref_id_object_copy_131函數中一樣調用了_Block_object_assign函數,而_Block_object_assign函數內部拿到dst指針即block對象本身的地址值加上40個字節。而且_Block_object_assign最後傳入的參數是131,同block直接對對象進行內存管理傳入的參數3,8都不一樣。能夠猜測_Block_object_assign內部根據傳入的參數不一樣進行不一樣的操做的。

經過對上面__Block_byref_person_0結構體佔用空間計算髮現__Block_byref_person_0結構體佔用的空間爲48個字節。而加40剛好指向的就爲person指針。

也就是說copy函數會將person地址傳入_Block_object_assign函數,_Block_object_assign中對Person對象進行強引用或者弱引用。

強引用示意圖

若是使用__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中沒有任何變化,__main_block_impl_0weakPerson依然是強引用,可是__Block_byref_weakPerson_0中對weakPerson變爲了__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;
};
複製代碼

也就是說不管如何block內部中對__block修飾變量生成的結構體都是強引用,結構體內部對外部變量的引用取決於傳入block內部的變量是強引用仍是弱引用。

弱引用示意圖

mrc環境下,儘管調用了copy操做,__block結構體不會對person產生強引用,依然是弱引用。

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會先釋放

block的copy[50480:8737001] -[Person dealloc]
block的copy[50480:8737001] 0x100669a50
複製代碼

當block從堆中移除的時候。會調用dispose函數,block塊中去除對__Block_byref_person_0 *person;的引用,__Block_byref_person_0結構體中也會調用dispose操做去除對Person *person;的引用。以保證結構體和結構體內部的對象能夠正常釋放。

循環引用

循環引用致使內存泄漏。

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

運行代碼打印內容

block的copy[55423:9158212] 大括號結束啦
複製代碼

能夠發現大括號結束以後,person依然沒有被釋放,產生了循環引用。

經過一張圖看一下他們之間的內存結構

產生循環引用示意圖

上圖中能夠發現,Person對象和block對象相互之間產生了強引用,致使雙方都不會被釋放,進而形成內存泄漏。

解決循環引用問題 - ARC

首先爲了能隨時執行block,咱們確定但願person對block對強引用,而block內部對person的引用爲弱引用最好。

使用__weak__unsafe_unretained修飾符能夠解決循環引用的問題

咱們上面也提到過__weak會使block內部將指針變爲弱指針。blockperson對象爲弱指針的話,也就不會出現相互引用而致使不會被釋放了。

使用`__weak`和`__unsafe_unretained`修飾

__weak__unsafe_unretained的區別。

__weak不會產生強引用,指向的對象銷燬時,會自動將指針置爲nil。所以通常經過__weak來解決問題。

__unsafe_unretained不會產生前引用,不安全,指向的對象銷燬時,指針存儲的地址值不變。

使用__block也能夠解決循環引用的問題。

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(@"大括號結束啦");
    return 0;
}
複製代碼

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

使用__block也能夠解決循環引用

上面咱們提到過,在block內部使用變量使用的實際上是__block修飾的變量生成的結構體__Block_byref_person_0內部的person對象,那麼當person對象置爲nil也就斷開告終構體對person的強引用,那麼三角的循環引用就自動斷開。該釋放的時候就會釋放了。可是有弊端,必須執行block,而且在block內部將person對象置爲nil。也就是說在block執行以前代碼是由於循環引用致使內存泄漏的。

解決循環引用問題 - MRC

使用__unsafe_unretained解決。在MRC環境下不支持使用__weak,使用原理同ARC環境下相同,這裏不在贅述。

使用__block也能解決循環引用的問題。由於上文__block內存管理中提到過,MRC環境下,儘管調用了copy操做,__block結構體不會對person產生強引用,依然是弱引用。所以一樣能夠解決循環引用的問題。

__strong__weak

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

block內部從新使用__strong修飾self變量是爲了在block內部有一個強指針指向weakSelf避免在block調用的時候weakSelf已經被銷燬。

面試題

上文中提到的面試題,仔細研讀兩篇文章中均可以找到答案。這裏不在贅述。

底層原理文章專欄

底層原理文章專欄


文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。須要視頻一塊兒探討學習的coder能夠加我Q:2336684744

相關文章
相關標籤/搜索