iOS 內存佈局&內存管理方案

內存佈局-五大區

image.png

  1. 棧區 0x7 建立臨時變量時由編譯器自動分配,在不須要的時候自動清除的變量的存儲區。 裏面的變量一般是局部變量、函數參數等。在一個進程中,位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數的調用。和堆同樣,用戶棧在程序執行期間能夠動態地擴展和收縮。html

  2. 堆區 0x6 那些由 new alloc 建立的對象所分配的內存塊,它們的釋放系統不會主動去管,由咱們的開發者去告訴系統何時釋放這塊內存(一個對象引用計數爲0是系統就會回銷毀該內存區域對象)。通常一個 new 就要對應一個 release。在ARC下編譯器會自動在合適位置爲OC對象添加release操做。會在當前線程Runloop退出或休眠時銷燬這些對象,MRC則需程序員手動釋放。 堆能夠動態地擴展和收縮。c++

  3. 靜態區(未初始化數據).bss 程序運行過程內存的數據一直存在,程序結束後由系統釋放程序員

  4. 常量區(已初始化數據).data 專門用於存放常量,程序結束後由系統釋放面試

  5. 代碼區 用於存放程序運行時的代碼,代碼會被編譯成二進制存進內存的程序代碼區數組

內存管理方案

TaggedPointer

一般咱們建立對象,對象存儲在堆中,對象的指針存儲在棧中,若是咱們要找到這個對象,就須要先在棧中,找到指針地址,而後根據指針地址找到在堆中的對象。 這個過程比較繁瑣,當存儲的對象只是一個很小的東西,好比一個字符串,一個數字。去走這麼一個繁瑣的過程,無非是耗費性能的,因此蘋果就搞出了TaggedPointer這麼一個東西。bash

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

  2. Tagged Pointer指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要malloc和free。async

  3. Tagged Pointer指針中包含了當前對象的地址、類型、具體數值。所以Tagged Pointer指針在內存讀取上有着3倍的效率,建立時比普通須要malloc跟free的類型快106倍。ide

這裏有對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, 0, &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~

相關文章
相關標籤/搜索