tft每日頭條

 > 圖文

 > 内存管理的基本需求是什麼

内存管理的基本需求是什麼

圖文 更新时间:2024-11-29 13:29:05

引言

果設備備受歡迎的背後離不開iOS優秀的内存管理,不同場景,系統提供了不同的内存管理方案來節省内存和提高執行效率,大緻有如下三種:

  • TaggedPointer (對于一些小對象,比如說NSNumber,NSString等)
  • NONPOINTER_ISA (不僅僅是指針)
  • 散列表SideTables

内存管理的基本需求是什麼(OC的内存管理方案)1

TaggedPointer

為了節省内存和提高執行效率,蘋果提出了Tagged Pointer的概念。對于 64 位程序,引入 Tagged Pointer 後,相關邏輯能減少一半的内存占用,蘋果對于Tagged Pointer特點的介紹:

  • Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate
  • Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它隻是一個披着對象皮的普通變量而已。所以,它的内存并不存儲在堆中,也不需要 malloc 和 free。
  • 在内存讀取上有着 3 倍的效率,創建時比以前快 106 倍。

為什麼會出現TaggedPointer

假設我們要存儲一個 NSNumber 對象,其值是一個整數。正常情況下,如果這個整數隻是一個 NSInteger 的普通變量,那麼它所占用的内存是與 CPU 的位數有關,在 32 位 CPU 下占 4 個字節,在 64 位 CPU 下是占 8 個字節的。而指針類型的大小通常也是與 CPU 位數相關,一個指針所占用的内存在 32 位 CPU 下為 4 個字節,在 64 位 CPU 下也是 8 個字節。

所以一個普通的 iOS 程序,如果沒有Tagged Pointer對象,從 32 位機器遷移到 64 位機器中後,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對象所占用的内存會翻倍。如下圖所示:

内存管理的基本需求是什麼(OC的内存管理方案)2

為了存儲和訪問一個 NSNumber 對象,我們需要在堆上為其分配内存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失,所以需要一種解決方案(TaggedPointer)來節省内存和提高執行效率。

TaggedPointer的原理

為了改進上面提到的内存占用和效率問題,蘋果提出了Tagged Pointer對象。由于 NSNumber、NSDate 一類的變量本身的值需要占用的内存大小常常不需要 8 個字節,拿整數來說,4 個字節所能表示的有符号整數就可以達到 20 多億(注:2^31=2147483648,另外 1 位作為符号位),對于絕大多數情況都是可以處理的。

所以我們可以将一個對象的指針拆成兩部分,一部分直接保存數據,另一部分作為特殊标記,表示這是一個特别的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之後,64 位 CPU 下 NSNumber 的内存圖變成了以下這樣:

内存管理的基本需求是什麼(OC的内存管理方案)3

方案對比:當NSNumber、NSDate、NSString存值很小的情況下

  • 在沒有使用TaggedPointer之前:NSNumber等對象需要動态分配内存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值(需要創建OC對象)
  • 使用TaggedPointer之後:NSNumber指針裡面存儲的數據變成了:Tag Data,也就是将數據直接存儲在了指針中(不需要創建OC對象)
  • 當存值很大,指針不夠存儲數據時(超過64位),才會使用動态分配内存的方式來存儲數據(創建OC對象)
  • 消息調用時,objc_msgSend 能識别TaggedPointer,比如NSNumber的intValue方法,直接從指針提取數據,節省了以前的調用開銷(而且這不是真的OC對象,根本就沒有isa去找方法)

demo

int main(int argc, const char * argv[]) { @autoreleasepool { NSNumber *num1 = @3; NSNumber *num2 = @4; NSNumber *num3 = @5; // 數值太大,64位不夠放,得alloc生成個對象來保存 NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF); // 小數值的NSNumber對象,并不是alloc出來放在堆中的對象,隻是一個單純的指針,目标值是存放在指針的地址值中 NSLog(@"%p %p %p %p", num1, num2, num3, num4); } } // 打印日志 2020-03-23 16:10:30.888204 0800 04-内存管理-Tagged Pointer[6079:225288] 0x2027be5cc632c957 0x2027be5cc632ce57 0x2027be5cc632cf57 0x100512050

說明: 猜測是iOS13之後底層多加了一層掩碼,以前輸出num1, num2, num3地址是0x327 0x427 0x527 ,直接可以從地址裡面看到NSNumber的值

如何判定是否是TaggedPointer

判定規則:将某個對象和1進行位運算

  • iOS平台的判定位為最高有效位(第64位)
  • Mac平台的判定位為最低有效位(第1位)

判定為是【1】就是TaggedPointer,否則這就是分配到堆中的OC對象的内存地址(OC對象在内存中以16對齊,因此有效位肯定是0,16 = 0x10 = 0b00010000)。

BOOL isTaggedPointer(id pointer) { return (long)(__bridge void *)pointer & (long)1; // Mac平台是最低有效位(第1位) } int main(int argc, const char * argv[]) { @autoreleasepool { NSNumber *num3 = @5; NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF); NSLog(@"%d %d ", isTaggedPointer(num3), isTaggedPointer(num4)); } } // 打印日志 2020-03-23 16:10:30.888286 0800 04-内存管理-Tagged Pointer[6079:225288] 1 0

優點

TaggedPointer技術的好處:

  1. 存值:直接把值存到指針中,不需要再新建一個OC對象來保存(額外多分配至少16個字節)--- 省内存
  2. 取值:直接從指針中把目标值抽取出來,不需要像OC對象那樣,先從類對象的方法列表中查找再調用來獲取那麼麻煩 --- 性能好、效率高

NONPOINTER_ISA

在arm64位下iOS操作系統,Objective-C對象的isa區域不再隻是一個指針,在64位架構下的isa指針是64bit位,實際上33位就能夠表示類對象(或元類對象)的地址,為了提供内存的利用率,在剩餘的bit位當中添加了内存管理的數據内容

isa結構

  • arm64架構之前,isa是一個普通的指針,存儲着Class、MetaClass對象的地址
  • 從arm64架構之後,蘋果對isa進行了優化,變成了一個公用體

# 隻看arm64情況下 union isa_t { Class cls; uintptr_t bits; struct { uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19 }; };

字段含義解釋

  1. nonpointer:0,代表普通的指針,存儲着Class、Meta-Class對象的内存地址。 1,代表優化過,使用位域存儲更多的信息
  2. has_assoc:是否有設置過關聯對象,如果沒有,釋放時會更快
  3. has_cxx_dtor:是否有C 的析構函數(.cxx_destruct),如果沒有,釋放時會更快
  4. shiftcls:存儲着Class、Meta-Class對象的内存地址信息
  5. magic: 用于在調試時分辨對象是否未完成初始化
  6. weakly_referenced:是否有被弱引用指向過,如果沒有,釋放時會更快
  7. deallocating:對象是否正在釋放
  8. extra_rc:裡面存儲的值是引用計數器減1
  9. has_sidetable_rc:引用計數器是否過大無法存儲在isa中,如果為1,那麼引用計數會存儲在一個叫SideTable的類的屬性中。

散列表(SideTables)

SideTables()實際是一個哈希表,我們可以通過對象指針,找到所對應的引用計數表或弱引用表位于哪個SideTable表中。也就是有多個sideTable表

内存管理的基本需求是什麼(OC的内存管理方案)4

思考:為什麼不是一個大表,而是多個表

回答:如果隻有一張表,所有對象的引用計數都放到一張表中,則如果在修改某個對象的引用計數的時候,由于對象可能在不同線程中被操作,則需要對表進行加鎖,這樣一來,效率就會極地。

什麼是哈希表

是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度,賦值和獲取都避免了遍曆,提高了效率

内存管理的基本需求是什麼(OC的内存管理方案)5

SideTable結構

底層源碼結構如下:

struct SideTable { spinlock_t slock;//自旋鎖 RefcountMap refcnts;//引用計數表 weak_table_t weak_table;//弱引用表 }

内存管理的基本需求是什麼(OC的内存管理方案)6

可以看到SideTable是由三部分組成

Spinlock_t自旋鎖

  • 自旋鎖來用來防止操作表結構時可能的競态條件,适用于輕量訪問。比如引用計數的修改
  • Spinlock_t是“忙等”的鎖,對SideTable加鎖,避免數據錯誤

引用計數表RefcountMap

引用計數表也是一個hash表,通過hash函數找到指針對應的引用計數的位置。

内存管理的基本需求是什麼(OC的内存管理方案)7

弱引用表weak_table_t

弱引用表也是一個hash表,通過hash函數找到對象對應的弱引用數組

底層結構:

struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; };

内存管理的基本需求是什麼(OC的内存管理方案)8

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved