iOS 內存管理 部分二

主要講解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, 來使blockself弱引用, 進而打到打破循環(huán)引用的問題;關于blockself的引用問題請看這篇文章;


測試代碼

- (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 計劃的項目之一, 我們都知道 iOSFoundation 框架是不開源的; 因此 GNUStepFoundation重新實現(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;
}

從源碼中我們可以看到, NSProxyisKindOfClass()方法不是跟NSObject那種進行判斷是否是一個類或者子類, 而是調用了消息轉發(fā)的相關方法, 因此實際去跟isKindOfClass()進行判斷的是NSInvocation內部的target; 由于[ProxyObj ShareTarget:self]初始化時傳入的當前VC并在消息轉發(fā)時將其設置為NSInvocation內部的target, 所以打印出二者相等, 打印出 1;


參考文章和下載鏈接
文中測試代碼
CADisplayLink 詳解
NSProxy 簡析
GNUStep
GNU 官網
Foundation 的 GNU 下載

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容