經常面試的時候,會被問到「談談你對OC中內存管理的理解」,我的以爲應該從如下三個部分來回答,才比較全面程序員
本文主要介紹OC的內存管理的模式(機制)來分析。 面試
因此,咱們須要對內存進行合理的分配內存、清除內存,回收那些不須要再使用的對象。從而保證程序的穩定性。數組
任何繼承了NSObject的對象須要進行內存管理,而其餘非對象類型(int、char、float、double、struct、enum等) 不須要進行內存管理bash
這是由於:數據結構
在OC中沒有垃圾回收機制,OC提供了一套機制來管理內存,即「引用計數」,每一個OC對象都有本身的引用計數器多線程
《iOS與OS X多線程和內存管理》這本書說的是函數
可是我的以爲太繞了,簡單來講: 對於全部的對象而言,你只要記住Apple的官網上的內存管理三定律就能夠:oop
在開發時引用計數又分爲ARC(自動引用計數)和MRC(手動引用計數)。ARC的本質其實就是MRC,只不過是系統幫助開發者管理已建立的對象或內存空間,自動在系統認爲合適的時間和地點釋放掉已經失去做用的內存空間,原理是同樣的。佈局
而對於自動釋放池(Autorelease Pool能夠算是半自動的機制,因此這裏我單獨歸爲一類,不和MRC一塊兒分析。post
遵循誰申請、誰添加、誰釋放的原則。須要手動處理內存技術的增長和修改。將從如下幾個方面來深刻了解MRC種的內存管理。
-(void)test{
@autoreleasepool {
Person *p = [[Person alloc] init];
NSLog(@"retainCount = %lu", [p retainCount]); // 1
[p retain];// 只要給對象發送一個retain消息, 對象的引用計數器就會+1
NSLog(@"retainCount = %lu", [p retainCount]); // 2
// 經過指針變量p,給p指向的對象發送一條release消息
// 只要對象接收到release消息, 引用計數器就會-1
[p release];
// 須要注意的是: release並不表明銷燬\回收對象, 僅僅是計數器-1
NSLog(@"retainCount = %lu", [p retainCount]); // 1
[p release]; // 0
}
NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 14:58:10.251949+0800 02-內存管理-MRC開發[13803:235592] retainCount = 1
2020-03-25 14:58:10.252081+0800 02-內存管理-MRC開發[13803:235592] retainCount = 2
2020-03-25 14:58:10.252147+0800 02-內存管理-MRC開發[13803:235592] retainCount = 1
2020-03-25 14:58:10.252239+0800 02-內存管理-MRC開發[13803:235592] Person被釋放了
2020-03-25 14:58:10.252332+0800 02-內存管理-MRC開發[13803:235592] ----自動釋放池已釋放------
複製代碼
當引用計數爲0的時候,person對象就被釋放了,Peson中得dealloc方法就會打印「Person被釋放了」****
在MRC中會引發引用計數變化的關鍵字有:alloc,retain,copy,release,autorelease。(strong關鍵字只用於ARC,做用等同於retain)
MRC中經常使用的屬性關鍵自主要是:assign、reatin、copy
@property (nonatomic,assign) int val;
複製代碼
@property (nonatomic,copy) NSString *name;
// copy修飾的屬性,內部setter方法的實現大概醬紫:
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name copy];
}
}
複製代碼
@property (nonatomic,retain) NSString *name;
// retain修飾的屬性,內部setter方法的實現大概醬紫:
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name retain];
}
}
複製代碼
我門先來弄清楚一些概念:
iOS提供了兩個拷貝方法:
拷貝的目的
淺拷貝和深拷貝
思考🤔:對於copy,引用計數是否可能小於0?
下面來看幾個列子:
void copyTest1) {
NSString *str1 = [[NSString alloc] initWithFormat:@"abc"]; // TaggedPointer
NSString *str2 = str1.copy; // 淺拷貝 TaggedPointer
NSMutableString *str3 = str1.mutableCopy; // 深拷貝 對象
NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
// 打印結果
2020-03-25 16:00:37.509103+0800 03-內存管理-copy[14479:285375] str1 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509190+0800 03-內存管理-copy[14479:285375] str2 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509270+0800 03-內存管理-copy[14479:285375] str3 abc --- 1 --- 0x1007029d0
複製代碼
void copyTest2() {
NSString *str1 = @"ABC"; // 直接寫出來的,不是經過方法建立的字符串,編譯時會生成爲【字符串常量】
NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
NSString *str2 = str1.copy; // 淺拷貝 字符串常量
NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
NSString *str3 = [[NSString alloc] initWithFormat:@"efg"]; // TaggedPointer
NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
NSMutableString *str4 = str3.mutableCopy; // 深拷貝 對象
NSString *str5 = str4.copy; // 深拷貝 對象
NSLog(@"str4 %@ --- %zd --- %p", str4, str4.retainCount, str4);
NSLog(@"str5 %@ --- %zd --- %p", str5, str5.retainCount, str5);
//打印結果
2020-03-25 16:12:07.188709+0800 03-內存管理-copy[14642:295736] str1 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.188866+0800 03-內存管理-copy[14642:295736] str2 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.189023+0800 03-內存管理-copy[14642:295736] str3 efg --- -1 --- 0xd9de73a142fc7bc1
2020-03-25 16:12:07.189245+0800 03-內存管理-copy[14642:295736] str4 efg --- 1 --- 0x10077d8d0
2020-03-25 16:12:07.189316+0800 03-內存管理-copy[14642:295736] str5 efg --- -1 --- 0xd9de73a142fc7bc1
}
複製代碼
總結:
對於常量區的數據(字符串常量),TaggedPointer的引用計數一直都爲【-1】,TaggedPointer不是對象,是個指針。
複製代碼
思考🤔:對於copy,引用計數是否可能大於1,網上不少文章說copy不會改變引用計數?
void copyTest3() {
NSString *str1 = [[NSString alloc] initWithFormat:@"老鄭的技術雜貨鋪"]; // 對象 str1.retainCount = 1
NSString *str2 = str1.copy; // 淺拷貝 對象 str1.retainCount = 2
NSMutableString *str3 = str1.mutableCopy; // 深拷貝 對象 str3.retainCount = 1
NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
//打印結果:
2020-03-25 16:18:45.498256+0800 03-內存管理-copy[14728:302195] str1 老鄭的技術雜貨鋪 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498313+0800 03-內存管理-copy[14728:302195] str2 老鄭的技術雜貨鋪 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498349+0800 03-內存管理-copy[14728:302195] str3 老鄭的技術雜貨鋪 --- 1 --- 0x100605820
複製代碼
總結
可見對不可變對象進行copy操做,引用計數會+1
複製代碼
在App編譯階段,由Xcode添加了內存管理的代碼,自動加入了 retain 、 release 後的代碼
只要沒有強指針指向(沒有被強引用),對象就會被釋放。
Person *person1 = [[Person alloc] init];
__strong Person *person2 = [[Person alloc] init];
複製代碼
相應的也有弱指針(弱引用),但不影響對象的釋放
__weak Person *p = [[Person alloc] init];
複製代碼
簡單看幾種狀況:
#import "Person.h"
@implementation Person
-(void)dealloc{
NSLog(@"Person已釋放-----dealloc");
}
@end
複製代碼
//Test.m
-(void)test{
@autoreleasepool {
int a = 10; // 棧
int b = 20; // 棧
//p在棧上 Person對象(計數器==1) : 堆
Person *p = [[Person alloc] init];
}// 執行到這一行局部變量p釋放
// 因爲沒有強指針指向對象, 因此對象也釋放
NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 16:32:39.073845+0800 Test[14845:312200] Person已釋放-----dealloc
2020-03-25 16:32:39.073965+0800 Test[14845:312200] ----自動釋放池已釋放------
複製代碼
-(void)test{
@autoreleasepool {
Person *p = [[Person alloc] init];
p = nil; // 執行到這一行, 因爲沒有強指針指向對象, 因此對象被釋放
NSLog(@"----當前還在函數做用域內------");
}
NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 16:39:12.924164+0800 Test[14915:317802] Person已釋放-----dealloc
2020-03-25 16:39:12.924311+0800 Test[14915:317802] ----當前還在函數做用域內------
2020-03-25 16:39:12.924446+0800 Test[14915:317802] ----自動釋放池已釋放------
複製代碼
它不會像ARC或者MRC那樣在對象再也不會被使用時立刻被釋放,而是等到一個時機去釋放它,內存池的釋放操做分爲自動和手動。自動釋放受runloop機制影響
好比for循環,多是內存飆升,這個時候我門能夠手動釋放內存(@autoreleasepool)
// ARC
NSMutableArray * arr = [NSMutableArray array];
for (int i = 0; i < largeCount; i++) {
@autoreleasepool {//開始表明建立自動釋放池
NSNumber * numTep = [NSNumber numberWithInt:i];
[arr addObject:numTep];
}//結束表明銷燬自動釋放池
}
複製代碼
方式一:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
複製代碼
方式二:
@autoreleasepool
{ // 建立一個自動釋放池
Person *p = [[Person new] autorelease];
// 將代碼寫到這裏就放入了自動釋放池
} // 銷燬自動釋放池(會給池子中全部對象發送一條release消息)
複製代碼
這裏涉及到runloop,不作詳細的描述,會在runloop相關文章中在作深刻講解,有個大概瞭解便可
蘋果在主線程 RunLoop 裏註冊了兩個 Observer:
當咱們再也不使用一個對象的時候應該將其空間釋放,可是有時候咱們不知道什麼時候應該將其釋放。爲了解決這個問題,Objective-C提供了autorelease方法,在MRC中才能使用。
//錯誤的寫法
@autoreleasepool {
}
// 沒有與之對應的自動釋放池, 只有在自動釋放池中調用autorelease纔會放到釋放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
複製代碼
// 正確寫法
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
// 正確寫法
Person *p = [[Person alloc] init];
@autoreleasepool {
[p autorelease];
}
複製代碼
參考文章: