iOS底層原理探索—內存管理(一)

探索底層原理,積累從點滴做起

往期回顧

iOS底層原理探索 — OC對象的本質

iOS底層原理探索 — class的本質

iOS底層原理探索 — KVO的本質

iOS底層原理探索 — KVC的本質

iOS底層原理探索 — Category的本質(一)

iOS底層原理探索 — Category的本質(二)

iOS底層原理探索 — 關聯對象的本質

iOS底層原理探索 — block的本質(一)

iOS底層原理探索 — block的本質(二)

iOS底層原理探索 — Runtime之isa的本質

iOS底層原理探索 — Runtime之class的本質

iOS底層原理探索 — Runtime之消息機制

iOS底層原理探索 — RunLoop的本質

iOS底層原理探索 — RunLoop的應用

iOS底層原理探索 — 多線程的本質

iOS底層原理探索 — 多線程的經典面試題

iOS底層原理探索 — 多線程的“鎖”

前言

內存管理在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,即引用計數,然后返回。

今天對于內存管理的分析就到這里,我會在后續的文章中繼續為大家分析有關內存管理的知識。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,121評論 1 32
  • iOS中內存管理機制是開發中一項很重要的知識,了解iOS中內存管理的規則不管是在開發中還是在學習中都能很大程度的幫...
    Horson19閱讀 1,210評論 0 4
  • 夜寒濕枕巾, 起衣披霜華。 遙望舊里居, 月是故鄉明。
    蘭亭西94閱讀 201評論 0 0
  • 人生最踏實珍貴的幸福,就在日常瑣碎的生活中。慢下來,享受生活中小小的幸福,就是在為未來儲備力量。只有這樣,我們的人...
    布大叔閱讀 114評論 0 0
  • 進度:整本書終結。 今天主要是第二十章 智人末日和后記。 印象最深的是,作者在最后的反思: 問題不是“我們究竟想要...
    大林_Rbenefit閱讀 176評論 0 0