以前在使用clang將.m文件轉成.cpp文件,查看裏面的內容,發現屬性上的編譯也頗有意思,因此本篇探究下iOS內存管理順便探究下屬性,成員變量,實例變量c++
iOS中的內存管理方案,大體能夠分爲兩類:MRC
(手動內存管理)和ARC
(自動內存管理)面試
MRC
時代,系統是經過對象的引用計數來判斷一個是否銷燬,有如下規則建立時
引用計數都爲1
被其餘指針引用
時,須要手動調用[objc retain]
,使對象的引用計數+1
[objc release]
來釋放對象
,使對象的引用計數-1
引用計數爲0
時,系統就會銷燬
這個對象【總結】:在MRC模式下,必須遵照:誰建立
,誰釋放
,誰引用
,誰管理
數組
ARC
模式是在WWDC2011
和iOS5引入的自動管理機制
,即自動引用計數,是編譯器的一種特性ARC模式下不須要手動retain、release、autorelease。編譯器會在適當的位置插入release和autorelease
。以前在OC基礎知識點之-內存管理初識(內存分區)介紹了內存的五大區,其實除了五大區還有內核區
和保留區
,以4G手機爲例,以下圖所示:系統將其中的3GB給了五大區+保留區
,剩餘的1GB給內核區使用
安全
內核區
:系統用來進行內核處理操做的區域保留區
:預留給系統處理nil等【說明】:之因此最後的內存地址是從0x00400000
開始的,是由於0x00000000表示nil
,不能直接用nil
表示一個段,因此單獨給一段內存用於處理nil
等狀況markdown
內存管理方案除了前文說起的MRC
和ARC
,還有如下三種app
Tagged Pointer
:專門用來處理小對象
,例如NSNumber、NSDate、小NSString等Nonpointer_isa
:非指針類型的isa,主要是用來優化64位地址
。這個在OC底層原理之-OC對象(下)isa指針結構分析對isa進行了介紹SideTables
:散列表,在散列表中主要有兩個表
,分別是引用計數表
、弱引用表
這裏主要介紹Tagged Pointer
和SideTables
ide
咱們經過一個面試題來引入Tagged Pointer
上面代碼運行有沒有問題,爲何?函數
運行上面的代碼咱們發現taggedPointerDemo
是正常
的,可是在touchesBegan
方法出現了崩潰
(taggedPointerDemo執行在viewDidLoad方法中) oop
崩潰緣由是
多條線程
對同一個對象
進行釋放
,致使對象過分釋放
,因此纔會崩潰。
【思考】:taggedPointerDemo和touchesBegan內部實現基本同樣,惟一的區別就是nameStr不同,可是一個沒有任何問題,一個卻崩潰了,是否是由於nameStr形成的呢?
咱們先看下nameStr有什麼不同的地方,在NSLog處打斷點,運行代碼,分別打印nameStr
咱們發現他們的類型不一樣,
taggedPointerDemo
中的nameStr
的類型是NSTaggedPointerString類型
,而touchesBegan
中的類型是__NSCFString
類型。
NSTaggedPointerString類型
的小對象
,存儲在常量區
,由於nameStr在alloc本來分配是在堆區
,可是因爲taggedPointerDemo的nameStr較小
,通過iOS優化
,就成了NSTaggedPointerString類型
,存在常量區
touchesBegan
方法中的nameStr
類型是NSCFString
類型,存儲在堆區
上咱們能夠經過NSString初始化的兩種方式,來測試NSString的內存管理
withString+@""
方式初始化WithFormat
方式初始化 運行結果:
經過打印咱們能夠看到NSString的`內存管理``主要分爲3種
NSTaggedPointerString
:標籤指針,是蘋果在64位
環境下對NSString、NSNumber
等對象作的優化
。對於NSString對象來講
字符串是由數字、英文字母組合且長度小於等於9
時,會自動成爲NSTaggedPointerString
類型,存儲在常量區
中文或者其餘特殊符號
時,會直接成爲__NSCFString
類型,存儲在堆區
__NSCFString
:是在運行時
建立的NSString子類
,建立後引用計數會加1
,存儲在堆上
__NSCFConstantString
:字符串常量
,是一種編譯時常量
,retainCount值很大
,對其操做,不會引發引用計數變化,存儲在字符串常量區
上面咱們經過面試題引出了Tagged Pointer,那麼我下面就來探究下Tagged Pointer底層實現,看看爲何Tagged Pointer類型不會存在過分釋放問題,咱們進入objc源碼中查看
查看reallySetProperty源碼,後面咱們會仔細將reallySetProperty方法
看到不過
不是copy修飾
就會經過objc_retain賦新值
,objc_release釋放舊值
,再看objc_retain,objc_release底層實現
經過源碼咱們能夠看到,在
objc_retain
,objc_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位
上是否有爲1
(taggedpointer
指針地址即表示指針地址,也表示值)
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表示NSString
,3表示NSNumber
驗證下:咱們能夠定義一個
NSDate對象
,來驗證其tagType
是否爲6
。 經過打印結果,其地址高位是
0xe
,轉換爲二進制爲1 110
,排除64位的1,剩餘的3位正好轉換爲十進制是6
,符合上面的枚舉值
Tagged Pointer
小對象類型(用於存儲NSNumber、NSDate、小NSString
),小對象指針再也不是簡單的地址,而是地址 + 值
,即真正的值
,因此,實際上它再也不是一個對象
了,它只是一個披着對象外衣的普通變量
而以。因此能夠直接進行讀取。優勢是佔用空間小,節省內存
Tagged Pointer
小對象,不會進入retain和release
,而是直接返回了,意味着不須要ARC進行管理
,因此能夠直接被系統自主的釋放和回收
Tagged Pointer
的內存並不存儲在堆區
中,而是在常量區
中,也不須要malloc和free
,因此能夠直接讀取
,相比存儲在堆區的數據讀取,效率
上快了3倍
左右。建立
的效率
相比堆區快
了近100倍
左右。taggedPointer的內存管理方案,比常規的內存管理,要快不少
Tagged Pointer
的64位地址中,前4位表明類型
,後4位主要適用於系統作一些處理
,中間56位用於存儲值
NSString
來講,當字符串較小
時,建議直接經過@""
初始化,由於存儲在常量區
,能夠直接
進行讀取
。會比WithFormat初始化方式更加快速
咱們在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
。
咱們探究下爲何會使用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
,由於以前不存在nameStr
,objc_retain(id obj)
方法裏進行判斷,若是存在就返回
,不存在就進行建立
(經過objc_msgSend的方式發送retain方法)。咱們發現調用objc_retain方法
,下面咱們探究下strong屬性(objc_retain後面講)
咱們準備下代碼:
@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
。
上面咱們知道對屬性進行copy
,以及屬性使用strong修飾
,都會走objc_retain
,咱們看看objc_retain
究竟作了些什麼屬性使用copy修飾是不走objc_retain
。 咱們全局搜objc_retain,發現以下代碼
- 1586:若是obj不存在,就直接返回
下面看下retain()方法
- 455:判斷不是Tagged Pointer對象,這個方法不但願處理Tagged Pointer
hasCustomRR函數檢查類(包括其父類)
中是否含有默認的retain方法
沒有調用rootRetain()
。有就進行消息轉發
,去調用自定義的retain方法
走下來咱們發現只會走461行,不會走458行。這是由於hasCustomRR再檢查的時候,會經過isa指針一直向上查找,直到找到NSObject,在NSObject中重寫了retain方法
而
_objc_rootRetain
會調用rootRetain()
方法。 下面咱們看下rootRetain()方法 發現調用了
rootRetain(false, false)注意傳值:false,false
,下面咱們看下rootRetain方法。 rootRetain的方法比較多,咱們會挑比較重要的地方進行解釋說明
是Tagged Pointer直接返回
transcribeToSideTable
用於表示extra_rc是否溢出
,默認爲false
(不抄寫到SideTable)atomic(原子性)獲取isa
isa是否是被優化過nonpointer
就是isa結構體中的nonpointer
,具體看OC底層原理之-OC對象(下)isa指針結構分析清空對象的isa.bits
元類,說明是類對象
,就直接返回
tryRetain
爲false且sideTable被鎖
,就打開鎖
(retain必需要爲true
,若是當前線程鎖住了sideTable對象
,則須要解鎖
)。判斷tryRetain
是否爲true
來肯定retain是否成功
,若是成功,就判斷sidetable_retain()
是否存在,若是存在就返回this
,若是不存在就返回nil
。若是失敗
繼續往下走調用sidetable_retain()
。newisa的bits進行處理
,進行addc操做
(註釋說對引用計數進行+1
,RC_ONE
是1想左偏移56
位,而isa指針的extra_rc
:引用計數,在63-56之間
。注:在x86
下)有進位,說明溢出
了,這時候表示extra_rc已經不能存儲在isa指針中
了。若是不處理溢出狀況
,在520行對bits進行清空
,在521行再次調用rootRetain
,此時的handleOverflow的會被rootRetain_overflow置爲true
。從而直接走下對sideTable進行加鎖
,528行是將引用計數減半
(RC_HALF爲1左移7爲,extra_rc滿爲8位,少了一位就是減半),繼續存在isa中
,529行將has_sidetable_rc設置爲true
,代表借用了sideTable存儲
transcribeToSideTable置爲true
。因此若是溢出了就會進來
。一半放在isa指針中,另外一半就存在sideTable中
。tryRetain爲false
,SideTable鎖了
,那就解鎖
。經過上面的解釋咱們能夠肯定幾個問題:1.調用rootRetain會讓引用計數+1. 2.當引用計數過大溢出時,會將引用計數一半存在isa的extra_rc中,另外一半存在sideTable中
咱們上面說了,屬性直接copy以及strong修飾
屬性賦值時都會調用rootRetain
,說明對對象copy以及strong修飾屬性賦值時都會致使引用計數+1
。由於strong是強引用,因此+1
,而copy修飾屬性
,只是調用copyWithZone
,而不可變對象進行copy時
是對內存地址進行copy
,也就是此處也指向該內存,因此須要+1
。 可看下圖 咱們看到內存地址是同樣的。
上面咱們說了引用計數存儲到必定的值
時,就不會存在isa指針中的extra_rc
中,而是將一半存到SideTables散列表
中,爲何是將一半存在SideTables而不是所有呢?
緣由:若是都存儲在散列表中,每次對散列表操做都須要開解鎖,操做耗時,消耗性能大,因此對半分的操做目的是爲了提升性能
。 咱們看下sidetable_addExtraRC_nolock源碼 發現獲取
SideTable
是從SideTables
取的,說明SideTable是有多張的
散列表只有一張表
,意味着全局全部的對象
都會存儲在一張表中
,操做任意一個對象
,都會進行解鎖
(鎖是鎖整個表的讀寫)。當開鎖
時,其它對象可能也操做這張表,則意味着數據不安全
每一個對象都開一個表
,會耗費性能
,因此也不能有無數個表
咱們看下SideTable結構,SideTables的底層實現
咱們發現
sideTable
包含互斥鎖slock
,引用計數表refcnts
,以及一個弱引用表weak_table
,而SideTables
是經過SideTablesMap的get方法獲取
,而SideTablesMap
是經過StripedMap<SideTable>定義
的。咱們再看下StripedMap源碼
從這裏能夠看到,同一時間,真機中的
散列表最多隻能有8張
數組
:特色在於查詢方便(即經過下標訪問)
,增刪比較麻煩
(相似於以前講過的methodList
,經過memcopy、memmove增刪
,很是麻煩),因此數組的特性是讀取快,但存儲不方便
鏈表
:特色在於增刪方便
,查詢慢
(須要從頭節點開始遍歷查詢
),因此鏈表的特性是存儲快,但讀取慢
散列表
:其本質就是一張哈希表
,哈希表集合了數組和鏈表的長處
,增刪改查都比較方便
,例如拉鍊哈希表
(在以前鎖的文章中,講過的tls
的存儲結構就是拉鍊形式
的),是最經常使用的鏈表上面對對象的copy,strong(retain是同樣的,都會調用retain()方法,結合上面的小對象。咱們總結下retain()做了什麼操做
retain
在底層首先會判斷是不是Nonpointer isa
,若是不是,則直接操做散列表 進行+1操做
是Nonpointer isa
,還須要判斷是否正在釋放
,若是正在釋放,則執行dealloc流程
,釋放弱引用表和引用計數表,最後free釋放對象內存不是正在釋放,則對Nonpointer isa進行常規的引用計數+1
。這裏須要注意一點的是,extra_rc在真機上只有8位用於存儲引用計數的值
,當存儲滿了
時,須要藉助散列表用於存儲
。須要將滿了的extra_rc對半分
,一半(即2^7)存儲在散列表中
。另外一半仍是存儲在extra_rc中
,用於常規的引用計數的+1或者-1操做
,而後再返回上面分析了+1操做,下面分析下-1操做:release,看下release底層實現 上面的reallySetProperty
最後對舊值進行objc_release
,咱們就從objc_release
開始,objc_release->release()->rootRelease()->rootRelease()
大體上和上面的rootRetain方法相似,只不過是相反的操做。下面簡要分析一下
Nonpointer isa
,若是不是,則直接對散列表進行-1操做
Nonpointer isa
,則對extra_rc
中的引用計數值進行-1操做
,並存儲此時的extra_rc狀態到carry
中carray爲0
,則走到underflow流程
underflow
流程有如下幾步:
散列表
中是否存儲了一半的引用計數
散列表
中取出
存儲的一半引用計數,進行-1操做
,而後存儲到extra_rc中
extra_rc沒有值
,散列表中也是空
的,則直接進行析構
,即dealloc操做
,屬於自動觸發
在retain和release的底層實現中,都說起了dealloc析構函數,下面來分析dealloc的底層的實現,經過delloc->_objc_rootDealloc->rootDealloc
判斷是否有isa、cxx、關聯對象、弱引用表、引用計數表
,若是沒有
,則直接free釋放內存
有
,則進入object_dispose方法
經過上面能夠看到,object_dispose方法的目的有一下幾個
到如今爲止,retain -> release -> dealloc就所有串聯起來了
上面提到了retainCount,咱們來看下retainCount底層是如何操做的,咱們先看個面試題 問:打印結果是多少?答案:1,爲何?若是回答由於NSObject被alloc了,因此引用計數+1,那麼你是說對告終果,可是不知道緣由
咱們在文章對alloc理解中歷來沒說過allock會對引用計數+1
。那爲何答案會是1呢,下面咱們來分析下
上面就是retainCount源碼,咱們在rootRetainCount打斷點,進行調試
答案:
alloc
建立的對象實際的引用計數爲0
,其引用計數打印結果爲1
,是由於在底層rootRetainCount
方法中,引用計數默認+1
了,可是這裏只有
對引用計數的讀取
操做,是沒有寫入
操做的,簡單來講就是:爲了防止alloc建立的對象被釋放(引用計數爲0會被釋放),因此在編譯階段,程序底層默認進行了+1操做。實際上在extra_rc中的引用計數仍然爲0
alloc
建立的對象沒有retain和release
alloc
建立對象的引用計數爲0
,會在編譯時期
,程序默認加1
,因此讀取引用計數時爲1咱們再看上面的.cpp文件發現以下: 咱們看到
每一個屬性都有一個get方法和set方法
咱們解釋下紅框部分的意思,紅框是簽名,以
@16@0:8
爲例,
第一個@是返回值爲id類型
,16表示的返回值爲16字節
,第二個@表示第一個參數
"v24@0:8@16",v指無返回值
爲了方便理解,我附上一張官方解釋的圖,以及官方鏈接,你們能夠去官方看更詳細的解釋。 附:Type Encoding-官方文檔 ,Property Type String-官方文檔