《iOS底層原理文章匯總》
上一篇文章《iOS-底層原理27-鎖和Block》介紹了NSLock,NSCondition,NSConditionLock,條件變量和條件鎖的底層原理及三種類型Block,本文接著介紹Block底層原理
1.Block循環(huán)引用
I.循環(huán)引用無法釋放的原因


self中持有block,block中持有self,pop時無法進行release,從而無法進入VC的dealloc方法從而無法給block發(fā)送release消息來進行block的retainCount減一到0釋放

II.循環(huán)引用解決辦法
思維誤區(qū):我們經常會想到一個解決辦法就是,通過中間變量WeakSelf解除相互強引用,中間變量WeakSelf儲存在弱引用表中,不會對self的引用計數retainCount進行加1,為什么呢?后面分析。

此種解除循環(huán)引用的方式會有漏洞,若延時執(zhí)行block中的內容將會無法釋放,self的生命周期無法保全,于是就有下面的解決辦法

A.強弱共舞:weakSelf在block內是臨時變量,block內部語句執(zhí)行完后strongSelf釋放,weakSelf從弱引用表里面置為nil,self也可以進行釋放了,屬于自動釋放

B.中介者模式:手動釋放,vc使用完后就置為nil,vc使用__block修飾使block內部能對外界變量進行修改,vc雖為臨時變量在viewDidLoad中,但vc在函數viewDidLoad執(zhí)行完后并未釋放,vc被block捕獲到內存里面進行持有,底層進行了三層拷貝

C.傳值模式:self作為臨時變量傳入block中壓棧,block中不再持有self,不構成持有引用關系

D.proxy和NSObject平行的類,后續(xù)展開分析
2.Block底層原理

I.Block的本質:能進行%@打印,是對象結構體,匿名函數,能存放代碼也叫代碼塊,Blcok的本質是結構體,里面有結構體同名的構造函數__main_block_impl_0在初始化是傳入兩個參數__main_block_func_0和&__main_block_desc_0_DATA,也稱為函數,函數沒有名字引申為匿名函數

__main_block_func_0作為參數傳入構造函數中impl.FuncPtr = fp,調用時傳入block相當于((block)->FuncPtr)(block),相當于執(zhí)行__main_block_func_0函數來執(zhí)行block中的內容printf("LG_Cooci")

II.Block捕獲外部變量:編譯時就在結構體__main_block_impl_0中生成變量int a,值拷貝,a++無法改變a的值,編譯時就存在和調不調用沒有關系,編譯時默認給的isa為NSConcreteStackBlock

編譯時就在結構體__main_block_impl_0中生成變量int a
編譯時默認給的isa為NSConcreteStackBlock
兩個a并不是同一個a,值拷貝,a++無法改變__cself->a的值
a只讀不能寫
III.Block中的捕獲外部變量加__block修飾,在block中對a進行++,會生成結構體__Block_byref_a_0并初始化賦值,&a = *__forwarding,傳入結構體__Block_byref_a_0 a的地址&a到block中,傳入a的指針地址_a->__forwarding賦值給block中的結構體__Block_byref_a_0指針變量a,(a->__forwarding->a)++就是生成的外部變量結構體__Block_byref_a_0中的變量a++,是指針拷貝,指針地址中指向的值進行++

3.__block在底層做了什么,__main_block_copy_0,__main_block_dispose_0,__main_block_desc_0,__main_block_dispose_0,_Block_object_assign底層做了什么
I.Block的copy:在ViewDidLoad中對block下斷點查看匯編,發(fā)現進入objc_retainBlock中,下符號斷點往下執(zhí)行_Block_copy,找到源碼libsystem_blocks.dylib


在通過clang編譯的block.cpp文件中也能發(fā)下Block_private.h文件,Block在底層的形式是Block_layout類型


探索Block內存變化-調用情況-簽名
開始的Block為全局Block類型NSGlobalBlock,若捕獲外界變量,則會由NSStackBlock變?yōu)镹SMallocBlock,在NSStackBlock中進行Block_copy操作變?yōu)镹SMallocBlock
通過匯編分析捕獲外界變量的block類型為NSStackBlock經過拷貝類型為NSMallocBlock

由上文知道,捕獲外界變量的Block本來為NSStackBlock,經過objc_retainBlock-->Block_copy之后變?yōu)镹SMallocBlock

4.Block的invoke
進入匯編查看block_invoke,經過平移操作后得到x8直接調用NSLog打印,開始輸出

為什么會平移16個字節(jié)得到block_invoke呢?通過查看Block的源碼結構,Block_layout中isa8字節(jié),flags4字節(jié),reserved4字節(jié),8+4+4=16字節(jié),平移16字節(jié)得到invoke,invoke為傳入的Block中的代碼塊

5.Block的簽名signature
flags用來做辨識碼,根據flags知道是否存在可選變量Block_descriptor_2和Block_descriptor_3,Block_descriptor_1一定會有,flags為BLOCK_HAS_COPY_DISPOSE,有Block_descriptor_2,flags為BLOCK_HAS_SIGNATURE有Block_descriptor_3


flags是否為BLOCK_HAS_COPY_DISPOSE決定是否有Block_descriptor_2
flags是否為BLOCK_HAS_SIGNATURE決定是否有Block_descriptor_3

若存在Block_descriptor_2和Block_descriptor_3則采取
內存平移的方式獲取
存在Block_descriptor_2則從Block_descriptor_1起
始地址平移Block_descriptor_1的size
存在Block_descriptor_3則從Block_descriptor_1起
始地址平移Block_descriptor_1的size,判斷是否存在
Block_descriptor_2,若存在Block_descriptor_2則再從
當前地址平移Block_descriptor_2的size

通過Block的內存分布情況驗證Block的結構
flags和reserved共同組成8字節(jié),reserved為0則在后面補0可忽略,Block_descriptor_1中的reserved為0,uintptr_t reserved占用8字節(jié),uintptr_t size占8字節(jié)
0x0000000102f9f153為Block_descriptor_3中的第一個元素char *signature
1 << 25為BLOCK_HAS_COPY_DISPOSE的值,與上flags的值為0,則沒有Block_descriptor_2
1 << 30為BLOCK_HAS_SIGNATURE的值,與上flags的值不為0,則有Block_descriptor_3

底層繼續(xù)深入block_hook biffi
6.Block_copy怎么將棧區(qū)對象拷貝到堆區(qū)的,Block_copy源碼
- I.進行強制類型轉換 Block_layout是block底層類型
- II.判斷是否需要釋放
- III.若是全局Block,不需要拷貝
- IV.不是全局Block,只能是棧區(qū)或堆區(qū)Block,而堆區(qū)Block無法在編譯時期編譯出來,只能是棧區(qū)Block
- V.申請堆區(qū)空間
- VI.進行內存拷貝,原對象的Block拷貝到新的對象Block中
- VII.原對象的invoke拷貝到新對象的invoke中,這兒就能執(zhí)行之前例子中的內容NSLog
- VIII.是否正在進行析構
-
IX.isa標識為NSMallocBlock
image.png
7.Block的三層拷貝:怎么對外界變量操作
A.Block_discriptor_2中有copy變量和dispose變量進行copy和dispose操作,對外界變量進行copy處理底層調用Block_object_assign,對外界變量進行dispose操作,底層調用Block_object_dispose

Block結構體中flags是否為BLOCK_HAS_COPY_DISPOSE
決定是否有Block_descriptor_2,若有會有copy和dispose
函數賦值,copy對外界變量進行copy處理,底層調用
_Block_object_assign((void*)&dst->lg_name,dispose對外界變量進行dispose處理,底層調用_Block_object_dispose((void*)src->lg_name

B.對外界變量操作調用Block_object_assign,根據Block中flags的值與BLOCK_ALL_COPY_DISPOSE_FLAGS運算后的類型進行不同的操作,用的最多的是第一種純對象類型BLOCK_FIELD_IS_OBJECT,在第三層拷貝,如NSString的值拷貝時會應用,第三種__block修飾的外界變量類型BLOCK_FIELD_IS_BYREF,編譯時__block修飾的類型會變?yōu)榻Y構體類型Block_byref如__Block_byref_lg_name_0,在第二層拷貝時會應用

C.byref_keep在什么時候賦值的?int a=10,不是對象類型,clang編譯不會有byref_keep,可以進行第二層拷貝不會有第三層拷貝,先進入第二層拷貝*dest = _Block_byref_copy(object)Block里面的結構體指針指向的內存地址就是外部的結構體地址,故結構體內部修改a=11,能改變外部的變量的值

D.滿足第三種情況BLOCK_FIELD_IS_BYREF,需要捕獲的外部變量為對象類型即編譯生成的結構體類型中有對象類型如NSString進行clang編譯,NSString是對象類型,Block_byref結構體中包含對象類型NSString *lg_name,才滿足進行第三層拷貝的條件,將byref_keep保存對象后在合適的時候進行調用,byref_keep在什么時候賦值的呢?及是否還有Block_byref_3?




E.是否有Block_byref_2和Block_byref_3由flags的值BLOCK_BYREF_HAS_COPY_DISPOSE和BLOCK_BYREF_LAYOUT_EXTENDED決定,編譯器拷貝,下層進行識別有Block_byref_2,進行拷貝時調用(*src2->byref_keep)(copy, src),即調用編譯時傳入的__Block_byref_id_object_copy,調用__Block_byref_id_object_copy,又一次調用_Block_object_assign,此時出入的為純對象類型BLOCK_FIELD_IS_OBJECT,進行指針地址拷貝*dest = object,結構體__Block_byref_lg_name_0內存平移40得到NSString lg_name的內存地址的值,指向外部變量存放字符串常量LG_Cooci的地址,故Block內部對lg_name進行改變,外部也會跟著變,屬于同一片地址空間,任何對象都是平移58嗎?則由Block對象中flags的值是否為BLOCK_BYREF_LAYOUT_EXTENDED決定,即是否有Block_byref_3,若有則會增加一個變量char *layout,則去平移6 * 8 = 48個字節(jié)



針對捕獲外界變量__block修飾的Block的三層拷貝
- I.第一層拷貝:整個Block的拷貝從棧區(qū)NSStackBlock拷貝到堆區(qū)NSMallocBlock,前文在匯編代碼中已經分析
- II.__block修飾的捕獲的外部變量結構體__Block_byref_lg_name_0的拷貝,調用
*dest = _Block_byref_copy(object)第二層拷貝,對捕獲的外部對象變量編譯為結構體__Block_byref_lg_name_0后的拷貝,因為要在block內部去修改外部__block修飾的對象(編譯為__Block_byref_lg_name_0類型的結構體了)的值,所以拷貝整個結構體到block中- III.捕獲的外部變量為對象類型如NSString,作為成員NSString lg_name存在于結構體__Block_byref_lg_name_0中,通過內存平移58 = 40個字節(jié)得到值,要修改則進行指針拷貝,第三層拷貝,指針指向同一片內存地址*dest=object,內存地址中存放著字符串常量的值如LG_Cooci
image.png

