iOS內(nèi)存管理:從MRC到ARC實(shí)踐
對(duì)于iOS程序員來說,內(nèi)存管理是入門的必修課。引用計(jì)數(shù)、自動(dòng)釋放等概念,都是與C語(yǔ)言完全不同的。搞明白這些,代碼才有可能不crash。然而就是這么牛逼的內(nèi)存管理,著實(shí)讓我這個(gè)從 C 轉(zhuǎn)過來的老程序員頭疼了一段時(shí)間。
[C++ 程序員的迷惑和憤怒]iOS 內(nèi)存管理的核心是引用計(jì)數(shù)。與眾多五年甚至更多以上開發(fā)經(jīng)驗(yàn)的程序員一樣,筆者當(dāng)初是從 C/C++轉(zhuǎn)到的 OC,接觸到 MRC。當(dāng)時(shí)遇到最頭疼的問題就是:為什么那么多 release?到底什么地方會(huì) release?同樣初始化一個(gè)字符串的兩個(gè)方法為什么不同?上邊一個(gè)不需要調(diào)用 release,后邊一個(gè)就需要調(diào)用 release?
NSString * str1 = [NSString stringWithFormat:”qqstock“]; NSString * str2 = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];
再加上一個(gè)屬性賦值與成員變量賦值,一個(gè)導(dǎo)致計(jì)數(shù)器加一,一個(gè)就不會(huì)!真他媽奇葩了!
self.name = @“qqstock”; _name = @“qqstock”;
不知道是不是所有從 C/C++ 轉(zhuǎn)過來的程序員都遇到過類似的迷惑和憤怒。
[MRC 的初衷和實(shí)現(xiàn)方式]那么,蘋果為什么要做這個(gè)?
首先,C/C++ 傳統(tǒng)的內(nèi)存管理方式,所有的內(nèi)存都需要業(yè)務(wù)代碼自己處理,程序員自己一定要知道一個(gè)內(nèi)存對(duì)象什么時(shí)候不再使用了,一定要知道這個(gè)內(nèi)存對(duì)象的終點(diǎn)在哪里。當(dāng)代碼越來越復(fù)雜,參與開發(fā)的程序員越來越多,甚至隨著歲月的流逝更換了新的程序員,這個(gè)時(shí)候,很難有人說的清了。于是,要么那個(gè)內(nèi)存對(duì)象一直留在那里,沒人敢釋放,整個(gè)程序占用的空間越來越大;要么,一個(gè)膽大的程序員將它釋放掉,某處發(fā)生了crash。盡管大家總結(jié)出許多類似“誰(shuí)創(chuàng)建誰(shuí)釋放”、“誰(shuí)持有誰(shuí)釋放” 的原則,但都導(dǎo)致存儲(chǔ)空間的浪費(fèi):為了保留僅僅一個(gè)內(nèi)存對(duì)象,卻要將與它關(guān)聯(lián)的一大堆對(duì)象保留住,而其中大部分已經(jīng)不再使用了。要么,自己寫許許多多的代碼,頻繁對(duì)容器進(jìn)行主動(dòng)操作。
于是,蘋果要解決這個(gè)問題。初衷就是:任何一個(gè)內(nèi)存對(duì)象由系統(tǒng)自己處理釋放的問題,無論創(chuàng)建者也好,持有者也好,不需要去考慮別人是否還在使用同一個(gè)內(nèi)存對(duì)象,做好自己該做的就是了,別人的事情別人負(fù)責(zé)。蘋果實(shí)現(xiàn)此目的的手段就是引用計(jì)數(shù)。所有使用到同一內(nèi)存對(duì)象的地方,使用者只要保證自己 retain 一次,release 一次,就 OK 了,即便別人還在使用,你只要調(diào)用 release 將自己的引用次數(shù)清零就好了,不用管別人!
與 C/C++傳統(tǒng)的內(nèi)存管理方式相比,MRC 是不是顯得非常智能?是不是更加方便?而且,這樣做的代價(jià)也非常低廉,每一個(gè)內(nèi)存對(duì)象增加一個(gè)計(jì)數(shù)器就 OK 了,每一次 release,只需要檢查一遍計(jì)數(shù)器是否為零,如果為零就釋放,如果不為零就不執(zhí)行真正的釋放邏輯。
另外,為了解決函數(shù)返回值的問題,需要搞一個(gè) autorelease 的東西,否則就會(huì)打破這個(gè)良好的初衷:“只負(fù)責(zé)自己范圍內(nèi)的事情就 OK了,不要管別人!”
那么,為什么不將所有內(nèi)存對(duì)象都統(tǒng)一成 retain呢?對(duì)于一種編譯器,它能夠用一個(gè)技術(shù)解決所有問題,就堅(jiān)決不會(huì)用兩種并列的技術(shù)導(dǎo)致問題更復(fù)雜。
OC 有一個(gè) delegate 的東西,這個(gè)東西的出現(xiàn)也是有其現(xiàn)實(shí)需求的,在此先跳過。如果所有地方都使用 retain,delegate 的問題一定會(huì)導(dǎo)致循環(huán)引用,除了 delegate,蘋果不敢保證所有用戶代碼的邏輯都是樹形結(jié)構(gòu)的,最簡(jiǎn)單的比如說循環(huán)鏈表、雙向鏈表,除此之外,業(yè)務(wù)層肯定也有某些地方必須做成“循環(huán)引用”,如果都是 retain,那么,最終處于循環(huán)中的內(nèi)存對(duì)象誰(shuí)也不會(huì)被最終釋放掉。為了解決這個(gè)問題,蘋果依然保留了 C/C++的那種弱引用方式。——至少給程序員留個(gè)過渡的空間。
[MRC 的優(yōu)點(diǎn)和無奈]總結(jié)一下:
MRC 的計(jì)數(shù)器機(jī)制改善了內(nèi)存管理的方式,減少了各個(gè)模塊的邏輯耦合,釋放了程序員對(duì)“何時(shí)該釋放”的心理壓力,解決了大部分的問題 為了應(yīng)對(duì)各種復(fù)雜的場(chǎng)景,很無奈的留了一個(gè)口子; 兩種模式的并存,對(duì) C++程序員轉(zhuǎn)移到 OC戰(zhàn)場(chǎng),樹立了一個(gè)無形的心理門檻,使得起步階段問題更加復(fù)雜,比如:retain、assign、release、autorelease 等。難道就沒有更好的方式么?當(dāng)然有更好的方式,而且一定有許多公司的 C++程序員或者 C 程序員寫了類似引用計(jì)數(shù)的程序,甚至比引用計(jì)數(shù)還要高級(jí),只不過大多數(shù)公司沒有實(shí)力推廣一個(gè)編程語(yǔ)言而已。
而且,略微深入思考,一定許多人想到:如果讓系統(tǒng)對(duì)所有內(nèi)存對(duì)象在運(yùn)行時(shí)統(tǒng)一管理,問題就能徹底解決了。是的,的確如此,一定有人設(shè)計(jì)出來了。但是,代價(jià)比較高。
系統(tǒng)在運(yùn)行時(shí)統(tǒng)一管理所有內(nèi)存對(duì)象的釋放,會(huì)導(dǎo)致增加額外的內(nèi)存和 CPU 開銷,在硬件設(shè)備尚且處于低級(jí)階段的時(shí)候,當(dāng)程序員們依然在努力降低內(nèi)存降低 CPU 消耗的時(shí)候,推出這樣的機(jī)制,是不合時(shí)宜的!
引用計(jì)數(shù)器的方式,編譯器并沒有增加太多的邏輯,只是在創(chuàng)建的時(shí)候增加一個(gè)計(jì)數(shù)器,在釋放的時(shí)候編譯器自動(dòng)幫程序員增加一個(gè)邏輯判斷。這個(gè)邏輯并沒有增加太多的內(nèi)存和 CPU 開銷。
再來看 autorelease,這個(gè)邏輯增加的成本可就大了去了,系統(tǒng)要一直持有該類型的內(nèi)存對(duì)象,直到本次 runloop 結(jié)束。所以,無論蘋果,還是有經(jīng)驗(yàn)的程序員,都建議:能不用就盡量不用,能縮短范圍就盡量縮短范圍。
由于留了無奈的口子,野指針依然會(huì)出現(xiàn),該 crash 的時(shí)候依然 crash。許多人說:這是程序員的問題,如果代碼寫的足夠好,一定不會(huì)出現(xiàn)野指針,一定不會(huì)出現(xiàn) crash。是的,如果大家足夠小心,如果大家足夠盡力,這個(gè)世界上不會(huì)有任何沖突。
然而,編程語(yǔ)言和編譯器的發(fā)展,一定向著便利、易用、穩(wěn)健、職能,甚至傻瓜!如果一個(gè)編譯器能夠讓一個(gè)對(duì)計(jì)算機(jī)毫無了解的人一天之內(nèi)搞出自己想要的業(yè)務(wù)應(yīng)用,誰(shuí)又會(huì)拒絕呢?
許多程序員都是技術(shù)控,自己能做的事情盡量不讓別人做,自己能實(shí)現(xiàn)的邏輯盡量不用別人的。比如:C++的各種封裝、引用,我用 C 也能實(shí)現(xiàn),有什么大不了的!系統(tǒng)提供的各種類庫(kù),我自己用底層的代碼也能實(shí)現(xiàn),而且性能更優(yōu),代碼更少!但是,如果你連一個(gè)磚頭都要自己燒制,連一堵墻都要自己去砌,其它更重要的事情誰(shuí)去做?
更何況,人,總有打盹的時(shí)候。
隨著硬件的升級(jí),條件已經(jīng)成熟了,ARC到來了!
ARC 的初衷是為了讓程序員寫代碼的時(shí)候更加便利,最好不用再關(guān)注任何內(nèi)存釋放的問題(也不用關(guān)注用什么方式初始化的問題)。當(dāng)然了,解決野指針的問題也是很重要的!總之,讓編碼更加簡(jiǎn)單,程序更加健壯!
之前對(duì) C++程序員頭疼的問題變得異常簡(jiǎn)單:
NSString * str1 = [NSString stringWithFormat:”qqstock“];NSString * str2 = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];self.name = @“qqstock”; _name = @“qqstock”;
到底何時(shí)釋放?總之,你不用管了,用你的就好! 到底有何區(qū)別?沒啥區(qū)別,只管用就好了!
筆者之前一直很疑惑,因?yàn)樽约阂恢毕敫忝靼椎降子泻螀^(qū)別——技術(shù)控本質(zhì)。現(xiàn)在,了解了ARC的初衷,也就敢于放心大膽的用了——許多刨根究底的程序員從匯編代碼也印證了這個(gè)“猜想”。ARC 的目的就是將程序員從 MRC 的各種”不同點(diǎn)“上解脫出來,對(duì)于尚未接觸過 MRC 的 C 程序員,是非常容易理解的,而對(duì)于已經(jīng)習(xí)慣了 MRC 的程序員,反倒有點(diǎn)”不敢相信“!
如果讓你做,你會(huì)如何實(shí)現(xiàn)?邏輯其實(shí)很簡(jiǎn)單。 首先,強(qiáng)引用依然保留 MRC 的方式,因?yàn)檫@樣實(shí)現(xiàn)的方式代價(jià)很低; 其次,一旦出現(xiàn)弱引用,則將內(nèi)存對(duì)象在系統(tǒng)中建立映射表;一旦內(nèi)存對(duì)象因?yàn)樗袕?qiáng)引用歸零而釋放,則將所有弱引用指針歸零(指向 nil)——應(yīng)該有一個(gè)鏈表。
其實(shí),將弱引用強(qiáng)制指向 nil,也是一種無奈的方式,按理說,這依然是個(gè)隱患,是代碼邏輯的缺陷,只是人家?guī)湍銓㈠e(cuò)誤的代價(jià)降到最低而已。
總之,強(qiáng)引用的邏輯是:如果都不用了,我就釋放掉;弱引用的邏輯是:如果釋放了,我就置 nil!最終,程序員不需要關(guān)注內(nèi)存的持有和釋放問題,更不需要關(guān)注別的模塊是否依然在使用同一個(gè)內(nèi)存。做好自己分內(nèi)的事情,別的事情交給系統(tǒng)和編譯器!
其實(shí),筆者之前對(duì) ARC 的了解也僅僅在 coding 層面,最近打算將老的項(xiàng)目從 MRC 轉(zhuǎn)到 ARC,需要提前讓團(tuán)隊(duì)的所有人了解代碼如何遷移,否則即便依靠一兩個(gè)人的力量將代碼遷移了,開發(fā)人員的意識(shí)和 coding 依然停留在 MRC,那后續(xù)的開發(fā)任務(wù)將會(huì)極其危險(xiǎn)。但凡做大的動(dòng)作就應(yīng)該首先在團(tuán)隊(duì)層面無論是意識(shí)還是能力上做好準(zhǔn)備,否則就等著填坑吧。
于是突發(fā)奇想,想對(duì)蘋果問一個(gè)為什么?即:蘋果為什么要搞一個(gè) ARC?任何一件事情,都不是毫無來由的。一個(gè)極客程序員可能會(huì)突發(fā)奇想搞個(gè)牛逼的技術(shù)來展現(xiàn)自己的才華,但蘋果這么大一個(gè)公司,做這么大的改動(dòng),一定是有緣由的。果不其然,當(dāng)自己費(fèi)盡心思將這個(gè)問題搞清楚之后,如何 coding 的問題也得到了大幅提升!
回頭想想,這條路是很牛逼的,如果所有地方都用強(qiáng)引用,或者所有地方都交予系統(tǒng)管理,勢(shì)必會(huì)導(dǎo)致內(nèi)存的快速膨脹。某些其它語(yǔ)言的例子就非常明顯,無論程序員如何努力,內(nèi)存也很難降低下來。
一個(gè)心得就是:許多問題,如果我們能夠站在設(shè)計(jì)者的立場(chǎng)上考慮,就能夠更加清楚自己該如何 coding,設(shè)計(jì)者的初衷決定了我們 coding 的方式,設(shè)計(jì)者的 coding 決定了我們的思維方式。
以下是一個(gè)簡(jiǎn)單的 demo,從代碼運(yùn)行結(jié)果能夠很明顯的驗(yàn)證 ARC 下 strong、weak、assign、局部變量、類方法初始化以及 autorelease 等使用方法與MRC下的不同。
首先:使用 retain 類型初始化方法給 weak 和 assign 類型變量賦值時(shí),編譯器會(huì)報(bào)警。
其次:weak 變量當(dāng)其指向的變量的所有強(qiáng)引用置零后,自己會(huì)被置 nil,而 assign 卻不會(huì)。
再有:weak 變量被置 nil,不是當(dāng)其指向變量析構(gòu)的時(shí)候,而是在強(qiáng)引用歸零的時(shí)候就已經(jīng)發(fā)生了。
還有,各種類方法初始化的 autorelease 對(duì)象,依然是在 runloop 結(jié)束的時(shí)候析構(gòu)的,而 retain 類型的對(duì)象,卻是在代碼模塊終止的時(shí)候析構(gòu)的。所以,出于內(nèi)存管理的考慮,依然建議少用 autorelease。
最后,strong 和 weak 對(duì)應(yīng)的 set 方法,簡(jiǎn)單了許多哦!
來自:http://dev.qq.com/topic/59194943f473278853516915
相關(guān)文章:
1. Gitlab CI-CD自動(dòng)化部署SpringBoot項(xiàng)目的方法步驟2. 淺談SpringMVC jsp前臺(tái)獲取參數(shù)的方式 EL表達(dá)式3. ajax請(qǐng)求添加自定義header參數(shù)代碼4. JS sort方法基于數(shù)組對(duì)象屬性值排序5. 使用Python和百度語(yǔ)音識(shí)別生成視頻字幕的實(shí)現(xiàn)6. ASP中解決“對(duì)象關(guān)閉時(shí),不允許操作。”的詭異問題……7. JAVA上加密算法的實(shí)現(xiàn)用例8. 基于javascript處理二進(jìn)制圖片流過程詳解9. ASP刪除img標(biāo)簽的style屬性只保留src的正則函數(shù)10. Django-migrate報(bào)錯(cuò)問題解決方案
