1. 程式人生 > IOS開發 >iOS 記憶體佈局&記憶體管理方案

iOS 記憶體佈局&記憶體管理方案

記憶體佈局-五大區

image.png

  1. 棧區 0x7 建立臨時變數時由編譯器自動分配,在不需要的時候自動清除的變數的儲存區。 裡面的變數通常是區域性變數、函式引數等。在一個程序中,位於使用者虛擬地址空間頂部的是使用者棧,編譯器用它來實現函式的呼叫。和堆一樣,使用者棧在程式執行期間可以動態地擴充套件和收縮。

  2. 堆區 0x6 那些由 new alloc 建立的物件所分配的記憶體塊,它們的釋放系統不會主動去管,由我們的開發者去告訴系統什麼時候釋放這塊記憶體(一個物件引用計數為0是系統就會回銷毀該記憶體區域物件)。一般一個 new 就要對應一個 release。在ARC下編譯器會自動在合適位置為OC物件新增release操作。會在當前執行緒Runloop退出或休眠時銷燬這些物件,MRC則需程式設計師手動釋放。 堆可以動態地擴充套件和收縮。

  3. 靜態區(未初始化資料).bss 程式執行過程記憶體的資料一直存在,程式結束後由系統釋放

  4. 常量區(已初始化資料).data 專門用於存放常量,程式結束後由系統釋放

  5. 程式碼區 用於存放程式執行時的程式碼,程式碼會被編譯成二進位制存進記憶體的程式程式碼區

記憶體管理方案

TaggedPointer

通常我們建立物件,物件儲存在堆中,物件的指標儲存在棧中,如果我們要找到這個物件,就需要先在棧中,找到指標地址,然後根據指標地址找到在堆中的物件。 這個過程比較繁瑣,當儲存的物件只是一個很小的東西,比如一個字串,一個數字。去走這麼一個繁瑣的過程,無非是耗費效能的,所以蘋果就搞出了TaggedPointer這麼一個東西。

  1. TaggedPointer是蘋果為了解決32位CPU到64位CPU的轉變帶來的記憶體佔用和效率問題,針對NSNumber、NSDate以及部分NSString的記憶體優化方案。

  2. Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。

  3. Tagged Pointer指標中包含了當前物件的地址、型別、具體數值。因此Tagged Pointer指標在記憶體讀取上有著3倍的效率,建立時比普通需要malloc跟free的型別快106倍。

這裡有對TaggedPointer進行詳細介紹

面試題

為什麼第二個for會崩潰?

    dispatch_queue_t queue = dispatch_queue_create("queue",DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue,^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = @"abcd";
        }
    });
    dispatch_async(queue,^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = [NSString stringWithFormat:@"adfalkdjfldkasjflakjsdkflasf-- %d",I];
        }
    });
複製程式碼

答:taggedpointer。 在setproperty函式中,執行了objc_release(id obj)。 由於大量的迴圈,導致了執行緒問題,使引用計數<=-1。 但是由於第一個迴圈中的obj是taggedpointer型別的string,會直接return obj,並不會release。 但是這裡release,retain的時候咋辦呢,引用計數是一直往上增嗎?並不是,在objc_retain(id obj)中,同樣判斷了obj->isTaggedPointer,如果是true,就直接return obj。

NONPOINTER_ISA

要說isa,得先從物件開始。

NSObject繼承關係:

NSObject -> Class -> objc_class -> objc_object -> isa_t

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製程式碼

isa_t是聯合體,然後重點看ISA_BITFIELD,

image.png

引數解釋:

nonpointer:表示是否對 isa 指標開啟指標優化 , 0:純isa指標,1:不止是類物件地址,isa 中包含了類資訊、物件的引用計數等。

has_assoc:關聯物件標誌位,0沒有,1存在。

has_cxx_dtor:該物件是否有 C++ 或者 Objc 的析構器,如果有解構函式,則需要做析構邏輯,如果沒有,則可以更更快的釋放物件。

shiftcls:儲存類指標的值。開啟指標優化的情況下,在 arm64 架構中有 33 位⽤來儲存類指標的值。

magic:用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間 。

weakly_referenced:標誌物件是否被指向或者曾經指向⼀一個 ARC 的弱變數,沒有弱引用的物件可以更快釋放。

deallocating:標誌物件是否正在釋放記憶體。

has_sidetable_rc:是否有用到散列表,當物件引⽤計數大於 10 時,則需要借⽤該變數儲存進位。

extra_rc:當表示該物件的引用計數值,實際上是引用計數值減 1。例如,如果物件的引用計數為 10,那麼 extra_rc 為 9。 例:在__x86_64(mac)__的架構下,如果引用計數大於 255,引用計數將會發生溢位。 溢位時,則需要將has_sidetable_rc標記為1,將會將拿出**2的7次方(128,就是上面的RC_HALF)**放入散列表(sidetable)

那麼has_sidetable_rc是怎麼操作的呢?

SideTables 散列表

散列表.png

SideTables

SideTables是一個數組,裡面存著很多SideTable。***(注意看有沒有s)*** 這是物件引用計數溢位時,會呼叫這個方法,將一半的引用計數存入sideTable。

image.png
在這方法裡,我們可以看到這個方法,通過SideTables獲取一個SideTable

SideTable& table = SideTables()[this];
複製程式碼

那麼這裡要看的就應該是SideTables()

SideTables.png

這裡我覺得可以理解成SideTables()就是一個StripedMap,繼續看StripedMap

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    
    //指標下標
    static unsigned int indexForPointer(const void *p) {
        //reinterpret_cast是C++裡的強制型別轉換符。
        //這裡是將16進位制轉成10進位制
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        // 這裡StripeCount是64,看上面第755行
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    //過載中括號 , c++特有
    //要讓”[]”內的運算元支援const void型別
    T& operator[] (const void *p) {
        // 呼叫indexForPointer(),獲取sidetable
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    ...
}
複製程式碼

看完上面的程式碼+註釋,我們走一波lldb除錯,分別列印各個引數

image.png

這裡就是一個獲取SideTable的過程

<1> SideTable& table = SideTables()[this];傳入一個this指標物件 <2> 通過indexForPointer獲取當前指標物件所對應的下標 <3> 通過array[indexForPointer(p)].value 返回一個SideTable

#####初探SideTable spinlock_t:自旋鎖、 RefcountMap:引用計數Map,是個C++的Map

weak_table_t:全域性弱引用表

image.png

#####SideTable操作 這裡舉個例子 sidetable_addExtraRC_nolock在sideTable中新增RetainCount(RC)

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    // 通過SideTables() 獲取SideTable
    SideTable& table = SideTables()[this];

    //獲取引用計數的size
    size_t& refcntStorage = table.refcnts[this];
    // 賦值給oldRefcnt
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    // 如果oldRefcnt & SIDE_TABLE_RC_PINNED = 1
    // 就是 oldRefcnt = 2147483648 (32位情況)
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    
    //引用計數也溢位判斷引數
    uintptr_t carry;
    
    // 引用計數 add
    //delta_rc左移兩位,右邊的兩位分別是DEALLOCATING(銷燬ing) 跟WEAKLY_REFERENCED(弱引用計數)
    size_t newRefcnt = 
        addc(oldRefcnt,delta_rc << SIDE_TABLE_RC_SHIFT,&carry);
    //如果sidetable也溢位了。
    //這裡我for了幾百萬次,也沒有溢位,可見sidetable能容納很多的引用計數
    if (carry) {
        // 如果是32位的情況 SIDE_TABLE_RC_PINNED = 1<< (32-1)
        // int的最大值 SIDE_TABLE_RC_PINNED = 2147483648
        //  SIDE_TABLE_FLAG_MASK = 3
        // refcntStorage = 2147483648 | (oldRefcnt & 3)
        // 如果溢位,直接把refcntStorage 設定成最大值
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}
複製程式碼

以上,to be continue~