OC底層原理三十五:內(nèi)存管理(TaggedPointer、引用計(jì)數(shù))

OC底層原理 學(xué)習(xí)大綱

  • 本節(jié),進(jìn)入內(nèi)存管理篇章,將從以下幾部分講解:
  1. 內(nèi)存布局
  2. TaggedPointer
  3. 引用計(jì)數(shù)(retain、release、dealloc) & SideTables 散列表
  4. retainCount

準(zhǔn)備工作:


1. 內(nèi)存布局

按照地址排列: 棧區(qū) -> 堆區(qū) -> 全局靜態(tài)區(qū) -> 常量區(qū) -> 代碼區(qū)內(nèi)核區(qū)保留部分不在考慮范圍內(nèi))

image.png

補(bǔ)充說(shuō)明:

  1. 內(nèi)存五大區(qū),實(shí)際是指虛擬內(nèi)存,而不是真實(shí)物理內(nèi)存。(詳情可查看?? 本文第3點(diǎn) 虛擬內(nèi)存與物理內(nèi)存
  2. 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ā)retainrelease,異步線程中,可能導(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
  • 可以看到namesetter方法實(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));
}
image.png
  • 可以看到解碼后,從地址就可以直接當(dāng)前內(nèi)容。所以混淆機(jī)制就是為了讓小對(duì)象地址降低識(shí)別度。

Q: 解碼后小對(duì)象地址,前面a/b是啥意思?

image.png

  • 當(dāng)前系統(tǒng),為了新舊兼容,TaggedPointer指針的后四位存儲(chǔ)值。

taggedpointer類型的優(yōu)點(diǎn):

    1. 節(jié)省內(nèi)存開銷:充分利用指針地址空間。
    1. 執(zhí)行效率高: 不需要retainrelease系統(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ù),自然就想到了retainrelease。上面示例中,如果不是taggedPointer,就會(huì)進(jìn)入obj->retain,我們進(jìn)入retain內(nèi)部:
    (ps: 為了內(nèi)容不太分散,我梳理成一張大圖,可查看原圖,放大觀看)

    image.png

  • 哈希表結(jié)構(gòu)如下:


    image.png

【reatin總結(jié)】

  1. taggedPointer類直接返回原對(duì)象
  2. 操作時(shí),使用新isa拷貝當(dāng)前isa對(duì)象,隔離操作
  3. do-while】循環(huán),是由于多線程環(huán)境下,當(dāng)前isa可能在變化,只要變化就需要再次操作
  4. 如果支持指針優(yōu)化,直接操作散列表進(jìn)行計(jì)數(shù)。
  5. 如果isa記錄了正在釋放,就retain
  6. 引用計(jì)數(shù)+1,首先嘗試isaextra_rc+1:
    • 成功:【do-whileretain操作結(jié)束
    • 失敗: 表示extra_rc存儲(chǔ)滿了,此時(shí)將extra_rc計(jì)數(shù)減半has_sidetable_rc(使用散列表)標(biāo)記true,transcribeToSideTable(轉(zhuǎn)移給散列表)標(biāo)記true。
  7. do-while】外,判斷transcribeToSideTable散列表添加extra_rc最大容量的一半計(jì)數(shù)。
  • 接著,我們來(lái)了解release:
    image.png

【release總結(jié)】

  1. taggedPointer類直接返回原對(duì)象
  2. 操作時(shí),使用新isa拷貝 當(dāng)前isa對(duì)象,隔離操作。
  3. 【retry】:
    do-while循環(huán),監(jiān)測(cè)多線程環(huán)境下,當(dāng)前isa是否變化
    - 如果支持指針優(yōu)化,直接操作散列表進(jìn)行計(jì)數(shù)
    - 嘗試isaextra_rc - 1: 如果失敗,跳轉(zhuǎn)【underflow】
  4. 【underflow】:
    (表示extra_rc計(jì)數(shù)為不可以進(jìn)行-1,需要去散列表計(jì)數(shù)操作直接釋放)
    • 如果有散列表has_sidetable_rctrue):
      • 如果散列表沒(méi)鎖關(guān)鎖重新 【retry】
      • 嘗試從散列表讀取RC_HALF(extra_rc一半容量)計(jì)數(shù)
        • 如果取到值,嘗試更新extra_rcisa更新失敗再次讀取更新。如果還失敗,就將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é)】

    1. 優(yōu)化指針,但無(wú)弱引用表,無(wú)關(guān)聯(lián)對(duì)象,無(wú)析構(gòu)函數(shù)、無(wú)散列表,直接free。
    1. 其他情況,依次檢查:
    • 析構(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é)論

  1. alloc、init不處理對(duì)象引用計(jì)數(shù)
  2. retainCount返回的計(jì)數(shù),是讀取內(nèi)存+1的。 (實(shí)際計(jì)數(shù)打印值-1)
  3. 引用計(jì)數(shù)0,對(duì)象為啥沒(méi)釋放?
    因?yàn)?code>ARC會(huì)自動(dòng)將對(duì)象加入autoRelasePool中,autoReleasePool對(duì)象被持有時(shí)間,就是代碼作用域{ }周期,所以達(dá)到延遲釋放功能。

下一節(jié),將進(jìn)行強(qiáng)弱引用分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容