block的原理是怎樣埃叭?本質(zhì)是什么?
block本質(zhì)上也是一個OC對象晒他,block是封裝了函數(shù)調(diào)用與及調(diào)用環(huán)境的OC對象,源碼實例圖:
另外block布局底層結(jié)構(gòu)圖如下
block的變量捕捉(capture)
為了保證block能夠正常訪問外部的變量,block有個變量捕捉機制
auto 變量捕捉
另外auto變量的捕捉是值傳遞笼平,這個和static和全局變量都是不一樣的园骆,可以通過clang的源碼查看auto和static的區(qū)別:
可以看到static是指針傳遞,而auto是值傳遞寓调,有這樣的差異是因為在執(zhí)行函數(shù)之后age就變成了垃圾數(shù)據(jù)锌唾,所以執(zhí)行block的時候,不可能去訪問age的內(nèi)存夺英,但是static 的內(nèi)存是一直貫穿整個運行的生命周期的晌涕?
全局變量,static全局變量痛悯,staitc局部變量
可以看到全局變量并沒有捕獲到block里面余黎,但是局部的static會,因為局部變量只能在局部中訪問灸蟆,并且是跨域訪問驯耻,所以為了能訪問正確,一定需要捕捉進去炒考,因為全局變量直接在data區(qū)域可缚,是可以直接訪問的,不需要捕獲進去斋枢,循環(huán)引用也是因為基于這種捕捉情況下產(chǎn)生的帘靡,例如
- (void)test{
? ? void (^block)(void) = ^{
? ? ? ? NSLog(@"-----%p",self);
????}
}
因為OC的機制,test方法會自動攜帶(id self,SEL _cmd)兩個默認參數(shù)瓤帚,所以可以當做是局部變量捕捉進去描姚,當出現(xiàn)block引用self涩赢,self又引用block的時候就會導(dǎo)致循環(huán)引用了,上面的例子轩勘,目前這樣看來當然是不會的筒扒,這個只是說明是導(dǎo)致循環(huán)引用的一個原理而已
對象類型的auto變量捕捉
當block內(nèi)部訪問了對象類型的auto變量時,如果block是在棧上绊寻,將不會auto變量產(chǎn)生強引用花墩,如果block被拷貝到堆上,會調(diào)用block內(nèi)部的copy函數(shù)澄步,copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)冰蘑,_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong,__weak,__unsafe_unretained)作出相應(yīng)的操作,形成強引用(retain)或者弱引用,如果block從堆上移除村缸,會調(diào)用block內(nèi)部的dispose函數(shù)祠肥,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會自動釋放引用的auto變量(release)
_Block_object_assign((void *)&dst->a,(void *)src->a,3/*BLOCK_FIELD_IS_BYREF*)
_Block_object_dispose((void *))src->a ,3/*BLOCK_FIELD_IS_BYREF*/);
對象類型的__block變量
_Block_object_assign((void *)&dst->p,(void *)src->p,8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void *)&src-p,8/*BLOCK_FIELD_IS_OBJECT*/);
當__block被拷貝到堆上梯皿,會調(diào)用__block內(nèi)部的copy函數(shù)仇箱,copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong,__weak,__unsafe_unretained)作出相應(yīng)的操作索烹,形成強引用(retain)或者弱引用(注意:這里僅僅限于ARC時會retain工碾,MRC時不會retain),如果block從堆上移除,會調(diào)用block內(nèi)部的dispose函數(shù)百姓,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會自動釋放引用的auto變量(release)
block的類型
block有3中類型况木,可以通過調(diào)用class方法或者isa指針查看具體類型垒拢,最終都是繼承NSBlock類型
__NSGlobalBlock__(_NSConcreteGlobalBlock)
__NSStackBlock__(_NSConcreteStackBlock)
__NSMallocBlock__(_NSConcreteMallocBlock)
證明:
從圖中我們可以看到這和上面的圖片描述不一樣,原因是什么呢火惊?在解釋這個問題之前求类,我們在看看這個例子:
從上面的圖是不是可以悟出什么東西來呀,上面arc的情況下能正常打印結(jié)果屹耐,說明了arc幫我們額外做了一些操作尸疆,例如copy block從棧到堆,打印[block copy] 的值和block的地址值是一樣就說明了這一點惶岭,另外從clang出來的全部都是satckblock類型的寿弱,說明llvm編譯器在運行時幫我們做了一些轉(zhuǎn)換類型的操作
block中的copy
ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復(fù)制到堆上按灶,比如以下情況:
1.block作為函數(shù)返回值時
SLBlock myblock(){
? ? int age = 10;
? ? return ^{
? ? ? ? NSLog(@"------%d",age);
? ? }
}
終端打印結(jié)果圖:
注意:如果沒有age1變量的話症革,因為沒有訪問auto變量,會當做全局變量處理鸯旁,打印出來是global類型
2.將block賦值給__strong指針時
int main(int argc,const char* argv[]) {
? ? @autoreleasepool {
? ? ? ? int age =10;
? ? ? ? SLBlock block = ^{//默認是strong指針引用
? ? ? ? ? ? NSLog(@"---------%d", age);
? ? ? ? };
? ? ? ? NSLog(@"%@", [block class]);
?}
? ? NSLog(@"%@", [block class]);
}
如果將上述的代碼更改成
int age = 10
NSLog(@"%@",[^{NSLog(@"----------%d",age);} class]);
這個時候這個打印應(yīng)該是stack噪矛,因為沒有strong指針引用
3.block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時
4.block作為GCD API的方法參數(shù)時
MRC下block屬性的建議寫法
@property (copy, nonatomic) void(^block)(void);
ARC下block屬性的建議寫法
@property (strong, nonatomic) void(^block)(void);
@property (copy, nonatomic) void(^block)(void);
__blcok 修飾符
__block 可以用于解決block內(nèi)部無法修改auto變量值的問題量蕊,__block不能修飾全局變量,靜態(tài)變量(static),編譯器會將__block變量包裝成一個對象
__block 的內(nèi)存管理
當block在棧上艇挨,并不會對__block變量產(chǎn)生強引用残炮,當block被copy到堆上時,會調(diào)用block內(nèi)部的copy函數(shù)缩滨,copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)势就,_Block_object_assign函數(shù)會對_block變量形成強引用(retain),如下圖所示:
當block從堆中移除時楷怒,會調(diào)用block內(nèi)部的dispose函數(shù)蛋勺,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose 函數(shù)會自動釋放引用的_block變量(release),如下圖所示:
從clang出來的源碼中,我們可以發(fā)現(xiàn)里面__block修飾的會有一個__forwarding指針鸠删,這個指針是干嘛用的呢抱完?然而這里__forwarding指針真的永遠指向自己么?我們來做一個實驗
可以看到MRC環(huán)境下刃泡,var->forwarding->var(age的指針)是相同的巧娱,說明棧中的block并沒有拷貝到堆中,并且此時的forwarding指向的就是自己棧上block的內(nèi)存地址烘贴,再看一下這個情形:
可以看到MRC下block使用copy禁添,和ARC下直接定義是一樣的情況,說明ARC下桨踪,自動幫我們將strong 引用的block拷貝到了堆中老翘,這個時候,可以發(fā)現(xiàn)兩個age的地址值并不一樣锻离,由此可以說明_forwarding 指針指向的不是同一個并且由打印結(jié)果age的值可以看到铺峭,block里面age的值和block外age的值是一樣,都是30汽纠,所以在使用__block之后卫键,在棧中訪問age的時候,val->forwarding->val 訪問的也是堆中的age值虱朵,在堆中訪問age的時候莉炉,val->forwarding->val 訪問的是堆中的age值,那這個說明了forwarding是為了讓我們更好的管理內(nèi)存的碴犬,不論現(xiàn)在block是出于棧中還是堆中絮宁,都不會影響到尋找到的相關(guān)信息,當block是在棧中翅敌,__forwarding指向的就是棧本身的地址羞福,當block copy到堆中的時候,__forwarding指針指向的就是堆本身的地址蚯涮,如下圖所示:
上面中有沒有人存在一個這樣的問題治专,就是age的地址值卖陵,到底是__Block_byref_age_0 *age 的地址值,還是__Block_byref_age_0 *age 里面的變量age的地址值呢张峰?我們來證明一下:
__weak問題
在使用clang轉(zhuǎn)換OC為C++代碼時泪蔫,可能會遇到以下問題
cannot create __weak reference in file using manual reference
解決方案:支持ARC,指定運行時系統(tǒng)版本喘批,比如:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
循環(huán)引用
例子1:
可以看到在打印之前撩荣,person因為出了作用域,應(yīng)該被銷毀才對的饶深,但是這里斌沒有執(zhí)行dealloc操作餐曹,說明person沒有被釋放,內(nèi)存泄漏了敌厘,我們來分析一下內(nèi)存分布圖:
可以看到運行到大括號外面的時候台猴,箭頭1銷毀,但是這個時候2俱两,3還是相互引用饱狂,導(dǎo)致不能釋放,所以出現(xiàn)了循環(huán)引用宪彩,這個時候應(yīng)該怎么解決呢休讳?直接讓其中一根線是弱引用就可以了,3弱引用也可以尿孔,2弱引用也可以俊柔,因為這里block是person的一個屬性,所以想person銷毀的時候活合,block就銷毀婆咸,那么我這里就是讓block里面的引用弄成弱引用就可以了,例如下圖所示:
相應(yīng)的代碼更改成為:
大括號結(jié)束后芜辕,引用1銷毀,person沒有強引用指向块差,person銷毀侵续,person銷毀之后,因為3是若引用憨闰,所以block銷毀状蜗,內(nèi)存循環(huán)應(yīng)用解除,這里除了用__weak 之外鹉动,使用__unsafe__unretained也不會產(chǎn)生強引用轧坎,但是不安全,指向的對象銷毀之后泽示,指針存儲的地址不變缸血,再次訪問的時候會導(dǎo)致野指針蜜氨,__weak會賦值成nil,安全捎泻;當然通過__block 也可以飒炎,具體可以看下面的補充要點;
補充要點
1.block的屬性修飾詞為什么是copy笆豁?使用block有哪些使用注意郎汪?
block一旦沒有進行copy操作,就不會在堆上闯狱,需要注意循環(huán)引用問題,例如下圖展示:
使用ARC解決循環(huán)引用
使用MRC解決循環(huán)引用
MRC情況下煞赢,如果不加__unsafe_unretained ,或者__block 修飾的話,如果block使用了copy拷貝到堆上的時候哄孤,因為聲明的變量是強引用照筑,在內(nèi)部會對變量使用retain一次,這樣最后就會導(dǎo)致強引用了录豺,也就是會循環(huán)引用
可以添加微信一起交流學習:fslskz