iOS中內(nèi)存管理機(jī)制是開發(fā)中一項(xiàng)很重要的知識(shí),了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫助我們提升效率寻馏。下面我就根據(jù)自己的理解训枢,詳細(xì)梳理一下內(nèi)存管理相關(guān)的知識(shí)踩寇。
在說內(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存儲(chǔ)順序是由高地址存向低地址靡馁,而堆區(qū)是由低地址向高地址存儲(chǔ)欲鹏。內(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對(duì)象斥黑,在32位中的系統(tǒng)中占用4個(gè)字節(jié)的空間,但是遷移至64位系統(tǒng)中后眉厨,其占用空間達(dá)到了8字節(jié)锌奴,以此類推,所有在64位系統(tǒng)中占用空間會(huì)翻倍的對(duì)象憾股,在遷移后會(huì)導(dǎo)致系統(tǒng)內(nèi)存劇增鹿蜀,即時(shí)他們根本用不到這么多的空間,所以蘋果對(duì)于一些小型數(shù)據(jù)服球,采用了taggedPointer這種方式管理內(nèi)存茴恰。
其主要的原理就是在對(duì)象的指針中加入特定需要記錄的信息,以及對(duì)象所對(duì)應(yīng)的值斩熊,在64位的系統(tǒng)中往枣,一個(gè)指針?biāo)加玫膬?nèi)存空間為8個(gè)字節(jié),已足以存下一些小型的數(shù)據(jù)量了粉渠,當(dāng)對(duì)象指針的空間中存滿后分冈,再對(duì)指針?biāo)赶虻膬?nèi)存區(qū)域進(jìn)行存儲(chǔ),這就是taggedPointer霸株。距離NSNumber雕沉,最低4位用于標(biāo)記是什么類型的數(shù)據(jù)(long為3,float則為4去件,Int為2坡椒,double為5)饺著,而最高4位的“b”表示是NSNumber類型;其余56位則用來存儲(chǔ)數(shù)值本身內(nèi)容肠牲。
之前runtime文章中有提到過objc_objcet對(duì)象中isa指針分為指針型isa與非指針型isa(NONPOINTER_ISA)幼衰,運(yùn)用的便是類似這種技術(shù)。下面詳細(xì)解讀一下NONPOINTER_ISA:
在一個(gè)64位的指針內(nèi)存中缀雳,第0位存儲(chǔ)的是indexed標(biāo)識(shí)符渡嚣,它代表一個(gè)指針是否為NONPOINTER型,0代表不是肥印,1代表是识椰。第1位has_assoc,顧名思義深碱,1代表其指向的實(shí)例變量含有關(guān)聯(lián)對(duì)象腹鹉,0則為否。第2位為has_cxx_dtor敷硅,表明該對(duì)象是否包含C++相關(guān)的內(nèi)容或者該對(duì)象是否使用ARC來管理內(nèi)存功咒,如果含有C++相關(guān)內(nèi)容或者使用了ARC來管理對(duì)象,這一塊都表示為YES绞蹦,第3-35位shiftcls存儲(chǔ)的就是這個(gè)指針的地址力奋。第42位為weakly_referenced,表明該指針對(duì)象是否有弱引用的指針指向幽七。第43位為deallocing景殷,表明該對(duì)象是否正在被回收。第44位為has_sidetable_rc澡屡,顧名思義猿挚,該指針是否引用了sidetable散列表。第45-63位extra_rc裝的就是這個(gè)實(shí)例變量的引用計(jì)數(shù)驶鹉,當(dāng)對(duì)象被引用時(shí)绩蜻,其引用計(jì)數(shù)+1,但少量的引用計(jì)數(shù)是不會(huì)直接存放在sideTables表中的梁厉,對(duì)象的引用計(jì)數(shù)會(huì)先存在NONPOINTER_ISA的指針中的45-63位辜羊,當(dāng)其被存滿后,才會(huì)相應(yīng)存入sideTables散列表中词顾。
所以綜上所述,iOS中內(nèi)存管理的方式主要有三大碱妆,1.taggedPointer肉盹,2.NONPOINTER_ISA,3.散列表疹尾。
下面再進(jìn)行散列表的分析:
散列表在系統(tǒng)中的提現(xiàn)是一個(gè)sideTables的哈希映射表上忍,其中所有對(duì)象的引用計(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ù)訪問都會(huì)出一個(gè)進(jìn)一個(gè),單線程進(jìn)行幌墓,非常影響效率但壮,而且會(huì)帶來不好的用戶體驗(yàn),針對(duì)這種情況常侣,將一張SideTables分為多張表的SideTable茵肃,再各自加鎖保證數(shù)據(jù)的安全,這樣就增加了并發(fā)量袭祟,提高了數(shù)據(jù)訪問的效率验残,所以這就是一張SideTables表下涵蓋眾多SideTable表的原因。
基于此巾乳,我們進(jìn)行SideTable的表分析您没,那么當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)增加或減少時(shí),需要去查找對(duì)應(yīng)的SideTable并進(jìn)行引用計(jì)數(shù)或者弱引用計(jì)數(shù)的操作時(shí)胆绊,系統(tǒng)又是怎樣實(shí)現(xiàn)的呢氨鹏。
當(dāng)一個(gè)對(duì)象訪問SideTables時(shí),首先會(huì)取到對(duì)象的地址压状,將地址進(jìn)行哈希運(yùn)算仆抵,與SideTables的個(gè)數(shù)取余,最后得到的結(jié)果就是該對(duì)象所要訪問的SideTable所在SideTables中的位置种冬,隨后在取到的SideTable中的RefcountMap表中再次進(jìn)行一次哈希查找镣丑,找到該對(duì)象在引用計(jì)數(shù)表中所對(duì)應(yīng)的位置,如果該位置存在對(duì)應(yīng)的引用計(jì)數(shù)娱两,則對(duì)其進(jìn)行操作莺匠,如果沒有對(duì)應(yīng)的引用計(jì)數(shù),則創(chuàng)建一個(gè)對(duì)應(yīng)的size_t對(duì)象十兢,其實(shí)就是一個(gè)uint類型的無符號(hào)整型趣竣。
對(duì)于Spinlock_t自旋鎖摇庙,其本質(zhì)是一種“忙等”的鎖,所謂“忙等”就是當(dāng)一條線程被加上Spinlock自旋鎖后遥缕,當(dāng)線程執(zhí)行時(shí)卫袒,會(huì)不斷的去獲取這個(gè)鎖的信息,一旦獲取到這個(gè)鎖单匣,便進(jìn)行線程的執(zhí)行夕凝。這對(duì)于一般的高性能鎖比如信號(hào)量不同,信號(hào)量是當(dāng)線程獲取到信號(hào)量小于等0時(shí)封孙,便自動(dòng)進(jìn)行休眠迹冤,當(dāng)信號(hào)量發(fā)出時(shí),對(duì)線程進(jìn)行喚醒操作虎忌,這樣就致使了兩種鎖的性質(zhì)不同泡徙。Spinlock自旋鎖只適用于一些小型數(shù)據(jù)操作,耗時(shí)很少的線程操作膜蠢。
對(duì)于每張SideTable表中的弱引用表weak_table_t堪藐,其也是一張哈希表的結(jié)構(gòu),其內(nèi)部包含了每個(gè)對(duì)象對(duì)應(yīng)的弱引用表weak_entry_t挑围,而weak_entry_t是一個(gè)結(jié)構(gòu)體數(shù)組礁竞,其中包含的則是每一個(gè)對(duì)象弱引用的對(duì)象所對(duì)應(yīng)的弱引用指針。
以上大概就是內(nèi)存在ios中的管理方式杉辙,以及關(guān)于內(nèi)存管理相關(guān)的一些數(shù)據(jù)類型模捂。
關(guān)于iOS的兩種管理方式:MRC與ARC
MRC是上古時(shí)期iOS開發(fā)程序員用的一種手動(dòng)管理對(duì)象引用計(jì)數(shù)的方式,但這也是內(nèi)存管理的立足之本蜘矢,ARC就是現(xiàn)代程序員常用的對(duì)象引用計(jì)數(shù)管理方式狂男,ARC是由編譯器和runtime協(xié)作,共同完成對(duì)對(duì)象引用計(jì)數(shù)的控制品腹,而不需要程序員自己手動(dòng)控制岖食。在MRC中可以調(diào)用alloc,retain,release,retainCount,dealloc等方法,這些方法在ARC中只能調(diào)用alloc方法舞吭,調(diào)用其他的會(huì)引起編譯報(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í)澎剥,對(duì)象的引用計(jì)數(shù)并沒有+1.
retain:這個(gè)方法是先在SideTables中通過哈希查找找到對(duì)象所在的那張SideTable表,隨后在SideTable中的引用計(jì)數(shù)表中再次通過哈希查找找到對(duì)象所對(duì)應(yīng)的size_t赶舆,再加上一個(gè)系統(tǒng)的(引用計(jì)數(shù)+1宏)哑姚。為什么這里沒有+1而是加上一個(gè)系統(tǒng)的宏呢,因?yàn)樵趕ize_t結(jié)構(gòu)中芜茵,前兩位不是儲(chǔ)存引用計(jì)數(shù)的叙量,第一位存儲(chǔ)的是是否有弱引用指針指向,第二位存儲(chǔ)的是對(duì)象是否在被回收中九串。所以绞佩,在增加其引用計(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表,并找到對(duì)象對(duì)應(yīng)的SideTable表烤低,但在之前要先申明一個(gè)size_t為1的對(duì)象肘交,隨后在對(duì)應(yīng)的引用計(jì)數(shù)表中找到了對(duì)象對(duì)應(yīng)的引用計(jì)數(shù)后,通過右移找到的count對(duì)象扑馁,與之前創(chuàng)建好的1相加涯呻,最后返回其結(jié)果便是引用計(jì)數(shù)。所以這就是為什么系統(tǒng)在調(diào)用alloc方法后并沒有給對(duì)象的引用計(jì)數(shù)+1腻要,但retainCount方法調(diào)用后對(duì)象的引用計(jì)數(shù)就是1的原因复罐。
dealloc:對(duì)象在被回收時(shí),就會(huì)調(diào)用dealloc方法雄家,其內(nèi)部實(shí)現(xiàn)流程首先要調(diào)用一個(gè)_objc_rootDealloc()方法效诅,再方法內(nèi)部再調(diào)用一個(gè)rootDealloc()方法,此時(shí)在rootDealloc中會(huì)判斷該對(duì)象的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)對(duì)象,沒有c++相關(guān)的內(nèi)容勾效,沒有使用ARC模式嘹悼,沒有關(guān)聯(lián)到散列表中,即判斷的內(nèi)容都為否层宫,則可以直接調(diào)用c語言中的free()函數(shù)進(jìn)行相應(yīng)的內(nèi)存釋放杨伙,否則就會(huì)調(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è)銷毀對(duì)象的函數(shù)毁菱,那么objc_destructInstence()函數(shù)內(nèi)部實(shí)現(xiàn)是什么樣的結(jié)構(gòu)呢米死?
首先锌历,destructInstence()函數(shù)內(nèi)部會(huì)來判斷該對(duì)象是否有C++相關(guān)內(nèi)容以及ARC相關(guān)的內(nèi)容,如果有的話就會(huì)調(diào)用object_cxxDestruct函數(shù)來銷毀相應(yīng)的內(nèi)容峦筒,隨后會(huì)判斷改對(duì)象是否有關(guān)聯(lián)對(duì)象相關(guān)的內(nèi)容究西,如果有的話就會(huì)調(diào)用_object_remove_associations()這個(gè)方法來清楚相關(guān)的關(guān)聯(lián)對(duì)象內(nèi)容,在這兩個(gè)判斷步驟完成之后物喷,調(diào)用clearDeallocating()方法卤材。
在clearDeallocating()方法中,會(huì)調(diào)用一個(gè)sidetable_clearDeallocating()的方法峦失,在方法內(nèi)部會(huì)調(diào)用兩個(gè)方法扇丛,1.weak_clear_no_lock()這個(gè)方法會(huì)將每個(gè)弱引用表中的指向該對(duì)象的弱引用指針,置為nil尉辑。2.table.refcnts.erase()方法帆精,這個(gè)方法會(huì)從引用計(jì)數(shù)表中,擦除該對(duì)象的引用計(jì)數(shù)材蹬。最后再調(diào)用c函數(shù)的free()方法实幕,完成一次對(duì)象的回收。
所以總結(jié)一下dealloc方法的內(nèi)容大致就是:1.先調(diào)用_objc_rootDealloc()方法——>2.在方法內(nèi)部調(diào)用rootDealloc()方法——>3.依次判斷5個(gè)要素:是否非指著型isa堤器,是否含有關(guān)聯(lián)對(duì)象相關(guān)內(nèi)容昆庇,是否含有弱引用指針的指向,是否含有c++相關(guān)內(nèi)容以及在ARC模式下使用闸溃,是否使用了散列表整吆,如果判斷都為否,則直接調(diào)用C函數(shù)的free()釋放對(duì)象空間辉川,反之則調(diào)用object_dispose()方法表蝙。
在object_dispose()方法中的調(diào)用流程為:1.調(diào)用objc_destructInsetence()方法——>2.調(diào)用C函數(shù)的free()釋放對(duì)象空間
在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)對(duì)象相關(guān)內(nèi)容——>4.調(diào)用_object_remove_associations()方法清除關(guān)聯(lián)對(duì)象相關(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)用汇跨,旨在將對(duì)象對(duì)應(yīng)的弱引用表中對(duì)應(yīng)的弱引用結(jié)構(gòu)體數(shù)組中的指針全部置為nil——>2.調(diào)用table.refcnts.erase(),將對(duì)象對(duì)應(yīng)的引用計(jì)數(shù)表中的引用計(jì)數(shù)擦除妆距。
完成之后回到之前的步驟穷遂,調(diào)用free()函數(shù),完成對(duì)一個(gè)對(duì)象的內(nèi)存回收娱据。
其實(shí)通過以上對(duì)一些內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)的解釋以及對(duì)象回收的一個(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è)對(duì)象的SideTable表蓬推,再通過一次哈希查找妆棒,利用object對(duì)象的地址澡腾,在SideTable中的弱引用表中找到其對(duì)應(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()方法中將對(duì)象對(duì)應(yīng)的弱引用列表找到,將所有弱引用指針置為nil的時(shí)候就把相應(yīng)的弱引用指針擦除了间学,這樣說就一目了然了殷费。
希望通過這篇文章也可以幫助到同樣在學(xué)習(xí)iOS內(nèi)存管理的你!
本文章由作者原創(chuàng)低葫,未經(jīng)允許禁止轉(zhuǎn)載
---------------------本文來自 Horson19 的CSDN 博客 详羡,全文地址請(qǐng)點(diǎn)擊:https://blog.csdn.net/Horson19/article/details/82593484?utm_source=copy