不一樣的系統版本對 App 運行時佔用內存的限制不一樣,超過限制時,App就會被強制殺死,因此對於內存的要求也就愈來愈高,因此本章來探索一下iOS中的內存管理方案c++
移動端的內存管理技術,主要有 GC(Garbage Collection
,垃圾回收)的標記清除算法和蘋果公司使用的引用計數方法。程序員
相比較於 GC 標記清除算法,引用計數法能夠及時地回收引用計數爲 0 的對象,減小查找次數。可是,引用計數會帶來循環引用的問題,好比當外部的變量強引用 Block 時,Block 也會強引用外部的變量,就會出現循環引用。咱們須要經過弱引用,來解除循環引用的問題。面試
另外,在 ARC(自動引用計數)以前,一直都是經過 MRC(手動引用計數)這種手寫大量內存管理代碼的方式來管理內存,所以蘋果公司開發了 ARC 技術,由編譯器來完成這部分代碼管理工做。可是,ARC 依然須要注意循環引用的問題。當 ARC 的內存管理代碼交由編譯器自動添加後,有些狀況下會比手動管理內存效率低,因此對於一些內存要求較高的場景,咱們仍是要經過 MRC 的方式來管理、優化內存的使用。算法
首先咱們再來回顧一下,OS/iOS系統的內存佈局c#
在iOS程序的內存中,從底地址開始,到高地址一次分爲:程序區域、數據區域、堆區、棧區。其中程序區域主要是代碼段,數據區域包括數據段和BSS段。咱們具體分析一下各個區域所表明的含義數組
new
,alloc
、block
、copy
建立的對象存儲在這裏,是由開發者管理的,須要告訴系統何時釋放內存。ARC下編譯器會自動在合適的時候釋放內存,而在MRC下須要開發者手動釋放。堆區的內存地址通常是0x6開頭,從底地址到高地址分配內存空間首先先查看下面代碼,age
的打印結果是多少呢?bash
static int age = 10;
@interface Person : NSObject
-(void)add;
+(void)reduce;
@end
@implementation Person
- (void)add {
age++;
NSLog(@"Person內部:%@-%p--%d", self, &age, age);
}
+ (void)reduce {
age--;
NSLog(@"Person內部:%@-%p--%d", self, &age, age);
}
@end
@implementation Person (WY)
- (void)wy_add {
age++;
NSLog(@"Person (wy)內部:%@-%p--%d", self, &age, age);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"vc:%p--%d", &age, age);
age = 40;
NSLog(@"vc:%p--%d", &age, age);
[[Person new] add];
NSLog(@"vc:%p--%d", &age, age);
[Person reduce];
NSLog(@"vc:%p--%d", &age, age);
[[Person new] wy_add];
}
複製代碼
經過打印結果,咱們能夠得出這麼一個結論:數據結構
靜態變量的做用域與對象、類、分類不要緊,只與文件有關係。架構
iOS除了使用ARC來進行自動引用計數之外,還有一些其餘的內存優化方案,主要有Tagged Ponter
,NONPOINTER_ISA
,SideTable
3種async
在 2013 年 9 月,蘋果推出了 iPhone5s,與此同時,iPhone5s 配備了首個採用 64 位架構的 A7 雙核處理器,爲了節省內存和提升執行效率,蘋果提出了Tagged Pointer
的概念。
使用Tagged Pointer
以前,若是聲明一個NSNumber *number = @10;
變量,須要一個佔8字節的指針變量number,和一個佔16字節的NSNumber對象,指針變量number指向NSNumber對象的地址。這樣須要耗費24個字節內存空間。
而使用Tagged Pointer
以後,NSNumber指針裏面存儲的數據變成了:Tag + Data
,也就是將數據直接存儲在了指針中。直接將數據10保存在指針變量number中,這樣僅佔用8個字節。
可是當指針不夠存儲數據時,就會使用動態分配內存的方式來存儲數據。
經過下面面試題的打印結果能夠很好的反映出來
//第1段代碼
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"asdasdefafdfa"];
});
}
NSLog(@"end");
複製代碼
//第2段代碼
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
NSLog(@"end");
複製代碼
上面兩段代碼的打印結果過是: 第1段會發生崩潰,而第2段不會。
按道理講,建立多個線程來對name
進行操做時,name
的值會不斷的retain
和release
,此時就會出現資源競爭而崩潰,可是第二段卻不會崩潰,說明在Tagged Pointer
下,較小的值,不會調用set和get等方法,因爲其值是直接存儲在指針變量中的,因此能夠直接修改。
經過源碼,咱們也能夠比較直觀的看出,Tagged Pointer
類型對象是直接返回的。
總結一下使用Tagged Pointer的好處
Tagged Pointer
是專⻔⽤來存儲⼩的對象,例如NSNumber
,NSDate
等。Tagged Pointer
指針的值再也不是地址了,⽽是真正的值。因此,實際上它再也不是⼀個對象了,它只是⼀個披着對象⽪的普通變量⽽已。因此,它的內存並不存儲在堆中,也不須要malloc
和free
。static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
if (tag <= OBJC_TAG_Last60BitPayload) {
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
複製代碼
從上面的這代碼能夠看出來,系統調用了_objc_decodeTaggedPointer
和_objc_taggedPointersEnabled
這兩個方法對於taggedPointer
對象的指針進行了編碼和解編碼,這兩個方法都是將指針地址和objc_debug_taggedpointer_obfuscator
進行異或操做
咱們都知道將a和b異或操做獲得c再和a進行異或操做即可以從新獲得a的值,一般可使用這個方式來實現不用中間變量實現兩個值的交換。Tagged Pointer
正是使用了這種原理。經過這種解碼的方法,咱們能夠獲得對象真正的指針地址
下面是系統定義的各類Tagged Pointer
標誌位。
enum objc_tag_index_t : uint16_t
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
複製代碼
關於NONPOINTER_ISA
咱們在以前的博客中有講過,這也是蘋果的一種內存優化的方案。用 64 bit 存儲一個內存地址顯然是種浪費,畢竟不多有那麼大內存的設備。因而能夠優化存儲方案,用一部分額外空間存儲其餘內容。isa 指針第一位爲 1 即表示使用優化的 isa 指針。能夠閱讀☞iOS底層學習 - OC對象前世此生
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
};
#endif
};
複製代碼
在NONPOINTER_ISA
中有兩個成員變量has_sidetable_rc
和extra_rc
,當extra_r
c的19位內存不夠存儲引用計數時has_sidetable_rc
的值就會變爲1,那麼此時引用計數會存儲在SideTable中。
SideTable
s能夠理解爲一個全局的hash數組
,裏面存儲了SideTable
類型的數據,其長度爲64,就是裏面有64個SideTable。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
複製代碼
查看SideTable的源碼,其3個成員變量,表明的意義以下:
spinlock_t
:自旋鎖,用於上鎖/解鎖 SideTable。RefcountMap
:用來存儲OC對象的引用計數的 hash表(僅在未開啓isa優化或在isa優化狀況下isa_t的引用計數溢出時纔會用到)。weak_table_t
:存儲對象弱引用指針的hash表。是OC中weak功能實現的核心數據結構。有關於弱引用表weak_table_t
,能夠閱讀☞iOS底層學習 - 內存管理之weak原理探究
// RefcountMap disguises its pointers because we
// do not want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
複製代碼
實質上是模板類型objc::DenseMap。模板的三個類型參數DisguisedPtr<objc_object>
、size_t
、true
分別表示DenseMap的 key類型
、value類型
、是否須要在value == 0 的時候自動釋放掉響應的hash節點,這裏是true。
在MRC時代,程序員須要手動的去管理內存,建立一個對象時,須要在set方法和get方法內部添加釋放對象的代碼。而且在對象的dealloc裏面添加釋放的代碼。
@property (nonatomic, strong) Person *person;
- (void)setPerson:(Person *)person {
if (_person != person) {
[_person release];
_person = [person retain];
}
}
- (Person *) person {
return _person;
}
複製代碼
在ARC環境中,咱們再也不像之前同樣本身手動管理內存,系統幫助咱們作了release
或者autorelease
等事情。 ARC是LLVM編譯器
和RunTime
協做的結果。其中LLVM編譯器自動生成release
、reatin
、autorelease
的代碼,像weak
弱引用這些則靠RunTime在運行時釋放。
引用計數是一種內存管理技術,是指將資源(能夠是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法。
在iOS中,對象執行reatin
等操做後,該對象的引用計數就會+1
,調用release
等操做後,改對象的引用計數就會-1
關於引用計數的規則,能夠總結以下:
有關alloc
的流程,能夠閱讀☞iOS底層學習 - OC對象前世此生來進行查看,就不作過多的贅述。
可是有一個細節須要注意,alloc
自己是隻申請內存空間,不增長引用計數的。此時isa
中extra_rc
爲0。
可是爲何咱們打印retainCount
時,顯示的是1呢,咱們經過查看源碼能夠發現uintptr_t rc = 1 + bits.extra_rc;
其自己前面是會加一個常量1的,用來標記本身生成的對象的引用計數。
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
複製代碼
經過如下源碼,咱們能夠知道:
hasCustomRR
,會走消息發送inline id objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
複製代碼
接着咱們查看rootRetain
方法,其主要處理以下:
TaggedPointer
,若是是則返回。extra_rc
進行加1操做。extra_rc
中已經存儲滿了,則調用sidetable_addExtraRC_nolock
方法將一半的引用計數移存到SideTable中。ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
✅//若是是TaggedPointer 直接返回
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable =false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
✅// 若是isa未通過NONPOINTER_ISA優化
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();//引用計數存儲於SideTable中
}
// donot check newisa.fast_rr; we already called any RR overrides
✅//檢查對象是都正在析構
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
✅//isa的bits中的extra_rc進行加1
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
✅//若是bits的extra_rc已經存滿了,則將其中的一半存儲到sidetable中
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;//extra_rc置空一半的數值
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
✅//將另外的一半引用計數存儲到sidetable中
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
複製代碼
關於release
的相關原理,和retain
的原理相反,你們能夠本身去探究一下;
關於dealloc
的相關原理,在iOS底層學習 - 內存管理之weak原理探究中也有解析
棧區
(自動處理內存)、堆區
(開發者管理,ARC下會自動釋放)、靜態區
(全局變量,靜態變量)、數據段
(常量)、代碼段
(編寫的二進制代碼區)Tagged Pointer
NONPOINTER_ISA
nonpointer
標誌是否開啓指針優化extra_rc
來存儲引用計數,根據系統架構不一樣,長度不一樣has_sidetable_rc
來判斷超出extra_rc
後,是否有全局SideTable
存儲引用計數SideTable
SideTables
是一個全局的哈希數組,裏面存儲了SideTable
類型數據SideTable
由spinlock_t
(自旋鎖,用於上/解鎖)、RefcountMap
(存儲extra_rc
溢出或者未開啓優化的引用計數)、weak_table_t
(存儲弱引用表)rootRetainCount
其自己前面是會加一個常量1的,用來標記本身生成的對象的引用計數。TaggedPointer
,若是是則返回。NONPOINTER_ISA
優化,若是未通過優化,則將引用計數存儲在SideTable
中。64位的設備不會進入到這個分支。extra_rc
中已經存儲滿了,則調用sidetable_addExtraRC_nolock
方法將一半的引用計數移存到SideTable中。