主要講解CADisplayLink 和 NSTimer 的循環(huán)引用問題
iOS 內存管理 部分一
iOS 內存管理 部分二
iOS 內存管理 部分三
iOS 內存管理 部分四
1. CADisplayLink 和 NSTimer的循環(huán)引用
關于什么是 CADisplayLink不再贅述, 網上有很多講解很好的教程; 正常的使用時我們這樣寫, 但是這樣寫即使是在dealloc中寫了invalid也不會釋放, 因為有強引用環(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__);
}
他們的引用關系如下, 所以導致不能釋放;

2. 解決方案
1 . 使用 Block
通過使用_weak, 來使block對 self弱引用, 進而打到打破循環(huán)引用的問題;關于block對self的引用問題請看這篇文章;

測試代碼
- (void)timerBlockAction {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf printAction];
}];
}
2. 使用中轉轉發(fā)對象
使用一個三方轉發(fā)對象來斷開這個引用環(huán)

2.1 為了對比我們對
NSTimer使用NSObject的類型來中轉 轉發(fā);代碼如下
///NSObject 類型中轉轉發(fā)對象的.h文件
#import <Foundation/Foundation.h>
@interface ObjectObj : NSObject
+ (ObjectObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSObject 類型中轉轉發(fā)對象的.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
#調用部分
- (void)timerAction {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ObjectObj ShareTarget:self] selector:@selector(printAction) userInfo:nil repeats:YES];
[self.timer fire];
}
我們從上面的圖中可以確定只要環(huán)中有一個弱引用就可以破環(huán)循環(huán)引用;但是為什么這種方式可行呢, 其實這樣做的本質是使用了消息轉發(fā), 中轉對象并不能響應方法printAction(), 所以會進行方法查找(父類/緩存)-動態(tài)解析- 消息轉發(fā), 最終返回一個可以處理printAction()方法的對象, 最后的效果就是 VC在執(zhí)行pop操作時可以進行調用dealloc()方法釋放內存; 關于消息發(fā)送方法的查找過程;
2.2為了對比我們對CADisplayLink使用NSProxy的類型來中轉 轉發(fā);
代碼如下
///NSProxy 類型中轉轉發(fā)對象的.h文件
#import <Foundation/Foundation.h>
@interface ProxyObj : NSProxy
+ (ProxyObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSProxy 類型中轉轉發(fā)對象的.m文件
#import "ProxyObj.h"
@implementation ProxyObj
+ (ProxyObj *)ShareTarget:(id)obj {
///注意NSProxy實例對象創(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 設置為 self.target
invocation.target = self.target;
[invocation invoke];
}
@end
#調用部分
- (void)displaylinkAction {
self.link = [CADisplayLink displayLinkWithTarget:[ProxyObj ShareTarget:self] selector:@selector(printAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
跟上面的NSObject 類型的中轉轉發(fā)一樣, 也可以實現(xiàn)最終效果, 但是使用NSProxy效率更高, 因為NSObject的過程要經過方法查找(父類/緩存)-動態(tài)解析-消息轉發(fā)三個階段, 而 NSProxy只有消息轉發(fā)這個步驟(從其 API可以查看到它只有消息轉發(fā)的方法, 沒有其他兩個步驟的方法), 省去前面兩個步驟, 從而使效率更高;
2. 使用 NSProxy 的注意事項
在討論這個問題之前, 我們先補充下什么是 GNUStep:
GNUStep是 GNU 計劃的項目之一, 我們都知道 iOS中Foundation 框架是不開源的; 因此 GNUStep將Foundation重新實現(xiàn)了一遍, 雖然不是Apple官方的源碼, 但是目前仍然是最有參考價值的源碼;
通過上面我們知道了NSProxy的用法, 但是有一些注意事項我們需要注意, 看下面代碼
- (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]]);
}
#打印結果為
2020-08-03 15:13:22.910122+0800 MemoryMore1[6035:1149461] 0___1
至于第一個為什么會打印0, 我們可以看這篇文章中的isKindOfClass()方法的講解, 但是為什么第二個會打印1呢;
這個就需要去GNUStep源碼中找下NSProxy的實現(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那種進行判斷是否是一個類或者子類, 而是調用了消息轉發(fā)的相關方法, 因此實際去跟isKindOfClass()進行判斷的是NSInvocation內部的target; 由于[ProxyObj ShareTarget:self]初始化時傳入的當前VC并在消息轉發(fā)時將其設置為NSInvocation內部的target, 所以打印出二者相等, 打印出 1;
參考文章和下載鏈接
文中測試代碼
CADisplayLink 詳解
NSProxy 簡析
GNUStep
GNU 官網
Foundation 的 GNU 下載