主要講解CADisplayLink 和 NSTimer 的循環(huán)引用問(wèn)題
iOS 內(nèi)存管理 部分一
iOS 內(nèi)存管理 部分二
iOS 內(nèi)存管理 部分三
iOS 內(nèi)存管理 部分四
1. CADisplayLink 和 NSTimer的循環(huán)引用
關(guān)于什么是 CADisplayLink
不再贅述, 網(wǎng)上有很多講解很好的教程; 正常的使用時(shí)我們這樣寫, 但是這樣寫即使是在dealloc
中寫了invalid
也不會(huì)釋放, 因?yàn)橛袕?qiáng)引用環(huán)的存在,
#NSTiemr 的使用
- (void)timerAction {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(printAction) userInfo:nil repeats:YES];
[self.timer fire];
}
- (void)printAction {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
#CADisplayLink 的使用
- (void)displaylinkAction {
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(printAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)printAction {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.link invalidate];
NSLog(@"%s", __func__);
}
他們的引用關(guān)系如下, 所以導(dǎo)致不能釋放;
2. 解決方案
1 . 使用 Block
通過(guò)使用_weak
, 來(lái)使block
對(duì) self
弱引用, 進(jìn)而打到打破循環(huán)引用的問(wèn)題;關(guān)于block
對(duì)self
的引用問(wèn)題請(qǐng)看這篇文章;
測(cè)試代碼
- (void)timerBlockAction {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf printAction];
}];
}
2. 使用中轉(zhuǎn)轉(zhuǎn)發(fā)對(duì)象
使用一個(gè)三方轉(zhuǎn)發(fā)對(duì)象來(lái)斷開這個(gè)引用環(huán)
2.1 為了對(duì)比我們對(duì)
NSTimer
使用NSObject
的類型來(lái)中轉(zhuǎn) 轉(zhuǎn)發(fā);代碼如下
///NSObject 類型中轉(zhuǎn)轉(zhuǎn)發(fā)對(duì)象的.h文件
#import <Foundation/Foundation.h>
@interface ObjectObj : NSObject
+ (ObjectObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSObject 類型中轉(zhuǎn)轉(zhuǎn)發(fā)對(duì)象的.m文件
#import "ObjectObj.h"
@implementation ObjectObj
+ (ObjectObj *)ShareTarget:(id)obj {
ObjectObj *object = [[ObjectObj alloc] init];
object.target = obj;
return object;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
#調(diào)用部分
- (void)timerAction {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ObjectObj ShareTarget:self] selector:@selector(printAction) userInfo:nil repeats:YES];
[self.timer fire];
}
我們從上面的圖中可以確定只要環(huán)中有一個(gè)弱引用就可以破環(huán)循環(huán)引用;但是為什么這種方式可行呢, 其實(shí)這樣做的本質(zhì)是使用了消息轉(zhuǎn)發(fā), 中轉(zhuǎn)對(duì)象并不能響應(yīng)方法printAction()
, 所以會(huì)進(jìn)行方法查找(父類/緩存)-動(dòng)態(tài)解析- 消息轉(zhuǎn)發(fā), 最終返回一個(gè)可以處理printAction()
方法的對(duì)象, 最后的效果就是 VC
在執(zhí)行pop
操作時(shí)可以進(jìn)行調(diào)用dealloc()
方法釋放內(nèi)存; 關(guān)于消息發(fā)送方法的查找過(guò)程;
2.2為了對(duì)比我們對(duì)CADisplayLink
使用NSProxy
的類型來(lái)中轉(zhuǎn) 轉(zhuǎn)發(fā);
代碼如下
///NSProxy 類型中轉(zhuǎn)轉(zhuǎn)發(fā)對(duì)象的.h文件
#import <Foundation/Foundation.h>
@interface ProxyObj : NSProxy
+ (ProxyObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSProxy 類型中轉(zhuǎn)轉(zhuǎn)發(fā)對(duì)象的.m文件
#import "ProxyObj.h"
@implementation ProxyObj
+ (ProxyObj *)ShareTarget:(id)obj {
///注意NSProxy實(shí)例對(duì)象創(chuàng)建不需要 init
ProxyObj *object = [ProxyObj alloc] ;
object.target = obj;
return object;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
///返回 target 的方法簽名
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
///將invocation的 target 設(shè)置為 self.target
invocation.target = self.target;
[invocation invoke];
}
@end
#調(diào)用部分
- (void)displaylinkAction {
self.link = [CADisplayLink displayLinkWithTarget:[ProxyObj ShareTarget:self] selector:@selector(printAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
跟上面的NSObject
類型的中轉(zhuǎn)轉(zhuǎn)發(fā)一樣, 也可以實(shí)現(xiàn)最終效果, 但是使用NSProxy
效率更高, 因?yàn)?code>NSObject的過(guò)程要經(jīng)過(guò)方法查找(父類/緩存)-動(dòng)態(tài)解析-消息轉(zhuǎn)發(fā)三個(gè)階段, 而 NSProxy
只有消息轉(zhuǎn)發(fā)這個(gè)步驟(從其 API
可以查看到它只有消息轉(zhuǎn)發(fā)的方法, 沒(méi)有其他兩個(gè)步驟的方法), 省去前面兩個(gè)步驟, 從而使效率更高;
2. 使用 NSProxy 的注意事項(xiàng)
在討論這個(gè)問(wèn)題之前, 我們先補(bǔ)充下什么是 GNUStep
:
GNUStep
是 GNU 計(jì)劃的項(xiàng)目之一, 我們都知道 iOS
中Foundation
框架是不開源的; 因此 GNUStep
將Foundation
重新實(shí)現(xiàn)了一遍, 雖然不是Apple
官方的源碼, 但是目前仍然是最有參考價(jià)值的源碼;
通過(guò)上面我們知道了NSProxy
的用法, 但是有一些注意事項(xiàng)我們需要注意, 看下面代碼
- (void)proxyAttention {
ObjectObj *obj1 = [ObjectObj ShareTarget:self];
ProxyObj *obj2 = [ProxyObj ShareTarget:self];
NSLog(@"%ld___%ld", (long)[obj1 isKindOfClass:[ViewController1 class]],
(long)[obj2 isKindOfClass:[ViewController1 class]]);
}
#打印結(jié)果為
2020-08-03 15:13:22.910122+0800 MemoryMore1[6035:1149461] 0___1
至于第一個(gè)為什么會(huì)打印0, 我們可以看這篇文章中的isKindOfClass()
方法的講解, 但是為什么第二個(gè)會(huì)打印1呢;
這個(gè)就需要去GNUStep
源碼中找下NSProxy
的實(shí)現(xiàn);
/**
* Calls the -forwardInvocation: method to determine if the 'real' object
* referred to by the proxy is an instance of the specified class.
* Returns the result.<br />
* NB. The default operation of -forwardInvocation: is to raise an exception.
*/
- (BOOL) isKindOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;
sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}
從源碼中我們可以看到, NSProxy
的isKindOfClass()
方法不是跟NSObject
那種進(jìn)行判斷是否是一個(gè)類或者子類, 而是調(diào)用了消息轉(zhuǎn)發(fā)的相關(guān)方法, 因此實(shí)際去跟isKindOfClass()
進(jìn)行判斷的是NSInvocation
內(nèi)部的target
; 由于[ProxyObj ShareTarget:self]
初始化時(shí)傳入的當(dāng)前VC
并在消息轉(zhuǎn)發(fā)時(shí)將其設(shè)置為NSInvocation
內(nèi)部的target
, 所以打印出二者相等, 打印出 1;
參考文章和下載鏈接
文中測(cè)試代碼
CADisplayLink 詳解
NSProxy 簡(jiǎn)析
GNUStep
GNU 官網(wǎng)
Foundation 的 GNU 下載