iOS 内存管理 部分二

主要讲解CADisplayLink 和 NSTimer 的循环引用问题

iOS 内存管理 部分一
iOS 内存管理 部分二
iOS 内存管理 部分三
iOS 内存管理 部分四


1. CADisplayLink 和 NSTimer的循环引用

关于什么是 CADisplayLink不再赘述, 网上有很多讲解很好的教程; 正常的使用时我们这样写, 但是这样写即使是在dealloc中写了invalid也不会释放, 因为有强引用环的存在,

#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弱引用, 进而打到打破循环引用的问题;关于blockself的引用问题请看这篇文章;


测试代码

- (void)timerBlockAction {
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf printAction];
    }];
}
2. 使用中转转发对象

使用一个三方转发对象来断开这个引用环


2.1 为了对比我们对NSTimer使用NSObject的类型来中转 转发;
代码如下

///NSObject 类型中转转发对象的.h文件
#import 
@interface ObjectObj : NSObject
+ (ObjectObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSObject 类型中转转发对象的.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];
}

我们从上面的图中可以确定只要环中有一个弱引用就可以破环循环引用;但是为什么这种方式可行呢, 其实这样做的本质是使用了消息转发, 中转对象并不能响应方法printAction(), 所以会进行方法查找(父类/缓存)-动态解析- 消息转发, 最终返回一个可以处理printAction()方法的对象, 最后的效果就是 VC在执行pop操作时可以进行调用dealloc()方法释放内存; 关于消息发送方法的查找过程;
2.2为了对比我们对CADisplayLink使用NSProxy的类型来中转 转发;
代码如下

///NSProxy 类型中转转发对象的.h文件
#import 
@interface ProxyObj : NSProxy
+ (ProxyObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end

///NSProxy 类型中转转发对象的.m文件
#import "ProxyObj.h"
@implementation ProxyObj
+ (ProxyObj *)ShareTarget:(id)obj {
    ///注意NSProxy实例对象创建不需要 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 类型的中转转发一样, 也可以实现最终效果, 但是使用NSProxy效率更高, 因为NSObject的过程要经过方法查找(父类/缓存)-动态解析-消息转发三个阶段, 而 NSProxy只有消息转发这个步骤(从其 API可以查看到它只有消息转发的方法, 没有其他两个步骤的方法), 省去前面两个步骤, 从而使效率更高;

2. 使用 NSProxy 的注意事项

在讨论这个问题之前, 我们先补充下什么是 GNUStep:
GNUStep是 GNU 计划的项目之一, 我们都知道 iOSFoundation 框架是不开源的; 因此 GNUStepFoundation重新实现了一遍, 虽然不是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的实现;

/**
 * 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.
* 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那种进行判断是否是一个类或者子类, 而是调用了消息转发的相关方法, 因此实际去跟isKindOfClass()进行判断的是NSInvocation内部的target; 由于[ProxyObj ShareTarget:self]初始化时传入的当前VC并在消息转发时将其设置为NSInvocation内部的target, 所以打印出二者相等, 打印出 1;


参考文章和下载链接
文中测试代码
CADisplayLink 详解
NSProxy 简析
GNUStep
GNU 官网
Foundation 的 GNU 下载

你可能感兴趣的:(iOS 内存管理 部分二)