iOS 記憶體佈局&記憶體管理方案
記憶體佈局-五大區
-
棧區 0x7 建立臨時變數時由編譯器自動分配,在不需要的時候自動清除的變數的儲存區。 裡面的變數通常是區域性變數、函式引數等。在一個程序中,位於使用者虛擬地址空間頂部的是使用者棧,編譯器用它來實現函式的呼叫。和堆一樣,使用者棧在程式執行期間可以動態地擴充套件和收縮。
-
堆區 0x6 那些由 new alloc 建立的物件所分配的記憶體塊,它們的釋放系統不會主動去管,由我們的開發者去告訴系統什麼時候釋放這塊記憶體(一個物件引用計數為0是系統就會回銷毀該記憶體區域物件)。一般一個 new 就要對應一個 release。在ARC下編譯器會自動在合適位置為OC物件新增release操作。會在當前執行緒Runloop退出或休眠時銷燬這些物件,MRC則需程式設計師手動釋放。 堆可以動態地擴充套件和收縮。
-
靜態區(未初始化資料).bss 程式執行過程記憶體的資料一直存在,程式結束後由系統釋放
-
常量區(已初始化資料).data 專門用於存放常量,程式結束後由系統釋放
-
程式碼區 用於存放程式執行時的程式碼,程式碼會被編譯成二進位制存進記憶體的程式程式碼區
記憶體管理方案
TaggedPointer
通常我們建立物件,物件儲存在堆中,物件的指標儲存在棧中,如果我們要找到這個物件,就需要先在棧中,找到指標地址,然後根據指標地址找到在堆中的物件。 這個過程比較繁瑣,當儲存的物件只是一個很小的東西,比如一個字串,一個數字。去走這麼一個繁瑣的過程,無非是耗費效能的,所以蘋果就搞出了TaggedPointer這麼一個東西。
-
TaggedPointer是蘋果為了解決32位CPU到64位CPU的轉變帶來的記憶體佔用和效率問題,針對NSNumber、NSDate以及部分NSString的記憶體優化方案。
-
Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。
-
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,
引數解釋:
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 散列表
SideTables
SideTables是一個數組,裡面存著很多SideTable。***(注意看有沒有s)*** 這是物件引用計數溢位時,會呼叫這個方法,將一半的引用計數存入sideTable。
在這方法裡,我們可以看到這個方法,通過SideTables獲取一個SideTableSideTable& table = SideTables()[this];
複製程式碼
那麼這裡要看的就應該是SideTables()
這裡我覺得可以理解成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除錯,分別列印各個引數
這裡就是一個獲取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:全域性弱引用表
#####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~