記憶體管理的一些概念
-
為什麼要使用記憶體管理?
- 嚴格的記憶體管理,能夠是我們的應用程在效能上有很大的提高
- 如果忽略記憶體管理,可能導致應用佔用記憶體過高,導致程式崩潰
-
OC的記憶體管理主要有三種方式:
- ARC(自動記憶體計數)
- 手動記憶體計數
- 記憶體池
-
OC中記憶體管理的基本思想:
保證任何時候指向物件的指標個數和物件的引用計數相同,多一個指標指向這個物件這個物件的引用計數就加1,少一個指標指向這個物件這個物件的引用計數就減1。沒有指標指向這個物件物件就被釋放了。- 每個物件都有一個引用計數器,每個新物件的計數器是1,當物件的計數器減為0時,就會被銷燬
- 通過retain可以讓物件的計數器+1、release可以讓物件的計數器-1
- 還可以通過autorelease pool管理記憶體
- 如果用ARC,編譯器會自動生成管理記憶體的程式碼
-
蘋果官方基礎記憶體管理規則:
- 你擁有你建立的任何物件
- 你可以使用retain獲取一個物件的擁有權
- 當你不再需要它,你必須放棄你擁有的物件的擁有權
- 你一定不能釋放不是你擁有的物件的擁有權
自動記憶體管理
-
談談你對
ARC
的認識和理解?ARC
是iOS 5推出的新功能。編譯器在程式碼裡適當的地方自動插入retain
/release
完成記憶體管理(引用計數)。 -
ARC機制中,系統判斷物件是否被銷燬的依據是什麼?
指向物件的強指標是否被銷燬
引用計數器
- 給物件傳送一條retain訊息,可以使引用計數器+1(retain方法返回物件本身)
- 給物件傳送一條release訊息,可以使引用計數器-1(注意release並不代表銷燬/回收物件,僅僅是計數器-1)
- 給物件傳送retainCount訊息,可以獲得當前的引用計數值
自動釋放池
-
自動釋放池底層怎麼實現?
(以棧的方式實現的)(系統自動建立,系統自動釋放)棧裡面的(先進後出)
記憶體裡面有棧,棧裡面有自動釋放池。
自動釋放池以棧的形式實現:當你建立一個新的自動釋放池時,它將被新增到棧頂。當一個物件收到傳送autorelease訊息時,它被新增到當前執行緒的處於棧頂的自動釋放池中,當自動釋放池被回收時,它們從棧中被刪除,並且會給池子裡面所有的物件都會做一次release操作。 -
什麼是自動釋放池?
答:自動釋放池是用來儲存多個物件型別的指標變數 -
自動釋放池對池內物件的作用? 被存入到自動釋放池內的物件,當自動釋放池被銷燬時,會對池內的物件全部做一次release操作
-
物件如何放入到自動釋放池中? 當你確定要將物件放入到池中的時候,只需要呼叫物件的
autorelease
物件方法就可以把物件放入到自動釋放池中 -
多次呼叫物件的autorelease方法會導致什麼問題?
答:多次將地址存到自動釋放池中,導致野指標異常 -
自動釋放池作用
將物件與自動釋放池建立關係,池子內呼叫autorelease
方法,在自動釋放池銷燬時銷燬物件,延遲release
銷燬時間 -
自動釋放池,什麼時候建立?
- 程式剛啟動的時候,也會建立一個自動釋放池
- 產生事件以後,執行迴圈開始處理事件,就會建立自動釋放池
-
什麼時候銷燬的?
- 程式執行結束之前銷燬
- 事件處理結束以後,會銷燬自動釋放池
- 還有在池子滿的時候,也會銷燬
-
自動釋放池使用注意:
不要把大量迴圈操作放在釋放池下,因為這會導致大量迴圈內的物件沒有被回收,這種情況下應該手動寫release
程式碼。儘量避免對大記憶體物件使用autorelease
,否則會延遲大記憶體的回收。 -
autorelease的物件是在什麼時候被release的?
答:autorelease實際上只是把對release的呼叫延遲了,對於每一個Autorelease,系統只是把該Object放入了當前的 Autoreleasepool中,當該pool被釋放時,該pool中的所有Object會被呼叫Release。對於每一個Runloop,系統會隱式建立一個Autoreleasepool,這樣所有的releasepool會構成一個象CallStack一樣的一個棧式結構,在每一個 Runloop結束時,當前棧頂的Autoreleasepool會被銷燬,這樣這個pool裡的每個Object(就是autorelease的物件)會被release。那什麼是一個Runloop呢?一個UI事件,Timer call,delegate call, 都會是一個新的Runloop。 -
If we don’t create any autorelease pool in our application then is there any autorelease pool already provided to us?
系統會預設會不定時地建立和銷燬自動釋放池 -
When you will create an autorelease pool in your application?
當不需要精確地控制物件的釋放時間時,可以手動建立自動釋放池
@property記憶體管理策略的選擇
讀寫屬性:readwrite
、readonly
setter語意:assign
、retain
/ copy
原子性(多執行緒管理):atomic
、 nonatomic
強弱引用:strong
、 weak
-
讀寫屬性:
readwrite
:同時生成set
和get
方法(預設)
readonly
:只會生成get
方法 -
控制set方法的記憶體管理:
retain
:release
舊值,retain
新值。希望獲得源物件的所有權時,對其他NSObject
和其子類(用於OC
物件)
copy
:release
舊值,copy
新值。希望獲得源物件的副本而不改變源物件內容時(一般用於NSString
,block
)
assign
:直接賦值,不做任何記憶體管理(預設屬性),控制需不需生成set
方法。對基礎資料型別 (NSInteger
,CGFloat
)和C資料型別(int
,float
,double
,char
, 等等) -
原子性(多執行緒管理):
- atomic
預設屬性,訪問方法都為原子型事務訪問。鎖被加到所屬物件例項級,效能低。原子性就是說一個操作不可以中途被 cpu 暫停然後排程, 即不能被中斷, 要不就執行完, 要不就不執行. 如果一個操作是原子性的,那麼在多執行緒環境下, 就不會出現變數被修改等奇怪的問題。原子操作就是不可再分的操作,在多執行緒程式中原子操作是一個非常重要的概念,它常常用來實現一些同步機制,同時也是一些常見的多執行緒 Bug 的源頭。當然,原子性的變數在執行效率上要低些。 - nonatomic
非原子性訪問。不加同步,儘量避免多執行緒搶奪同一塊資源。是直接從記憶體中取數值,因為它是從記憶體中取得資料,它並沒有一個加鎖的保護來用於cpu中的暫存器計算Value,它只是單純的從記憶體地址中,當前的記憶體儲存的資料結果來進行使用。 多執行緒併發訪問會提高效能,但無法保證資料同步。儘量避免多執行緒搶奪同一塊資源,否則儘量將加鎖資源搶奪的業務邏輯交給伺服器處理,減少移動客戶端的壓力。
當有多個執行緒需要訪問到同一個資料時,OC中,我們可以使用@synchronized
(變數)來對該變數進行加鎖(加鎖的目的常常是為了同步或保證原子操作)。
- atomic
-
強指標(strong)、弱指標(weak)
strong
strong
系統一般不會自動釋放,在oc
中,物件預設為強指標。作用域銷燬時銷燬引用。在實際開放中一般屬性物件一般strong
來修飾(NSArray
,NSDictionary
),在使用懶載入定義控制元件的時候,一般也用strong。weak
weak
所引用物件的計數器不會加一,當物件被釋放時指標會被自動賦值為nil
,系統會立刻釋放物件。__unsafe_unretained
弱引用 當物件被釋放時指標不會被自動賦值為ni
在ARC時屬性的修飾符是可以用assign
的(相當於__unsafe_unretained
)
在ARC時屬性的修飾符是可以用retain
的 (相當於__strong
)- 假定有N個指標指向同一個物件,如果至少有一個是強引用,這個物件只要還在作用域內就不會被釋放。相反,如果這N個指標都是弱引用,這個物件馬上就被釋放
- 在使用
sb
或者xib
給控制元件拖線的時候,為什麼拖出來的先屬性都是用 weak 修飾呢?
由於在向xib
或者sb
裡面新增控制元件的時候,新增的子檢視是新增到了跟檢視View
上面,而 控制器Controller
對其根檢視View
預設是強引用的,當我們的子控制元件新增到view
上面的時候,self.view addSubView:
這個方法會對新增的控制元件進行強引用,如果在用strong
對新增的子控制元件進行修飾的話,相當於有兩條強指標對子控制元件進行強引用, 為了避免這種情況,所以用weak
修飾。
注意:
(1)addSubView 預設對其 subView 進行了強引用
(2)在純手碼實現介面佈局時,如果通過懶載入處理介面控制元件,需要使用strong強指標
-
ARC管理記憶體是用
assign
還是用weak
?
assign
: 如果由於某些原因代理物件被釋放了,代理指標就變成了野指標。
weak
: 如果由於某些原因代理物件被釋放了,代理指標就變成了空指標,更安全(weak
不能修飾基本資料型別,只能修飾物件)。
記憶體分析
- 靜態分析(Analyze)
- 不執行程式, 直接檢測程式碼中是否有潛在的記憶體問題(不一定百分百準確, 僅僅是提供建議)
- 結合實際情況來分析, 是否真的有記憶體問題
- 動態分析(Profile == Instruments)
- 執行程式, 通過使用app,檢視記憶體的分配情況(Allocations):可以檢視做出了某個操作後(比如點選了某個按鈕\顯示了某個控制器),記憶體是否有暴增的情況(突然變化)
- 執行程式, 通過使用app, 檢視是否有記憶體洩漏(Leaks):紅色區域代表記憶體洩漏出現的地方
什麼情況下會發生記憶體洩漏和記憶體溢位?
記憶體洩漏:堆裡不再使用的物件沒有被銷燬,依然佔據著記憶體。
記憶體溢位:一次記憶體洩露危害可以忽略,但記憶體洩露多了,記憶體遲早會被佔光,最終會導致記憶體溢位!當程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如資料長度比較小的資料型別 儲存了資料長度比較大的資料。
關於圖片佔用記憶體管理
-
圖片載入佔用記憶體對比
- 使用
imageName:
載入圖片:- 載入到記憶體當中後,佔據記憶體空間較大
- 相同的圖片,圖片不會重複載入
- 載入記憶體當中之後,會一直停留在記憶體當中,不會隨著物件銷燬而銷燬
- 載入進去圖片之後,佔用的記憶體歸系統管理,我們無法管理
- 使用
imageWithContentsOfFile:
載入圖片- 載入到記憶體當中後,佔據記憶體空間較小
- 相同的圖片會被重複載入記憶體當中
- 物件銷燬的時候,載入到記憶體中圖片會隨著一起銷燬
- 結論:
- 圖片較小,並且使用頻繁,使用
imageName:
來載入(按鈕圖示/主頁裡面圖片) - 圖片較大,並且使用較少,使用
imageWithContentsOfFile:
來載入(版本新特性/相簿)
- 圖片較小,並且使用頻繁,使用
- 使用
-
圖片在沙盒中的存在形式
- 部署版本在>=iOS8的時候,打包的資源包中的圖片會被放到Assets.car。圖片有被壓縮;
部署版本在<iOS8的時候,打包的資源包中的圖片會被放在MainBudnle裡面。圖片沒有被壓縮 - 沒有放在Images.xcassets裡面的所有圖片會直接暴露在沙盒的資源包(main Bundle), 不會壓縮到Assets.car檔案,會被放到MainBudnle裡面。圖片沒有被壓縮
- 結論:
- 小圖片\使用頻率比較高的圖片放在Images.xcassets裡面
- 大圖片\使用頻率比較低的圖片(一次性的圖片, 比如版本新特性的圖片)不要放在Images.xcassets裡面
- 部署版本在>=iOS8的時候,打包的資源包中的圖片會被放到Assets.car。圖片有被壓縮;
記憶體管理問題
單個物件記憶體管理的問題
- 關於記憶體我們主要研究的問題是什麼?
野指標:物件的retainCount已經為0,儲存了物件指標地址的變數就是野指標。使用野指標呼叫物件的方法,會導致野指標異常,導致程式直接崩潰
記憶體洩露:已經不在使用的物件,沒有正確的釋放掉,一直駐留在記憶體中,我們就說是記憶體洩漏 - 殭屍物件?
retainCount = 0的物件被稱之為殭屍物件,也就是不能夠在訪問的物件
- 是什麼問題導致,訪問殭屍物件,時而正確時而錯誤?
- 如何開始xcode的時時檢測殭屍物件功能?
- 當物件的retainCount = 0 時 能否呼叫 retain方法使物件復活? 已經被釋放的物件是無法在復活的
- 如何防止出現野指標操作? 通常在呼叫完release方法後,會把儲存了物件指標地址的變數清空,賦值為nil 在oc中沒有空指標異常,所以使用[nil retain]呼叫方法不會導致異常的發生
- 記憶體洩漏有幾種情況?
- 沒有配對釋放,不符合記憶體管理原則
- 物件提前賦值為nil或者清空,導致release方法沒有起作用
多個物件記憶體管理的問題
- 物件與物件之間存在幾種關係?
- 繼承關係
- 組合關係
- 物件作為方法引數傳遞
- 物件的組合關係中,如何確保作為成員變數的物件,不會被提前釋放? 重寫set方法,在set方法中,retain該對像,使其retainCount值增加 1
- 組合關係導致記憶體洩漏的原因是什麼? 在set方法中,retain了該物件,但是並沒有配對釋放
- 作為成員變數的物件,應該在那裡配對釋放? 在dealloc函式中釋放
記憶體相關的一些資料結構的對比
-
簡述記憶體分割槽情況
- 程式碼區:存放函式二進位制程式碼
- 資料區:系統執行時申請記憶體並初始化,系統退出時由系統釋放。存放全域性變數、靜態變數、常量
- 堆區:通過malloc等函式或new等操作符動態申請得到,需程式設計師手動申請和釋放
- 棧區:函式模組內申請,函式結束時由系統自動釋放。存放區域性變數、函式引數
-
手機的儲存空間分為記憶體(RAM)和快閃記憶體(Flash)兩種
- 記憶體一般較小:1G、2G、3G、4G。快閃記憶體空間相對較大16G、32G、64G;
- 記憶體的讀寫速度較快、快閃記憶體的讀寫速度相對較慢;
- 記憶體裡的東西掉電後全部丟失、快閃記憶體裡的東西掉電也不丟;
- 記憶體相當於電腦的記憶體條、快閃記憶體相當於電腦的硬碟;
-
堆和棧的區別?
- 管理方式:
堆釋放工作由程式設計師控制,容易產生memory leak;
棧是由編譯器自動管理,無需我們手工控制。 - 申請大小:
堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。
棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 Windows下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。 - 碎片問題:
堆:頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。
棧:則不會存在這個問題,因為棧是先進後出的佇列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出 - 分配方式:
堆都是動態分配的,沒有靜態分配的堆。
棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloc函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。 - 分配效率:
棧:是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。
堆:則是C/C++函式庫提供的,它的機制是很複雜的。 - 每個App有個記憶體空間,假定是4G,分為堆和棧兩大部分。一般來說每個程式有一個堆(這個程式的所有執行緒共用這個堆),程式中的執行緒有自己棧。
通過alloc、new或malloc獲得的記憶體在堆中分配,堆中的記憶體需要寫相應的程式碼釋放。如果程式結束了在堆中分配的記憶體會自動釋放。
區域性變數、函式引數是在棧空間中分配,如果函式返回這個函式中的區域性變數、引數所佔的記憶體系統自動釋放(回收)。
程式在編譯期對變數和函式分配記憶體都在棧上進行,且程式執行過程中函式呼叫時引數的傳遞也在棧上進行。
- 管理方式:
-
佇列和棧有什麼區別:
佇列和棧是兩種不同的資料容器。從”資料結構”的角度看,它們都是線性結構,即資料元素之間的關係相同。
佇列是一種先進先出的資料結構,它在兩端進行操作,一端進行入佇列操作,一端進行出列隊操作。
棧是一種先進後出的資料結構,它只能在棧頂進行操作,入棧和出棧都在棧頂操作。 -
連結串列和陣列的區別在哪裡?
二者都屬於一種資料結構。如果需要快速訪問資料,很少或不插入和刪除元素,就應該用陣列;相反, 如果需要經常插入和刪除元素就需要用連結串列資料結構。- 從邏輯結構來看
- 陣列必須事先定義固定的長度(元素個數),不能適應資料動態地增減的情況。當資料增加時,可能超出原先定義的元素個數;當資料減少時,造成記憶體浪費;陣列可以根據下標直接存取。
- 連結串列動態地進行儲存分配,可以適應資料動態地增減的情況,且可以方便地插入、刪除資料項。(陣列中插入、刪除資料項時,需要移動其它資料項,非常繁瑣)連結串列必須根據next指標找到下一個元素
- 從記憶體儲存來看
- 陣列從棧中分配空間,對於程式設計師方便快速,但是自由度小
- 連結串列從堆中分配空間, 自由度大但是申請管理比較麻煩
- 從邏輯結構來看
面試題
-
如何讓程式儘量減少記憶體洩漏
- 非ARC
Foundation
物件(OC
物件) : 只要方法中包含了alloc\new\copy\mutableCopy\retain
等關鍵字,那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次release
或者1次autorelease
。
CoreFoundation
物件(C
物件) : 只要函式中包含了create\new\copy\retain
等關鍵字, 那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次CFRelease
或者其他release
函式。 - ARC(只自動管理OC物件, 不會自動管理C語言物件)
CoreFoundation
物件(C
物件) : 只要函式中包含了create\new\copy\retain
等關鍵字, 那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次CFRelease
或者其他release
函式。
- 非ARC
-
block的注意
// block的記憶體預設在棧裡面(系統自動管理) void (^test)() = ^{ }; // 如果對block進行了Copy操作, block的記憶體會遷移到堆裡面(需要通過程式碼管理記憶體) Block_copy(test); // 在不需要使用block的時候, 應該做1次release操作 Block_release(test); [test release]; 複製程式碼
-
野指標舉例
建了個檢視控制器(ARC時)某個函式裡寫了如下程式碼。當這個函式返回時因為沒有指標指向b所以b會被釋放、但是b.view不會被釋放。如果在b裡有需要操作b的地方(比如代理的方法),就會產生野指標(提前釋放)B *b = [[B alloc]init]; [self.view addSubview:b.view]; 複製程式碼
-
set方法
- 在物件的組合關係中,導致記憶體洩漏有幾種情況? 1.set方法中沒有retain物件 2.沒有release掉舊的物件 3.沒有判斷向set方法中傳入的是否是同一個物件
- 該如何正確的重寫set方法?
1.先判斷是否是同一個物件
2.release一次舊的物件
3.retain新的物件
寫一個setter方法用於完成@property (nonatomic,retain)NSString *name,
寫一個setter方法用於完成@property(nonatomic,copy)NSString *name。@property (nonatomic, retain) NSString *name; - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } @property(nonatomic, copy) NSString *name; - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name copy]; } } - (void)dealloc { self.name = nil; // 上邊這句相當於下邊兩句 [_name release]; _name = nil; } 複製程式碼
-
引用計數的使用
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1
Person *p = [[Person alloc] init];
p.age = 20;
// 0 (p指向的記憶體已經是壞記憶體, 稱person物件為殭屍物件)
// p稱為野指標, 野指標: 指向殭屍物件(壞記憶體)的指標
[p release];
// p稱為空指標
p = nil;
p.age = 40;
// [0 setAge:40];
// message sent to deallocated instance 0x100201950
// 給空指標發訊息不會報錯
[p release];
}
return 0;
}
複製程式碼
堆和棧
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 棧
int b = 20; // 棧
// c : 棧
// Car物件(計數器==1) : 堆
Car *c = [[Car alloc] init];
}
// 當autoreleasepool執行完後後, 棧裡面的變數a\b\c都會被回收
// 但是堆裡面的Car物件還會留在記憶體中, 因為它是計數器依然是1
return 0;
}
複製程式碼
-
看下面的程式,三次NSLog會輸出什麼?為什麼?
結果:3、2、1NSMutableArray* ary = [[NSMutableArray array] retain]; NSString *str = [NSString stringWithFormat:@"test"]; // 1 [str retain]; // 2 [ary addObject:str]; // 3 NSLog(@"%d", [str retainCount]); [str retain]; // 4 [str release]; // 3 [str release]; // 2 NSLog(@"%d", [str retainCount]); [ary removeAllObjects]; // 1 NSLog(@"%d", [str retainCount]); 複製程式碼
-
[NSArray arrayWithobject:]後需要對這個陣列做釋放操作嗎?
答: 不需要,這個物件被放到自動釋放池中 -
老版本的工程是可以轉換成使用ARC的工程,轉換規則包括:
- 去掉所有的retain,release,autorelease
- 把NSAutoRelease替換成@autoreleasepool{}塊
- 把assign的屬性變為weak使用ARC的一些強制規定
- dealloc方法來管理一些資源,但不能用來釋放例項變數,也不能在dealloc方法裡面去掉[super dealloc]方法,在ARC下父類的dealloc同樣由編譯器來自動完成
- Core Foundation型別的物件任然可以用CFRetain,CFRelease這些方法
- 不能在使用NSAllocateObject和NSDeallocateObject物件
- 不能在c結構體中使用物件指標,如果有類似功能可以建立一個Objective-c類來管理這些物件
- 在id和void *之間沒有簡便的轉換方法,同樣在Objective-c和core Foundation型別之間的轉換都需要使用編譯器制定的轉換函式
- 不能使用記憶體儲存區(不能再使用NSZone)
- 不能以new為開頭給一個屬性命名
- 宣告outlet時一般應當使用weak,除了對StoryBoard,這樣nib中間的頂層物件要用strong
- weak 相當於老版本的assign,strong相當於retain