在 iOS 中,我們通常將內存分為五大部分:
* 代碼區:用於存放程序的代碼,即 CPU 執行的機器指令,並且是只讀的。
* 全局區 / 靜態區:它主要存放靜態數據、全局數據和常量。分為未初始化全局區(BSS 段)、初始化全局區:(數據段)。程序結束后由系統釋放。
* 數據段:用於存放可執行文件中已經初始化的全局變量,也就是用來存放靜態分配的變量和全局變量。
* BSS 段:用於存放程序中未初始化的全局變量。
* 常量區:用於存儲已經初始化的常量。程序結束后由系統釋放。
* 棧區(Stack):用於存放程序 臨時創建的變量、存放函數的參數值、局部變量等。從上往下,地址是連續的,由編譯器自動分配釋放。
* 堆區(Heap):用於存放alloc分配的對象,copy之后的block變量(copy后其實是一個對象)等,從下往上,是鏈表結構,地址不連續的,由程序員分配和釋放。
從上邊內存的各個部分說明可以看出:只有堆區存放的數據需要由程序員分配和釋放。
堆區存放的,主要是繼承了 NSObject 的對象,需要由程序員進行分配和釋放。其他非對象類型(int、char、float、double、struct、enum 等)則存放在棧區,由系統進行分配和釋放,對象的成員變量進行賦值后,會從棧區復制一份到堆區。
內存管理方案的三種:
1.Tagged Pointer
2.NONPOINTER_ISA(非指針isa)
3.散列表(引用計數表和弱引用計數表)
1.iOS的尋址空間擴大到了64位。我們可以用63位來表示一個數字(一位做符號位)。那么這個數字的范圍是2^63 ,很明顯我們一般不會用到這么大的數字,那么在我們定義一個數字時NSNumber *num = @100,實際上內存中浪費了很多的內存空間。
Tagged Pointer專門用來存儲小的對象,例如NSNumber, NSDate, NSString。
Tagged Pointer是一種特殊的“指針”,指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,也不需要malloc和free。
在內存讀取上有着3倍的效率,創建時比以前快106倍。
2.NONPOINTER_ISA,nonpointer:表示是否對isa開啟指針優化 。index位是0代表是純isa指針,1代表除了地址外,還包含了類的一些信息、對象的引用計數等
sildetable是多張表組合到一個組里,不然又成千上萬個對象都放一張表就會有效率問題。
引用計數表是hash表,存儲和取值都是通過同一個函數計算得到,避免了循環遍歷,提高查找效率。
引用計數機制,一種內存管理技術,是指將資源(可以是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變為零時就將其釋放的過程,分為mrc(手動) arc(自動)
01-當你通過new、alloc或copy方法創建一個對象時,它的引用計數為1,當不再使用該對象時,應該向對象發送release或者autorelease消息釋放對象。
02-當你通過其他方法獲得一個對象時,如果對象引用計數為1且被設置為autorelease,則不需要執行任何釋放對象的操作;
03-如果你打算取得對象所有權,就需要保留對象並在操作完成之后釋放,且必須保證retain和release的次數對等。
retain:持有,對原對象引用計數加1,強引用。ARC中使用strong。 copy:拷貝,復制一個對象並創建strong關聯,引用計數為1 ,原來對象計數不變。 assign:賦值,不涉及引用計數的變化,弱引用。ARC中對象不使用assign,但原始類型(BOOL、int、float)仍然可以使用,assign修飾的對象,當對象釋放之后,即引用計數為0時,指針變量並不會同時置為nil,全局變量就是變為野指針,不知道指向哪,再向該對象發消息,非常容易崩潰。
因此,當屬性類型是對象時,不要使用assign,會帶來一些風險。
weak:賦值(ARC),比assign多了一個功能,對象釋放后把指針置為nil,避免了野指針,weak只能用來修飾對象,不能用來修飾基本數據類型。
strong:持有(ARC),等同於retain。
在你打開ARC時,你是不能使用retain、release、autorelease 操作的,原先需要手動添加的用來處理內存管理的引用計數的代碼可以自動地由編譯器完成了,但是你需要在對象屬性上使用weak 和strong, 其中strong就相當於retain屬性,而weak相當於assign,基礎類型只需聲明非原子鎖即可。
僵屍對象:內存已經被回收的對象。
野指針:指向僵屍對象的指針,向野指針發送消息會導致崩潰。
alloc會調用c函數的calloc,此時並沒有設置引用計數器為1.
retain是怎么將對象的引用計數器+1?經過2次hash表的查找,找到size_t加1實現的。
release的查找和retain一樣,后面是做-1操作。
對象創建alloc的時候是沒有修改引用計數表的,但調用reatanCout后會加1返回。
關聯對象釋放原理,
weak_clear_no_lock()函數會根據hash算法查詢到當前對象的弱引用表,遍歷表置為nil.
weak 實現原理的概括
Runtime維護了一個weak表,用於存儲指向某個對象的所有weak指針。weak表其實是一個hash(哈希)表,Key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象指針的地址,就是地址的地址)集合(當weak指針的數量小於等於4時,是數組, 超過時,會變成hash表)。
weak 的實現原理可以概括以下三步:
1、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。
2、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數, objc_storeWeak() 的作用是更新指針指向,創建對應的弱引用表或者加入已創建的弱引用表。
3、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然后遍歷這個數組把其中的數據設為nil,最后把這個entry從weak表中刪除,清理對象的記錄。
autorelease實現機制
看出AutoreleasePoolPage是一個雙向鏈表結構,同時內部是一個棧結構,和線程一一對應
自動釋放池,系統有一個現成的自動內存管理池,他會隨着每一個mainRunloop的結束而釋放其中的對像;自動釋放池也可以手動創建,他可以讓pool中的對象在執行完代碼后馬上被釋放,可以起到優化內存,防止內存溢出的效果(如視頻針圖片的切換時、創建大量臨時對象時等)
使用:@autoreleasepool {}代碼塊(ARC和MRC下均可以使用)
該循環內產生大量的臨時對象,直至循環結束才釋放,可能導致內存泄漏,解決方法和上文中提到的自動釋放池常見問題類似:在循環中創建自己的autoReleasePool,及時釋放占用內存大的臨時變量,減少內存占用峰值。
for (int i = 0; i < 10000; i ++) { @autoreleasepool { Person* soldier = [[Person alloc]init]; [soldier fight]; } }
在ARC有效的情況下編譯源代碼,必須遵守一定的規則。
不能使用retain/release/retainCount/autorelease
ARC有效時,實現retain/release/retainCount/autorelease會引起編譯錯誤。代碼會標紅,編譯不通過。
不能使用NSAllocateObject/NSDeallocateObject
——————————————————
NSTimer的循環引用
01-引起原因
當你在 ViewController (簡稱 VC )中使用 timer 屬性,由於 VC 強引用 timer,timer 的target 又是 VC 造成循環引用。當你在 VC 的 dealloc 方法中銷毀 timer,發現 VC 被 pop,VC 的 dealloc 方法沒走,VC 在等 timer 釋放才走 dealloc,timer 釋放在 dealloc 中,所以引起循環引用。
解決01-蘋果 API 接口解決方案(iOS 10.0 以上可用)蘋果官方新增了關於 NSTimer 的三個 API:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats: (BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats: (BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); - (instancetype)initWithFireDate:(NSDate *)date interval: (NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
定時器在執行時,將自身作為參數傳遞給 block,來幫助避免循環引用。使用很簡單,但是要注意兩點:
1. 避免 block 的循環引用,使用 __weak 和 __strong 來避免
2. 在持用 NSTimer 對象的類的方法中 -(void)dealloc 調用 NSTimer 的- (void)invalidate 方法;
解決02-對定時器 NSTimer 封裝PFTimer,封裝多一層,讓self去強引用PFTimer,PFTimer強引用time
代碼如下:PFTime.h
//PFTimer.h文件 #import <Foundation/Foundation.h>@interface PFTimer : NSObject //開啟定時器- (void)startTimer; //暫停定時器- (void)stopTimer;@end
PFTime.m
#import "PFTimer.h" @implementation PFTimer { NSTimer *_timer; } - (void)stopTimer{ if (_timer == nil) { return; } [_timer invalidate]; _timer = nil; } - (void)startTimer{ _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(work) userInfo:nil repeats:YES]; } - (void)work{ NSLog(@"正在計時中。。。。。。"); } - (void)dealloc{ NSLog(@"%s",__func__); [_timer invalidate]; _timer = nil; } @end
外部使用:
#import "ViewController1.h" #import "PFTimer.h" @interface ViewController1 () @property (nonatomic, strong) PFTimer *timer; @end @implementation ViewController1 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"VC1"; self.view.backgroundColor = [UIColor whiteColor]; //自定義timer PFTimer *timer = [[PFTimer alloc] init]; self.timer = timer; [timer startTimer]; } - (void)dealloc { [self.timer stopTimer]; NSLog(@"%s",__func__); }
其他:使用 NSProxy 來解決循環引用,偽造一個控制器類,來避免循環引用。

代碼如下:
//PFProxy.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface PFProxy : NSProxy //通過創建對象 - (instancetype)initWithObjc:(id)object; //通過類方法創建創建 + (instancetype)proxyWithObjc:(id)object; @end NS_ASSUME_NONNULL_END
#import "PFProxy.h" @interface PFProxy() @property (nonatomic, weak) id object; @end @implementation PFProxy - (instancetype)initWithObjc:(id)object { self.object = object; return self; } + (instancetype)proxyWithObjc:(id)object { return [[self alloc] initWithObjc:object]; } - (void)forwardInvocation:(NSInvocation *)invocation { if ([self.object respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:self.object]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.object methodSignatureForSelector:sel]; } @end
使用
#import "ViewController1.h" #import "PFProxy.h" @interface ViewController1 () //使用NSProxy @property (nonatomic, strong) NSTimer *timer2; @end @implementation ViewController1 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"VC1"; self.view.backgroundColor = [UIColor whiteColor]; PFProxy *proxy = [[PFProxy alloc] initWithObjc:self]; self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerHandle) userInfo:nil repeats:YES]; } //定時觸發的事件 - (void)timerHandle { NSLog(@"正在計時中。。。。。。"); } - (void)dealloc { [self.timer2 invalidate]; self.timer2 = nil; NSLog(@"%s",__func__); } @end
面試題:在viewDidLoad方法中創建一個數組,問,數組對象什么時候被釋放?
答:在單次runloop將要結束的時候調用AutoreleasePoolPage::pop()釋放。
面試題:Autorelease為何可以嵌套調用?
答:多層嵌套就是多次插入哨兵對象,以區分不同的Autorelease
面試題:什么時候需要手動創建AutoreleasePool?
答:在for循環中alloc圖片數據等內存消耗較大的場景下。