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

xx_cc發表於2018-06-03

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

block對物件變數的捕獲

block一般使用過程中都是對物件變數的捕獲,那麼物件變數的捕獲同基本資料型別變數相同嗎?

檢視一下程式碼思考:當在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不會進行強引用。

//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不會被釋放。

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

上文中也提到過,只需要對棧空間的block進行一次copy操作,將棧空間的block拷貝到堆中,person就不會被釋放,說明堆空間的block可能會對person進行一次retain操作,以保證person不會被銷燬。堆空間的block自己銷燬之後也會對持有的物件進行release操作。

也就是說棧空間上的block不會對物件強引用,堆空間的block有能力持有外部呼叫的物件,即對物件進行強引用或去除強引用的操作。

__weak

__weak新增之後,person在作用域執行完畢之後就被銷燬了。

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

相關文章