? 前言:隨著手機(jī)市場日新月異的更新袖瞻,目前無論安卓手機(jī)還是iPhone手機(jī)的內(nèi)存都越來越大忠寻,但是手機(jī)系統(tǒng)和App也越來越大,越來越復(fù)雜瑟蜈,這時內(nèi)存管理仍然是必要的,不然再大的內(nèi)存也會消耗殆盡习勤,從而手機(jī)卡死踪栋,App閃退崩潰。
一图毕、基本概念
1夷都、內(nèi)存管理概念
? 移動設(shè)備的內(nèi)存有限,每個app所能占用的內(nèi)存也是有限制的,當(dāng)app所占用的內(nèi)存較多時囤官,這時得回收一些不需要再使用的內(nèi)存空間冬阳。iOS使用引用計數(shù)來管理OC對象的內(nèi)存,有兩種內(nèi)存管理方案:
? 1)ARC:Automatic Reference Counting党饮,自動引用計數(shù)肝陪,系統(tǒng)自動幫你管理引用計數(shù),從iOS 5開始以后使用刑顺,主要是對OC的對象類型有用(因為對象類型存儲在堆區(qū)氯窍,需要自己手動管理),對于基本數(shù)據(jù)類型比如int蹲堂、float等無效(因為基本數(shù)據(jù)類型在棧區(qū)狼讨,由系統(tǒng)自動管理),ARC目前是主流柒竞;說白了政供,就是編譯器自動幫你在合適的位置插入retain/release方法。
? 2)MRC:Manual Reference Counting朽基,手動引用計數(shù)布隔,需要手動管理引用計數(shù),iOS 5之前的方案稼虎,目前基本不使用了衅檀,但是在CoreGraphics、CoreFoundation框架里渡蜻,還是要經(jīng)常手動釋放對象的术吝。
// 畫一條直線
CGMutablePathRef path = CGPathCreateMutable(); //創(chuàng)建path
CGPathMoveToPoint(path, nil, 100, 100);
CGPathAddLineToPoint(path, nil, 150, 100);
CGPathRelease(path); //需要手動Release對象
// CFRelease(path); // 等效
2、引用計數(shù)
? 一個新創(chuàng)建的OC對象引用計數(shù)默認(rèn)是1茸苇,當(dāng)引用計數(shù)減為0排苍,OC對象就會銷毀,釋放其占用的內(nèi)存空間学密。調(diào)用retain或strong會讓OC對象的引用計數(shù)+1淘衙,調(diào)用release會讓OC對象的引用計數(shù)-1。
? 簡單總結(jié):當(dāng)調(diào)用alloc腻暮、new彤守、copy、mutableCopy方法返回了一個對象哭靖,在不需要這個對象時具垫,要調(diào)用release或者autorelease(引用計數(shù)不會立刻減一)來釋放它;想擁有某個對象试幽,就讓它的引用計數(shù)+1筝蚕,不想再擁有某個對象,就讓它的引用計數(shù)-1。
3起宽、AutoreleasePool
? 官方文檔-NSAutoreleasePool洲胖,Autorelease延遲了對象的銷毀的時間。
// 在MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code here
[pool release];
// 在ARC
@autoreleasepool {
// Code here
}
1)AutoreleasePool概念:自動釋放池坯沪,OC中的一種內(nèi)存自動回收機(jī)制绿映,它可以延遲加入AutoreleasePool中的變量release的時機(jī)矛双。
2)AutoreleasePool原理:ARC下殿如,我們使用@autoreleasepool{}
來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:
// atautoreleasepoolobj:哨兵對象
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(atautoreleasepoolobj);
而這兩個函數(shù)都是對AutoreleasePoolPage
的簡單封裝登下,所以自動釋放機(jī)制的核心就在于這個類藻糖。
3)AutoreleasePoolPage:AutoreleasePoolPage是一個C++實現(xiàn)的類卸奉,結(jié)構(gòu)代碼為:
class AutoreleasePoolPage {
...
id *next; //指向當(dāng)前可插入對象的地址
pthread_t const thread; //當(dāng)前線程
AutoreleasePoolPage * const parent; //前驅(qū)指針
AutoreleasePoolPage *child; //后繼指針
static void * operator new(size_t size) { //創(chuàng)建PoolPage,SIZE為4096
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
/**
begin()和end()方法標(biāo)記了被自動管理對象的范圍
*/
id * begin() { //最低地址
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() { //最高地址
return (id *) ((uint8_t *)this+SIZE);
}
...
}
- AutoreleasePool并沒有單獨的結(jié)構(gòu)颖御,而是由若干個AutoreleasePoolPage以
雙向鏈表
的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針,即前驅(qū)和后繼指針)凝颇。 - AutoreleasePoolPage每個對象會開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大信斯啊),一部分存自己的實例變量拧略,大部分存autorelease對象的地址芦岂。
- 結(jié)構(gòu)中的thread指針指向當(dāng)前線程。
- 一個AutoreleasePoolPage的空間被占滿時垫蛆,會新建一個AutoreleasePoolPage對象禽最,連接鏈表,后來的autorelease對象在新的page加入袱饭。
向一個對象發(fā)送- autorelease
消息川无,就是將這個對象加入到當(dāng)前AutoreleasePoolPage的棧頂next指針指向的位置。
4)Autorelease對象什么時候釋放:每當(dāng)進(jìn)行一次objc_autoreleasePoolPush
調(diào)用時虑乖,runtime向當(dāng)前的AutoreleasePoolPage中add進(jìn)一個哨兵對象
懦趋,objc_autoreleasePoolPush
的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop()
作為入?yún)⒄钗叮敲矗?/p>
1仅叫、根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page;
2糙捺、在當(dāng)前page中诫咱,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次
- release
消息,并移動next
指針到正確位置洪灯;3坎缭、從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page幻锁。
總結(jié):在objc_autoreleasePoolPop的時候?qū)utorelease對象進(jìn)行釋放凯亮。
5)嵌套的AutoreleasePool:知道了上面的原理,嵌套的AutoreleasePool就非常簡單了哄尔,pop的時候總會釋放到上次push的位置為止假消,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣岭接,每次一層富拗,互不影響。
6)AutoreleasePool本身什么時候釋放:每個線程(包括主線程)擁有NSAutoreleasePool的棧鸣戴。如果新的pool被創(chuàng)建(調(diào)用objc_autoreleasePoolPush)啃沪,它就會被添加到棧頂;當(dāng)pool被銷毀(調(diào)用objc_autoreleasePoolPop)窄锅,它就從棧頂被移除创千;最新發(fā)送autorelease消息的對象,會被添加到最近的自動釋放池(即棧頂?shù)尼尫懦兀┤胪担划?dāng)線程終止時追驴,會把棧內(nèi)所有的釋放池移除。
7)main函數(shù)的autoreleasepool作用:從技術(shù)角度看疏之,不是非要有個自動釋放池殿雪。因為塊的末尾恰好是應(yīng)用程序的終止處,而此時操作系統(tǒng)會將引用程序所占的全部內(nèi)存都釋放掉锋爪。雖說如此丙曙,但是如果不寫這個塊的話,那么由UIApplicationMain函數(shù)所自動釋放的那些對象其骄,就沒有自動釋放池可用亏镰,于是系統(tǒng)發(fā)出了警告,所以說年栓,這個池子可以理解成最外圍捕捉自動釋放對象用的拆挥。
8)Autoreleasepool 與 Runloop 的關(guān)系:主線程默認(rèn)為我們開啟 Runloop,Runloop 會自動幫我們創(chuàng)建Autoreleasepool某抓,并進(jìn)行Push纸兔、Pop 等操作來進(jìn)行內(nèi)存管理。準(zhǔn)確的說否副,在kCFRunLoopEntry即將進(jìn)入的時候進(jìn)行push汉矿,創(chuàng)建pool;在kCFRunLoopBeforeWaiting即將休眠的時候备禀,進(jìn)行pop和push洲拇,即釋放舊的池并創(chuàng)建新池奈揍,在kCFRunLoopExit即將退出RunLoop的時候進(jìn)行pop,釋放舊的池赋续。更多詳情-iOS RunLoop男翰。
9)ARC 下什么樣的對象由 Autoreleasepool 管理:所有的對象都?xì)wAutoreleasePool管理嗎?非也纽乱,在ARC環(huán)境下蛾绎,對于普通的對象(通過alloc、new鸦列、copy租冠、mutableCopy創(chuàng)建)是由編譯器在合適的地方為我們 Realease,只有收到Autorelease消息的對象才歸AutoreleasePool管理薯嗤。
那到底什么情況才歸AutoreleasePool管理呢顽爹?
- 系統(tǒng)自帶的方法中,如果不包含alloc new copy mutableCopy骆姐,則這些方法返回的對象都是autorelease的镜粤。比如[NSDate date],[NSString stringWithFormat:@"%ld", i]等玻褪。
- 開發(fā)者自己通過方法創(chuàng)建并返回一個對象繁仁,比如自己創(chuàng)建的類方法返回的對象就是autorelease的,因為需要延遲調(diào)用归园。
10)子線程默認(rèn)不會開啟 Runloop,那出現(xiàn) Autorelease 對象如何處理稚矿?不手動處理會內(nèi)存泄漏嗎庸诱?:如果在子線程你創(chuàng)建了 Pool 的話,產(chǎn)生的 Autorelease 對象就會交給 pool 去管理晤揣;如果你沒有創(chuàng)建 Pool 桥爽,但是產(chǎn)生了 Autorelease 對象,就會調(diào)用 autoreleaseNoPage 方法昧识,在這個方法中钠四,會自動幫你創(chuàng)建一個 hotpage(hotPage 可以理解為當(dāng)前正在使用的 AutoreleasePoolPage),并把Autorelease對象放進(jìn)該Pool中跪楞。也就是說你不進(jìn)行手動的內(nèi)存管理缀去,也不會內(nèi)存泄漏啦。參考-各個線程 Autorelease 對象的內(nèi)存管理甸祭。
4缕碎、內(nèi)存分區(qū)
? 內(nèi)存分區(qū)大體分為5個區(qū):
1、代碼區(qū):存放App代碼池户,App程序會拷貝到這里咏雌。
2凡怎、常量區(qū):常量字符串就是放在這里的,還有const常量赊抖。
3统倒、全局區(qū)/靜態(tài)區(qū)(static):全局變量和靜態(tài)變量的存儲是放在一塊的,初始化的全局變量和靜態(tài)變量在一塊區(qū)域氛雪, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域房匆,程序結(jié)束后由系統(tǒng)釋放。
4注暗、堆區(qū)(heap):需要我們自己管理內(nèi)存坛缕,alloc申請內(nèi)存release釋放內(nèi)存。創(chuàng)建的對象也都放在這里捆昏, 地址是從低到高分配赚楚。堆是所有程序共享的內(nèi)存,當(dāng)N個這樣的內(nèi)存得不到釋放骗卜,堆區(qū)會被擠爆宠页,程序立馬癱瘓。
5寇仓、棧區(qū)(stack):由系統(tǒng)去管理內(nèi)存举户,地址從高到低分配,F(xiàn)irst In Last Out先進(jìn)后出原則遍烦。會存一些局部變量俭嘁,函數(shù)跳轉(zhuǎn)時現(xiàn)場保護(hù)(寄存器值保存于恢復(fù)),這些系統(tǒng)都會幫我們自動實現(xiàn)服猪,無需我們干預(yù)供填。所以大量的局部變量,深遞歸罢猪,函數(shù)循環(huán)調(diào)用都可能耗盡棧內(nèi)存而造成程序崩潰 近她。
來一張高清大圖表示各個分區(qū)的地址高低:
5、淺拷貝和深拷貝
? 1)淺拷貝:指針拷貝膳帕,并沒有創(chuàng)建新的對象粘捎,比如NSString、NSArray危彩、NSDictionary調(diào)用copy方法攒磨;
? 2)深拷貝:創(chuàng)建一個新的對象,比如NSMutableString汤徽、NSMutableArray咧纠、NSMutableDictionary調(diào)用copy方法,生成一個不可變的新對象泻骤,或者NSString漆羔、NSArray梧奢、NSDictionary調(diào)用mutableCopy生成一個新的可變的對象;
- (void)viewDidLoad {
[super viewDidLoad];
// 淺拷貝:NSString演痒、NSArray亲轨、NSDictionary調(diào)用copy方法
NSString *str1 = @"Hello";
NSString *str2 = [str1 copy]; //淺拷貝
// 深拷貝:NSMutableString、NSMutableArray鸟顺、NSMutableDictionary調(diào)用copy方法
NSMutableString *str3 = [[NSMutableString alloc] initWithString:@"World"];
NSString *str4 = [str3 copy]; //深拷貝
// 深拷貝:NSString惦蚊、NSArray、NSDictionary調(diào)用mutableCopy方法
NSMutableString *str5 = [str1 mutableCopy]; //深拷貝
NSLog(@"str1 = %p", str1);
NSLog(@"str2 = %p", str2);
NSLog(@"str3 = %p", str3);
NSLog(@"str4 = %p", str4);
NSLog(@"str5 = %p", str5);
}
運行結(jié)果:
str1 = 0x10e0bd440
str2 = 0x10e0bd440
str3 = 0x600002d7f240
str4 = 0xe641bba2b7d9af9f
str5 = 0x600002d7c750
二讯嫂、循環(huán)引用
? 既然現(xiàn)在都是ARC的時代了蹦锋,系統(tǒng)幫你管理引用計數(shù),幫你管理內(nèi)存分配欧芽,你就可以安枕無憂嗎莉掂?非也,如果代碼使用不當(dāng)千扔,可能造成對象之間的循環(huán)引用憎妙,導(dǎo)致引用計數(shù)大于0,系統(tǒng)沒法回收內(nèi)存曲楚,導(dǎo)致內(nèi)存泄漏厘唾;如果反復(fù)出現(xiàn)內(nèi)存泄漏,當(dāng)使用的內(nèi)存超過系統(tǒng)限制時龙誊,App被系統(tǒng)kill抚垃,App程序閃退。
? 下面介紹三種常見的循環(huán)引用模式:
1趟大、三種循環(huán)引用模式
? 1)自循環(huán)引用:對象的強(qiáng)持有變量指向自身讯柔,比如ViewController強(qiáng)持有一個block,在block里又捕獲持有ViewController护昧,造成自循環(huán)引用;
? 2)相互循環(huán)引用:比如定義一個A類和B類粗截,A類有一個B類的屬性惋耙,B類有一個A類的屬性,修飾詞都是strong類型熊昌,在A類里面訪問B類屬性和B類里邊訪問A類屬性绽榛,那么就會出現(xiàn)相互持有,不會走dealloc方法婿屹;
? 3)多循環(huán)引用:比如類似于三角戀關(guān)系灭美,A持有B,B持有C昂利,C又持有A届腐,造成循環(huán)引用铁坎;
2、如何解決循環(huán)引用
? 1)_ _weak:弱引用犁苏,項目中使用最多的方式硬萍,無論是使用weak修飾self,還是修飾delegate等围详,使用廣泛朴乖。weak指針指向的對象在被廢棄之后會被自動置為nil:
? 當(dāng)weak指針指向的對象被廢棄之后,dealloc的內(nèi)部實現(xiàn)當(dāng)中會調(diào)用清除弱引用的一個方法助赞。然后在清除弱引用的方法當(dāng)中买羞,會通過哈希算法來查找被廢棄對象在弱引用表當(dāng)中的位置,來提取所對應(yīng)的弱引用指針的列表數(shù)組雹食,然后進(jìn)行for循環(huán)遍歷畜普,把每一個weak指針都置為nil。
? 2)_ _unsafe_unretained:不安全引用婉徘,修飾對象不會增加引用計數(shù)漠嵌,當(dāng)指針指向的對象被廢棄后,指針不會置為nil盖呼,成為懸垂指針儒鹿;
? 3)_ _block:在MRC模式下,_ _block修飾對象不會增加引用計數(shù)几晤,避免了循環(huán)引用约炎;在ARC模式下, _ _block一般用來在block內(nèi)部修改外部的局部變量蟹瘾。如果對于block有興趣圾浅,移步-iOS Block。
@property(nonatomic, copy) void (^testBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) wself = self;
// __unsafe_unretained typeof(self) wself = self;
self.testBlock = ^{
//當(dāng)然憾朴,如果不在block內(nèi)使用self狸捕,就不會捕獲self,自然就沒有循環(huán)引用了
NSLog(@"testBlock = %@", wself);
};
self.testBlock();
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
}
三众雷、解決NSTimer灸拍、CADisplayLink內(nèi)存泄漏問題
? 由于NSTimer在項目中經(jīng)常使用,并且就算使用weak修飾砾省,還是會存在內(nèi)存泄漏問題鸡岗,所以單獨拿出來用代碼解釋說明,并提供優(yōu)雅的解決方案编兄。CADisplayLink也是定時器轩性,想詳細(xì)了解-iOS RunLoop。
@interface BViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, weak) NSTimer *timer; //使用weak修飾也不能解決內(nèi)存泄漏
@end
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
// 默認(rèn)自動添加到RunLoop狠鸳,也就是RunLoop持有timer揣苏,直到調(diào)用invalidate悯嗓,才移除timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES];
}
- (void)dealloc {
NSLog(@"BViewController dealloc");
// invalidate:停止定時器,并把timer從RunLoop中移除舒岸,并把timer的target強(qiáng)引用去除
[self.timer invalidate];
self.timer = nil;
}
- (void)timerFire:(NSTimer *)timer {
self.num ++;
NSLog(@"num = %ld", self.num);
}
@end
先從一個AViewController跳轉(zhuǎn)至BViewController绅作,點擊返回按鈕,發(fā)現(xiàn)并沒有調(diào)用dealloc析構(gòu)函數(shù)蛾派,為什么俄认?首先BViewController弱持有timer,timer強(qiáng)持有BViewController洪乍,當(dāng)點擊返回按鈕時眯杏,NavigationController不再持有BViewController,當(dāng)前的RunLoop仍然強(qiáng)持有timer壳澳,而timer強(qiáng)持有BViewController岂贩,所以BViewController引用計數(shù)不為0,自然得不到釋放巷波。
其實不管BViewController用strong還是用weak修飾timer萎津,最終timer都會強(qiáng)持有BViewController,造成內(nèi)存泄漏∧鳎現(xiàn)在列出三種解決方案:
1锉屈、使用block回調(diào)
? 使用block回調(diào)處理事件,而不是使用target垮耳,這樣就不會強(qiáng)持有target颈渊,但是是iOS 10之后才有的方法。
@interface BViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
__weak typeof(self) wself = self;
// 注意:這個方法是iOS 10之后的
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[wself timerFire:timer];
}];
}
- (void)dealloc {
NSLog(@"BViewController dealloc");
// invalidate:停止定時器终佛,并把timer從RunLoop中移除俊嗽,并把timer的target強(qiáng)引用去除
[self.timer invalidate];
self.timer = nil;
}
- (void)timerFire:(NSTimer *)timer {
self.num ++;
NSLog(@"num = %ld", self.num);
}
@end
2、使用動態(tài)消息解析
? RunLoop會對timer強(qiáng)引用铃彰,timer對BViewController強(qiáng)引用绍豁,那么我們可以創(chuàng)建一個中間類來弱引用BViewController,讓timer對中間類強(qiáng)引用牙捉,方法2和下面的方法3都是基于這個思想做的竹揍。當(dāng)然這里還牽扯到OC的消息轉(zhuǎn)發(fā)機(jī)制,如果有興趣請看-iOS Runtime鹃共。
創(chuàng)建一個中間類TimerMiddleware:
// .h文件
@interface TimerMiddleware : NSObject
+ (instancetype)middlewareWithTarget:(id)target;
@end
// .m文件
@interface TimerMiddleware ()
/**
這里使用weak,弱引用驶拱,才能對BViewController弱引用霜浴,才能解決內(nèi)存泄漏
如果使用strong,那么還是會內(nèi)存泄漏
*/
@property(nonatomic, weak) id target;
@end
@implementation TimerMiddleware
+ (instancetype)middlewareWithTarget:(id)target {
TimerMiddleware *middleware = [TimerMiddleware new];
middleware.target = target;
return middleware;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
在BViewController中:
// 其他代碼和上面一樣
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
TimerMiddleware *middleware = [TimerMiddleware middlewareWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:middleware selector:@selector(timerFire:) userInfo:nil repeats:YES];
}
運行結(jié)果:BViewController會走dealloc方法蓝纲,說明解決了內(nèi)存泄漏問題阴孟。
3晌纫、使用NSProxy轉(zhuǎn)發(fā)消息
1、NSProxy是跟NSObject一個級別的基類永丝,用來設(shè)計做消息轉(zhuǎn)發(fā)的锹漱;
2、NSProxy是抽象類慕嚷,使用時候我們需要使用其子類哥牍;
3、NSProxy和NSObject都遵循NSObject協(xié)議喝检;
4嗅辣、NSProxy不會跟NSObject類一樣去父類搜索方法實現(xiàn),會直接進(jìn)入消息轉(zhuǎn)發(fā)流程挠说,所以效率更高澡谭。
先創(chuàng)建一個TimerProxy,繼承自NSProxy:
// .h文件
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
// .m文件
@interface TimerProxy ()
@property(nonatomic, weak) id target;
@end
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target {
TimerProxy *proxy = [TimerProxy alloc]; //注意:沒有init方法
proxy.target = target;
return proxy;
}
// NSProxy接收到消息會自動進(jìn)入到調(diào)用這個方法 進(jìn)入消息轉(zhuǎn)發(fā)流程
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
在BViewController中:
// 其他代碼和上面一樣
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
TimerProxy *proxy = [TimerProxy proxyWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerFire:) userInfo:nil repeats:YES];
}
總結(jié):如果不管iOS系統(tǒng)版本损俭,那么使用block方式簡單蛙奖;如果需要兼容系統(tǒng)版本,那么使用NSProxy更加高效杆兵,一次封裝雁仲,終生受用??。