OC底層知識點之 - 內存管理(上)

系列文章:OC底層原理系列OC基礎知識系列html

前言

以前在使用clang將.m文件轉成.cpp文件,查看裏面的內容,發現屬性上的編譯也頗有意思,因此本篇探究下iOS內存管理順便探究下屬性,成員變量,實例變量c++

ARC & MRC

iOS中的內存管理方案,大體能夠分爲兩類:MRC(手動內存管理)和ARC(自動內存管理)面試

  • MRC
    • MRC時代,系統是經過對象的引用計數來判斷一個是否銷燬,有如下規則
    • 對象被建立時引用計數都爲1
    • 當對象被其餘指針引用時,須要手動調用[objc retain],使對象的引用計數+1
    • 當指針變量再也不使用對象時,須要手動調用[objc release]釋放對象,使對象的引用計數-1
    • 當一個對象的引用計數爲0時,系統就會銷燬這個對象

【總結】:在MRC模式下,必須遵照:誰建立誰釋放誰引用誰管理數組

  • ARC
    • ARC模式是在WWDC2011和iOS5引入的自動管理機制,即自動引用計數,是編譯器的一種特性
    • 規則與MRC一致,區別在於,ARC模式下不須要手動retain、release、autorelease。編譯器會在適當的位置插入release和autorelease

內存佈局

以前在OC基礎知識點之-內存管理初識(內存分區)介紹了內存的五大區,其實除了五大區還有內核區保留區,以4G手機爲例,以下圖所示:系統將其中的3GB給了五大區+保留區,剩餘的1GB給內核區使用 安全

  • 內核區:系統用來進行內核處理操做的區域
  • 保留區:預留給系統處理nil等

【說明】:之因此最後的內存地址是從0x00400000開始的,是由於0x00000000表示nil,不能直接用nil表示一個段,因此單獨給一段內存用於處理nil等狀況markdown

內存管理方案

內存管理方案除了前文說起的MRCARC,還有如下三種app

  • 1.Tagged Pointer:專門用來處理小對象,例如NSNumber、NSDate、小NSString等
  • 2.Nonpointer_isa:非指針類型的isa,主要是用來優化64位地址。這個在OC底層原理之-OC對象(下)isa指針結構分析對isa進行了介紹
  • 3.SideTables:散列表,在散列表中主要有兩個表,分別是引用計數表弱引用表

這裏主要介紹Tagged PointerSideTableside

Tagged Pointer

咱們經過一個面試題來引入Tagged Pointer 上面代碼運行有沒有問題,爲何?函數

運行上面的代碼咱們發現taggedPointerDemo正常的,可是在touchesBegan方法出現了崩潰(taggedPointerDemo執行在viewDidLoad方法中) oop

崩潰緣由是多條線程同一個對象進行釋放,致使對象過分釋放,因此纔會崩潰。

【思考】:taggedPointerDemo和touchesBegan內部實現基本同樣,惟一的區別就是nameStr不同,可是一個沒有任何問題,一個卻崩潰了,是否是由於nameStr形成的呢?

驗證

咱們先看下nameStr有什麼不同的地方,在NSLog處打斷點,運行代碼,分別打印nameStr 咱們發現他們的類型不一樣,taggedPointerDemo中的nameStr的類型是NSTaggedPointerString類型,而touchesBegan中的類型是__NSCFString類型。

  • 1.NSTaggedPointerString類型小對象,存儲在常量區,由於nameStr在alloc本來分配是在堆區,可是因爲taggedPointerDemo的nameStr較小,通過iOS優化,就成了NSTaggedPointerString類型,存在常量區
  • 2.touchesBegan方法中的nameStr類型是NSCFString類型,存儲在堆區

NSString的內存管理

咱們能夠經過NSString初始化的兩種方式,來測試NSString的內存管理

  • 1.經過withString+@""方式初始化
  • 2.經過WithFormat方式初始化

運行結果: 經過打印咱們能夠看到NSString的`內存管理``主要分爲3種

  • 1.NSTaggedPointerString:標籤指針,是蘋果在64位環境下對NSString、NSNumber等對象作的優化。對於NSString對象來講
    • 字符串是由數字、英文字母組合且長度小於等於9時,會自動成爲NSTaggedPointerString類型,存儲在常量區
    • 當有中文或者其餘特殊符號時,會直接成爲__NSCFString類型,存儲在堆區
  • 2.__NSCFString:是在運行時建立的NSString子類,建立後引用計數會加1存儲在堆上
  • 3.__NSCFConstantString字符串常量,是一種編譯時常量retainCount值很大,對其操做,不會引發引用計數變化,存儲在字符串常量區

agged Pointer 小對象底層原理

上面咱們經過面試題引出了Tagged Pointer,那麼我下面就來探究下Tagged Pointer底層實現,看看爲何Tagged Pointer類型不會存在過分釋放問題,咱們進入objc源碼中查看

小對象的引用計數處理分析

查看reallySetProperty源碼,後面咱們會仔細將reallySetProperty方法

看到不過不是copy修飾就會經過objc_retain賦新值objc_release釋放舊值,再看objc_retain,objc_release底層實現

經過源碼咱們能夠看到,在objc_retainobjc_release中都對agged Pointer進行了判斷,若是小對象,就直接返回。由此咱們能夠得出一個結論:小對象是不會進行retain和release操做的

小對象的地址分析

咱們繼續以NSString爲例,對於NSString來講

  • 通常的NSString對象指針,都是string值 + 指針地址二者是分開
  • 對於Tagged Pointer指針,其指針+值,都能在小對象中體現。因此Tagged Pointer 既包含指針,也包含值

在以前的文章OC底層原理之-類的加載過程-上( _objc_init實現原理)中講類加載時,其中_read_images源碼有一個方法對小對象進行了處理,即initializeTaggedPointerObfuscator方法,下面咱們查看下initializeTaggedPointerObfuscator方法實現 在iOS12後,Tagged Pointer採用了混淆處理,咱們能夠設置OBJC_DISABLE_TAG_OBFUSCATION爲YES來關閉Tagged Pointer的混淆 咱們能夠經過源碼中objc_debug_taggedpointer_obfuscator查找taggedPointer的編碼和解碼,來查看底層是如何混淆處理的 上面咱們知道編碼_objc_encodeTaggedPointer是經過objc_debug_taggedpointer_obfuscator異或傳入值,解碼_objc_decodeTaggedPointer也是經過objc_debug_taggedpointer_obfuscator異或傳入值,至關因而兩層異或。下面咱們舉例說明:傳入值1010 0100,mask值0101 0010

1010 0100
^0101 0010 mask (編碼)
 1111 0110
^0101 0010 mask (解碼)
 1010 0100
複製代碼

咱們看下解碼後的小對象地址,其中61表示a的ASCII碼63表示c的ASCII碼,咱們再以NSNumber爲例 咱們看到地址確實存儲值了。可是小對象後面的0xa,0xb又是什麼含義呢?最後咱們在判斷是否爲小對象的判斷裏找到了答案 因此0xa、0xb主要是用於判斷是不是小對象TaggedPointer,判斷第64位上是否有爲1taggedpointer指針地址即表示指針地址,也表示值)

  • 0xa轉換成二進制爲1 010(64爲爲1,63~61後三位表示tagType類型-2),表示NSString類型
  • 0xb轉換爲二進制爲1 011(64爲爲1,63~61後三位表示tagType類型-3),表示NSNumber類型,這裏須要注意一點,若是NSNumber的值是-1,其地址中的值是用補碼表示的

這裏能夠經過_objc_makeTaggedPointer方法的參數tag類型objc_tag_index_t進入其枚舉,其中2表示NSString3表示NSNumber 驗證下:咱們能夠定義一個NSDate對象,來驗證其tagType是否爲6 經過打印結果,其地址高位是0xe,轉換爲二進制爲1 110,排除64位的1,剩餘的3位正好轉換爲十進制是6符合上面的枚舉值

Tagged Pointer 總結

  • 1.Tagged Pointer小對象類型(用於存儲NSNumber、NSDate、小NSString),小對象指針再也不是簡單的地址,而是地址 + 值,即真正的值,因此,實際上它再也不是一個對象了,它只是一個披着對象外衣的普通變量而以。因此能夠直接進行讀取。優勢是佔用空間小,節省內存
  • 2.Tagged Pointer小對象,不會進入retain和release,而是直接返回了,意味着不須要ARC進行管理,因此能夠直接被系統自主的釋放和回收
  • 3.Tagged Pointer內存並不存儲在堆區中,而是在常量區中,也不須要malloc和free,因此能夠直接讀取,相比存儲在堆區的數據讀取,效率上快了3倍左右。建立效率相比堆區了近100倍左右。taggedPointer的內存管理方案,比常規的內存管理,要快不少
  • 4.Tagged Pointer的64位地址中,前4位表明類型後4位主要適用於系統作一些處理中間56位用於存儲值
  • 5.優化內存建議:對於NSString來講,當字符串較小時,建議直接經過@""初始化,由於存儲在常量區,能夠直接進行讀取。會比WithFormat初始化方式更加快速

探究strong和copy的內存管理

準備代碼

咱們在ViewController.h文件寫的有以下屬性

@interface ViewController ()
{
    Man *man;
    NSString *workTime;
    NSInteger times;
}
@property (nonatomic, strong)Student *student;
@property (nonatomic, copy)NSString *schoolName;
@property (nonatomic, copy)NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong)NSMutableArray *houses;
@end
複製代碼

名詞解釋

屬性:有前綴 @property修飾的變量

成員變量:就是{}內的變量,上面咱們寫的man,workTime,times都是成員變量

實例變量:若是成員變量的數據類型是個類,能被實例化,那它就是實例變量,上面咱們寫的man就是實例變量

經過clang轉成.cpp文件。咱們在它的.cpp文件發現屬性他們在c++底層是如圖(截取部分) 這是屬性的get和set方法,發現name的set方法和age,houses有區別(區別用下劃線標記出來了)緣由是name1使用的是copy,二age用的assign,houses用的strong

探究copy

咱們探究下爲何會使用objc_setProperty,咱們去先取LLVM源碼中找一下objc_setProperty,咱們找到了getOptimizedSetPropertyFn放發,看下圖: 經過圖咱們看到對屬性使用不一樣的修飾詞,對象的set方法修飾也不一樣

  • 若是使用natomic和copy修飾:objc_setProperty_atomic_copy
  • 若是使用natomic和非copy修飾:objc_setProperty_atomic
  • 若是使用非natomic和copy修飾:objc_setProperty_nonatomic_copy
  • 若是使用非natomic和非copy修飾:objc_setProperty_nonatomic

咱們的name是非natomic和copy修飾,因此應該是objc_setProperty_nonatomic_copy 下面咱們再去源碼中查看下objc_setProperty_nonatomic_copy如何實現。 上圖就是咱們找到的源碼實現,咱們注意到方法reallySetProperty,去這個方法看看 上圖咱們知道若是是copy就是調用copyWithZone方法,因爲不是atomic,因此會走90,91這裏就是取出以前的值,將新值賦給*slot,最後將舊值釋放。 下面咱們代碼驗證下

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p  = [Person alloc];
        p.name = @"小明";
        p.name = @"小張";
        NSLog(@"--->");
        NSString *nameStr = [p.name copy];
    }
    return 0;
}
複製代碼

運行代碼,打斷點 打印由於小明是第一次賦值,因此不存在舊值。咱們繼續 此次再給name賦值小張舊值就存在了。最後在100行會將舊值釋放掉。 咱們再看下NSString *nameStr = [p.name copy];咱們發現並沒有走咱們打斷點的reallySetProperty方法,那這個方法會走哪呢?咱們打斷點,在斷點停留處看看彙編(截取關鍵部分) 咱們發現後面會調用objc_storeStrong,下面咱們在源碼中找一下該方法 打斷點,再也不看彙編,繼續下一步 但此時的obj是nil,由於以前不存在nameStrobjc_retain(id obj)方法裏進行判斷,若是存在就返回不存在就進行建立(經過objc_msgSend的方式發送retain方法)。咱們發現調用objc_retain方法,下面咱們探究下strong屬性(objc_retain後面講)

探究strong

咱們準備下代碼:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p  = [Person alloc];
        p.name = @"aaa";
    }
    return 0;
}
複製代碼

打斷點,當來到這裏是,咱們查看彙編 咱們發現這個方法和上面講的nameStr同樣,都會走objc_storeStrong 此時咱們打印,發現此時·obj爲咱們對name的賦值。接着對obj進行objc_retain

探究objc_retain

上面咱們知道對屬性進行copy,以及屬性使用strong修飾,都會走objc_retain,咱們看看objc_retain究竟作了些什麼屬性使用copy修飾是不走objc_retain。 咱們全局搜objc_retain,發現以下代碼

  • 1586:若是obj不存在,就直接返回
  • 1587:若是obj是Tagged Pointer對象也直接返回
  • 1588:上面都不知足調用retain()

下面看下retain()方法

  • 455:判斷不是Tagged Pointer對象,這個方法不但願處理Tagged Pointer
  • 457:經過hasCustomRR函數檢查類(包括其父類)中是否含有默認的retain方法
  • 458:若是沒有調用rootRetain()
  • 461:若是有就進行消息轉發,去調用自定義的retain方法

走下來咱們發現只會走461行,不會走458行。這是由於hasCustomRR再檢查的時候,會經過isa指針一直向上查找,直到找到NSObject,在NSObject中重寫了retain方法 _objc_rootRetain會調用rootRetain()方法。 下面咱們看下rootRetain()方法 發現調用了rootRetain(false, false)注意傳值:false,false,下面咱們看下rootRetain方法。 rootRetain的方法比較多,咱們會挑比較重要的地方進行解釋說明

  • 489:若是是Tagged Pointer直接返回
  • 492:transcribeToSideTable用於表示extra_rc是否溢出,默認爲false(不抄寫到SideTable)
  • 499:經過atomic(原子性)獲取isa
  • 501:isa是否是被優化過nonpointer就是isa結構體中的nonpointer,具體看OC底層原理之-OC對象(下)isa指針結構分析
  • 502:若是不是,就清空對象的isa.bits
  • 503:若是是元類,說明是類對象,就直接返回
  • 504:若是tryRetainfalse且sideTable被鎖,就打開鎖retain必需要爲true,若是當前線程鎖住了sideTable對象,則須要解鎖)。
  • 505:經過判斷tryRetain是否爲true來肯定retain是否成功,若是成功,就判斷sidetable_retain()是否存在,若是存在就返回this,若是不存在就返回nil。若是失敗繼續往下走調用sidetable_retain()
  • 515:將newisa的bits進行處理,進行addc操做(註釋說對引用計數進行+1RC_ONE1想左偏移56位,而isa指針的extra_rc:引用計數,在63-56之間。注:在x86下)
  • 519:有進位,說明溢出了,這時候表示extra_rc已經不能存儲在isa指針中了。
  • 520-521:若是不處理溢出狀況,在520行對bits進行清空,在521行再次調用rootRetain,此時的handleOverflow的會被rootRetain_overflow置爲true。從而直接走下
  • 525-529:525行就是對sideTable進行加鎖,528行是將引用計數減半(RC_HALF爲1左移7爲,extra_rc滿爲8位,少了一位就是減半),繼續存在isa中,529行將has_sidetable_rc設置爲true,代表借用了sideTable存儲
  • 533:由於上面講若是溢出了,就會將transcribeToSideTable置爲true。因此若是溢出了就會進來
  • 535:將上面說的引用計數溢出,一半放在isa指針中,另外一半就存在sideTable中
  • 538:若是tryRetain爲falseSideTable鎖了,那就解鎖
  • 539:返回

經過上面的解釋咱們能夠肯定幾個問題:1.調用rootRetain會讓引用計數+1. 2.當引用計數過大溢出時,會將引用計數一半存在isa的extra_rc中,另外一半存在sideTable中 咱們上面說了,屬性直接copy以及strong修飾屬性賦值時都會調用rootRetain,說明對對象copy以及strong修飾屬性賦值時都會致使引用計數+1由於strong是強引用,因此+1,而copy修飾屬性,只是調用copyWithZone,而不可變對象進行copy時是對內存地址進行copy,也就是此處也指向該內存,因此須要+1 可看下圖 咱們看到內存地址是同樣的。

SideTables 散列表

上面咱們說了引用計數存儲到必定的值時,就不會存在isa指針中的extra_rc中,而是將一半存到SideTables散列表中,爲何是將一半存在SideTables而不是所有呢?

緣由:若是都存儲在散列表中,每次對散列表操做都須要開解鎖,操做耗時,消耗性能大,因此對半分的操做目的是爲了提升性能 咱們看下sidetable_addExtraRC_nolock源碼 發現獲取SideTable是從SideTables取的,說明SideTable是有多張的

問題1.爲何在內存中有多張?最多可以多少張?**

  • 若是散列表只有一張表,意味着全局全部的對象都會存儲在一張表中,操做任意一個對象,都會進行解鎖(鎖是鎖整個表的讀寫)。當開鎖時,其它對象可能也操做這張表,則意味着數據不安全
  • 若是每一個對象都開一個表,會耗費性能,因此也不能有無數個表

咱們看下SideTable結構,SideTables的底層實現

咱們發現sideTable包含互斥鎖slock,引用計數表refcnts,以及一個弱引用表weak_table,而SideTables經過SideTablesMap的get方法獲取,而SideTablesMap是經過StripedMap<SideTable>定義的。咱們再看下StripedMap源碼

從這裏能夠看到,同一時間,真機中的散列表最多隻能有8張

問題2.爲何在用散列表,而不用數組、鏈表?

  • 數組:特色在於查詢方便(即經過下標訪問)增刪比較麻煩(相似於以前講過的methodList經過memcopy、memmove增刪,很是麻煩),因此數組的特性是讀取快,但存儲不方便
  • 鏈表:特色在於增刪方便查詢慢(須要從頭節點開始遍歷查詢),因此鏈表的特性是存儲快,但讀取慢
  • 散列表:其本質就是一張哈希表,哈希表集合了數組和鏈表的長處增刪改查都比較方便,例如拉鍊哈希表(在以前鎖的文章中,講過的tls的存儲結構就是拉鍊形式的),是最經常使用的鏈表

上面對對象的copy,strong(retain是同樣的,都會調用retain()方法,結合上面的小對象。咱們總結下retain()做了什麼操做

總結retain做了什麼

  • 1.retain在底層首先會判斷是不是Nonpointer isa,若是不是,則直接操做散列表 進行+1操做
  • 2.若是是Nonpointer isa,還須要判斷是否正在釋放,若是正在釋放,則執行dealloc流程,釋放弱引用表和引用計數表,最後free釋放對象內存
  • 3.若是不是正在釋放,則對Nonpointer isa進行常規的引用計數+1。這裏須要注意一點的是,extra_rc在真機上只有8位用於存儲引用計數的值,當存儲滿了時,須要藉助散列表用於存儲。須要將滿了的extra_rc對半分一半(即2^7)存儲在散列表中另外一半仍是存儲在extra_rc中,用於常規的引用計數的+1或者-1操做,而後再返回

release 源碼分析

上面分析了+1操做,下面分析下-1操做:release,看下release底層實現 上面的reallySetProperty最後對舊值進行objc_release,咱們就從objc_release開始,objc_release->release()->rootRelease()->rootRelease()

大體上和上面的rootRetain方法相似,只不過是相反的操做。下面簡要分析一下

  • 1.判斷是不是Nonpointer isa,若是不是,則直接對散列表進行-1操做
  • 2.若是是Nonpointer isa,則對extra_rc中的引用計數值進行-1操做,並存儲此時的extra_rc狀態到carry
  • 3.若是此時的狀態carray爲0,則走到underflow流程
  • 4.underflow流程有如下幾步:
    • 判斷散列表是否存儲了一半的引用計數
    • 若是是,則從散列表取出存儲的一半引用計數,進行-1操做,而後存儲到extra_rc中
    • 若是此時extra_rc沒有值散列表中也是空的,則直接進行析構,即dealloc操做,屬於自動觸發

dealloc 源碼分析

在retain和release的底層實現中,都說起了dealloc析構函數,下面來分析dealloc的底層的實現,經過delloc->_objc_rootDealloc->rootDealloc

  • 1.根據條件判斷是否有isa、cxx、關聯對象、弱引用表、引用計數表,若是沒有,則直接free釋放內存
  • 2.若是,則進入object_dispose方法

經過上面能夠看到,object_dispose方法的目的有一下幾個

  • 1.銷燬實例,主要有如下操做
    • 1.調用c++析構函數
    • 2.刪除關聯引用
    • 3.釋放散列表
    • 4.清空弱引用表
  • 2.free釋放內存

到如今爲止,retain -> release -> dealloc就所有串聯起來了

retainCount 源碼分析

上面提到了retainCount,咱們來看下retainCount底層是如何操做的,咱們先看個面試題 問:打印結果是多少?答案:1,爲何?若是回答由於NSObject被alloc了,因此引用計數+1,那麼你是說對告終果,可是不知道緣由

咱們在文章對alloc理解歷來沒說過allock會對引用計數+1。那爲何答案會是1呢,下面咱們來分析下 上面就是retainCount源碼,咱們在rootRetainCount打斷點,進行調試

答案:alloc建立的對象實際的引用計數爲0,其引用計數打印結果爲1,是由於在底層rootRetainCount方法中,引用計數默認+1了,可是這裏只有對引用計數的讀取操做,是沒有寫入操做的,簡單來講就是:爲了防止alloc建立的對象被釋放(引用計數爲0會被釋放),因此在編譯階段,程序底層默認進行了+1操做。實際上在extra_rc中的引用計數仍然爲0

總結

  • 1.alloc建立的對象沒有retain和release
  • 2.alloc建立對象的引用計數爲0,會在編譯時期,程序默認加1,因此讀取引用計數時爲1

擴展

咱們再看上面的.cpp文件發現以下: 咱們看到每一個屬性都有一個get方法和set方法

咱們解釋下紅框部分的意思,紅框是簽名,以@16@0:8爲例,

  • 第一個@是返回值爲id類型
  • 16表示的返回值爲16字節
  • 第二個@表示第一個參數
  • 0表示從0開始,到8(0-8)
  • :是指sel方法編號
  • 8是指8-16

"v24@0:8@16",v指無返回值

爲了方便理解,我附上一張官方解釋的圖,以及官方鏈接,你們能夠去官方看更詳細的解釋。 附:Type Encoding-官方文檔Property Type String-官方文檔

相關文章
相關標籤/搜索