探索底層原理,積累從點滴做起
往期回顧
前言
內存管理在APP開發過程中占據著一個很重要的地位,在iOS中,系統為我們提供了ARC的開發環境,幫助我們做了很多內存管理的內容,其實在MRC時代,內存管理對于開發者是個很頭疼的問題。我們會通過幾篇文章的分析,來幫助我們了解iOS中內存管理的原理,以及在ARC的開發環境下系統幫助我們做了哪些內存管理的操作。
iOS程序的內存布局
我們通過一張圖展示iOS程序的內存布局:
內存布局.png
在iOS程序的內存中,從底地址開始,到高地址一次分為:程序區域、數據區域、堆區、棧區。其中程序區域主要是代碼段,數據區域包括數據段和BSS段。我們具體分析一下各個區域所代表的含義:
代碼段: 存放編譯后的代碼,內存區域較小。程序結束時系統會自動回收存儲在代碼段中的數據。
數據段: 也叫常量區,保存已初始化的全局變量、靜態變量等。直到程序結束的時候才會被回收。
BSS段: 也叫靜態區,保存未被初試化的全局變量、靜態變量。一旦初始化就會被回收,并且將數據轉存到數據段中。
堆區(heap): 保存由alloc創建出來的對象,動態分配內存。需要程序員來進行內存管理。從底地址到高地址分配內存空間
棧區(stack): 保存局部變量,自動分配內存,系統管理。當局部變量的作用域執行完畢后就會被系統立即回收。從高地址到底地址分配內存空間
Tagged Pointer技術
在 2013 年 9 月,蘋果推出了 iPhone5s 。iPhone5s 配備了首個采用 64 位架構的 A7 雙核處理器。為了節省內存和提高執行效率,蘋果提出了Tagged Pointer的概念,用于優化NSNumber、NSDate、NSString等小對象的存儲。
在沒有使用Tagged Pointer之前,?NSNumber等對象需要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值。
例如下面這句代碼:
NSNumber*number=@10;
在沒有使用Tagged Pointer之前,內存中包括一個占8字節的指針變量number,和一個占16字節的NSNumber對象,指針變量number指向NSNumber對象的地址。這樣需要耗費24個字節內存空間。
未使用TaggedPointer.png
使用Tagged Pointer之后,NSNumber指針里面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中。
直接將數據10保存在指針變量number中,這樣僅占用8個字節。
使用了TaggedPointer.png
當然,當指針不夠存儲數據時,就會使用動態分配內存的方式來存儲數據。
我們用代碼來驗證一下:
測試.png
在測試代碼中創建7個NSNumber類型的對象,分別賦值后打印地址,可以看出使用Tagged Pointer之后,NSNumber指針里面存儲著對象的值。其中number7由于賦了一個很大的值,指針不夠存儲,就使用了動態分配內存的方式來存儲number7的值。
當然,以上測試代碼要運行在64位環境下。
接下來我們通過一道面試題來幫助我們理解:
以下兩段代碼的執行結果是什么?
//第1段代碼dispatch_queue_t queue=dispatch_get_global_queue(0,0);for(inti=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(inti=0;i<1000;i++){dispatch_async(queue,^{self.name=[NSString stringWithFormat:@"abc"];});}NSLog(@"end");
答案是第1段代碼會崩潰,報出壞內存訪問的錯誤;第2段代碼正常打印end
這是為什么呢?
這就涉及到我們上文講到的Tagged Pointer技術。我們先來看第1段代碼中self.name = [NSString stringWithFormat:@"asdasdefafdfa"];這句代碼,這句代碼的意思將后面的值賦給self.name。注意,此時要賦的值是一長串字符串,name的指針的8個字節已經存儲不下這個字符串了,那么就會動態分配內存的方式來存儲,就是調用name的set方法。
我們知道,在set方法內部,會首先調用[_name release]釋放舊值,再賦新值。但是我們賦值的代碼是在子線程中異步執行的,那么就存在同時會有多條線程同時調用[_name release],這就出現問題了。
問題的解決方法很簡單,可以把name的nonatomic修飾符改成atomic,這一點我們在iOS底層原理探索 —多線程的讀寫安全中講到過atomic的作用,這里不再贅述。或者最直接有效的解決方案就是在異步復制時進行加鎖和解鎖即可。以保證線程安全。
那么第2段代碼為什么能執行成功呢?原因很簡單,由于Tagged Pointer技術,name的指針的8個字節足以存放字符串abc,就不涉及調用name的set方法。所以能夠成功打印end。
MRC中的內存管理
在iOS中,使用引用計數的技術來管理OC對象的內存:
一個新創建的OC對象引用計數默認是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的內存空間。調用retain會讓OC對象的引用計數+1,調用release或者autorelease會讓OC對象的引用計數-1
我們在上文中提到了在set方法內部,會首先調用[_name release]釋放舊值,再賦新值。
在MRC時代,程序員需要手動的去管理內存,創建一個對象時,需要在set方法和get方法內部添加釋放對象的代碼。并且在對象的dealloc里面添加釋放的代碼。
我們用幾個簡單的例子來看一下:
使用assign關鍵字修飾的數據常量,set方法和get方法內部直接賦值和取值
@property(nonatomic,assign)intage;-(void)setAge:(int)age{_age=age;}-(int)age{return_age;}
使用strong關鍵字修飾的對象,set方法內部需要先釋放舊值,再retain新值
@property(nonatomic,strong)Person*person;-(void)setPerson:(Person*)person{if(_person!=person){[_person release];_person=[person retain];}}-(Person*)person{return_person;}
使用copy關鍵字修飾的對象,set方法內部需要先釋放舊值,再copy新值
@property(nonatomic,copy)NSArray*data;-(void)setData:(NSArray*)data{if(_data!=data){[_data release];_data=[data copy];}}
ARC的內存管理
在ARC環境中,我們不再像以前一樣自己手動管理內存,系統幫助我們做了release或者autorelease等事情。
ARC是LLVM編譯器和RunTime協作的結果。其中LLVM編譯器自動生成release、reatin、autorelease的代碼,像weak弱引用這些則靠RunTime在運行時釋放。
引用計數
上文我們講到在iOS中,使用引用計數的技術來管理OC對象的內存,那么引用計數是如何存儲的呢?我們之前在iOS底層原理探索 — Runtime之isa的本質一文中講過在__arm64__架構之后,isa指針不單單只存儲了類對象和元類對象的內存地址,而是使用共用體的方式存儲了更多信息。其中就包括引用計數。
我們再來回顧一下isa指針內部存儲的內容:
struct {? ? // 0代表普通的指針,存儲著類對象、元類對象的內存地址。? ? // 1代表優化后的使用位域存儲更多的信息。? ? uintptr_t nonpointer? ? ? ? : 1;? ? // 是否有設置過關聯對象,如果沒有,釋放時會更快? ? uintptr_t has_assoc? ? ? ? : 1;? ? // 是否有C++析構函數,如果沒有,釋放時會更快? ? uintptr_t has_cxx_dtor? ? ? : 1;? ? // 存儲著類對象、元類對象對象的內存地址信息? ? uintptr_t shiftcls? ? ? ? ? : 33;? ? // 用于在調試時分辨對象是否未完成初始化? ? uintptr_t magic? ? ? ? ? ? : 6;? ? // 是否有被弱引用指向過。? ? uintptr_t weakly_referenced : 1;? ? // 對象是否正在釋放? ? uintptr_t deallocating? ? ? : 1;? ? // 引用計數器是否過大無法存儲在isa中? ? // 如果為1,那么引用計數會存儲在一個叫SideTable的類的屬性中? ? uintptr_t has_sidetable_rc? : 1;? ? // 里面存儲的值是引用計數器減1? ? uintptr_t extra_rc? ? ? ? ? : 19;};
我們可以看到,在extra_rc里面存儲的值是引用計數器減1,但是當extra_rc的19位內存不夠存儲引用計數時,has_sidetable_rc的值就會變為1,那么此時引用計數會存儲在一個叫SideTable的類的屬性中。
SideTable.png
SideTable類中有一個RefcountMap類型的散列表,這個散列表中就存放著引用計數。
我們來到源碼文件NSObject.mm文件看一下源碼:
在源碼中,retainCount方法內部會調用rootRetainCount方法,在rootRetainCount方法,內部會做一系列的引用計數操作:
rootRetainCount源碼.png
經過一系列判斷,如果has_sidetable_rc的值就會為1時,說明此時引用計數會存儲在SideTable的類RefcountMap散列表中。然后通過sidetable_getExtraRC_nolock()函數去獲取引用計數。
sidetable_getExtraRC_nolock.png
sidetable_getExtraRC_nolock函數內部,也是先通過key找到對應的SideTable,在SideTable中通過key找到RefcountMap散列表,在散列表中拿到refcnts,即引用計數,然后返回。
今天對于內存管理的分析就到這里,我會在后續的文章中繼續為大家分析有關內存管理的知識。