內存管理系列文章:數組
蘋果設備受歡迎的背後離不開iOS優秀的內存管理,不一樣場景,系統提供了不一樣的內存管理方案來節省內存和提升執行效率,大體有以下三種:bash
爲了節省內存和提升執行效率,蘋果提出了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 一類的對象所佔用的內存會翻倍。以下圖所示: ide
爲了存儲和訪問一個 NSNumber 對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增長了額外的邏輯,形成運行效率上的損失,因此須要一種解決方案(TaggedPointer)來節省內存和提升執行效率。爲了改進上面提到的內存佔用和效率問題,蘋果提出了Tagged Pointer對象。因爲 NSNumber、NSDate 一類的變量自己的值須要佔用的內存大小經常不須要 8 個字節,拿整數來講,4 個字節所能表示的有符號整數就能夠達到 20 多億(注:2^31=2147483648,另外 1 位做爲符號位),對於絕大多數狀況都是能夠處理的。函數
因此咱們能夠將一個對象的指針拆成兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。因此,引入了Tagged Pointer對象以後,64 位 CPU 下 NSNumber 的內存圖變成了如下這樣: 佈局
方案對比: 當NSNumber、NSDate、NSString存值很小的狀況下post
在沒有使用TaggedPointer以前:性能
使用TaggedPointer以後:優化
當存值很大,指針不夠存儲數據時(超過64位),纔會使用動態分配內存的方式來存儲數據(建立OC對象)
消息調用時,objc_msgSend 能識別TaggedPointer,好比NSNumber的intValue方法,直接從指針提取數據,節省了之前的調用開銷(並且這不是真的OC對象,根本就沒有isa去找方法)
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位當中添加了內存管理的數據內容
有些數據在存儲時並不須要佔用一個完整的字節,只須要佔用一個或幾個二進制位就能夠了。
正是基於這種考慮,C語言又提供了一種叫作位域的數據結構。在結構體定義時,咱們能夠指定某個成員變量所佔用的二進制位數(Bit),這就是位域。
上個demo
struct {
char name : 1;
char number : 1;
char sex : 1;
} Person;
複製代碼
簡單總結:
// 0b00000 0 0 0
↓ ↓ ↓
sex number name
複製代碼
union {
int number; // 佔4字節
float age; // 佔8字節
} person; // 以最大的那一個成員的內存來分配,因此共同體佔8字節
test.number = 3;
test.age = 20;
複製代碼
猜想下number的結果:
此時再次訪問 test.number 就再也不是3,而是20了,由於這兩個成員共用一塊內存,以前的3被覆蓋了
union {
char content;
//【這個結構體純屬擺設】自始至終只操做content,不會用到這個結構體,不影響存儲
struct {
char name : 1;
char number : 1;
char sex : 1;
};
}person
複製代碼
簡單總結:
# 只看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
};
};
複製代碼
字段含義解釋
可是若是 extar_rc不夠存儲的話,就須要將引用計數存入一個叫 Side Table 的數據結構中。
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;
};
複製代碼
參考文章: