上一篇文章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
依然不會被釋放。上一篇文章提到過,person
為aotu
變數,傳入的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修飾的變數,在生成的__main_block_impl_0
中也是使用__weak
修飾。
__main_block_copy_0 和 __main_block_dispose_0
當block中捕獲物件型別的變數時,我們發現block結構體__main_block_impl_0
的描述結構體__main_block_desc_0
中多了兩個引數copy
和dispose
函式,檢視原始碼:
copy
和dispose
函式中傳入的都是__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
物件自己的引用計數。
總結
-
一旦block中捕獲的變數為物件型別,
block
結構體中的__main_block_desc_0
會出兩個引數copy
和dispose
。因為訪問的是個物件,block希望擁有這個物件,就需要對物件進行引用,也就是進行記憶體管理的操作。比如說對物件進行retarn操作,因此一旦block捕獲的變數是物件型別就會會自動生成copy
和dispose
來對內部引用的物件進行記憶體管理。 -
當block內部訪問了物件型別的auto變數時,如果block是在棧上,block內部不會對person產生強引用。不論block結構體內部的變數是
__strong
修飾還是__weak
修飾,都不會對變數產生強引用。 -
如果block被拷貝到堆上。
copy
函式會呼叫_Block_object_assign
函式,根據auto變數的修飾符(__strong,__weak,unsafe_unretained)做出相應的操作,形成強引用或者弱引用 -
如果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
內部拿到的age
是block
結構體內部的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
修飾的age
變數宣告變為名為age
的__Block_byref_age_0
結構體,也就是說加上__block
修飾的話捕獲到的block
內的變數為__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
__size
:sizeof(__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
,首先取出__main_block_impl_0
中的age
,通過age結構體拿到__forwarding
指標,上面提到過__forwarding中
儲存的就是__Block_byref_age_0
結構體本身,這裡也就是age(__Block_byref_age_0)
,在通過__forwarding
拿到結構體中的age(10)
變數並修改其值。
後續NSLog中使用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_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;
}
複製程式碼
列印斷點檢視結構體內部結構
通過檢視blockImpl結構體其中的內容,找到age
結構體,其中重點觀察兩個元素:
__forwarding
其中儲存的地址確實是age結構體變數自己的地址age
中儲存這修改後的變數20。
上面也提到過,在block
中使用或修改age
的時候都是通過結構體__Block_byref_age_0
找到__forwarding
在找到變數age
的。
另外apple為了隱藏__Block_byref_age_0
結構體的實現,列印age變數的地址發現其實是__Block_byref_age_0
結構體內age
變數的地址。
通過上圖的計算可以發現列印age
的地址同__Block_byref_age_0
結構體內age
值的地址相同。也就是說外面使用的age,代表的就是結構體內的age值。所以直接拿來用的age
就是之前宣告的int age
。
__block記憶體管理
上文提到當block中捕獲物件型別的變數時,block中的__main_block_desc_0
結構體內部會自動新增copy
和dispose
函式對捕獲的變數進行記憶體管理。
那麼同樣的當block內部捕獲__block
修飾的物件型別的變數時,__Block_byref_person_0
結構體內部也會自動新增__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
對被__block
包裝成結構體的物件進行記憶體管理。
當block
記憶體在棧上時,並不會對__block
變數產生記憶體管理。當blcok
被copy
到堆上時
會呼叫block
內部的copy
函式,copy
函式內部會呼叫_Block_object_assign
函式,_Block_object_assign
函式會對__block
變數形成強引用(相當於retain)
首先通過一張圖看一下block複製到堆上時記憶體變化
當block
被copy
到堆上時,block
內部引用的__block
變數也會被複制到堆上,並且持有變數,如果block
複製到堆上的同時,__block
變數已經存在堆上了,則不會複製。
當block從堆中移除的話,就會呼叫dispose函式,也就是__main_block_dispose_0
函式,__main_block_dispose_0
函式內部會呼叫_Block_object_dispose
函式,會自動釋放引用的__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_2
對weakPerson
就是弱引用,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
指標的作用
因此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_0
對weakPerson
依然是強引用,但是__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
內部將指標變為弱指標。block
對person
物件為弱指標的話,也就不會出現相互引用而導致不會被釋放了。
__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_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