內存管理系列—OC的內存管理方案

內存管理系列文章:數組

引言

蘋果設備受歡迎的背後離不開iOS優秀的內存管理,不一樣場景,系統提供了不一樣的內存管理方案來節省內存和提升執行效率,大體有以下三種:bash

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

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 一類的對象所佔用的內存會翻倍。以下圖所示: ide

爲了存儲和訪問一個 NSNumber 對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增長了額外的邏輯,形成運行效率上的損失,因此須要一種解決方案(TaggedPointer)來節省內存和提升執行效率。

TaggedPointer的原理

爲了改進上面提到的內存佔用和效率問題,蘋果提出了Tagged Pointer對象。因爲 NSNumber、NSDate 一類的變量自己的值須要佔用的內存大小經常不須要 8 個字節,拿整數來講,4 個字節所能表示的有符號整數就能夠達到 20 多億(注:2^31=2147483648,另外 1 位做爲符號位),對於絕大多數狀況都是能夠處理的。函數

因此咱們能夠將一個對象的指針拆成兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。因此,引入了Tagged Pointer對象以後,64 位 CPU 下 NSNumber 的內存圖變成了如下這樣: 佈局

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

  • 在沒有使用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位當中添加了內存管理的數據內容

位域簡介

有些數據在存儲時並不須要佔用一個完整的字節,只須要佔用一個或幾個二進制位就能夠了。

正是基於這種考慮,C語言又提供了一種叫作位域的數據結構。在結構體定義時,咱們能夠指定某個成員變量所佔用的二進制位數(Bit),這就是位域。

上個demo

struct {
        char name : 1;
        char number : 1;
        char sex : 1;
 } Person;
複製代碼

簡單總結:

  • 「:1」表明只佔1位的意思,這裏聲明的這3個成員就各佔1bit,共3bit,因此這個結構體只須要用到3bit的內存,這樣系統只須要分配1個字節就夠用了(內存分配至少也得1個字節)
  • 結構體定義的順序,在內存裏面對應的字節順序是從右往左的
// 0b00000 0    0    0
           ↓    ↓    ↓
          sex number name
複製代碼

共同體union簡介

  • union和struct區別
  1. 內存佔用方式:與結構體不一樣的是,共用體的全部成員佔用同一段內存,修改一個成員會影響其他成員。可是結構體的各個成員會佔不一樣的內存
  2. 內存大小:結構體佔用的內存大於等於全部成員佔用的內存總和(成員之間可能存在縫隙),共用體佔用的內存等於最長的成員佔用的內存
  • **union的使用 **
union {
     int  number; // 佔4字節
     float age;  // 佔8字節
   } person; // 以最大的那一個成員的內存來分配,因此共同體佔8字節
    
   test.number = 3;
   test.age = 20;
複製代碼

猜想下number的結果:

此時再次訪問 test.number 就再也不是3,而是20了,由於這兩個成員共用一塊內存,以前的3被覆蓋了

  • union的變體
union {
        char content;
        //【這個結構體純屬擺設】自始至終只操做content,不會用到這個結構體,不影響存儲
        struct {  
             char name : 1;
             char number : 1;
             char sex : 1;
        };
     
    }person
複製代碼

簡單總結

  1. union中的struct主要是爲了提升可讀性,用來講明content裏面存放的是這3個成員信息,而且每個成員佔1位,描述性做用。
  2. 外部操做的是content,不會對struct進行操做

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的類的屬性中。

可是若是 extar_rc不夠存儲的話,就須要將引用計數存入一個叫 Side Table 的數據結構中。

散列表(SideTables)

SideTables()實際是一個哈希表,咱們能夠經過對象指針,找到所對應的引用計數表或弱引用表位於哪一個SideTable表中。也就是有多個sideTable表

思考:爲何不是一個大表,而是多個表

回答:若是隻有一張表,全部對象的引用計數都放到一張表中,則若是在修改某個對象的引用計數的時候,因爲對象可能在不一樣線程中被操做,則須要對錶進行加鎖,這樣一來,效率就會極地。

什麼是哈希表

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

SideTable結構

底層源碼結構以下:

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

能夠看到SideTable是由三部分組成

Spinlock_t自旋鎖

  • 自旋鎖來用來防止操做表結構時可能的競態條件,適用於輕量訪問。好比引用計數的修改
  • Spinlock_t是「忙等」的鎖,對SideTable加鎖,避免數據錯誤

引用計數表RefcountMap

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

弱引用表weak_table_t

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

底層結構

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
};
複製代碼

參考文章:

相關文章
相關標籤/搜索