iOS中內(nèi)存管理機(jī)制是開發(fā)中一項(xiàng)很重要的知識概耻,了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫助我們提升效率。下面我就根據(jù)自己的理解,詳細(xì)梳理一下內(nèi)存管理相關(guān)的知識。
在說內(nèi)存管理之前,我們首先要了解什么內(nèi)存干茉。一塊內(nèi)存條,是一個(gè)從下至上地址依次遞增的結(jié)構(gòu)很泊,內(nèi)存條中主要分為幾大類:棧區(qū)(stack)角虫、堆區(qū)(heap)沾谓、常量區(qū)、代碼區(qū)(.text)戳鹅、保留區(qū)均驶。常量區(qū)分為未初始化區(qū)域(.bss)和已初始化區(qū)域(.data),棧區(qū)stack存儲順序是由高地址存向低地址枫虏,而堆區(qū)是由低地址向高地址存儲妇穴。內(nèi)存條中地址由低到高的區(qū)域分別為:保留區(qū),代碼區(qū)隶债,已初始化區(qū)(.data)腾它,未初始化區(qū)(.bss),堆區(qū)(heap)死讹,棧區(qū)(stack)携狭,內(nèi)核區(qū)。而程序員操作的主要是棧區(qū)與堆區(qū)還有常量區(qū)回俐。
關(guān)于iOS內(nèi)存管理的方案其實(shí)并非只有散列表一種,還有一種更為高效且為內(nèi)存高效節(jié)省空間的方法叫做TaggedPointer,表明加標(biāo)記的指針稀并,我們可以理解為在是指針內(nèi)部增加一些特殊的信息仅颇。
那么為什么要使用taggedPointer這種內(nèi)存管理方法呢,其如何達(dá)到節(jié)省內(nèi)存的目的呢碘举。舉個(gè)例子忘瓦,比如在OC中一個(gè)NSNumber對象,在32位中的系統(tǒng)中占用4個(gè)字節(jié)的空間引颈,但是遷移至64位系統(tǒng)中后耕皮,其占用空間達(dá)到了8字節(jié),以此類推蝙场,所有在64位系統(tǒng)中占用空間會翻倍的對象凌停,在遷移后會導(dǎo)致系統(tǒng)內(nèi)存劇增,即時(shí)他們根本用不到這么多的空間售滤,所以蘋果對于一些小型數(shù)據(jù)罚拟,采用了taggedPointer這種方式管理內(nèi)存。
其主要的原理就是在對象的指針中加入特定需要記錄的信息完箩,以及對象所對應(yīng)的值赐俗,在64位的系統(tǒng)中,一個(gè)指針?biāo)加玫膬?nèi)存空間為8個(gè)字節(jié)弊知,已足以存下一些小型的數(shù)據(jù)量了阻逮,當(dāng)對象指針的空間中存滿后,再對指針?biāo)赶虻膬?nèi)存區(qū)域進(jìn)行存儲秩彤,這就是taggedPointer叔扼。距離NSNumber事哭,最低4位用于標(biāo)記是什么類型的數(shù)據(jù)(long為3,float則為4币励,Int為2慷蠕,double為5),而最高4位的“b”表示是NSNumber類型食呻;其余56位則用來存儲數(shù)值本身內(nèi)容流炕。
之前runtime文章中有提到過objc_objcet對象中isa指針分為指針型isa與非指針型isa(NONPOINTER_ISA),運(yùn)用的便是類似這種技術(shù)仅胞。下面詳細(xì)解讀一下NONPOINTER_ISA:
在一個(gè)64位的指針內(nèi)存中每辟,第0位存儲的是indexed標(biāo)識符,它代表一個(gè)指針是否為NONPOINTER型干旧,0代表不是渠欺,1代表是。第1位has_assoc椎眯,顧名思義挠将,1代表其指向的實(shí)例變量含有關(guān)聯(lián)對象,0則為否编整。第2位為has_cxx_dtor舔稀,表明該對象是否包含C++相關(guān)的內(nèi)容或者該對象是否使用ARC來管理內(nèi)存,如果含有C++相關(guān)內(nèi)容或者使用了ARC來管理對象掌测,這一塊都表示為YES内贮,第3-35位shiftcls存儲的就是這個(gè)指針的地址。第42位為weakly_referenced汞斧,表明該指針對象是否有弱引用的指針指向夜郁。第43位為deallocing,表明該對象是否正在被回收粘勒。第44位為has_sidetable_rc竞端,顧名思義,該指針是否引用了sidetable散列表庙睡。第45-63位extra_rc裝的就是這個(gè)實(shí)例變量的引用計(jì)數(shù)婶熬,當(dāng)對象被引用時(shí),其引用計(jì)數(shù)+1埃撵,但少量的引用計(jì)數(shù)是不會直接存放在sideTables表中的赵颅,對象的引用計(jì)數(shù)會先存在NONPOINTER_ISA的指針中的45-63位,當(dāng)其被存滿后暂刘,才會相應(yīng)存入sideTables散列表中饺谬。
所以綜上所述,iOS中內(nèi)存管理的方式主要有三大,1.taggedPointer募寨,2.NONPOINTER_ISA族展,3.散列表。
下面再進(jìn)行散列表的分析:
散列表在系統(tǒng)中的提現(xiàn)是一個(gè)sideTables的哈希映射表拔鹰,其中所有對象的引用計(jì)數(shù)(除上述存在NONPOINTER_ISA中的外)都存在這個(gè)sideTables散列表中仪缸,而一個(gè)散列表中又包含眾多sideTable。每個(gè)SideTable中又包含了三個(gè)元素列肢,spinlock_t自旋鎖恰画,RefcountMap引用計(jì)數(shù)表,weak_table_t弱引用表瓷马。所以既然SideTables是一個(gè)哈希映射的表拴还,為什么不用SideTables直接包含自旋鎖,引用技術(shù)表和弱引用表呢欧聘?因?yàn)樵诒姸嗑€程同時(shí)訪問這個(gè)SideTables表的時(shí)候片林,為了保證數(shù)據(jù)安全,需要給其加上自旋鎖怀骤,如果只有一張SideTable的表费封,那么所有數(shù)據(jù)訪問都會出一個(gè)進(jìn)一個(gè),單線程進(jìn)行蒋伦,非常影響效率孝偎,而且會帶來不好的用戶體驗(yàn),針對這種情況凉敲,將一張SideTables分為多張表的SideTable,再各自加鎖保證數(shù)據(jù)的安全寺旺,這樣就增加了并發(fā)量爷抓,提高了數(shù)據(jù)訪問的效率,所以這就是一張SideTables表下涵蓋眾多SideTable表的原因阻塑。
基于此蓝撇,我們進(jìn)行SideTable的表分析,那么當(dāng)一個(gè)對象的引用計(jì)數(shù)增加或減少時(shí)陈莽,需要去查找對應(yīng)的SideTable并進(jìn)行引用計(jì)數(shù)或者弱引用計(jì)數(shù)的操作時(shí)渤昌,系統(tǒng)又是怎樣實(shí)現(xiàn)的呢。
當(dāng)一個(gè)對象訪問SideTables時(shí)走搁,首先會取到對象的地址独柑,將地址進(jìn)行哈希運(yùn)算,與SideTables的個(gè)數(shù)取余私植,最后得到的結(jié)果就是該對象所要訪問的SideTable所在SideTables中的位置忌栅,隨后在取到的SideTable中的RefcountMap表中再次進(jìn)行一次哈希查找,找到該對象在引用計(jì)數(shù)表中所對應(yīng)的位置曲稼,如果該位置存在對應(yīng)的引用計(jì)數(shù)索绪,則對其進(jìn)行操作湖员,如果沒有對應(yīng)的引用計(jì)數(shù),則創(chuàng)建一個(gè)對應(yīng)的size_t對象瑞驱,其實(shí)就是一個(gè)uint類型的無符號整型娘摔。
對于Spinlock_t自旋鎖,其本質(zhì)是一種“忙等”的鎖唤反,所謂“忙等”就是當(dāng)一條線程被加上Spinlock自旋鎖后凳寺,當(dāng)線程執(zhí)行時(shí),會不斷的去獲取這個(gè)鎖的信息拴袭,一旦獲取到這個(gè)鎖读第,便進(jìn)行線程的執(zhí)行。這對于一般的高性能鎖比如信號量不同拥刻,信號量是當(dāng)線程獲取到信號量小于等0時(shí)怜瞒,便自動(dòng)進(jìn)行休眠,當(dāng)信號量發(fā)出時(shí)般哼,對線程進(jìn)行喚醒操作吴汪,這樣就致使了兩種鎖的性質(zhì)不同。Spinlock自旋鎖只適用于一些小型數(shù)據(jù)操作蒸眠,耗時(shí)很少的線程操作漾橙。
對于每張SideTable表中的弱引用表weak_table_t,其也是一張哈希表的結(jié)構(gòu)楞卡,其內(nèi)部包含了每個(gè)對象對應(yīng)的弱引用表weak_entry_t霜运,而weak_entry_t是一個(gè)結(jié)構(gòu)體數(shù)組,其中包含的則是每一個(gè)對象弱引用的對象所對應(yīng)的弱引用指針蒋腮。
以上大概就是內(nèi)存在ios中的管理方式淘捡,以及關(guān)于內(nèi)存管理相關(guān)的一些數(shù)據(jù)類型。
關(guān)于iOS的兩種管理方式:MRC與ARC
MRC是上古時(shí)期iOS開發(fā)程序員用的一種手動(dòng)管理對象引用計(jì)數(shù)的方式池摧,但這也是內(nèi)存管理的立足之本焦除,ARC就是現(xiàn)代程序員常用的對象引用計(jì)數(shù)管理方式,ARC是由編譯器和runtime協(xié)作作彤,共同完成對對象引用計(jì)數(shù)的控制膘魄,而不需要程序員自己手動(dòng)控制。在MRC中可以調(diào)用alloc,retain,release,retainCount,dealloc等方法竭讳,這些方法在ARC中只能調(diào)用alloc方法创葡,調(diào)用其他的會引起編譯報(bào)錯(cuò),不過在ARC模式中可以重寫dealloc方法绢慢。相比起MRC蹈丸,在ARC中新增了weak和strong等屬性關(guān)鍵字。下面詳細(xì)解讀一下MRC ARC中的各個(gè)方法。
alloc:這個(gè)方法實(shí)質(zhì)上是經(jīng)過了一系列封裝調(diào)用之后的calloc逻杖,需要注意的是調(diào)用該方法時(shí)奋岁,對象的引用計(jì)數(shù)并沒有+1.
retain:這個(gè)方法是先在SideTables中通過哈希查找找到對象所在的那張SideTable表,隨后在SideTable中的引用計(jì)數(shù)表中再次通過哈希查找找到對象所對應(yīng)的size_t荸百,再加上一個(gè)系統(tǒng)的(引用計(jì)數(shù)+1宏)闻伶。為什么這里沒有+1而是加上一個(gè)系統(tǒng)的宏呢,因?yàn)樵趕ize_t結(jié)構(gòu)中够话,前兩位不是儲存引用計(jì)數(shù)的蓝翰,第一位存儲的是是否有弱引用指針指向,第二位存儲的是對象是否在被回收中女嘲。所以畜份,在增加其引用計(jì)數(shù)時(shí)需要右移兩位再進(jìn)行增加,所以用到了這個(gè)系統(tǒng)的宏SIDE_TABLE_RC_ONE欣尼。
release:這個(gè)方法跟retain方法原理一樣爆雹,只不過是減一個(gè)系統(tǒng)的宏SIDE_TABLE_RC_ONE
retainCount:這個(gè)方法的實(shí)現(xiàn)同樣是先查找系統(tǒng)的SideTables表,并找到對象對應(yīng)的SideTable表愕鼓,但在之前要先申明一個(gè)size_t為1的對象钙态,隨后在對應(yīng)的引用計(jì)數(shù)表中找到了對象對應(yīng)的引用計(jì)數(shù)后,通過右移找到的count對象菇晃,與之前創(chuàng)建好的1相加册倒,最后返回其結(jié)果便是引用計(jì)數(shù)。所以這就是為什么系統(tǒng)在調(diào)用alloc方法后并沒有給對象的引用計(jì)數(shù)+1磺送,但retainCount方法調(diào)用后對象的引用計(jì)數(shù)就是1的原因驻子。
dealloc:對象在被回收時(shí),就會調(diào)用dealloc方法估灿,其內(nèi)部實(shí)現(xiàn)流程首先要調(diào)用一個(gè)_objc_rootDealloc()方法崇呵,再方法內(nèi)部再調(diào)用一個(gè)rootDealloc()方法,此時(shí)在rootDealloc中會判斷該對象的isa指針甲捏,依次判斷指針內(nèi)的內(nèi)容:nonpointer_isa,weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc鞭执,如果判斷結(jié)果為:該isa指針不是非指針型的isa指針司顿,沒有弱引用的指針指向,沒有相應(yīng)的關(guān)聯(lián)對象兄纺,沒有c++相關(guān)的內(nèi)容大溜,沒有使用ARC模式,沒有關(guān)聯(lián)到散列表中估脆,即判斷的內(nèi)容都為否钦奋,則可以直接調(diào)用c語言中的free()函數(shù)進(jìn)行相應(yīng)的內(nèi)存釋放,否則就會調(diào)用objc_dispose()這個(gè)函數(shù)。
而objc_dispose()函數(shù)的內(nèi)部是經(jīng)由objc_destructInstence()函數(shù)調(diào)用付材,隨后調(diào)用c函數(shù)的free()的朦拖,顧名思義,objc_destructInstence()函數(shù)就是一個(gè)銷毀對象的函數(shù)厌衔,那么objc_destructInstence()函數(shù)內(nèi)部實(shí)現(xiàn)是什么樣的結(jié)構(gòu)呢璧帝?
首先,destructInstence()函數(shù)內(nèi)部會來判斷該對象是否有C++相關(guān)內(nèi)容以及ARC相關(guān)的內(nèi)容富寿,如果有的話就會調(diào)用object_cxxDestruct函數(shù)來銷毀相應(yīng)的內(nèi)容睬隶,隨后會判斷改對象是否有關(guān)聯(lián)對象相關(guān)的內(nèi)容,如果有的話就會調(diào)用_object_remove_associations()這個(gè)方法來清楚相關(guān)的關(guān)聯(lián)對象內(nèi)容页徐,在這兩個(gè)判斷步驟完成之后苏潜,調(diào)用clearDeallocating()方法。
在clearDeallocating()方法中变勇,會調(diào)用一個(gè)sidetable_clearDeallocating()的方法恤左,在方法內(nèi)部會調(diào)用兩個(gè)方法,1.weak_clear_no_lock()這個(gè)方法會將每個(gè)弱引用表中的指向該對象的弱引用指針贰锁,置為nil赃梧。2.table.refcnts.erase()方法,這個(gè)方法會從引用計(jì)數(shù)表中豌熄,擦除該對象的引用計(jì)數(shù)授嘀。最后再調(diào)用c函數(shù)的free()方法,完成一次對象的回收锣险。
所以總結(jié)一下dealloc方法的內(nèi)容大致就是:1.先調(diào)用_objc_rootDealloc()方法——>2.在方法內(nèi)部調(diào)用rootDealloc()方法——>3.依次判斷5個(gè)要素:是否非指著型isa蹄皱,是否含有關(guān)聯(lián)對象相關(guān)內(nèi)容,是否含有弱引用指針的指向芯肤,是否含有c++相關(guān)內(nèi)容以及在ARC模式下使用巷折,是否使用了散列表,如果判斷都為否崖咨,則直接調(diào)用C函數(shù)的free()釋放對象空間锻拘,反之則調(diào)用object_dispose()方法。
在object_dispose()方法中的調(diào)用流程為:1.調(diào)用objc_destructInsetence()方法——>2.調(diào)用C函數(shù)的free()釋放對象空間
在objc_destructInstence()方法內(nèi)部實(shí)現(xiàn)原理為:1.判斷是否含有C++相關(guān)內(nèi)容以及使用了ARC模式——>2.調(diào)用objcet_cxxDestruct()函數(shù)進(jìn)行清除相關(guān)內(nèi)容——>3.判斷是否含有關(guān)聯(lián)對象相關(guān)內(nèi)容——>4.調(diào)用_object_remove_associations()方法清除關(guān)聯(lián)對象相關(guān)內(nèi)容——>5.調(diào)用clearDeallocating()方法击蹲。
在clearDeallocating()方法內(nèi)部署拟,調(diào)用了一個(gè)sidetable_clearDeallocating()方法,旨在清除散列表相關(guān)的數(shù)據(jù)信息歌豺。
在sidetable_clearDeallocating()方法內(nèi)部推穷,進(jìn)行了兩個(gè)步驟:1.調(diào)用weak_clear_no_lock()方法的調(diào)用,旨在將對象對應(yīng)的弱引用表中對應(yīng)的弱引用結(jié)構(gòu)體數(shù)組中的指針全部置為nil——>2.調(diào)用table.refcnts.erase()类咧,將對象對應(yīng)的引用計(jì)數(shù)表中的引用計(jì)數(shù)擦除馒铃。
完成之后回到之前的步驟蟹腾,調(diào)用free()函數(shù),完成對一個(gè)對象的內(nèi)存回收区宇。
其實(shí)通過以上對一些內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)的解釋以及對象回收的一個(gè)完整過程娃殖,相應(yīng)的弱引用創(chuàng)建的的過程以及回收的過程自然也一目了然了。
當(dāng)我們創(chuàng)建一個(gè)弱引用變量weakPointer的時(shí)候在編譯器中可以這么寫
id __weak weakPointer = object;
這行代碼實(shí)際上在系統(tǒng)內(nèi)部實(shí)現(xiàn)的時(shí)候轉(zhuǎn)化為了兩行代碼:
id weakPointer;
objc_initWeak(&weakPointer,object);
首先定義了一個(gè)變量weakPointer萧锉,其次調(diào)用objc_initWeak方法來給weakPointer的這個(gè)弱引用指針來賦值珊随。
內(nèi)部原理與上面相似,當(dāng)弱引用指針指向這個(gè)objcet變量時(shí)柿隙,首先去SideTables散列表中通過哈希查找叶洞,來找到object這個(gè)對象的SideTable表,再通過一次哈希查找禀崖,利用object對象的地址衩辟,在SideTable中的弱引用表中找到其對應(yīng)的弱引用結(jié)構(gòu)體數(shù)組,如果這個(gè)數(shù)組存在則在里面添加一個(gè)之前weakPointer的地址作為弱引用指針指向object波附,如果沒有這個(gè)結(jié)構(gòu)體數(shù)組艺晴,則創(chuàng)建一個(gè)數(shù)組,將這個(gè)指針添加到第0個(gè)元素掸屡,同時(shí)給第2封寞,3,4個(gè)元素設(shè)置為nil仅财。這樣就完成了一個(gè)弱引用指針的定義實(shí)現(xiàn)過程了狈究。
關(guān)于如何清除弱引用指針的,Dealloc方法調(diào)用過程已經(jīng)說的很明白了盏求,過程也與上面一行說的類似抖锥,就是在最后調(diào)用sidetable_clearDeallocating()方法中將對象對應(yīng)的弱引用列表找到,將所有弱引用指針置為nil的時(shí)候就把相應(yīng)的弱引用指針擦除了碎罚,這樣說就一目了然了磅废。