OC底層知識(十二) : 記憶體管理
-
一、丟擲一個問題:使用
CADisplayLink
、NSTimer
有什麼注意點? ofollow,noindex">1.1-1.6的demo-
1.1、分析:
CADisplayLink
、NSTimer
會對target
產生強引用,如果target又對它們產生強引用,那麼就會引發迴圈引用,如下在控制器裡面的程式碼會產生 相互強引用 的問題-
CADisplayLink(在當前控制器按返回按鈕,你會發現 dealloc 方法不會走,而linkTest還在一直呼叫,原因是:self強引用CADisplayLink,而CADisplayLink內部又在強引用self(
displayLinkWithTarget:self
))。@property(nonatomic,strong) CADisplayLink *link; // 保證呼叫頻率和螢幕的刷幀頻率一致 60FPS self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; -(void)linkTest{ NSLog(@"%s",__func__); } -(void)dealloc{ NSLog(@"%s", __func__); [self.link invalidate]; }
-
NSTimer(在當前控制器按返回按鈕,你會發現 dealloc 方法不會走,而timerTest還在一直呼叫,原因是:self強引用NSTimer,而NSTimer內部又在強引用self(target:self ))。
@property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.2、解決上面 互相強引用 的辦法
-
NSTimer 有一個block的方法,我們可以利用block的弱指標來解決
__weak typeof(self) weakSelf = self;
,傳weakSelf
進去,如下@property (strong, nonatomic) NSTimer *timer; __weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { [weakSelf timerTest]; }]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.3、通過中間物件(代理物件)的方式來解決,下面用到了訊息轉發機制( 會先發送訊息、再動態解析、最後再訊息轉發 )
通過中間物件(代理物件)的方式來解決
下面是建立了一個繼承於類 :
JKMiddleProxy : NSObject
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface JKMiddleProxy : NSObject + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKMiddleProxy.h" @implementation JKMiddleProxy + (instancetype)proxyWithTarget:(id)target { JKMiddleProxy *proxy = [[JKMiddleProxy alloc] init]; proxy.target = target; return proxy; } // 訊息轉發機制(會先發送訊息、再動態解析、最後再訊息轉發) - (id)forwardingTargetForSelector:(SEL)aSelector { return self.target; } @end
- 使用如下(不管是CADisplayLink還是NSTimer,把self換為中間物件
[JKMiddleProxy proxyWithTarget:self]
就好)
#import "JKMiddleProxy.h" @property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKMiddleProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
- 使用如下(不管是CADisplayLink還是NSTimer,把self換為中間物件
-
1.4、效率更加高的中間物件( 不需要進行傳送訊息和再動態解析,直接進行訊息轉發 ),利用
NSProxy
可以略過 傳送訊息和動態解析。-
下面是建立了一個繼承於類 :JKProxy : NSProxy
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface JKProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKProxy.h" @implementation JKProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy物件不需要呼叫init,因為它本來就沒有init方法 JKProxy *proxy = [JKProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end
使用和上面1.3一樣,直接(
[JKProxy proxyWithTarget:self]
)self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
-
-
1.5、看下面的列印結果
1 和 0
(原因是JKProxy繼承於 NSProxy,在呼叫isKindOfClass的時候直接走的訊息轉發(- forwardInvocation),會轉換成ViewController的呼叫isKindOfClass,而JKMiddleProxy繼承於NSObject,不會進入forwardInvocation進而invokeWithTarget)JKProxy *proxy1 = [JKProxy proxyWithTarget:vc]; JKMiddleProxy *proxy2 = [JKMiddleProxy proxyWithTarget:vc]; NSLog(@"%d %d", [proxy1 isKindOfClass:[ViewController class]], [proxy2 isKindOfClass:[ViewController class]]);
-
-
二、GCD定時器:比較準時,它直接和系統核心掛鉤的(NSTimer依賴於RunLoop,如果RunLoop的任務過於繁重,可能會導致NSTimer不準時)
-
2.1、使用如下:(可以看demo裡面的
GCDTimerViewController
有具體的原始碼)// 定義GCD定時器物件 dispatch_source_t @property(nonatomic,strong) dispatch_source_t gcdTimer; // 建立佇列 dispatch_queue_t queue = dispatch_get_main_queue(); // 建立定時器 self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 設定時間 /* dispatch_source_t_Nonnull source: 定時器 dispatch_time_t start: 開始的時間,dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),start多長時間後開始,NSEC_PER_SEC(納秒) uint64_t interval:時間間隔 uint64_t leeway: 誤差,寫0就好 */ uint64_t start = 2.0; uint64_t interval = 1.0; dispatch_source_set_timer(self.gcdTimer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),interval * NSEC_PER_SEC,0); // 設定回撥 static int count = 0; dispatch_source_set_event_handler(self.gcdTimer, ^{ count ++; NSLog(@"count== %d",count); }); // 啟動定時器 dispatch_resume(self.gcdTimer);
-
2.2、如果上面的想在子執行緒執行的話,我們可以自己建立佇列(下面是一個序列佇列)
// DISPATCH_QUEUE_SERIAL 序列 // DISPATCH_QUEUE_CONCURRENT 並行 dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
在 2.1 裡面回撥是用的block,咱們還可以用函式,把
dispatch_source_set_event_handler
換為dispatch_source_set_event_handler_f
dispatch_source_set_event_handler_f(self.gcdTimer, timerFire); void timerFire(void *param) { NSLog(@"定時器列印 - %@", [NSThread currentThread]); }
-
2.3、對上面GCD定時器的一個封裝 JKGCDTimer 自己下載,下面展示一下使用
-
第1種使用方式(block返回執行的任務)
// 匯入,這個是封裝的類名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; // 第1種使用方式(Block裡面做task) static int number = 0; /** task 定時器開啟後執行的任務 startTime 多長時間後開啟任務 intervalTime 時間間隔 repeats 是否重複執行任務YES: 重複NO: 執行一次 async 同步還是非同步執行任務YES:async(全域性併發佇列)NO: sync(主佇列) */ self.gcdTimerKeyName = [JKGCDTimer execTask:^{ number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); } startTime:2.0 intervalTime:1.0 repeats:YES async:YES];
-
第2種使用方式(在自己的控制器裡面的方法 實現任務)
// 匯入,這個是封裝的類名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; /** target 自己VC的 self selector 自己VC裡面的 方法 startTime 多長時間後開啟任務 intervalTime 時間間隔 repeats 是否重複執行任務YES: 重複NO: 執行一次 async 同步還是非同步執行任務YES:async(全域性併發佇列)NO: sync(主佇列) */ self.gcdTimerKeyName = [JKGCDTimer execTaskTarget:self selector:@selector(timerExecTask) startTime:2.0 intervalTime:1.0 repeats:YES async:YES]; #pragma mark 採用自己控制器執行任務的方法 -(void)timerExecTask{ static int number = 0; number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); }
-
-
-
三、iOS 程式的記憶體佈局
-
3.1、先用一個圖展示
iOS 程式的記憶體佈局
-
程式碼段:編譯之後的程式碼
-
資料段
static int c = 20; static int d;
-
堆:通過alloc、malloc、calloc等動態分配的空間, 分配的記憶體空間地址越來越大 ,如:
NSObject *obj = [[NSObject alloc] init];
-
棧:函式呼叫開銷,比如區域性變數。分配的記憶體空間地址越來越小,如:
int e; int f = 20;
-
-
3.2、Tagged Pointer ( 推薦部落格一 、 推薦部落格二 、 推薦部落格三 ),這是一個蘋果對記憶體做的優化技術,將一個物件的指標拆成兩部分,一部分直接儲存資料,另一部分作為特殊標記,表示這是一個特別的指標,不指向任何一個地址。
-
(1)、從64bit開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小物件的儲存
以字串為例
@"123"
是比較小的,記憶體地址最後一位 是 9,轉化為 二進位制是:1001
,最後一位是 1,而 str2存的@"fffffffffffff"
比較大,Tagged Pointer不能再存,只能放到堆區
-
(2)、在沒有使用Tagged Pointer之前, NSNumber等物件需要動態分配記憶體、維護引用計數等,NSNumber指標儲存的是堆中NSNumber物件的地址值
-
(3)、使用Tagged Pointer之後,NSNumber指標裡面儲存的資料變成了:Tag + Data,也就是將資料直接儲存在了指標中
-
(4)、當指標不夠儲存資料時,才會使用動態分配記憶體的方式來儲存資料(
如上面的str2
) -
(5)、objc_msgSend能識別Tagged Pointer,比如NSNumber的intValue方法,直接從指標提取資料,節省了以前的呼叫開銷,如下:
-
(6)、那怎麼判斷一個指標是不是 Tagged Pointer 呢?可以通過 objc 原始碼看到對應的判斷方法如下:
static inline bool _objc_isTaggedPointer(const void *ptr) { return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; } #if OBJC_MSB_TAGGED_POINTERS #define _OBJC_TAG_MASK (1ULL<<63) #else #define _OBJC_TAG_MASK 1 #endif #if TARGET_OS_OSX && __x86_64__ // 64-bit Mac - tag bit is LSB #define OBJC_MSB_TAGGED_POINTERS 0 #else // Everything else - tag bit is MSB #define OBJC_MSB_TAGGED_POINTERS 1 #endif
iOS平臺,最高有效位是1(第64bit)
Mac平臺,最低有效位是1
看下面的例子:
NSString *str1 = [NSString stringWithFormat:@"%@",@"abc"]; NSString *str2 = [NSString stringWithFormat:@"%@",@"ffffffffffffffffffff"]; NSLog(@"%p %p %@ %@",str1,str2,[str1 class],[str2 class]); 列印結果為: 0xad16dee4304feb33 0x6000005dd470 NSTaggedPointerString __NSCFString
分析: str1 的記憶體地址是:0xad16dee4304feb33,最左邊a在十六進位制裡面是 10,轉化為二進位制是 1010,可以看到是最高有效位是: 1;而str2的記憶體地址是0x6000005dd470 ,結尾是0,就能確定在堆區。
-
-
-
3.3、思考以下2段程式碼能發生什麼事?有什麼區別?
-
第 1 段程式碼
@property (strong, nonatomic) NSString *name; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"abc"]; }); }
-
第 2 段程式碼(崩潰,壞記憶體訪問)
@property (strong, nonatomic) NSString *name; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; }); }
答:第 2 段程式碼會壞記憶體訪問,原因是:第2段程式碼在給 self.name賦值會走下面的方法,由於 第2段程式碼是 非同步並行的會多個執行緒呼叫
- (void)setName:(NSString *)name
, _name釋放[_name release]
兩次,從而造成壞記憶體訪問;然而第1段程式碼[NSString stringWithFormat:@"abc"]
就不是一個OC物件,僅僅是一個Tagged Pointer
中儲存的資料,把指標變數的值取出來給成員變數self.name
而已。解決第2段程式碼崩潰的辦法在self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; });
上下加鎖和解鎖就好了,來防止兩次release
。// set方法的本質 - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } // set方法在ARC下表面的現象 - (void)setName:(NSString *)name { _name =name }
-
-
四、copy 與 mutableCopy
-
4.1、拷貝的目的
- 產生一個副本物件,跟源物件互不影響
- 修改了源物件,不會影響副本物件
- 修改了副本物件,不會影響源物件
-
4.2、iOS 提供了2個拷貝方法
copy
與mutableCopy
- copy,不可變拷貝,產生不可變副本
- mutableCopy,可變拷貝,產生可變副本
-
4.3、深拷貝和淺拷貝
- 深拷貝:內容拷貝,產生新的物件
- 淺拷貝:指標拷貝,沒有產生新的物件
-
4.4.以字串為例舉例
-
原字串是不可變的(三個字串的記憶體地址 一樣 )
NSString *str1 = [NSString stringWithFormat:@"123"]; // 淺拷貝:指標拷貝,同一塊記憶體地址 NSString *str2 = [str1 copy]; // 深拷貝,物件拷貝,生成新的記憶體地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 列印結果:0xcd37ac23abfbc18c 0xcd37ac23abfbc18c 0x600000910840
原字串是不可變的
-
原字串是可變的(三個字串的記憶體地址 不一樣 )
NSMutableString *str1 = [NSMutableString stringWithFormat:@"123"]; // 深拷貝,物件拷貝,生成新的記憶體地址 NSString *str2 = [str1 copy]; // 深拷貝,物件拷貝,生成新的記憶體地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 列印結果:0x6000038c9470 0xdae0103e19bd5106 0x6000038c9140
原字串是可變的
-
-
4.5、copy和mutableCopy的總結圖
copy和mutableCopy的總結圖
-
4.6、自定義一個類的copy方法
-
自定義的類(JKStudent)遵守
<NSCopying>
協議@property (strong, nonatomic) int number;
-
實現
- (id)copyWithZone:(NSZone *)zone
方法- (id)copyWithZone:(NSZone *)zone { JKStudent *student = [[JKStudent allocWithZone:zone] init]; student.number = self. number; return student; }
-
-
-
五、引用計數的儲存在哪裡?
-
在64bit中,引用計數可以直接儲存在優化過的isa指標中,也可能儲存在SideTable類中
引用計數的儲存
- extra_rc : 裡面儲存的值是引用計數器減1
- has_sidetable_rc: 引用計數器是否過大無法儲存在isa中; 如果為1,那麼引用計數器會儲存在一個叫sideTable的類的屬性中, refcnts是一個存放著物件引用計數的散列表
-
-
六、 看兩個面試題
-
6.1、weak指標的實現原理?
答:將那些弱引用存在一個雜湊表裡面,到時候這個物件要銷燬,它就會取出當前物件對應的弱引用表,把若引用表裡面儲存的若引用都給清除掉。
-
6.2、__weak與__unsafe_unretained的區別?
-
定義一個 JKString 類繼承於 NSObject,
__strong JKString *string1; __weak JKString *string2; // 不安全的,當JKString物件銷燬的時候,string3不會被賦空,會產生野指標的情況 __unsafe_unretained JKString *string3; NSLog(@"begin"); { JKString *string = [[JKString alloc]init]; string3 = string; } NSLog(@"%@",string3);
答: __weak與__unsafe_unretained共同點是:都不會產生強引用,__weak更加安全,當__weak指向的物件銷燬的時候,這個指標的值被清空(nil),防止野指標的錯誤。而__unsafe_unretained指向的物件銷燬的時候,這個指標的值不會被清空,會產生野指標的錯誤
-
-
-
七、ARC都幫我們做了什麼?
答:ARC是LLVM編譯器和Runtime系統相互協作的一個結果,具體是利用編譯器給我們生成記憶體管理相關的程式碼,然後在程式執行的過程中又幫我們處理弱引用這種操作。
-
八、autorelease自動釋放池
- 8.1、自動釋放池的主要底層資料結構是
__AtAutoreleasePool
、AutoreleasePoolPage
- 8.2、呼叫了autorelease的物件最終都是通過AutoreleasePoolPage物件來管理的
-
8.3、objc4原始碼:NSObject.mm
AutoreleasePoolPage
- 8.4、AutoreleasePoolPage的結構
- 每個AutoreleasePoolPage物件佔用4096位元組記憶體,除了用來存放它內部的成員變數,剩下的空間用來存放autorelease物件的地址
- 所有的AutoreleasePoolPage物件通過 雙向連結串列 的形式連線在一起
AutoreleasePoolPage的結構
- 呼叫push方法會將一個POOL_BOUNDARY入棧,並且返回其存放的記憶體地址
- 呼叫pop方法時傳入一個POOL_BOUNDARY(
boundary 美[ˈbaʊndəri, -dri] 分界線
)的記憶體地址,會從最後一個入棧的物件開始傳送release訊息,直到遇到這個POOL_BOUNDARY -
id *next
指向了下一個能存放autorelease物件地址的區域
- 8.5、Runloop和Autorelease
- iOS在主執行緒的Runloop中註冊了2個Observer
- 第1個Observer
- 監聽了kCFRunLoopEntry事件,會呼叫objc_autoreleasePoolPush()
- 第2個Observer
- 監聽了kCFRunLoopBeforeWaiting事件,會呼叫objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
- 監聽了kCFRunLoopBeforeExit事件,會呼叫objc_autoreleasePoolPop()
- 第1個Observer
- iOS在主執行緒的Runloop中註冊了2個Observer
- 8.1、自動釋放池的主要底層資料結構是
-
九、方法裡面有區域性變數,出了方法後會立即被釋放嗎?
- ARC與MRC的切換:
Build Settings
搜尋automatic re
ARC與MRC的切換
答:因下面演示的需要大家可以建一個類JKString類,解釋如下:
-
(1)、如果這個區域性物件最終是通過autorelease的形式(MRC)來去釋放的話,就意味著它不是馬上釋放,而是等它那次所處的RunLoop休眠之前就會進行相應的release操作;
區域性物件最終是通過autorelease的形式來去釋放的
-
(2)、如果ARC生成的是release程式碼的話, 確實區域性變數是立馬就會釋放。
ARC生成的是release程式碼的話
- ARC與MRC的切換:
-
十、OC物件的記憶體管理(下面是結論)
-
10.1、在iOS中,使用引用計數來管理OC物件的記憶體
-
10.2、一個新建立的OC物件引用計數預設是1,當引用計數減為0,OC物件就會銷燬,釋放其佔用的記憶體空間
-
10.3、呼叫retain會讓OC物件的引用計數+1,呼叫release會讓OC物件的引用計數-1
-
10.4、記憶體管理的經驗總結
- 當呼叫alloc、new、copy、mutableCopy方法返回了一個物件,在不需要這個物件時,要呼叫release或者autorelease來釋放它
- 想擁有某個物件,就讓它的引用計數+1;不想再擁有某個物件,就讓它的引用計數-1
-
10.5、可以通過以下私有函式來檢視自動釋放池的情況(宣告一下C的函式,程式會自動尋找該函式,extern),在MRC下測試,多在
@autoreleasepool { }
寫物件測試呼叫下面的函式extern void _objc_autoreleasePoolPrint(void); @autoreleasepool { JKString *string1 = [[[JKString alloc]init] autorelease]; _objc_autoreleasePoolPrint(); @autoreleasepool { JKString *string3 = [[[JKString alloc]init] autorelease]; @autoreleasepool { JKString *string4 = [[[JKString alloc]init] autorelease]; } } }
-