大概兩三周前通過(guò)學(xué)習(xí)《Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理》中的Block
章節(jié)慈鸠,系統(tǒng)深入了解了Block
相關(guān)原理和內(nèi)存管理的內(nèi)容起暮,昨天閑暇時(shí)回想起來(lái)麦向,感覺(jué)有些東西又模糊了,內(nèi)容記得七七八八,太碎片化了。索性好記性不如爛筆頭惰蜜,把自己的理解整理記錄一下。
將Objective-C代碼轉(zhuǎn)換為C\C++代碼
Clang
(LLVM
編譯器)具有轉(zhuǎn)換為我們可讀源代碼的功能受神。
//如果需要鏈接其他框架抛猖,使用-framework參數(shù)。比如-framework UIKit
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的cpp文件
設(shè)置了sdk
的平臺(tái)和cpu
架構(gòu),減少轉(zhuǎn)換出來(lái)的代碼量财著,方便查閱养交。
可能會(huì)遇到以下問(wèn)題:
cannot create __weak reference in file using manual reference
解決方案:支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本瓢宦,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 OC源文件 -o 輸出的cpp文件
Block
底層結(jié)構(gòu)
當(dāng)Block
沒(méi)有自動(dòng)捕獲變量時(shí):
//Block定義的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct _block_impl impl;
struct __main_block_desc_0 *Desc;
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我們代碼中寫的Block
在底層會(huì)被轉(zhuǎn)換成類似上面這樣子的結(jié)構(gòu)體類型,struct __main_block_impl_0
灰羽。
而struct __main_block_impl_0
中包含了兩個(gè)結(jié)構(gòu)體:struct _block_impl impl
和struct __main_block_desc_0 *Desc
驮履,以及一個(gè)構(gòu)造函數(shù)。再看一下兩個(gè)結(jié)構(gòu)體的定義廉嚼。
struct _block_impl {
void *isa; //指向了block所屬的類型
int Flags;
int Reserved; // 預(yù)留
void *FuncPtr; // 函數(shù)指針玫镐,指向block中方法實(shí)現(xiàn)
};
//存儲(chǔ)block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved; // 預(yù)留
unsigned long Block_size; // Block的大小
}
通過(guò)上面可以看出Blcok
也是包含一個(gè)isa
指針怠噪,因此也是一種OC
對(duì)象恐似。具體是什么類,因?yàn)樯婕暗?code>Blcok的內(nèi)存管理傍念,所以后面篇幅再深入討論矫夷。
再看一下給Blcok
結(jié)構(gòu)體賦值和調(diào)用的代碼:
//賦值部分
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//調(diào)用部分
(*blk->impl.FuncPtr)(blk);
賦值部分就是調(diào)用了__main_block_impl_0
的構(gòu)造函數(shù),將方法和__main_block_desc_0
類型的結(jié)構(gòu)體作為參數(shù)傳遞進(jìn)入憋槐。
方法調(diào)用是通過(guò)Blcok
的結(jié)構(gòu)體取出其中的函數(shù)指針双藕,直接調(diào)用該函數(shù),同時(shí)將Block
自身作為參數(shù)傳遞給方法實(shí)現(xiàn)阳仔。
先對(duì)簡(jiǎn)單的Block
有個(gè)印象忧陪。
Block
變量捕獲機(jī)制
int c = 30; // 全局變量(數(shù)據(jù)段,不需要捕獲)
- (void)blockTest {
auto int a = 10;//局部auto變量(棧區(qū),值捕獲)
auto __strong NSObject *object = [[NSObject alloc] init];//局部auto變量(棧區(qū)近范,值捕獲)
static int b = 20;//局部static變量(數(shù)據(jù)段嘶摊,指針捕獲)
void (^block)(void) = ^(void) {
NSLog(@"a:%d b:%d c:%d",a,b,c);
NSLog(@"object:%@",object);
};
block();
}
為了在Block
內(nèi)部可以訪問(wèn)外部的變量,Block
有個(gè)變量捕獲機(jī)制评矩。那么什么樣的變量才會(huì)捕獲叶堆,什么樣的不會(huì)捕獲呢?
-
局部變量
-
auto
變量(平時(shí)我們?cè)诜椒ㄖ新暶鞯姆?code>static局部變量稚照,只是省略了auto關(guān)鍵字)蹂空,這種情況是值捕獲。 -
static
變量和結(jié)構(gòu)體果录,這種情況是指針捕獲上枕。
-
全局變量:不會(huì)捕獲,因?yàn)椴恍枰东@就可以訪問(wèn)弱恒。
總結(jié)就是:只捕獲局部變量辨萍。
Block
捕獲變量之后代碼什么樣子?
將上面的- (void)blockTest
轉(zhuǎn)換C看一下:
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
int a;
int *b;
NSObject *object;
// 省略構(gòu)造函數(shù)...
};
嗯,備注的沒(méi)有錯(cuò)。變量a
和object
都是值捕獲锈玉,而變量b
捕獲的是*b
爪飘,是指針的捕獲,而c
沒(méi)有捕獲拉背。
Block
的內(nèi)存管理
Block
有3種類型师崎,可以通過(guò)調(diào)用class
方法或者isa
指針查看具體類型,最終都是繼承自NSBlock
類型椅棺。
- NSGlobalBlock ( _NSConcreteGlobalBlock )//全局
- NSStackBlock ( _NSConcreteStackBlock ) //棧
- NSMallocBlock ( _NSConcreteMallocBlock )//堆
除了打印犁罩,那么怎么判斷一個(gè)Block
的具體類型...?
-
NSGlobalBlock : 沒(méi)有訪問(wèn)
auto
變量两疚。 -
NSStackBlock : 訪問(wèn)了
auto
變量床估。 -
NSMallocBlock :
__NSStackBlock__
調(diào)用了copy
。
可能有的同學(xué)在這里這樣子測(cè)試一下诱渤,發(fā)現(xiàn)上面的判斷依據(jù)并不對(duì)...
明明Block訪問(wèn)的是auto
變量丐巫,但是Block
的類型是__NSMallocBlock__
吶,并不是__NSStackBlock__
,你說(shuō)的不對(duì)勺美。不著急递胧,其實(shí)這里還涉及到另外一個(gè)問(wèn)題:Block
的內(nèi)存管理。
對(duì)一個(gè)Blcok
進(jìn)行copy
操作后赡茸,對(duì)三種類型的Blcok
產(chǎn)生的影響:
-
__NSGlobalBlock__
:-
copy
前:Block
位于數(shù)據(jù)段中谓着; -
copy
后:不產(chǎn)生任何影響。
-
-
__NSStackBlock__
:-
copy
前:Block
位于函數(shù)棧中坛掠; -
copy
后:從棧中復(fù)制一份到堆中赊锚。
-
-
__NSMallocBlock__
:-
copy
前:Block
位于堆中; -
copy
后:Block
的引用計(jì)數(shù)增加屉栓。
-
在ARC環(huán)境下舷蒲,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上,比如以下情況:
-
Block
作為函數(shù)返回值時(shí)友多。 - 將
Block
賦值給__strong
指針時(shí)牲平。 -
Block
作為Cocoa API
中方法名含有usingBlock
的方法參數(shù)時(shí)。 -
Block
作為GCD API
的方法參數(shù)時(shí)域滥。
在之前的圖片(001)中纵柿,就是其中的第二種情況,Block
被賦值給__strong
指針启绰。
這也是為什么我們習(xí)慣于用copy
關(guān)鍵字昂儒,來(lái)修飾一個(gè)Block
。以及將Block
當(dāng)做參數(shù)傳遞時(shí)委可,安全起見渊跋,會(huì)對(duì)Block
參數(shù)執(zhí)行copy
操作。
Block
對(duì)對(duì)象類型變量的強(qiáng)弱引用問(wèn)題
-
當(dāng)
Block
內(nèi)部訪問(wèn)了對(duì)象類型的auto變量
時(shí):- 如果
Block
是在棧上,將不會(huì)對(duì)auto變量
產(chǎn)生強(qiáng)引用拾酝。就是說(shuō)棧上的Block
不會(huì)強(qiáng)引用一個(gè)對(duì)象
- 如果
-
當(dāng)
Block
被拷貝到堆上時(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ì)根據(jù)auto變量
的修飾符(__strong
燕少、__weak
、__unsafe_unretained
)做出相應(yīng)的操作蒿囤,形成強(qiáng)引用(retain
)或者弱引用
- 會(huì)調(diào)用
-
當(dāng)
Block
從堆上移除時(shí):- 會(huì)調(diào)用
Block
內(nèi)部的dispose
函數(shù) -
dispose
函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose
函數(shù) -
_Block_object_dispose
函數(shù)會(huì)自動(dòng)釋放引用的auto變量
(release
)
- 會(huì)調(diào)用
__block
修飾符
__block
的作用:
-
__block
可以用于解決Block
內(nèi)部無(wú)法修改auto變量
值的問(wèn)題客们。 -
__block
不能修飾全局變量、靜態(tài)變量(static
)材诽。
編譯器會(huì)將__block
變量包裝成一個(gè)對(duì)象:
void blockTest() {
__block int a = 10;
__block NSObject *object = [[NSObject alloc] init];
NSLog(@"a:%d",a);
void (^block)(void) = ^(void) {
a = 20;
NSLog(@"object --- %@",object);
};
block();
}
將上面的代碼轉(zhuǎn)換成C++之后可以看到:
// __block int a 被轉(zhuǎn)換為下面的結(jié)構(gòu)體
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
// __block NSObject *object 被轉(zhuǎn)換為下面的結(jié)構(gòu)體
struct __Block_byref_object_1 {
void *__isa;
__Block_byref_object_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *object;
};
__Block_byref_a_0 *__forwarding
是一個(gè)指向自身的指針镶摘。
當(dāng)我們的OC代碼中,去訪問(wèn)被__block
修飾的變量岳守,在底層中是如何去讀取變量呢?
上面的代碼中有一段打印的代碼:
NSLog(@"a:%d",a);
在C++中被轉(zhuǎn)成了:
NSLog((NSString *)//此處省略碌冶,影響閱讀//,(a.__forwarding->a));
在Blcok中修改a的值:
a = 20;
在C++中被轉(zhuǎn)成了:
// 從blcok取出blcok捕獲的被`__block`修飾的變量
__Block_byref_a_0 *a = __cself->a; //__cself是blcok
(a->__forwarding->a) = 20;
在Block
的外部和外部訪問(wèn)變量是通過(guò)a.__forwarding->a
湿痢,訪問(wèn)結(jié)構(gòu)體的__forwarding
指針找到值∑伺樱可能有的同學(xué)有疑問(wèn):不是多此一舉的嘛譬重?結(jié)構(gòu)體->__forwarding->結(jié)構(gòu)體->val
,直接結(jié)構(gòu)體->val
不就可以了嗎?
目前能看出的作用是保持統(tǒng)一的寫法罐氨,當(dāng)然還有其他的原因臀规,后面講解。
總結(jié):
- 當(dāng)沒(méi)有使用
__block
時(shí)栅隐,由于是值捕獲塔嬉,所以哪怕在Block
內(nèi)修改,也不能影響到Block
外變量的值租悄,因此蘋果不允許直接修改谨究。 - 而當(dāng)我們?cè)?code>Block去修改被
__block
修飾的變量時(shí),由于是捕獲到__block
結(jié)構(gòu)體的指針泣棋,這樣就可以我們可以修改Block
外面的值了胶哲。
__block
的內(nèi)存管理
- 開始時(shí)
__block
結(jié)構(gòu)體是一個(gè)在棧
上的結(jié)構(gòu)體,在棧上的內(nèi)存無(wú)所謂強(qiáng)弱引用的關(guān)系潭辈。而__block
結(jié)構(gòu)體包裝的對(duì)象是強(qiáng)或弱引用鸯屿,是通過(guò)你使用__weak
和__strong
哪個(gè)來(lái)修飾決定的。
NSObject *object = [[NSObject alloc] init];
__block __weak typeof(object) weakObject = object;
__block NSObject *strongObjce = object;
-
當(dāng)
Block
被拷貝到堆
上時(shí)把敢,會(huì)自動(dòng)將捕獲的__block
結(jié)構(gòu)體也拷貝到堆上寄摆。由于__block
的結(jié)構(gòu)體也有isa
指針,同時(shí)還在堆空間中修赞,我們可以將它理解成一個(gè)OC的對(duì)象冰肴,__block
對(duì)象。
-
于此同時(shí)還會(huì)將棧上的
__block
結(jié)構(gòu)體中的__forwarding
指針,指向堆空間中的__block
對(duì)象熙尉。Block
捕獲的指針联逻,會(huì)從棧上的__block
結(jié)構(gòu)體變?yōu)槎芽臻g中的__block
對(duì)象,同時(shí)對(duì)__block
對(duì)象強(qiáng)引用检痰。
- 當(dāng)
Block
從堆中移除時(shí):
這樣分析一下包归,除了會(huì)將__block
結(jié)構(gòu)體從棧移動(dòng)到堆之外,和普通形式的auto
對(duì)象內(nèi)存管理铅歼,流程上沒(méi)有什么差別公壤。當(dāng)然具體內(nèi)部調(diào)用的函數(shù)參數(shù)還是有點(diǎn)區(qū)別的:
當(dāng)Block
拷貝到堆上時(shí),都會(huì)通過(guò)copy
函數(shù)來(lái)處理它們
__block變量(假設(shè)變量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
當(dāng)Block
從堆上移除時(shí)椎椰,都會(huì)通過(guò)dispose
函數(shù)來(lái)釋放它們:
__block變量(假設(shè)變量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
能看到,雖然調(diào)用的方法相同厦幅,但是傳遞的參數(shù)類型不同:3和8。這決定了方法內(nèi)部如何去處理流程吧慨飘。
循環(huán)引用
想必讀到這里确憨,應(yīng)該可以理解循環(huán)引用是怎么產(chǎn)生的了。
注意:self
是方法調(diào)用時(shí)傳遞的第一個(gè)參數(shù)瓤的,是局部變量休弃。
怎么解決?
不讓Block
強(qiáng)引用self
,斷掉一條線圈膏,就不會(huì)產(chǎn)生循環(huán)引用塔猾。
我們一般通過(guò)__weak
來(lái)修飾變量,比如這樣:
__weak typeof(self) weakSelf = self;
也可以使用__unsafe_unretained
修飾變量解決稽坤。關(guān)于__unsafe_unretained
可以看這篇文章丈甸。
他們的區(qū)別:
__weak
: 對(duì)于__weak,指針的對(duì)象在它指向的對(duì)象釋放的時(shí)候回轉(zhuǎn)換為nil尿褪,這是一種特別安全的行為老虫。
__unsafe_unretained
: 就像他的名字表達(dá)那樣,__unsafe_unretained會(huì)繼續(xù)指向?qū)ο蟠嬖诘哪莻€(gè)內(nèi)存茫多,即使是在它已經(jīng)銷毀之后祈匙。這會(huì)導(dǎo)致因?yàn)樵L問(wèn)那個(gè)已釋放對(duì)象引起的崩潰。
為了更安全的使用天揖,我們經(jīng)常是這樣寫:
__weak typeof(self) weakSelf = self; //解決循環(huán)應(yīng)用
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
//在Block方法內(nèi)部夺欲,即:局部變量?jī)?nèi);對(duì)weakSelf進(jìn)行一個(gè)強(qiáng)引用今膊,
//這樣可以確保些阅,當(dāng)self其他的強(qiáng)引用都釋放時(shí),仍然保持有一個(gè)強(qiáng)引用斑唬,
//這樣self不會(huì)再block內(nèi)部突然釋放掉市埋,導(dǎo)致后面的代碼出現(xiàn)未知的問(wèn)題黎泣。
//do someThing...//
};