一、記憶體管理
1、為什麼要進行記憶體管理
由于移動裝置的記憶體極其有限,是以每個APP所占的記憶體也是有限制的,當APP所占用的記憶體較多時,系統就會發送記憶體警告,每個APP可用的記憶體是被限制的,如果一個APP使用的記憶體超過20M,則系統會向該APP發送Momory Warning消息,收到此消息後,需要回收一些不需要再繼續使用的記憶體空間,比如回收一些不再使用的對象和變量等,否則程式會崩潰。
2、OC記憶體管理的範圍
管理範圍:
管理繼承NSObject的對象,對其他的基本資料類型無效。
分析原因:
本質原因是因為對象 和其他資料類型在系統中的存儲空間不一樣,其它局部變量主要存放于棧中,而對象存儲于堆中,當代碼結束時這個代碼塊中涉及的所有局部變量會被回收,指向對象的指針也被回收,此時對象已經沒有指針指向,但依然存在于記憶體中,造成記憶體洩露。
小結:
1>對象存儲于 堆中
2>其他局部變量主要存放于 棧中
存儲于棧中的局部變量的特性:當代碼塊結束時這個代碼塊中的所有局部變量會被 回收(指向對象的指針也被回收)
堆的存儲特性:堆的管理是要 程式員來管理,系統自己不會管理
各個記憶體區如下:
2、記憶體管理的原理
1)對象的所有權及引用計數
1>對象所有權概念
如何對象 都可能擁有一個或多個所有者,如果一個對象至少還擁有一個所有者,它(對象)就會繼續存在
2>Cocoa所有權政策
任何自己建立的 對象 都歸自己所有,可以使用名字以“alloc”或“new”開頭或名字中包含“copy”的方法建立對象,可以使用retain來獲得一個對象的所有權
2)對象的引用計數器
每個OC對象都有自己的 引用計數器,是一個整數 表示對象被引用的次數,即現在有多少東西在使用這個 對象,對象剛被建立時,預設計數器值為1,當計數器的值變為0時,則對象銷毀。
在每個OC對象内部,都專門有 8個位元組 的存儲空間來存儲引用計數器
3)引用計數器的作用
引用計數器是判斷 對象 是否要回收的依據,就是當引用計數器從1變為0時,就回收(或引用計數器為0)。
注意:存在一種例外:對象為nil時,引用計數器為0,但不收回空間
4)對引用計數器的操作
給對象發送消息,進行相應的計數器操作
retain消息:使計數器+1,該方法傳回對象本身
release消息:使計數器-1,(并不代表釋放對象)
retainCount消息:獲得對象目前的引用計數器值(占位符為:%ld 或 %lu)
5)對象的銷毀
當一個對象的引用計數器為0時,那麼它将被銷毀,其占用的記憶體被系統收回。
當對象被銷毀時,系統會自動向對象發送一條dealloc消息,一般會重寫dealloc方法,在這裡釋放相關的資源,dealloc就像是對象的“臨終遺言”
一旦重寫了dealloc方法局必須調用[super dealloc],并且放在代碼塊的最後調用(不能直接調用dealloc方法)。
一旦對象被回收了,那麼他所占據的存儲空間就不再可用,堅持使用會導緻程式崩潰(野指針錯誤)
注意:
1>如果對象的計數器不為0,那麼在整個程式運作過程,它占用的記憶體就不可能被回收(除非整個程式已經推出)
2>任何一個對象,剛生下來的時候,引用計數器都為1。(對象一旦建立号,預設引用計數器就是1)當使用alloc、new或者copy建立一個對象時,對象的引用計數器預設就是1
3、OC記憶體管理分類
Objective-C提供了三種記憶體管理方式:
MannulReference Counting(MRC 手動管理,在開發 iOS4.1 之前的版本的項目時我們要自己負責使用引用計數來管理記憶體,比如要手動 retain、release、autorelease等,而在其後的版本可以使用ARC,讓系統自己管理記憶體)
automatic reference counting(ARC 自動引用計數,iOS4.1之後推出的)
garbage collection (垃圾回收)。iOS不支援垃圾回收
ARC作為蘋果判斷提供的技術,蘋果推薦開發者使用ARC技術來管理記憶體:
開發中如何使用:需要了解MRC,但實際使用時盡量用ARC
二、手動記憶體管理
記憶體管理的關鍵如何判斷對象被回收?
重寫dealloc方法,代碼規範
(1)一定要[super dealloc],而且要放到最後
意義是:先釋放子類占用的空間,再釋放父類占用的空間
(2)對self(目前)所擁有的其他對象做一次release操作
例如:
-(void) dealloc
{
[_car release];
[super dealloc];
}
注意:
永遠不要直接通過對象調用dealloc方法(實際上調用并不會出錯)
一旦對象被回收了,它占用的記憶體就不再可用,堅持使用會導緻程式崩潰(野指針錯誤)為了防止調用出錯,可以将“野指針”指向nil(0)。
下面我們通過執行個體程式的示範來體驗 retain、release、dealloc
Person.h
Person.m
main.m
列印輸出:
總結:
1>對象剛建立時,擁有者有一個就是自己,retainCount=1;
2>對象每調用一次retain,擁有者+1retainCount-1
3>對象每調用一次release,擁有者-1retainCount-1
4>dealloc方法要重寫
5>當retainCount=0時,會自動調用 dealloc方法
2、記憶體管理的原則
1)原則:
隻要還有人在使用某個對象,那麼這個對象就不會被回收
隻要你想使用這個對象,那麼就應該讓這個對象的引用計數器+1
當你不想使用這個對象時,應該讓對象的引用計數器-1
2)誰建立對象,誰release(建立對象和release是成對出現)
(1)如果你通過alloc、new、copy來建立了一個對象,那麼你就必須調用 release或者autorelease方法
(2)不是你建立的就不用你去負責釋放
3)誰retain,誰release(retain和release是成對出現)
隻要 你調用了retain,無論這個對象是如何生成的,你都要調用release
4)總結
有始有終,有加就應該有減。曾經讓某個對象電腦加1,就應該讓其在最後-1
三、手動記憶體管理之——單個對象記憶體管理
1、單個對象的野指針問題
思考一個問題:
對象在堆區的空間已經釋放了,還能 再使用p1?
1)野指針錯誤:通路了一塊壞的記憶體(已經被回收的,不可用的記憶體)
2)僵屍對象:所占記憶體已經被回收的對象,僵屍對象不能再被使用。(預設情況下Xcode為了提高編碼效率,不會時時檢查僵屍對象,打開僵屍對象檢測)
注意:
1>空指針:沒有指向任何東西的指針,給空指針發送消息不會報錯
關于nil和Nil及NULL的差別
nil:A null pointer to an Objective-CObject.
nil 是一個對象值
Nil:A null pointer to an Objective-Cclass.
NULL:A null pointer to anything else.
NULL 是一個通用指針(泛型指針)
2>不能使用[釋放對象 retain]讓僵屍對象起死複生
3>野指針操作
下面通過執行個體程式來示範
Dog.h
Dog.m
main.m
列印輸出:
小結:
通過上面的列印可以得到以下總結:
1>遵守在手動記憶體管理中的原則:
> 誰建立,誰release
> 誰retain,誰release
下面我們再通過實驗來體驗一下是否可以使用 僵屍對象
Dog.h和Dog.m和上面一樣:
main.m
列印輸出:
小結:
1》當程式運作到最後的一個[dog release]時,存儲在堆區的對象dog([Dog new])的存儲空間已經釋放,但是存儲在棧區的指針dog、jd 并沒有釋放,是以用dog、jd來方法run方法、retain方法時會報錯,以為指針dog、jd指向的對象空間已經不在,這些操作是違法的是在使用僵屍對象
2》當在[dog release]以後,把指針dog、jd重新指派來指向nil,在調用run方法和retain時并不會報錯,這也是nil的一種用法,但是不推薦這樣使用。
注意:3》如果dog、jd重新指派指向nil是放在[dog release]以前,那就會造成記憶體的洩露!
2、避免使用僵屍對象的方法
因為如果程式較為複雜時,我們也不清楚執行哪一個release時,retainCount變為0,是以就會造成使用僵屍對象
下面我們通過執行個體程式示範看怎樣使用nil來避免使用僵屍對象
Person.h
Person.m
main.m
列印輸出:
小結:
從程式可以看出在[person release]後直接跟上一句person=nil後再繼續調用run方法、retain、時不會報錯。
3、對象的記憶體洩露
原因有:
1)沒有按照記憶體規則來做,而造成retain和release 個數不配配,導緻記憶體洩露
2)對象使用的過程中被指派了nil,導緻記憶體洩露
3)在函數或者方法中不當的使用retain或者release造成的問題
下面通過執行個體程式來體驗上面的記憶體洩露情況:
1)沒有按照記憶體規則來做,而造成retain和release 個數不配配,導緻記憶體洩露
Person.h
Person.m
main.m
列印輸出:
小結:
因為retainCount不等于0,是以不會列印dealloc中的内容
2)對象使用的過程中被指派了nil,導緻記憶體洩露
Person.h
Person.m
main.m
列印輸出:
小結:
本來就是使用nil不當,造成記憶體洩露
3)在函數或者方法中不當的使用retain或者release造成的問題
Person.h
Person.m
main.m
列印輸出:
小結:
從main.m來看,程式是符合程式記憶體管理原則的,誰建立,誰release;最後一句
NSLog(@"\n2——%lu",[personretainCount]);本應該會因為調用僵屍對象而報錯,但是也沒有報錯。
四、手動記憶體管理之——多個對象記憶體管理
1、本節我們讨論的問題是:WR開着他的BMW汽車去兜風
分析這句話:有兩個類:人:WR、汽車:BMW
執行個體程式示範:
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出:
小結:
1>上面的程式就是我們在不考慮記憶體洩露問題時,我們一般會這樣寫程式,因為在前面我們就是這樣寫的
2>我們從列印輸出可以看出Person、Car的引用計數器是1,是以說明這樣寫程式造成了“記憶體洩露”
我們已經知道不能讓程式的記憶體洩露,是以下面我們将讨論如何管理“多個對象的記憶體管理”
執行個體程式示範如下:
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出:
小結:
從列印輸出可以看出:程式沒有造成記憶體洩露
但是該程式并不完善,是有缺陷的,比如改一下程式就會造成“記憶體洩露”
Car、Person類不變
main.m改變如下:
列印輸出:
小結:
上面的程式是脆弱的,當我們在[car release]之後增加調用driver方法後,這裡程式就造成了記憶體洩露,因為WR沒有釋放,是以我們在WR釋放前調用該對象方法是合理的,隻能說該程式太“脆弱”
分析上面的問題:
上面程式的脆弱就是因為在調用driver方法的時候,Car已經銷毀了,是以:程式最好是在Person沒有銷毀之前Car一直存在,也就是Car的引用計數器CarRetainCount=1
下面通過執行個體程式來示範
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出:
小結:
1>在我們的main.m中還是遵守記憶體管理的原則:
誰建立,誰release
2>這次隻要Person沒有銷毀前可以任意的調用 driver方法,并且沒有造成記憶體洩露
五、set方法記憶體管理
1、set方法存在的問題
1)set方法寫法(一)
原對象無法釋放造成的記憶體洩露
2)set方法寫法(二)
原對象讷讷公狗釋放,但是引起新的問題,set自己的時候,造成野指針
3)set方法寫法(三)
判斷新傳入的對象是否是原來的對象,如果不是原來的對象則 釋放,然後再retain
下面通過執行個體程式來示範
1)set方法寫法(一)
原對象無法釋放造成的記憶體洩露
對于第一個set方法寫法的解決方法在上面程式中已經解決了
小結:
上面的程式也隻适合給人傳送一次車,但給人多次傳送時,必定造成記憶體洩漏
2)set方法寫法(二)
原對象能夠釋放,但是引起新的問題,set自己的時候,造成野指針
下面通過執行個體程式示範
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出:
小結:
1>從這個列印輸出來看,發現BMW的引用計數器等于1,直到最後也沒有變為0,是以造成了記憶體洩露
2>在main.m中依然遵守記憶體管理原則,誰建立,誰release
下面我們通過修改程式來改進
分析:當我們第二次調用 setCar方法時,讓前一個傳來的car銷毀,然後讓新傳來的car retain就可以了
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出
其實這個程式還是有些“脆弱”,當程式第二次傳入的car還是第一傳入的car時,就會報錯
下面通過執行個體示範來體驗一下:
Car、Person類中的程式和上面的一樣
main.m
列印輸出
小結:
1>從這個列印輸出來看,程式報錯了,而此程式和上一個程式例子的不同僅僅是因為本程式第二次調用的setCar方法是傳入的car和第一次傳入的car是一樣的。
2>分析報錯的原因
總結:因為我們二次調用setCar方法所傳入的car是相同的BMW
(1)當第二次調用setCar方法在[BMW release]前面,那麼不會有任何問題
原因:當我們建立BMW時BMWCount=1,
當第一次調用setCar方法時
-(void)setCar:(Car *)car
{
[_carrelease]; 此時的_car=nil,是以[_car release]
等于[nil release]
_car=[carretain];此時_car=[BMW retain]
BMWCount=2
}
當第二次調用setCar方法時
-(void)setCar:(Car *)car
{
[_car release]; 此時的_car=BMW,是以[_car release]
等于[BMW release],BMWCount=1
_car=[car retain];此時_car=[BMW retain]
BMWCount=2
}
當執行[BMW release]後BMWCount=1
當執行[WR release]後BMWCount=0
是以不會造成記憶體洩露
(2)當第二次調用setCar方法在[BMW release]後面,就會發生報錯
原因:當我們建立BMW時BMWCount=1,
當第一次調用setCar方法時
-(void)setCar:(Car *)car
{
[_car release]; 此時的_car=nil,是以[_car release]
等于[nil release]
_car=[car retain];此時_car=[BMW retain]
BMWCount=2
}
當執行[BMW release]後BMWCount=1
當第二次調用setCar方法時
-(void)setCar:(Car *)car
{
[_car release]; 此時的_car=BMW,是以[_car release]
等于[BMW release],BMWCount=0
_car=[car retain];此時_car=[BMW retain]就是使用
僵屍對象,是以會 報錯
}
下面我們通過分析來改程序式:
報錯原因就是因為我們二次傳入的對象都是同一個對象而造成的,假如程式檢查到是同一個對象時就不再去執行
[_carrelease];
_car=[carretain];
就不會造成使用僵屍對象的錯誤
下面我們通過執行個體程式來示範:
Car.h
Car.m
Person.h
Person.m
main.m
列印輸出:
小結:
從列印輸出來看Person、Car類都銷毀了,沒有造成記憶體洩露,
說明這樣的set寫法是沒有問題的
3、set方法的記憶體管理
1)基本資料類型格式:直接指派
int float doublelongstructenum
-(void)setAge:(int)age
{
_age=age;
}
2)OC對象類型格式
-(void)setCar:(Car *)car
{
//1.先判斷是不是新傳進來的對象
//判斷新傳來的car是否和舊值(原來的)_car不同
if(_car!=car)
{
//2.對舊對象做一次release
//讓舊值(原來的)_car release釋放掉
[_carrelease];//如沒有舊對象,則沒有影響
//3、對新對象做一次retain
//再讓新傳來來的car retain過後指派給舊值_car
_car=[carretain];
}
}
3)set方法的記憶體管理 原則
1、如果在一個類中,有其他類的對象(關聯關系)
2、set方法書寫的時候,要遵守:
判斷是否是同一個對象,release 舊值 , retain 新值