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