寫在前面
一個優(yōu)秀的App
必然是對內(nèi)存"精打細(xì)算"的脉执,本文就來探索一下內(nèi)存管理中的一些門道與RunLoop
的相關(guān)知識.
一烙如、內(nèi)存布局
①. 五大區(qū)
接下來我從內(nèi)存中的低地址往高地址
依次介紹五大區(qū):
- 代碼段(.text)
- 存放著程序代碼,直接加載到內(nèi)存中
- 初始化區(qū)域(.data)
- 存放著初始化的全局變量、靜態(tài)變量
- 內(nèi)存地址:一般以
0x1
開頭
- 未初始化區(qū)域(.bss)
-
bss
段存放著未初始化的全局變量、靜態(tài)變量 - 內(nèi)存地址:一般以
0x1
開頭
-
- 堆區(qū)(heap)
- 堆區(qū)存放著通過
alloc
分配的對象、block copy
后的對象 - 堆區(qū)
速度比較慢
- 內(nèi)存地址:一般以
0x6
開頭
- 堆區(qū)存放著通過
- 棧區(qū)(stack)
- 棧區(qū)存儲著
函數(shù)
甘萧、方法
以及局部變量
- 棧區(qū)
比較小
,但是速度比較快
- 內(nèi)存地址:一般以
0x7
開頭
- 棧區(qū)存儲著
在這里提一句關(guān)于函數(shù)在內(nèi)存中的分布:
函數(shù)指針
存在棧區(qū)
梆掸,函數(shù)實現(xiàn)
存在堆區(qū)
除了五大區(qū)之外扬卷,內(nèi)存中還有保留字段和內(nèi)核區(qū)
- 內(nèi)核區(qū):以4GB手機為例,系統(tǒng)將其中的3GB給了
五大區(qū)+保留區(qū)
酸钦,剩余的1GB給內(nèi)核區(qū)使用,它主要是系統(tǒng)用來進(jìn)行內(nèi)核處理操作的區(qū)域 - 保留字段:保留一定的區(qū)域給保留字段怪得,進(jìn)行一些存儲或預(yù)留給系統(tǒng)處理
nil
等
這里有個疑問,為什么五大區(qū)的最后內(nèi)存地址是從0x00400000
開始的.其主要原因是0x00000000
表示nil
卑硫,不能直接用nil
表示一個段徒恋,所以單獨給了一段內(nèi)存用于處理nil
等情況.
以下的兩張圖,便于我們更好的理解內(nèi)存分布.
平時在使用App過程中,棧區(qū)就會向下增長欢伏,堆區(qū)就會向上增長.
接下來看看堆區(qū)和棧區(qū)中的一些內(nèi)容- 對于
alloc
創(chuàng)建的對象obj
入挣,分別打印了obj
的對象地址 和obj
對象的指針地址
(可以參考前文的匯總圖)-
obj
的對象地址
是以0x6
開頭,說明是存放在堆區(qū)
-
obj
對象的指針地址
是以0x7
開頭颜懊,說明是存放在棧區(qū)
-
那么在堆區(qū)和棧區(qū)訪問對象的順序是怎樣的呢?
- 堆區(qū)訪問對象的順序是先拿到棧區(qū)的指針财岔,再拿到指針指向的對象,才能獲取到對象的
isa
河爹、屬性方法等
- 棧區(qū)訪問對象的順序是
直接通過寄存器訪問到對象的內(nèi)存空間
匠璧,因此訪問速度快
②. 內(nèi)存布局相關(guān)面試題
面試題1:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有咸这,是什么區(qū)別夷恍?
- 有區(qū)別
- 全局變量保存在內(nèi)存的全局存儲區(qū)(即bss+data段),占用靜態(tài)的存儲單元
- 局部變量保存在棧區(qū)媳维,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元
- 兩者訪問的權(quán)限不一樣
面試題2:Block中可以修改全局變量酿雪,全局靜態(tài)變量,局部靜態(tài)變量侄刽,局部變量嗎指黎?
- 可以修改全局變量,全局靜態(tài)變量州丹,因為全局變量 和 靜態(tài)全局變量是全局的醋安,作用域很廣,
block
可以訪問到 - 可以修改局部靜態(tài)變量杂彭,不可以修改局部變量
- 局部靜態(tài)變量(
static
修飾的) 和 局部變量,被block
從外面捕獲吓揪,成為__main_block_impl_0
這個結(jié)構(gòu)體的成員變量 - 局部變量是
以值
方式傳遞到block
的構(gòu)造函數(shù)中的亲怠,只會捕獲block
中會用到的變量,由于只捕獲了變量的值柠辞,并非內(nèi)存地址团秽,所以在block
內(nèi)部不能改變局部變量的值 - 局部靜態(tài)變量是以
指針
形式,被block
捕獲的叭首,由于捕獲的是指針习勤,所以可以修改局部靜態(tài)變量的值
- 局部靜態(tài)變量(
-
ARC
環(huán)境下,一旦使用__block
修飾并在block
中修改放棒,就會觸發(fā)copy
操作姻报,block
就會從棧區(qū)copy
到堆區(qū),此時的block
是堆區(qū)block
-
ARC
模式下间螟,Block
中引用id類型
的數(shù)據(jù)吴旋,無論有沒有__block
修飾,都會retain
厢破,對于基礎(chǔ)數(shù)據(jù)類型荣瑟,沒有__block
修飾就無法修改變量值;如果有__block
修飾摩泪,也是在底層修改__Block_byref_a_0
結(jié)構(gòu)體笆焰,將其內(nèi)部的forwarding
指針指向copy
后的地址,來達(dá)到值的修改
面試題3:關(guān)于全局靜態(tài)變量的誤區(qū)
- 全局靜態(tài)變量是可變的
- 全局靜態(tài)變量的值
只針對文件而言
见坑,不同文件的全局靜態(tài)變量的內(nèi)存地址是不一樣的,也就是無論別的文件怎么修改嚷掠,本文件使用時都拿原有值/本文件修改后的值
二、內(nèi)存管理方案
①. taggedPointer
①.1 taggedPointer初探
分別調(diào)用下面兩種方法荞驴,哪個會崩潰不皆?為什么?
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *nameStr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = dispatch_queue_create("com.tcj.cn", DISPATCH_QUEUE_CONCURRENT);
[self taggedPointerDemo];
[self testNormal];
}
- (void)taggedPointerDemo {
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"tcj"]; // alloc 堆 iOS優(yōu)化 - taggedpointer
NSLog(@"%@",self.nameStr);
});
}
}
- (void)testNormal {
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"又一黑馬誕生12345"];
NSLog(@"%@",self.nameStr);
});
}
}
@end
經(jīng)過運行測試之后熊楼,會發(fā)現(xiàn)testNormal
會崩潰霹娄,而taggedPointerDemo
方法正常運行
首先來分析下為什么會崩潰的原因?其實是多線程
和setter鲫骗、getter
操作造成的
- 調(diào)用
setter
方法會objc_retain(newValue)
+objc_release(oldValue)
- 但是加上多線程就不一樣了——在某個時刻線程1對舊值進(jìn)行
relese
(沒有relese完畢
)犬耻,同時線程2也對舊值進(jìn)行relese
操作,即同一時刻對同一片內(nèi)存空間釋放多次执泰,會造成野指針問題
(訪問壞的地址)
但是為什么testNormal
會崩潰枕磁,而taggedPointerDemo
方法正常運行?
-
testNormal
中的對象為__NSCFString
類型,存儲在堆上
-
taggedPointerDemo
中的對象為NSTaggedPointerString
類型,存儲在常量區(qū)
.因為nameStr
在alloc``分配時在堆區(qū)术吝,由于較小
计济,所以經(jīng)過Xcode
中iOS的優(yōu)化
晴楔,成了NSTaggedPointerString
類型,存儲在常量區(qū)
其實之前在objc
源碼的方法中有看到過類似的身影——objc_retain
和objc_release
的對象如果是isTaggedPointer
類型就直接返回(不操作)
小對象的地址分析
以NSString
為例峭咒,對于NSString
來說
- 一般的
NSString
對象指針,都是string值
+指針地址
纪岁,兩者是分開的 - 對于
Tagged Pointer
指針凑队,其 指針 + 值,都能在小對象中體現(xiàn).所以Tagged Pointer
既包含指針幔翰,也包含值
在之前的文章講類的加載時漩氨,其中的_read_images
源碼有一個方法對小對象進(jìn)行了處理,即initializeTaggedPointerObfuscator
方法,我們下面介紹
①.2 taggedPointer深入
在推出iPhone 5s(iPhone首個采用64位架構(gòu))的時候遗增,為了節(jié)省內(nèi)存和提高執(zhí)行效率叫惊,同時也提出了taggedPointer
底層也做了對objc_debug_taggedpointer_obfuscator
進(jìn)行異或的操作(兩次異或同一個數(shù)相當(dāng)于編碼解碼 -- iOS10.14之后做的混淆操作)
我們可以在objc
源碼(818.2版本)中通過objc_debug_taggedpointer_obfuscator
查找taggedPointer
的編碼和解碼,來查看底層是如何混淆處理的
通過實現(xiàn)做修,我們可以得知霍狰,在編碼和解碼部分,經(jīng)過了兩層異或饰及,其目的是得到小對象自己,例如以 1010 0001
為例,假設(shè)mask為 0101 1000
1010 0001
^0101 1000 mask(編碼)
1111 1001
^0101 1000 mask(解碼)
1010 0001
所以在外界商膊,為了獲取小對象的真實地址军援,我們也可以通過類似的方法對taggedPointer
進(jìn)行解碼.我們可以將解碼的源碼拷貝到外面,將NSString
混淆部分進(jìn)行解碼屏箍,如下所示
觀察解碼后的小對象地址绘梦,其中的 62
表示 b
的 ASCII
碼,再以 NSNumber
為例赴魁,同樣可以看出卸奉,1
就是我們實際的值
到這里,我們驗證了小對象指針地址中確實存儲了值尚粘,那么小對象地址高位其中的0xa
择卦、0xb
又是什么含義呢?
//NSString
0xa000000000000621
//NSNumber
0xb000000000000012
0xb000000000000025
需要去源碼中查看_objc_isTaggedPointer
源碼郎嫁,主要是通過保留最高位的值(即64位的值)秉继,判斷是否等于_OBJC_TAG_MASK
(即2 ^ 63), 來判斷是否是小對象
所以0xa
、0xb
主要是用于判斷是否是小對象taggedpointer
泽铛,即判斷條件尚辑,判斷第64位
上是否為1
(taggedpointer
指針地址即表示指針地址,也表示值)
-
0xa
轉(zhuǎn)換成二進(jìn)制為 1 010
(64位為1
盔腔,63~61后三位
表示tagType類型
-2
)杠茬,表示NSString
類型 -
0xb
轉(zhuǎn)換為二進(jìn)制為1 011
(64位為1
月褥,63~61后三位
表示tagType類型
-3
),表示NSNumber
類型瓢喉,這里需要注意一點宁赤,如果NSNumber
的值是-1
,其地址中的值是用補碼
表示的
這里可以通過_objc_makeTaggedPointer
方法的參數(shù)tag
類型objc_tag_index_t
進(jìn)入其枚舉栓票,其中 2
表示NSString
决左,3
表示NSNumber
同理,我們可以定義一個NSDate
對象走贪,來驗證其tagType
是否為 6
.通過打印結(jié)果佛猛,其地址高位是0xe
,轉(zhuǎn)換為二進(jìn)制為1 110
坠狡,排除64位的1
继找,剩余的3位
正好轉(zhuǎn)換為十進(jìn)制是6
,符合上面的枚舉值
我們在來看看NSString的內(nèi)存管理
我們可以通過NSString
初始化的兩種方式逃沿,來測試NSString
的內(nèi)存管理
- 通過
WithString
+@""
方式初始化 - 通過
WithFormat
方式初始化
從上面可以總結(jié)出婴渡,NSString
的內(nèi)存管理主要分為3種
-
__NSCFConstantString
:字符串常量
,是一種編譯時
常量凯亮,retainCount值很大
缩搅,對其操作,不會引起引用計數(shù)變化
触幼,存儲在字符串常量區(qū)
-
__NSCFString
:是在運行時
創(chuàng)建的NSString子類
硼瓣,創(chuàng)建后引用計數(shù)會加1
,存儲在堆上
-
NSTaggedPointerString
:標(biāo)簽指針置谦,是蘋果在64位
環(huán)境下對NSString
堂鲤、NSNumber
等對象做的優(yōu)化
.對于NSString
對象來說- 當(dāng)
字符串是由數(shù)字、英文字母組合且長度小于等于9
時媒峡,會自動成為NSTaggedPointerString
類型瘟栖,存儲在常量區(qū)
- 當(dāng)有
中文或者其他特殊符號
時,會直接成為__NSCFString
類型谅阿,存儲在堆區(qū)
- 當(dāng)
①.3 taggedPointer總結(jié)
-
Tagged Pointer
小對象類型(用于存儲NSNumber
半哟、NSDate
、小NSString
)签餐,小對象指針不再是簡單的地址寓涨,而是地址 + 值
,即真正的值
氯檐,所以戒良,實際上它不再是一個對象
了,它只是一個披著對象皮的普通變量
而已.所以可以直接進(jìn)行讀取.優(yōu)點是占用空間小,節(jié)省內(nèi)存
-
Tagged Pointer
小對象,不會進(jìn)入 retain 和 release
冠摄,而是直接返回了糯崎,意味著不需要ARC進(jìn)行管理
几缭,所以可以直接被系統(tǒng)自主的釋放和回收
-
Tagged Pointer
的內(nèi)存并不存儲在堆
中,而是在常量區(qū)
中沃呢,也不需要malloc
和free
年栓,所以可以直接讀取
,相比存儲在堆區(qū)的數(shù)據(jù)讀取薄霜,效率
上快了3倍
左右.創(chuàng)建的效率
相比堆區(qū)快了近100倍
左右 -
taggedPointer
的內(nèi)存管理方案韵洋,比常規(guī)的內(nèi)存管理,要快很多
-
Tagged Pointer
的64位
地址中黄锤,前4位代表類型
,后4位主要適用于系統(tǒng)做一些處理
食拜,中間56位用于存儲值
- 優(yōu)化內(nèi)存建議:對于
NSString
來說鸵熟,當(dāng)字符串較小
時,建議直接通過@""
初始化负甸,因為存儲在常量區(qū)
流强,可以直接進(jìn)行讀取
.會比WithFormat
初始化方式更加快速
②. nonpointer_isa
nonpointer_isa
在前面章節(jié)已經(jīng)有提到過了,這是蘋果優(yōu)化內(nèi)存的一種方案: isa
是個8字節(jié)(64位)
的指針呻待,僅用來isa指向
比較浪費打月,所以isa
中就摻雜了一些其他數(shù)據(jù)來節(jié)省內(nèi)存
③. SideTable
當(dāng)引用計數(shù)存儲到一定值時,并不會再存儲到Nonpointer_isa
的位域的extra_rc
中蚕捉,而是會存儲到 SideTables
散列表中
③.1 散列表為什么在內(nèi)存中有多張奏篙?最多能夠多少張?
- 如果散列表只有一張表迫淹,意味著全局所有的對象都會存儲在一張表中秘通,操作任意一個對象,都會進(jìn)行開鎖解鎖(鎖是鎖整個表的讀寫).當(dāng)開鎖時,由于所有數(shù)據(jù)都在一張表敛熬,這意味著數(shù)據(jù)不安全
- 如果每個對象都開一個表肺稀,會耗費性能,所以也不能有無數(shù)個表
-
散列表的類型是SideTable应民,有如下定義
- 通過查看
sidetable_unlock
方法定位SideTables
话原,其內(nèi)部是通過SideTablesMap
的get
方法獲取. 而SideTablesMap
是通過StripedMap<SideTable>
定義的
從而進(jìn)入StripedMap
的定義,從這里可以看出诲锹,同一時間繁仁,真機中散列表最多只能有8張
③.2 為什么在用散列表,而不用數(shù)組归园、鏈表改备?
-
數(shù)組
:特點在于查詢方便(即通過下標(biāo)訪問
),增刪比較麻煩
蔓倍,所以數(shù)組的特性是讀取快悬钳,存儲不方便
-
鏈表
:特點在于增刪方便盐捷,查詢慢
(需要從頭節(jié)點開始遍歷查詢
),所以鏈表的特性是存儲快默勾,讀取慢
-
散列表的本質(zhì)
就是一張哈希表碉渡,哈希表集合了數(shù)組和鏈表的長處
,增刪改查都比較方便
母剥,例如拉鏈哈希表(在之前鎖的文章中滞诺,講過的tls
的存儲結(jié)構(gòu)就是拉鏈形式
的),是最常用的环疼,如下所示
三习霹、ARC&MRC
面試中常常會問到ARC
和MRC
,其實這兩者在內(nèi)存管理中才是核心所在
① MRC(手動內(nèi)存管理)
- 在
MRC
時代炫隶,系統(tǒng)是通過對對象的引用計數(shù)來判斷是否銷毀淋叶,有以下規(guī)則- 對象被
創(chuàng)建時
引用計數(shù)都為1
- 當(dāng)對象
被其他指針引用
時,需要手動調(diào)用[objc retain]
伪阶,使對象的引用計數(shù)+1
- 當(dāng)指針變量不再使用對象時煞檩,需要手動調(diào)用
[objc release]
來釋放對象
,使對象的引用計數(shù)-1
- 當(dāng)一個對象的
引用計數(shù)為0
時栅贴,系統(tǒng)就會銷毀
這個對象
- 對象被
- 所以斟湃,在
MRC
模式下,必須遵守:誰創(chuàng)建
檐薯,誰釋放
凝赛,誰引用
,誰管理
② ARC(自動內(nèi)存管理)
-
ARC
模式是在WWDC2011
和iOS5
引入的自動管理機制
坛缕,即自動引用計數(shù).是編譯器的一種特性.其規(guī)則與MRC一致哄酝,區(qū)別在于-
ARC
中禁止手動
調(diào)用retain/release/retainCount/dealloc
-
編譯器
會在適當(dāng)?shù)奈恢貌迦?code>release和autorelease
-
ARC
新加了weak
、strong
關(guān)鍵字
-
-
ARC
是LLVM
和Runtime
配合的結(jié)果
③ alloc
之前已經(jīng)對alloc流程有了一個詳細(xì)的介紹
④ retain
retain
會在底層調(diào)用 objc_retain
-
objc_retain
先判斷是否為isTaggedPointer
祷膳,是就直接返回不需要處理陶衅,不是在調(diào)用obj->retain()
-
objc_object::retain
通過fastpath
大概率調(diào)用rootRetain()
,小概率通過消息發(fā)送調(diào)用對外提供的SEL_retain
-
rootRetain
調(diào)用rootRetain(false, false)
-
rootRetain
內(nèi)部實現(xiàn)其實是個do-while
循環(huán):- 先判斷是否為
nonpointer_isa
(小概率事件)不是的話,則直接操作SideTables
散列表中的引用計數(shù)表直晨,此時的散列表并不是只有一張搀军,而是有很多張
- 找到對應(yīng)的散列表進(jìn)行
+=SIDE_TABLE_RC_ONE
,其中SIDE_TABLE_RC_ONE
是左移兩位找到引用計數(shù)表
- 找到對應(yīng)的散列表進(jìn)行
- 判斷是否正在釋放勇皇,如果正在釋放罩句,則執(zhí)行
dealloc
流程 - 調(diào)用
addc
函數(shù)執(zhí)行extra_rc+1
,即引用計數(shù)+1
操作敛摘,并給一個引用計數(shù)的狀態(tài)標(biāo)識carry
门烂,用于表示extra_rc是否滿了
- 對
isa
中的第45位(RC_ONE在arm64中為45)extra_rc
進(jìn)行操作處理
- 對
- 如果
carray
的狀態(tài)表示extra_rc
的引用計數(shù)滿了,此時需要操作散列表,即將滿狀態(tài)的一半拿出來存到extra_rc
屯远,另一半存在 散列表的rc_half
.這么做的原因是因為如果都存儲在散列表蔓姚,每次對散列表操作都需要開解鎖,操作耗時慨丐,消耗性能大坡脐,這么對半分操作的目的在于提高性能- 這里為什么優(yōu)先考慮使用
isa
進(jìn)行引用計數(shù)存儲是因為引用計數(shù)存儲在isa的bits中
- 這里為什么優(yōu)先考慮使用
- 先判斷是否為
retain 總結(jié):
-
retain
在底層首先會判斷是否是Nonpointer isa
,如果不是房揭,則直接操作散列表 進(jìn)行+1操作
- 如果
是Nonpointer isa
备闲,還需要判斷是否正在釋放
,如果正在釋放捅暴,則執(zhí)行dealloc流程
恬砂,釋放弱引用表和引用計數(shù)表,最后free
釋放對象內(nèi)存 - 如果
不是正在釋放
蓬痒,則對Nonpointer isa進(jìn)行常規(guī)的引用計數(shù)+1
.這里需要注意一點的是泻骤,extra_rc在真機上只有8位用于存儲引用計數(shù)的值
,當(dāng)存儲滿了
時乳幸,需要借助散列表用于存儲
.需要將滿了的extra_rc對半分
,一半(即2^7)存儲在散列表中
.另一半還是存儲在extra_rc中
钧椰,用于常規(guī)的引用計數(shù)的+1或者-1操作
粹断,然后再返回
⑤ release
release
與retain
相似,會在底層調(diào)用objc_release
-
objc_release
先判斷是否為isTaggedPointer
嫡霞,是就直接返回不需要處理瓶埋,不是在調(diào)用obj->release()
-
objc_object::release
通過fastpath
大概率調(diào)用rootRelease()
,小概率通過消息發(fā)送調(diào)用對外提供的SEL_release
-
rootRelease
調(diào)用rootRelease(true, false)
-
rootRelease
內(nèi)部實現(xiàn)也有個do-while
循環(huán)- 先判斷是否為
nonpointer_isa
(小概率事件)不是的
話則直接對散列表中的引用計數(shù)進(jìn)行-1操作
- 如果是
Nonpointer isa
诊沪,則對extra_rc中的引用計數(shù)值進(jìn)行-1操作
养筒,并存儲此時的extra_rc狀態(tài)到carry中
- 如果此時的狀態(tài)
carray為0
,則走到underflow
流程- 判斷
散列表中是否存儲了一半的引用計數(shù)
- 如果
是
端姚,則從散列表中取出存儲的一半引用計數(shù)
晕粪,進(jìn)行-1操作
,然后存儲到extra_rc中
- 如果此時
extra_rc沒有值
渐裸,散列表中也是空的
巫湘,則直接進(jìn)行析構(gòu)
,即dealloc
操作昏鹃,屬于自動觸發(fā)
- 判斷
- 先判斷是否為
⑥ retainCount
前面說了這么多引用計數(shù)洞渤,那么我們來看看retainCount
和引用計數(shù)有什么關(guān)系呢阅嘶?來看一個問題:
alloc創(chuàng)建的對象的引用計數(shù)為多少?
上述代碼打印輸出1
断箫,然而在alloc
流程中并沒有看到任何與retainCount
相關(guān)的內(nèi)容舱痘,這又是怎么一回事呢?接下來就來看看retainCount
的底層實現(xiàn)
- 進(jìn)入
retainCount -> _objc_rootRetainCount -> rootRetainCount
源碼降狠,其實現(xiàn)如下
在這里我們可以通過源碼斷點調(diào)試磷杏,來查看此時的extra_rc
的值溜畅,結(jié)果如下:
當(dāng)來到953行斷點時,此時的extra_rc為0
,而過到954行代碼,我們在來看extra_rc
的值為多少.
此時的值卻為1
了.
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED)
以上代碼將bits
里面的extra_rc進(jìn)行了+1
操作.
答案:alloc
創(chuàng)建的對象實際的引用計數(shù)為0
,其引用計數(shù)打印結(jié)果為1
极祸,是因為在底層rootRetainCount
方法中慈格,引用計數(shù)默認(rèn)+1
了,但是這里只有對引用計數(shù)的讀取操作遥金,是沒有寫入操作的浴捆,簡單來說就是:為了防止alloc創(chuàng)建的對象被釋放(引用計數(shù)為0會被釋放),所以在編譯階段稿械,程序底層默認(rèn)進(jìn)行了+1操作.(新版objc源碼)
-
alloc
創(chuàng)建的對象沒有retain和release
-
alloc
創(chuàng)建對象的引用計數(shù)為0
选泻,會在編譯時期
,程序默認(rèn)加1
美莫,所以讀取引用計數(shù)時為1
⑦ autorealese
將在自動釋放池章節(jié)講到.
⑧ dealloc
在retain
和release
的底層實現(xiàn)中页眯,都提及了dealloc
析構(gòu)函數(shù),下面來分析dealloc
的底層的實現(xiàn)
-
dealloc
在底層會調(diào)用_objc_rootDealloc
-
_objc_rootDealloc
調(diào)用rootDealloc
- 在
rootDealloc
方法中- 判斷是否為
isTaggedPointer
厢呵,是的話直接返回窝撵,不是的話繼續(xù)往下走 - 判斷
isa標(biāo)識位
中是否有弱引用
、關(guān)聯(lián)對象
襟铭、c++析構(gòu)函數(shù)
碌奉、額外的散列表
,有的話調(diào)用object_dispose
寒砖,否則直接free
- 判斷是否為
-
object_dispose
中- 先判空處理
- 接著調(diào)用
objc_destructInstance
(核心部分) - 最后再
free釋放對象
-
objc_destructInstance
- 判斷
是否有c++析構(gòu)函數(shù)和關(guān)聯(lián)對象
赐劣,有的話分別調(diào)用object_cxxDestruct
、_object_remove_assocations
進(jìn)行處理 - 然后再調(diào)用
clearDeallocating
- 判斷
-
clearDeallocating
中- 判斷是否是
nonpointer
哩都,是的話調(diào)用sidetable_clearDeallocating
清空散列表 - 判斷
是否
有弱引用和額外的引用計數(shù)表has_sidetable_rc
魁兼,是的話調(diào)用clearDeallocating_slow進(jìn)行弱引用表和引用計數(shù)表的處理
- 判斷是否是
所以綜上所述,dealloc
的流程可以總結(jié)為:
- 1:根據(jù)當(dāng)前對象的狀態(tài)是否直接調(diào)?free()釋放
- 2:是否存在C++的析構(gòu)函數(shù)、移除這個對象的關(guān)聯(lián)屬性
- 3:將指向該對象的弱引?指針置為nil
- 4:從弱引?表中擦除對該對象的引?計數(shù)
最后附上一張dealloc
流程圖
因此到目前為止漠嵌,從最開始的alloc -> retain -> release -> dealloc
就全部串聯(lián)起來了.
四璃赡、弱引用
①. weak原理
筆者在之前的[iOS之武功秘籍⑩: OC底層題目分析]中已經(jīng)講過了.
②. NSTimer中的循環(huán)引用
眾所周知使用NSTimer
容易出現(xiàn)循環(huán)引用,那么我們就來分析并解決一下
假設(shè)此時有A献雅、B兩個
界面,在B界面
中有如下定時器代碼.
代碼運行起來所發(fā)生的問題就是 B界面
pop
到 A界面
時不會觸發(fā) B 界面
的 dealloc
函數(shù).主要原因是B界面沒有釋放
碉考,即沒有執(zhí)行dealloc
方法,導(dǎo)致timer
也無法停止和釋放
前面我們已經(jīng)看到了release
在引用計數(shù)為0
時會調(diào)用dealloc
消息發(fā)送挺身,此時沒有觸發(fā)dealloc
函數(shù)必然是出現(xiàn)了循環(huán)引用
侯谁,那么循環(huán)引用出現(xiàn)在哪個環(huán)節(jié)?其實是NSTimer
的API是被強持有的
,直到Timer invalidated.
即此時timer
持有self
,self
也持有timer
墙贱,構(gòu)成了循環(huán)引用
那么能不能像block
一樣使用弱引用來解決循環(huán)引用呢热芹?答案是不能的!
此時他們之間的持有關(guān)系如下:
之前在Block
篇章說的是使用弱引用__weak typeof(self) weakSelf = self
可以解決循環(huán)引用; 不處理引用計數(shù)惨撇,使用弱引用表管理伊脓,怎么在這里就不好使了呢?
到這我又有兩個問題?
-
weakSelf
會對引用計數(shù)進(jìn)行+1操作
嗎魁衙? -
weakSelf
和self
的指針地址相同嗎报腔,是指向同一片內(nèi)存嗎?
帶著疑問剖淀,我們在weakSelf
前后打印self
的引用計數(shù)
運行后發(fā)現(xiàn)前后self
的引用計數(shù)都是8
.也就是 weakSelf沒有對內(nèi)存進(jìn)行+1操作
繼續(xù)打印weakSelf
和 self
對象纯蛾,以及他們的指針地址:
從打印結(jié)果可以看出 weakSelf
和 self
指向的都是 TCJTimerViewController對象
,但是weakSelf
和self
的指針并不相同
——兩者并不是一個東東纵隔,只是指向同一個TCJTimerViewController對象
.
通過block
底層原理的方法 _Block_object_assign
可知翻诉,block
捕獲的是 對象的指針地址
即 block
持有的是weakSelf
的指針地址;timer
持有的是weakSelf的指針指向的對象
捌刮,這里間接持有了self
碰煌,所以仍然存在循環(huán)引用導(dǎo)致釋放不掉.
③. 解決NSTimer的循環(huán)引用
解決思路 : 我們需要打破這一層強持有 - self
③.1 思路一:pop時在其他方法中銷毀timer
- 既然
dealloc
不能來,就在dealloc
函數(shù)調(diào)用前解決掉這層強引用 - 可以在
viewWillDisappear
绅作、viewDidDisappear
中處理NSTimer
芦圾,但這樣處理效果并不好,因為跳轉(zhuǎn)到下一頁定時器也會停止工作棚蓄,與業(yè)務(wù)不符 - 使用
didMoveToParentViewController
可以很好地解決這層強引用.這個方法是用于當(dāng)一個視圖控制器中添加或者移除viewController后
堕扶,必須調(diào)用的方法.目的是為了告訴iOS
碍脏,已經(jīng)完成添加/刪除子控制器的操作. - 在
B界面
中重寫didMoveToParentViewController
方法
③.2 思路二:中介者模式梭依,即不使用self,依賴于其他對象
- 使用其他全局變量典尾,此時
timer
持有全局變量役拴,self
也持有全局變量,只要頁面pop
钾埂,self
因為沒有被持有就能正常走dealloc
河闰,在dealloc
中再去處理timer
- 此時的持有鏈分別是
runloop->timer->target->timer
、self->target
褥紫、self->timer
#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif
#import "TCJTimerViewController.h"
#import <objc/runtime.h>
static int num = 0;
@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) id target;
@end
@implementation TCJTimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
}
void fireHomeObjc(id obj){
CJNSLog(@"%s -- %@",__func__,obj);
}
- (void)fireHome{
num++;
CJNSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
CJNSLog(@"%s",__func__);
}
③.3 思路三:自定義封裝timer(使用包裝者)
- 類似于方案二姜性,但是使用更便捷
- 如果傳入的響應(yīng)者
target
能響應(yīng)傳入的響應(yīng)事件selector
,就使用runtime
動態(tài)添加方法并開啟計時器 -
fireWapper
中如果有wrapper.target
髓考,就讓wrapper.target
(外界響應(yīng)者)調(diào)用wrapper.aSelector
(外界響應(yīng)事件) -
fireWapper
中沒有了wrapper.target
部念,意味著響應(yīng)者釋放了(無法響應(yīng)了),此時定時器也就可以休息了(停止并釋放) - 持有鏈分別是
runloop->timer->TCJTimerWrapper
、vc->TCJTimerWrapper-->vc
//*********** .h文件 ***********
@interface TCJTimerWapper : NSObject
- (instancetype)cj_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cj_invalidate;
@end
//*********** .m文件 ***********
#import "TCJTimerWapper.h"
#import <objc/message.h>
@interface TCJTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation TCJTimerWapper
- (instancetype)cj_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
//傳入vc
self.target = aTarget;
//傳入的定時器方法
self.aSelector = aSelector;
if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
//給timerWapper添加方法
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
//啟動一個timer儡炼,target是self妓湘,即監(jiān)聽自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
//一直跑runloop
void fireHomeWapper(TCJTimerWapper *wapper){
//判斷target是否存在
if (wapper.target) {
//如果存在則需要讓vc知道,即向傳入的target發(fā)送selector消息乌询,并將此時的timer參數(shù)也一并傳入榜贴,所以vc就可以得知`fireHome`方法,就這事這種方式定時器方法能夠執(zhí)行的原因
//objc_msgSend發(fā)送消息妹田,執(zhí)行定時器方法
void (*cj_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
cj_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
}else{
//如果target不存在唬党,已經(jīng)釋放了,則釋放當(dāng)前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中調(diào)用秆麸,通過vc釋放初嘹,從而讓timer釋放
- (void)cj_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif
#import "TCJTimerViewController.h"
#import "TCJTimerWapper.h"
static int num = 0;
@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TCJTimerWapper *timerWapper;
@end
@implementation TCJTimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timerWapper = [[TCJTimerWapper alloc] cj_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{
num++;
CJNSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timerWapper cj_invalidate];
CJNSLog(@"%s",__func__);
}
這種方式看起來比較繁瑣,步驟很多沮趣,而且針對timerWapper
屯烦,需要不斷的添加method
,需要進(jìn)行一系列的處理.
③.4 思路四:利用NSProxy虛基類的子類——NSProxy有著NSObject同等的地位房铭,多用于消息轉(zhuǎn)發(fā)
- 使用
NSProxy
打破NSTimer
的對vc
的強持有驻龟,但是強持有依然存在,需要手動關(guān)閉定時器 - 持有鏈分別是
runloop->timer->TCJProxy->timer
缸匪、vc->TCJProxy-->vc
//************TCJProxy.h文件************
@interface TCJProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
//************TCJProxy.m文件************
@interface TCJProxy()
@property (nonatomic, weak) id object;
@end
@implementation TCJProxy
+ (instancetype)proxyWithTransformObject:(id)object{
TCJProxy *proxy = [TCJProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
//************TCJTimerViewController.m文件************
#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif
#import "TCJTimerViewController.h"
#import "TCJProxy.h"
static int num = 0;
@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TCJProxy *proxy;
@end
@implementation TCJTimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [TCJProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{
num++;
CJNSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
CJNSLog(@"%s",__func__);
}
思路一較為簡便翁狐,思路二合理使用中介者但是很拉胯,思路三適合裝逼凌蔬,思路四更適合大型項目(定時器用的較多) 詳細(xì)代碼
五露懒、AutoReleasePool 自動釋放池
自動釋放池
是OC
中的一種內(nèi)存自動回收機制
,在MRC
中可以用AutoReleasePool來延遲內(nèi)存的釋放
砂心,在ARC
中可以用AutoReleasePool將對象添加到最近的自動釋放池
懈词,不會立即釋放
,會等到runloop休眠
或者超出autoreleasepool作用域{}
之后才會被釋放
.其機制可以通過下圖來表示
- 從程序
啟動到加載完成
辩诞,主線程
對應(yīng)的runloop
會處于休眠狀態(tài)
坎弯,等待用戶交互
來喚醒runloop
- 用戶的
每一次交互
都會啟動一次runloop
,用于處理
用戶的所有點擊
译暂、觸摸事件
等 -
runloop
在監(jiān)聽
到交互事件
后抠忘,就會創(chuàng)建自動釋放池
,并將所有延遲釋放
的對象添加到自動釋放池中
- 在一次
完整的runloop結(jié)束之前
外永,會向自動釋放池
中的所有對象發(fā)送release消息
崎脉,然后銷毀自動釋放池
① Clang分析 autoreleasepool結(jié)構(gòu)
通過clang
命令對空白的main.m
文件輸出一份main.cpp
文件來查看@autoreleasepool
的底層結(jié)構(gòu)
clang
命令為:xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
-
轉(zhuǎn)成
C++
代碼如下
-
通過上圖我們知道
@autoreleasepool
被轉(zhuǎn)化成__AtAutoreleasePool __autoreleasepool
,這是個結(jié)構(gòu)體
.__AtAutoreleasePool
結(jié)構(gòu)體定義如下:
通過上圖我們可以知道以下幾點:
-
__AtAutoreleasePool
是一個結(jié)構(gòu)體
伯顶,有構(gòu)造函數(shù) + 析構(gòu)函數(shù)
囚灼,結(jié)構(gòu)體定義的對象
在作用域結(jié)束后
呛踊,會自動調(diào)用析構(gòu)函數(shù)
- 其中
{}
是作用域
,優(yōu)點是結(jié)構(gòu)清晰
啦撮,可讀性強
谭网,可以及時創(chuàng)建銷毀
關(guān)于涉及的構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用時機,可以通過下面一個案例來驗證
從運行結(jié)果可以得出赃春,在TCJTest創(chuàng)建對象時
愉择,會自動調(diào)用構(gòu)造函數(shù)
,在出了{(lán)}作用域后
织中,會自動調(diào)用析構(gòu)函數(shù)
.
② 匯編分析 autoreleasepool結(jié)構(gòu)
在main
代碼部分加斷點锥涕,運行程序,并開啟匯編調(diào)試:
通過調(diào)試結(jié)果發(fā)現(xiàn)狭吼,和我們clang分析的結(jié)果是一樣的.
③ objc源碼分析 autoreleasepool
在objc源碼
中有一段對AutoreleasePool
的注釋.
從中可以得出幾點:
- 1.
自動釋放池
是一個關(guān)于指針的棧結(jié)構(gòu)
- 2.其中的
指針
是指向釋放的對象
或者pool_boundary哨兵
(現(xiàn)在經(jīng)常被稱為邊界
) - 3.
自動釋放池
是一個頁的結(jié)構(gòu)
(虛擬內(nèi)存中提及過)层坠,而且這個頁是一個雙向鏈表
(表示有父節(jié)點
和子節(jié)點
,在類中提及過刁笙,即類的繼承鏈) - 4.
自動釋放池
和線程是有關(guān)系
通過上面對自動釋放池
的說明破花,我們知道我們研究的幾個方向:
- 1.
自動釋放池什么時候創(chuàng)建?
- 2.
對象是如何加入自動釋放池的疲吸?
- 3.
哪些對象才會加入自動釋放池座每?
帶著這些問題,我們出發(fā)來探索自動釋放池的底層原理
③.1 AutoreleasePoolPage分析
從最初的clang
或者匯編分析
我們了解了自動釋放池其底層調(diào)用的
是objc_autoreleasePoolPush
和objc_autoreleasePoolPop
這兩個方法摘悴,其源碼實現(xiàn)如下[圖片上傳失敗...(image-d024be-1615298790653)]
從源碼中我們可以發(fā)現(xiàn)峭梳,都是調(diào)用AutoreleasePoolPage
的push
和pop
實現(xiàn),以下是其定義蹂喻,從定義中可以看出葱椭,自動釋放池是一個頁,同時也是一個對象口四,并且AutoreleasePoolPage
是繼承于AutoreleasePoolPageData
的.
從上面可以做出以下判斷:
- 1.
自動釋放池
是一個頁
孵运,同時也是一個對象
,這個頁的大小是4096字節(jié)
- 2.從其定義中發(fā)現(xiàn)窃祝,
AutoreleasePoolPage
是繼承自AutoreleasePoolPageData
,且該類的屬性也是來自父類掐松,以下是AutoreleasePoolPageData
的定義
可以發(fā)現(xiàn):
- 其中有
AutoreleasePoolPage
對象踱侣,所以有以下一個關(guān)系鏈AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage
粪小,從這里可以說明自動釋放池除了是一個頁
,還是一個雙向鏈表結(jié)構(gòu)
-
AutoreleasePoolPageData
結(jié)構(gòu)體的內(nèi)存大小為56字節(jié)
- 屬性
magic
的類型是magic_t結(jié)構(gòu)體
抡句,所占內(nèi)存大小為m[4]
其內(nèi)存(即4*4=16字節(jié)
) - 屬性
next(指針)
探膊、thread(對象)
、parent(對象)
待榔、child(對象)
均占8字節(jié)
(即4*8=32字節(jié)
) - 屬性
depth
逞壁、hiwat
類型為uint32_t
流济,實際類型是unsigned int
類型,均占4字節(jié)
(即2*4=8字節(jié)
)
通過上面可以知道一個空的AutoreleasePoolPage的結(jié)構(gòu)如下:
- 屬性
objc_autoreleasePoolPush 源碼分析
進(jìn)入push的源碼實現(xiàn):有以下邏輯:
- 首先進(jìn)行判斷
是否
存在pool
- 如果沒有腌闯,則通過
autoreleaseNewPage
方法創(chuàng)建 - 如果有绳瘟,則通過
autoreleaseFast
壓棧哨兵對象
autoreleaseNewPage創(chuàng)建新頁
先來看下autoreleaseNewPage
創(chuàng)建新頁的實現(xiàn)過程
通過上面的代碼實現(xiàn)(autoreleaseFullPage
后面會重點分析),我們可得到以下結(jié)論
- 1.獲取當(dāng)前操作頁,
- 2.如果當(dāng)前操作頁存在,則通過
autoreleaseFullPage
方法進(jìn)行壓棧對象 - 3.如果當(dāng)前操作頁不存在姿骏,則通過
autoreleaseNoPage
方法創(chuàng)建頁- 在
autoreleaseNoPage
方法中可知當(dāng)前線程的自動釋放池是通過AutoreleasePoolPage
創(chuàng)建 -
AutoreleasePoolPage
的構(gòu)造方法
是通過實現(xiàn)父類AutoreleasePoolPageData的初始化方法實現(xiàn)的
.
- 在
AutoreleasePoolPage構(gòu)造方法
上面說了當(dāng)前線程的自動釋放池
是通過AutoreleasePoolPage創(chuàng)建
糖声,看下AutoreleasePoolPage
構(gòu)造方法:
其中AutoreleasePoolPageData
方法傳入的參數(shù)含義為:
-
begin()
表示壓棧的位置
(即下一個要釋放對象的壓棧地址
).可以通過源碼調(diào)試begin
,發(fā)現(xiàn)其具體實現(xiàn)等于頁首地址+56
分瘦,其中的56
就是結(jié)構(gòu)體AutoreleasePoolPageData的內(nèi)存大小
.- 由于在
ARC
模式下蘸泻,是無法手動調(diào)用autorelease
,所以將Demo
切換至MRC
模式(Build Settings -> Objectice-C Automatic Reference Counting設(shè)置為NO
)
- 由于在
分析:AutoreleasePoolPageData
中的指針和對象
都占8字節(jié)
,uint
占4字節(jié)
去团,只有magic_t
未知(因為不是個指針抡诞,所以需要看具體類型);magic_t是個指針
土陪,由于靜態(tài)變量的存儲區(qū)域在全局段沐绒,所以magic_t
占用4*4=16
字節(jié),即AutoreleasePoolPageData
結(jié)構(gòu)體的內(nèi)存大小為56字節(jié)
.
-
objc_thread_self()
是表示當(dāng)前線程
,而當(dāng)前線程是通過tls獲取
newParent
表示父節(jié)點后續(xù)兩個參數(shù)是
通過父節(jié)點的深度
旺坠、最大入棧個數(shù)
計算的depth
以及hiwat
查看自動釋放池內(nèi)存結(jié)構(gòu)
接著我們使用_objc_autoreleasePoolPrint
函數(shù)來打印一下自動釋放池的相關(guān)信息(記得切換為MRC
模式調(diào)試,這里前面我們已經(jīng)切換了)
通過運行結(jié)果如下乔遮,我們發(fā)現(xiàn)release
是6個,但是我們壓棧的對象其實只有5個
取刃,其中的POOL表示哨兵對象
蹋肮,即邊界
,其目的是為了防止越界
,我們再看下打印地址璧疗,發(fā)現(xiàn)頁的首地址
(PAGE)和哨兵對象
(POOL)相差0x38
坯辩,轉(zhuǎn)成10進(jìn)制
正好是56
.也就是AutoreleasePoolPage自己本身的內(nèi)存大小
.
那么是否可以無限往AutoreleasePool
中添加對象呢?答案是不能崩侠!
將循環(huán)次數(shù)i
的上限改為505
漆魔,其內(nèi)存結(jié)構(gòu)如下,發(fā)現(xiàn)第一頁滿了
却音,存儲了504個
要釋放的對象
改抡,第二頁只存儲了一個
在將循環(huán)次數(shù)i
據(jù)改為505+506,來驗證第二頁是否也是存儲504個對象?
通過運行發(fā)現(xiàn)系瓢,第一頁存儲504
阿纤,第二頁存儲505
,第三頁存儲2個
.
通過上述測試夷陋,我們可以得出以下結(jié)論:
- 第一頁可以存放
504個對象
,且只有第一頁有哨兵對象
,當(dāng)一頁壓棧滿了博烂,就會開辟新的一頁 - 第二頁開始吕粹,
最多可以存放505個對象
- 一頁的大小等于
505 * 8 = 4040
這個結(jié)論我們之前講AutoreleasePoolPage
中的SIZE
的時候就說了,一頁的大小是4096字節(jié)
,而在其構(gòu)造函數(shù)中對象的壓棧位置
,是從首地址+56字節(jié)開始
的,所以可以一頁中實際可以存儲4096-56 = 4040字節(jié)
榛搔,轉(zhuǎn)換成對象是 4040 / 8 = 505
個,即一頁最多可以存儲505個對象
,其中第一頁有哨兵對象
(由于自動釋放池在初始化時會POOL_BOUNDARY哨兵對象push到棧頂东揣,所以第一頁只能存放504個對象践惑,接下來每一頁都能存放505個對象)只能存儲504個.其結(jié)構(gòu)圖示如下
通過上面的結(jié)論,我有一個疑問:哨兵對象在一個自動釋放池有幾個?
- 在
一個自動釋放池
中只有一個哨兵對象
嘶卧,且哨兵在第一頁
- 第一頁最多可以存
504
個對象尔觉,第二頁開始最多存505
個
③.2 哨兵對象 -- POOL_BOUNDARY
哨兵對象
本質(zhì)上是個nil
,它的作用主要在調(diào)用objc_autoreleasePoolPop
時體現(xiàn):
- 根據(jù)傳入的哨兵對象地址找到哨兵對象所在的
page
- 在當(dāng)前
page
中芥吟,將晚于哨兵對象插入的所有autorelese對象
都發(fā)送一次release
消息侦铜,并移動next指針
到正確位置 - 從最新加入的對象一直
向前
清理,可以向前跨越若干個page
钟鸵,直到哨兵對象所在的page
③.3 壓棧對象autoreleaseFast
進(jìn)入autoreleaseFast源碼:主要有以下幾步:
- 1.獲取當(dāng)前操作頁钉稍,并判斷頁是否存在以及是否滿了
- 2.如果頁
存在
,且未滿
棺耍,則通過add
方法壓棧對象
- 3.如果頁
存在
贡未,且滿了
,則通過autoreleaseFullPage
方法安排新的頁面
- 4.如果頁
不存在
蒙袍,則通過autoreleaseNoPage
方法創(chuàng)建新頁
autoreleaseFullPage方法
其源碼為:這個方法主要是用于判斷當(dāng)前頁是否已經(jīng)存儲滿了俊卤,如果當(dāng)前頁已經(jīng)滿了,通過do-while
循環(huán)查找子節(jié)點對應(yīng)的頁害幅,如果不存在
就開辟新的AutoreleasePoolPage并設(shè)為HotPage
,然后壓棧對象
.從上面AutoreleasePoolPage
初始化方法中可以看出消恍,主要是通過操作child對象
,將當(dāng)前頁的child指向新建頁面
以现,由此可以得出頁
是通過雙向鏈表連接
.
add方法
查看源碼:這個方法主要是添加釋放對象
狠怨,其底層是實現(xiàn)是通過next指針
存儲釋放對象,并將next指針遞增
邑遏,表示下一個釋放對象存儲的位置
.從這里可以看出頁
是通過棧結(jié)構(gòu)存儲
③.4 autorelease 底層分析
在demo
中佣赖,我們通過autorelease
方法,在MRC
模式下无宿,將對象壓棧到自動釋放池茵汰,下面來分析其底層實現(xiàn):
-
查看autorelease方法源碼
-
進(jìn)入對象的autorelease實現(xiàn)
從這里看出枢里,無論是壓棧哨兵對象
孽鸡,還是普通對象
蹂午,都會來到autoreleaseFast
方法,只是區(qū)別標(biāo)識不同
而以.
③.5 objc_autoreleasePoolPop 源碼分析&出棧
objc_autoreleasePoolPop 源碼分析
在objc_autoreleasePoolPop
方法中有個參數(shù)彬碱,在clang
分析時豆胸,發(fā)現(xiàn)傳入的參數(shù)是push壓棧后返回的哨兵對象
,即ctxt
巷疼,其目的是避免出椡砗混亂
,防止將別的對象出棧
嚼沿,其內(nèi)部是調(diào)用AutoreleasePoolPage
的pop
方法估盘,我們看下pop源碼:
pop源碼實現(xiàn),主要由以下幾步:
- 1.空頁面的處理骡尽,并根據(jù)token獲取page
- 2.容錯處理
- 3.通過
popPage
出棧頁
出棧 -- popPage
查看popPage源碼:進(jìn)入popPage源碼遣妥,其中傳入的allowDebug
為false
,則通過releaseUntil
出棧當(dāng)前頁stop位置之前的所有對象
攀细,即向棧中的對象發(fā)送release消息
箫踩,直到遇到傳入的哨兵對象
.
releaseUntil方法
看源碼我們可以知道:
-
releaseUntil
的實現(xiàn),主要是通過循環(huán)遍歷
谭贪,判斷對象是否等于stop
境钟,其目的是釋放stop之前的所有的對象
- 首先通過獲取
page的next釋放對象
(即page的最后一個對象),并對next
進(jìn)行遞減
俭识,獲取上一個對象
- 判斷
是否是哨兵對象
慨削,如果不是則自動調(diào)用objc_release
釋放
kill方法
通過kill實現(xiàn)我們知道,主要是銷毀當(dāng)前頁
套媚,將當(dāng)前頁賦值為父節(jié)點頁
理盆,并將父節(jié)點頁的child對象指針置為nil
③.6 總結(jié)
- 1.
autoreleasepool
其本質(zhì)是一個結(jié)構(gòu)體對象
,一個自動釋放池對象
就是頁
凑阶,是棧結(jié)構(gòu)存儲
猿规,符合先進(jìn)后出
的原則 - 2.
頁的棧底
是一個56
字節(jié)大小的空占位符
,一頁總大小為4096字節(jié)
- 3.只有
第一頁有哨兵對象
宙橱,最多存儲504個對象
姨俩,從第二頁開始
最多存儲505個對象
- 4.
autoreleasepool
在加入要釋放的對象
時,底層調(diào)用的是objc_autoreleasePoolPush
方法(push
操作)- 當(dāng)沒有
pool
师郑,即只有空占位符
(存儲在tls中)時环葵,則創(chuàng)建頁,壓棧哨兵對象
- 在頁中
壓棧普通對象
主要是通過next指針遞增
進(jìn)行的 - 當(dāng)
頁滿了
時宝冕,需要設(shè)置頁的child
對象為新建頁
-
objc_autoreleasePush
的整體底層的流程圖如下
- 當(dāng)沒有
- 5.
autoreleasepool
在調(diào)用析構(gòu)函數(shù)釋放
時张遭,內(nèi)部的實現(xiàn)是調(diào)用objc_autoreleasePoolPop
方法(pop操作)- 在頁中
出棧普通對象
主要是通過next指針遞減
進(jìn)行的 - 當(dāng)
頁空了
時,需要賦值頁的parent
對象為當(dāng)前頁 -
objc_autoreleasePoolPop出棧的流程圖如下
- 在頁中
④ 提出疑問
④.1 臨時變量什么時候釋放?
- 1.如果在
正常情況
下地梨,一般是超出其作用域就會立即釋放
- 2.如果將臨時變量加入了
自動釋放池
菊卷,會延遲釋放
缔恳,即在runloop休眠或者autoreleasepool作用域之后釋放
④.2 自動釋放池原理 即AutoreleasePool原理
- 1.
自動釋放池
的本質(zhì)
是一個AutoreleasePoolPage結(jié)構(gòu)體對象
,是一個棧結(jié)構(gòu)存儲的頁
洁闰,每一個AutoreleasePoolPage
都是以雙向鏈表的形式連接
- 2.
自動釋放池
的壓棧
和出棧
主要是通過結(jié)構(gòu)體的構(gòu)造函數(shù)
和析構(gòu)函數(shù)
調(diào)用底層的objc_autoreleasePoolPush
和objc_autoreleasePoolPop
歉甚,實際上是調(diào)用AutoreleasePoolPage
的push
和pop
兩個方法 - 3.每次
調(diào)用push操作
其實就是創(chuàng)建
一個新的AutoreleasePoolPage
,而AutoreleasePoolPage
的具體操作就是插入一個POOL_BOUNDARY
扑眉,并返回插入POOL_BOUNDARY的內(nèi)存地址
.而push
內(nèi)部調(diào)用autoreleaseFast
方法處理纸泄,主要有以下三種情況- 當(dāng)
page存在
,且不滿
時腰素,調(diào)用add方法
將對象添加至page的next指針處
聘裁,并next遞增
- 當(dāng)
page存在
,且已滿
時弓千,調(diào)用autoreleaseFullPage
初始化一個新的page
咧虎,然后調(diào)用add方法
將對象添加至page棧中
- 當(dāng)
page不存在
時,調(diào)用autoreleaseNoPage
創(chuàng)建一個hotPage
计呈,然后調(diào)用add方法
將對象添加至page棧中
- 當(dāng)
- 4.當(dāng)執(zhí)行
pop操作
時砰诵,會傳入一個值
,這個值就是push操作的返回值
捌显,即POOL_BOUNDARY的內(nèi)存地址token
.所以pop
內(nèi)部的實現(xiàn)就是根據(jù)token找到哨兵對象所處的page
中茁彭,然后使用objc_release
釋放token
之前的對象,并把next指針到正確位置
④.3 自動釋放池能否嵌套使用扶歪?
- 1.可以嵌套使用理肺,其目的是可以
控制應(yīng)用程序的內(nèi)存峰值
,使其不要太高 - 2.可以嵌套的原因是因為
自動釋放池是以棧為節(jié)點
善镰,通過雙向鏈表的形式連接
的妹萨,且是和線程一一對應(yīng)的
- 3.自動釋放池的
多層嵌套
其實就是不停的push哨兵對象
,在pop
時炫欺,會先釋放里面的乎完,在釋放外面的
④.4 哪些對象可以加入AutoreleasePool?alloc創(chuàng)建可以嗎品洛?
- 1.在
MRC
下使用new树姨、alloc、copy
關(guān)鍵字生成的對象和retain
了的對象需要手動釋放
桥状,不會被添加到自動釋放池中 - 2.在
MRC
下設(shè)置為autorelease
的對象不需要手動釋放
帽揪,會直接進(jìn)入自動釋放池
- 3.所有
autorelease
的對象,在出了作用域之后
辅斟,會被自動添加到最近創(chuàng)建的自動釋放池
中 - 4.在
ARC
下只需要關(guān)注引用計數(shù)
转晰,因為創(chuàng)建都是在主線程
進(jìn)行的,系統(tǒng)會自動為主線程創(chuàng)建AutoreleasePool
,所以創(chuàng)建的對象會自動放入自動釋放池
④.5 AutoreleasePool的釋放時機是什么時候查邢?
- 1.
App
啟動后蔗崎,蘋果在主線程RunLoop
里注冊了兩個Observer
,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
- 2.第一個
Observer
監(jiān)視的事件是Entry
(即將進(jìn)入 Loop)侠坎,其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()
創(chuàng)建自動釋放池.其order是-2147483647蚁趁,優(yōu)先級最高
裙盾,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前 - 3.第二個
Observer
監(jiān)視了兩個事件:BeforeWaiting
(準(zhǔn)備進(jìn)入休眠) 時調(diào)用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的池并創(chuàng)建新池实胸;Exit
(即 將退出Loop)時調(diào)用_objc_autoreleasePoolPop()
來釋放自動釋放池.這個Observer
的order
是 2147483647,優(yōu)先級最低
番官,保證其釋放池子發(fā)生在其他所有回調(diào)之后
④.6 thread和AutoreleasePool的關(guān)系
每個線程都有與之關(guān)聯(lián)的自動釋放池堆棧結(jié)構(gòu)庐完,新的pool
在創(chuàng)建時
會被壓棧到棧頂
,pool
銷毀時徘熔,會被出棧
门躯,對于當(dāng)前線程
來說,釋放對象會被壓棧到棧頂
酷师,線程停止
時讶凉,會自動釋放
與之關(guān)聯(lián)的自動釋放池.
④.7 RunLoop和AutoreleasePool的關(guān)系
- 1.主程序的RunLoop在每次事件循環(huán)之前,會自動創(chuàng)建一個autoreleasePool
- 2.并且會在事件循環(huán)結(jié)束時山孔,執(zhí)行drain操作懂讯,釋放其中的對象
六、NSRunLoop
① RunLoop介紹
RunLoop
是事件接收
和分發(fā)機制
的一個實現(xiàn)台颠,是線程相關(guān)的基礎(chǔ)框架的一部分褐望,一個RunLoop
就是一個事件處理的循環(huán),用來不停的調(diào)度工作以及處理輸入事件.
RunLoop
本質(zhì)是一個do-while
循環(huán)串前,沒事做就休息瘫里,來活了就干活.與普通的while
循環(huán)是有區(qū)別的,普通的while
循環(huán)會導(dǎo)致CPU進(jìn)入忙等待狀態(tài)
荡碾,即一直消耗cpu谨读,而RunLoop
則不會,RunLoop是一種閑等待
坛吁,即RunLoop具備休眠功能
.
RunLoop的作用
- 保持程序的
持續(xù)運行
- 處理
App
中的各種事件(觸摸
漆腌、定時器
、performSelector
) - 節(jié)省
cpu
資源阶冈,提供程序的性能闷尿,該做事就做事,該休息就休息
RunLoop源碼的下載地址女坑,在其中找到最新版下載即可
② RunLoop和線程的關(guān)系
②.1 獲取RunLoop
一般在日常開發(fā)中填具,對于RunLoop
的獲取主要有以下兩種方式
②.2 CFRunLoopGetMain源碼
②.3 _CFRunLoopGet0源碼
通過上面可以知道,Runloop
只有兩種,一種是主線程
的劳景,一個是其它線程
的.即Runloop和線程
是一一對應(yīng)
的.
③ RunLoop的創(chuàng)建
通過上面的_CFRunLoopGet0
可以知道Runloop
是通過__CFRunLoopCreate
創(chuàng)建(系統(tǒng)創(chuàng)建誉简,開發(fā)者自己是無法創(chuàng)建的
).我們查看下__CFRunLoopCreate
源碼:
我們發(fā)現(xiàn)__CFRunLoopCreate
主要是對runloop
屬性的賦值操作.我們繼續(xù)看CFRunLoopRef
的源碼
可以得出以下結(jié)論:
- 1.根據(jù)定義得知,其實
RunLoop
也是一個對象.是__CFRunLoop
結(jié)構(gòu)體的指針
類型 - 2.
一個RunLoop依賴于多個Mode
盟广,意味著一個RunLoop需要處理多個事務(wù)
闷串,即一個Mode對應(yīng)多個Item
,而一個item
中筋量,包含了timer
烹吵、source
、observer
桨武,可以用下圖說明
③.1 Mode類型
其中mode
在蘋果文檔中提及的有五
個肋拔,而在iOS中公開暴露出來的只有 NSDefaultRunLoopMode
和NSRunLoopCommonModes
. NSRunLoopCommonModes
實際上是一個Mode的集合
,默認(rèn)包括 NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
.
-
NSDefaultRunLoopMode
:默認(rèn)
的mode呀酸,正常情況下都是在這個model下運行(包括主線程) -
NSEventTrackingRunLoopMode
(cocoa
):追蹤mode
凉蜂,使用這個mode
去跟蹤來自用戶交互的事件
(比如UITableView上下滑動流暢,為了不受其他mode影響)UITrackingRunLoopMode(iOS) -
NSModalPanelRunLoopMode
:處理modal panels
事件 -
NSConnectionReplyMode
:處理NSConnection
對象相關(guān)事件性誉,系統(tǒng)內(nèi)部使用
窿吩,用戶基本不會使用 -
NSRunLoopCommonModes
:這是一個偽模式
,其為一組runloop mode的集合
错览,將輸入源加入此模式意味著在Common Modes
中包含的所有模式下都可以處理.在Cocoa
應(yīng)用程序中纫雁,默認(rèn)情況下Common Modes
包含default modes,modal modes,event Tracking modes
.可使用CFRunLoopAddCommonMode
方法將Common Modes
中添加自定義modes
.
③.2 Source & Timer & Observer
-
Source
表示可以喚醒RunLoop
的一些事件,例如用戶點擊了屏幕蝗砾,就會創(chuàng)建一個RunLoop
先较,主要分為Source0
和Source1
-
Source0
表示非系統(tǒng)事件
,即用戶自定義的事件 -
Source1
表示系統(tǒng)事件
悼粮,主要負(fù)責(zé)底層的通訊
闲勺,具備喚醒能力
-
-
Timer
就是常用NSTimer
定時器這一類 -
Observer
主要用于監(jiān)聽RunLoop的狀態(tài)變化
,并作出一定響應(yīng)
扣猫,主要有以下一些狀態(tài)
④ 測試驗證
④.1 驗證:RunLoop和mode是一對多
上面我們說過RunLoop
和mode
是一對多
的關(guān)系菜循,下面我們通過運行代碼來實操證明. 我們先通過lldb命令
獲取mainRunloop
、currentRunloop
的currentMode
運行結(jié)果表明runloop
在運行時的mode
只有一個.
下面我們獲取mainRunLoop
所有的模型
從上面的打印結(jié)果可以驗證runloop
和CFRunloopMode
具有一對多
的關(guān)系.
④.2 驗證:mode和Item也是一對多
我們繼續(xù)在斷點處申尤,通過bt查看堆棧信息癌幕,從這里看出timer的item類型如下所示(截取部分)在RunLoop
源碼中查看Item
類型,有以下幾種:
- block應(yīng)用:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
- 調(diào)用timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- 響應(yīng)source0:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
- 響應(yīng)source1:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
- GCD主隊列:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
- observer源:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
- 1.其實現(xiàn)主要判斷是否是
kCFRunLoopCommonModes
厅瞎,然后查找runloop
的mode
進(jìn)行匹配處理 - 2.其中
kCFRunLoopCommonModes不是一種模式
,是一種抽象的偽模式
初坠,比defaultMode
更加靈活 - 3.通過
CFSetAddValue(rl->_commonModeItems, rlt)
;可以得知和簸,runloop
與mode
是一對多
的,同時可以得出mode
與item
也是一對多的
.
⑤ RunLoop執(zhí)行
我們都知道碟刺,RunLoop
的執(zhí)行依賴于run
方法锁保,從下面的堆棧信息中可以看出,其底層執(zhí)行的是__CFRunLoopRun
方法
進(jìn)入__CFRunLoopRun
源碼:
通過__CFRunLoopRun
源碼可知半沽,針對不同的對象爽柒,有不同的處理
- 如果有observer,則調(diào)用__CFRunLoopDoObservers
- 如果有block抄囚,則調(diào)用__CFRunLoopDoBlocks
- 如果有timer霉赡,則調(diào)用__CFRunLoopDoTimers
- 如果是source0橄务,則調(diào)用__CFRunLoopDoSources0
- 如果是source1幔托,則調(diào)用__CFRunLoopDoSource1
_ _CFRunLoopDoTimers
查看下__CFRunLoopDoTimers源碼主要是通過for
循環(huán),對單個timer
進(jìn)行處理蜂挪,下面繼續(xù)看__CFRunLoopDoTimer
源碼:
通過源碼可知:主要邏輯就是timer
執(zhí)行完畢后重挑,會主動調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
函數(shù),正好與timer
堆棧調(diào)用中的一致.
timer執(zhí)行總結(jié)
- 1.為自定義的
timer
棠涮,設(shè)置Mode
谬哀,并將其加入RunLoop
中 - 2.在
RunLoop
的run
方法執(zhí)行時,會調(diào)用__CFRunLoopDoTimers
執(zhí)行所有timer
- 3.在
__CFRunLoopDoTimers
方法中严肪,會通過for循環(huán)執(zhí)行單個timer
的操作 - 4.在
__CFRunLoopDoTimer
方法中史煎,timer
執(zhí)行完畢后,會執(zhí)行對應(yīng)的timer
回調(diào)函數(shù)
以上驳糯,是針對timer
的執(zhí)行分析篇梭,對于observer
、block
酝枢、source0
恬偷、source1
,其執(zhí)行原理與timer
是類似的帘睦,這里就不再重復(fù)說明以下是蘋果官方文檔針對RunLoop
處理不同源的圖示
⑥ RunLoop底層原理
從上述的堆棧信息中可以看出袍患,run
在底層的實現(xiàn)路徑為CFRunLoopRun -> CFRunLoopRun -> __CFRunLoopRun
進(jìn)入CFRunLoopRun
源碼,其中傳入的參數(shù)1.0e10
(科學(xué)計數(shù))等于1* e^10
竣付,用于表示超時時間
- 首先根據(jù)
modeName
找到對應(yīng)的mode
诡延,然后主要分為三種情況:- 如果是
entry
,則通知observer
古胆,即將進(jìn)入runloop
- 如果是
exit
肆良,則通過observer
,即將退出runloop
- 如果是其他
中間狀態(tài)
,主要是通過runloop處理各種源
- 如果是
上面說到會調(diào)用__CFRunLoopRun
妖滔,上面講了在這一步里面會根據(jù)不同的事件源進(jìn)行不同的處理
敦间,當(dāng)RunLoop休眠時
,可以通過相應(yīng)的事件喚醒RunLoop
.
所以焊虏,綜上所述焰望,RunLoop
的執(zhí)行流程,如下所示
⑦ 提出疑問
⑦.1 當(dāng)前有個子線程曲秉,子線程中有個timer采蚀。timer是否能夠執(zhí)行,并進(jìn)行持續(xù)的打映卸榆鼠?
不可以,因為子線程的runloop默認(rèn)不啟動
亥鸠, 需要runloop run
手動啟動.
⑦.2 RunLoop和線程的關(guān)系
1.每個線程
都有一個與之對應(yīng)的RunLoop
妆够,所以RunLoop
與線程
是一一
對應(yīng)的,其綁定關(guān)系通過一個全局的Dictionary存儲
负蚊,線程為key
神妹,runloop為value
.
2.線程中的RunLoop
主要是用來管理線程
的,當(dāng)線程的RunLoop開啟
后家妆,會在執(zhí)行完任務(wù)后
進(jìn)行休眠狀態(tài)
鸵荠,當(dāng)有事件觸發(fā)喚醒
時,又開始工作
伤极,即有活時干活蛹找,沒活就休息
3.主線程
的RunLoop
是默認(rèn)開啟
的,在程序啟動之后哨坪,會一直運行庸疾,不會退出
4.其他線程
的RunLoop
默認(rèn)是不開啟
的,如果需要齿税,則手動開啟
⑦.3 NSRunLoop和CFRunLoopRef區(qū)別
- 1.
NSRunLoop
是基于CFRunLoopRef
面向?qū)ο蟮?code>API彼硫,是不安全
的 - 2.
CFRunLoopRef
是基于C
語言,是線程安全的
⑦.4 Runloop的mode作用是什么凌箕?
mode
主要是用于指定RunLoop
中事件優(yōu)先級
的
⑦.5 以+scheduledTimerWithTimeInterval:的方式觸發(fā)的timer拧篮,在滑動頁面上的列表時,timer會暫颓2眨回調(diào)串绩,為什么?如何解決芜壁?
- 1.
timer
停止的原因是因為滑動scrollView
時礁凡,主線程的RunLoop
會從NSDefaultRunLoopMode
切換到UITrackingRunLoopMode
高氮,而timer
是添加在NSDefaultRunLoopMode
。所以timer
不會執(zhí)行 - 2.將
timer
放入NSRunLoopCommonModes
中執(zhí)行.
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.