- 本節(jié),進(jìn)入
內(nèi)存管理篇章,將從以下幾部分講解:
- 內(nèi)存布局
- TaggedPointer
- 引用計(jì)數(shù)(retain、release、dealloc) & SideTables 散列表
- retainCount
準(zhǔn)備工作:
- 可編譯的
objc4-781源碼: http://www.itdecent.cn/p/45dc31d91000
1. 內(nèi)存布局
按照地址從高到低排列: 棧區(qū) -> 堆區(qū) -> 全局靜態(tài)區(qū) -> 常量區(qū) -> 代碼區(qū) (內(nèi)核區(qū)和保留部分不在考慮范圍內(nèi))

補(bǔ)充說(shuō)明:
內(nèi)存五大區(qū),實(shí)際是指虛擬內(nèi)存,而不是真實(shí)物理內(nèi)存。(詳情可查看?? 本文第3點(diǎn) 虛擬內(nèi)存與物理內(nèi)存)iOS系統(tǒng)中,應(yīng)用的虛擬內(nèi)存默認(rèn)分配4G大小,但五大區(qū)只占3G,還有1G是五大區(qū)之外的內(nèi)核區(qū)
內(nèi)存五大區(qū)的詳細(xì)功能,可查看?? 內(nèi)存五大區(qū)
2.TaggedPointer
-
TaggedPointer是標(biāo)記指針。標(biāo)記的是小對(duì)象,可以記錄小內(nèi)容的NSString、NSDate、NSNumber等內(nèi)容。是內(nèi)存管理(節(jié)省內(nèi)存開銷)的一種有效手段。
例如:使用
NSNumber記錄10
【常規(guī)操作】
去堆中開辟一個(gè)內(nèi)存,用于存儲(chǔ)這個(gè)對(duì)象,在對(duì)象內(nèi)部記錄這個(gè)內(nèi)容10,然后需要一個(gè)指針,指向堆中的內(nèi)存首地址。
(內(nèi)存開銷:指針8字節(jié)+堆中對(duì)象的空間大小)【
TaggedPointer小對(duì)象】
記錄一個(gè)10,根本不用去堆中開辟內(nèi)存,直接利用指針擁有的8字節(jié)空間即可。
(類似NonPointer_isa非指針型isa,使用union聯(lián)合體位域,中間shiftcls部分存儲(chǔ)類信息。其他部位記錄其他有效信息。 相關(guān)參考?? 剖析isa)
(內(nèi)存開銷:指針8字節(jié))結(jié)論:
占用空間較小的對(duì)象,直接使用指針內(nèi)部空間,節(jié)約內(nèi)存開銷
2.1案例分析
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, copy) NSString * name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10000 ; i++) {
dispatch_async(self.queue, ^{
self.name = [NSString stringWithFormat:@"ht"];
NSLog(@"%@ %p %s",self.name, self.name, object_getClassName(self.name));
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"來(lái)了");
for (int i = 0; i < 10000; i++) {
dispatch_async(self.queue, ^{
self.name = [NSString stringWithFormat:@"ht_學(xué)習(xí)不行,回家種田"]; //setter - 新值retain,舊值release
NSLog(@"%@ %p %s",self.name, self.name, object_getClassName(self.name)); // getter
});
}
}
@end
- 上面
打印結(jié)果:
image.png -
點(diǎn)擊觸發(fā)TouchBegin后的打印結(jié)果:
image.png
Q: 兩次
10000次的循環(huán),都是對(duì)name進(jìn)行賦值,為什么上面不會(huì)崩潰,下面會(huì)崩潰?A: 因?yàn)樯厦?code>name存儲(chǔ)在
棧中,不需要手動(dòng)釋放,而下面name存儲(chǔ)在堆中,在setter賦值時(shí)會(huì)觸發(fā)retain和release,異步線程中,可能導(dǎo)致指針過(guò)度釋放,造成了崩潰。
name賦值為ht時(shí),是小對(duì)象(NSTaggedPointerString),直接將內(nèi)容存儲(chǔ)在指針內(nèi)部,指針存儲(chǔ)在棧中,由系統(tǒng)負(fù)責(zé)管理。
name賦值為ht_學(xué)習(xí)不行,回家種田時(shí),由于內(nèi)容過(guò)多,指針內(nèi)部空間不夠存儲(chǔ),所以去堆中開辟空間,需要管理引用計(jì)數(shù)了。每次setter都會(huì)觸發(fā)新值retain和舊值release。異步線程中,可能導(dǎo)致retain未完成,但提前release了,導(dǎo)致指針過(guò)度釋放,造成了崩潰。
底層探索:
clang對(duì)ViewController.m文件編譯成.cpp文件:clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
- 可以看到
name的setter方法實(shí)際是調(diào)用objc_setProperty,打開objc4源碼,搜索objc_setProperty:
image.png- 可以看到
taggedPointer對(duì)象不參與引用計(jì)數(shù)的計(jì)算。
2.2 NSTaggedPointerString底層探索
- 沿著
上面看到的isTaggedPointer()的判斷,我們進(jìn)入isTaggedPointer()內(nèi)部:
image.png
【拓展】:
taggedPointer混淆機(jī)制
APP啟動(dòng)時(shí),dyld調(diào)用_read_images時(shí),第一步有一個(gè)初始化taggedPointer混淆機(jī)制
image.png- 內(nèi)部為:
iOS10.14之后,且打開了小對(duì)象混淆機(jī)制,就進(jìn)行小對(duì)象混淆:
image.png- 搜索
objc_debug_taggedpointer_obfuscator,可以看到混淆的編碼和解碼:
image.png- 編碼:
原地址進(jìn)行^異或操作一次,得到編碼地址- 解碼: 將
編碼地址再進(jìn)行^異或操作一次,得到原地址
- 混淆驗(yàn)證:
// 聲明(在其他庫(kù)中實(shí)現(xiàn))
extern uintptr_t objc_debug_taggedpointer_obfuscator;
// 從objc4源碼拷貝解碼代碼, 入?yún)⒏臑閕d類型
static inline uintptr_t
_objc_decodeTaggedPointer(id ptr) {
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- (void)demo {
NSString * str1 = [NSString stringWithFormat:@"a"];
NSNumber * num1 = @(1);
NSLog(@"str1: %p-%@", str1, str1);
NSLog(@"num1: %p-%@", num1, num1);
NSLog(@"str1 解碼地址: 0x%lx", _objc_decodeTaggedPointer(str1));
NSLog(@"num1 解碼地址: 0x%lx", _objc_decodeTaggedPointer(num1));
}

- 可以看到
解碼后,從地址就可以直接讀出當(dāng)前內(nèi)容。所以混淆機(jī)制就是為了讓小對(duì)象從地址上降低識(shí)別度。
Q:
解碼后的小對(duì)象地址,前面a/b是啥意思?
image.png
- 當(dāng)前系統(tǒng),為了
新舊兼容,TaggedPointer指針的后四位不再存儲(chǔ)值。
taggedpointer類型的優(yōu)點(diǎn):
節(jié)省內(nèi)存開銷:充分利用指針地址空間。
執(zhí)行效率高: 不需要retain和release,系統(tǒng)直接管理釋放、回收,少執(zhí)行很多代碼,不需要堆空間,直接創(chuàng)建和讀取。(官方說(shuō)內(nèi)存讀取快3倍,創(chuàng)建快106倍)
建議:
當(dāng)
字符串較長(zhǎng)時(shí),主動(dòng)使用@""創(chuàng)建,而不用stringFormat創(chuàng)建。因?yàn)槿绻皇?code>taggedPointer,反而會(huì)更耗時(shí)間。
image.png不同
字符串長(zhǎng)度的類型:
image.png
3. 引用計(jì)數(shù)(retain、release、dealloc) & SideTables 散列表
回憶:
在熟悉isa的指針內(nèi)部結(jié)構(gòu)時(shí)(參考 ?? 剖析isa),我們有提到:
isa結(jié)構(gòu)圖
has_sidetable_rc:當(dāng)對(duì)象引用技術(shù)大于10時(shí),則需要借用該變量存儲(chǔ)進(jìn)位
extra_rc:當(dāng)表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減 1。
如: 如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為 9。如果引用計(jì)數(shù)大于 10, 則需要使用到has_sidetable_rc。(這只是舉例,具體方式,我們下面解析)
-
說(shuō)到
引用計(jì)數(shù),自然就想到了retain、release。上面示例中,如果不是taggedPointer,就會(huì)進(jìn)入obj->retain,我們進(jìn)入retain內(nèi)部:
(ps: 為了內(nèi)容不太分散,我梳理成一張大圖,可查看原圖,放大觀看)
image.png -
哈希表結(jié)構(gòu)如下:
image.png
【reatin總結(jié)】
taggedPointer類直接返回原對(duì)象操作時(shí),使用新isa拷貝當(dāng)前isa對(duì)象,隔離操作。- 【
do-while】循環(huán),是由于多線程環(huán)境下,當(dāng)前isa可能在變化,只要變化就需要再次操作- 如果
不支持指針優(yōu)化,直接操作散列表進(jìn)行計(jì)數(shù)。- 如果
isa記錄了正在釋放,就不用retain了引用計(jì)數(shù)+1,首先嘗試在isa的extra_rc中+1:
- 成功:【
do-while】retain操作結(jié)束- 失敗: 表示
extra_rc存儲(chǔ)滿了,此時(shí)將extra_rc計(jì)數(shù)減半,has_sidetable_rc(使用散列表)標(biāo)記true,transcribeToSideTable(轉(zhuǎn)移給散列表)標(biāo)記true。- 【
do-while】外,判斷transcribeToSideTable給散列表添加extra_rc最大容量的一半計(jì)數(shù)。
- 接著,我們來(lái)了解
release:
image.png
【release總結(jié)】
taggedPointer類直接返回原對(duì)象操作時(shí),使用新isa拷貝當(dāng)前isa對(duì)象,隔離操作。- 【retry】:
(do-while循環(huán),監(jiān)測(cè)多線程環(huán)境下,當(dāng)前isa是否變化)
- 如果不支持指針優(yōu)化,直接操作散列表進(jìn)行計(jì)數(shù)。
-嘗試給isa的extra_rc - 1: 如果失敗,跳轉(zhuǎn)【underflow】- 【underflow】:
(表示extra_rc計(jì)數(shù)為空,不可以進(jìn)行-1,需要去散列表拿計(jì)數(shù)再操作或直接釋放)
- 如果有
散列表(has_sidetable_rc為true):
- 如果散列表
沒(méi)鎖,關(guān)鎖再重新【retry】- 嘗試從散列表
讀取RC_HALF(extra_rc的一半容量)計(jì)數(shù)
- 如果
取到值,嘗試更新extra_rc和isa。更新失敗再次讀取和更新。如果還失敗,就將borrowed加回給sidetable沒(méi)散列表或散列表內(nèi)沒(méi)計(jì)數(shù),如果正在釋放中,就不處理。 否則,將deallocating設(shè)為true(去釋放),重新【retry】- 如果
實(shí)現(xiàn)了dealloc函數(shù),就給發(fā)送消息調(diào)用dealloc
-
引用計(jì)數(shù)為0時(shí),會(huì)調(diào)用dealloc:
image.png
【dealloc總結(jié)】
- 是
優(yōu)化指針,但無(wú)弱引用表,無(wú)關(guān)聯(lián)對(duì)象,無(wú)析構(gòu)函數(shù)、無(wú)散列表,直接free。
- 其他情況,依次檢查:
- 有
析構(gòu)函數(shù):調(diào)用析構(gòu)函數(shù)- 有
關(guān)聯(lián)對(duì)象:移除關(guān)聯(lián)對(duì)象- 非
指針優(yōu)化:直接清除散列表- 有
弱引用表:直接清除弱引用表內(nèi)容- 使用
散列表:移除表內(nèi)引用計(jì)數(shù)
-現(xiàn)在,我們已熟悉了alloc->retain->release->dealloc的完整流程。
4. retainCount
- 關(guān)于
retainCount,有一個(gè)有意思的面試題:
// Q:打印的引用計(jì)數(shù)為多少,alloc、init改變了引用計(jì)數(shù)嗎?
- (void)demo {
NSObject * objc = [NSObject alloc];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
objc = [objc init];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
}
-
打印結(jié)果:
image.png -
進(jìn)入源碼,搜索
_objc_rootRetainCount->rootRetainCount:
image.png
結(jié)論
alloc、init是不處理對(duì)象引用計(jì)數(shù)retainCount返回的計(jì)數(shù),是讀取內(nèi)存后+1的。 (實(shí)際計(jì)數(shù)是打印值-1)引用計(jì)數(shù)為0,對(duì)象為啥沒(méi)被釋放?
因?yàn)?code>ARC會(huì)自動(dòng)將對(duì)象加入autoRelasePool中,autoReleasePool中對(duì)象的被持有時(shí)間,就是代碼作用域{ }周期,所以達(dá)到延遲釋放功能。
下一節(jié),將進(jìn)行強(qiáng)弱引用分析。
















