引言
果設備備受歡迎的背後離不開iOS優秀的内存管理,不同場景,系統提供了不同的内存管理方案來節省内存和提高執行效率,大緻有如下三種:
為了節省内存和提高執行效率,蘋果提出了Tagged Pointer的概念。對于 64 位程序,引入 Tagged Pointer 後,相關邏輯能減少一半的内存占用,蘋果對于Tagged Pointer特點的介紹:
假設我們要存儲一個 NSNumber 對象,其值是一個整數。正常情況下,如果這個整數隻是一個 NSInteger 的普通變量,那麼它所占用的内存是與 CPU 的位數有關,在 32 位 CPU 下占 4 個字節,在 64 位 CPU 下是占 8 個字節的。而指針類型的大小通常也是與 CPU 位數相關,一個指針所占用的内存在 32 位 CPU 下為 4 個字節,在 64 位 CPU 下也是 8 個字節。
所以一個普通的 iOS 程序,如果沒有Tagged Pointer對象,從 32 位機器遷移到 64 位機器中後,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對象所占用的内存會翻倍。如下圖所示:
為了存儲和訪問一個 NSNumber 對象,我們需要在堆上為其分配内存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失,所以需要一種解決方案(TaggedPointer)來節省内存和提高執行效率。
為了改進上面提到的内存占用和效率問題,蘋果提出了Tagged Pointer對象。由于 NSNumber、NSDate 一類的變量本身的值需要占用的内存大小常常不需要 8 個字節,拿整數來說,4 個字節所能表示的有符号整數就可以達到 20 多億(注:2^31=2147483648,另外 1 位作為符号位),對于絕大多數情況都是可以處理的。
所以我們可以将一個對象的指針拆成兩部分,一部分直接保存數據,另一部分作為特殊标記,表示這是一個特别的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之後,64 位 CPU 下 NSNumber 的内存圖變成了以下這樣:
方案對比:當NSNumber、NSDate、NSString存值很小的情況下
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的值
判定規則:将某個對象和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技術的好處:
在arm64位下iOS操作系統,Objective-C對象的isa區域不再隻是一個指針,在64位架構下的isa指針是64bit位,實際上33位就能夠表示類對象(或元類對象)的地址,為了提供内存的利用率,在剩餘的bit位當中添加了内存管理的數據内容
# 隻看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
};
};
SideTables()實際是一個哈希表,我們可以通過對象指針,找到所對應的引用計數表或弱引用表位于哪個SideTable表中。也就是有多個sideTable表
思考:為什麼不是一個大表,而是多個表
?
回答:如果隻有一張表,所有對象的引用計數都放到一張表中,則如果在修改某個對象的引用計數的時候,由于對象可能在不同線程中被操作,則需要對表進行加鎖,這樣一來,效率就會極地。
是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度,賦值和獲取都避免了遍曆,提高了效率
底層源碼結構如下:
struct SideTable {
spinlock_t slock;//自旋鎖
RefcountMap refcnts;//引用計數表
weak_table_t weak_table;//弱引用表
}
可以看到SideTable是由三部分組成
引用計數表也是一個hash表,通過hash函數找到指針對應的引用計數的位置。
弱引用表也是一個hash表,通過hash函數找到對象對應的弱引用數組
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
};
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!