美文网首页一步步学习ios
iOS-内存管理1-定时器、NSProxy

iOS-内存管理1-定时器、NSProxy

作者: Imkata | 来源:发表于2019-12-20 16:25 被阅读0次

    一. CADisplayLink、NSTimer

    代码如下:

    #import "ViewController.h"
    #import "MJProxy.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) CADisplayLink *link;
    @property (strong, nonatomic) NSTimer *timer;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
    
    - (void)timerTest
    {
        NSLog(@"%s", __func__);
    }
    
    - (void)linkTest
    {
        NSLog(@"%s", __func__);
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        [self.link invalidate]; //让定时器停止工作
        [self.timer invalidate]; //让定时器停止工作
    }
    @end
    

    关于上面两个定时器:

    1. CADisplayLink这个定时器不能设置时间,保证调用频率和屏幕刷帧频率一致。屏幕刷帧频率大概是60FPS,所以这个定时器一般一秒钟调用60次。
    2. 创建NSTimer,如果是通过scheduledTimer创建,就是定制好的timer,定时器已经添加到RunLoop里面了。如果是timerWithTimeInterval创建的,就需要自己手动添加定时器到RunLoop里面。
    3. CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。

    显而易见,上面两个定时器都有循环引用的问题。
    运行上面代码,从当前VC返回,但是两个定时器还是一直在打印,说明上面代码的确有循环引用问题。

    尝试:

    如何解决?
    可能你会想,使用weakSelf啊,我们试试:

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
    

    运行代码,从当前VC返回,timer定时器还是一直在打印,说明上面方式无效。

    为什么不能解决循环引用,以前我们不就是这么解决的吗?
    注意了,以前那是block,在block章节我们说过,如果外面是个强指针,blcok引用的时候内部就用强指针保存,如果外面是个弱指针,block引用的时候内部就用弱指针保存,所以对于block我们使用weakSelf有用。但是对于CADisplayLink、NSTimer,无论外面你传弱指针还是强指针,都是传入一个内存地址,定时器内部都是对这个内存地址产生强引用,所以传弱指针没用的。

    注意:

    就算不使用@property (nonatomic, strong) NSTimer *timer,使用@property (nonatomic, weak) NSTimer *timer,或者使用NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];也会产生循环引用,因为就算VC没有强引用timer,runLoop也会强引用timer,官方文档解释如下:

    IMG_0672.JPG

    或者我们从如下的结构图中也可以看出timer在RunLoop的哪个地方。

    结构.png

    MBProgressHUD里面关于定时器的使用:

    NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.minShowTimer = timer;
    

    MBProgressHUD里面这样使用定时器为什么没循环引用呢?因为设置了repeats:NO(就是不重复),设置不重复不会产生循环引用。
    如果我们只想使用一次定时器,并且不想产生循环引用,也可以仿照MBProgressHUD一样设置repeats:NO。

    解决方案①

    那么如何解决?
    使用block试试,将NSTimer改成block形式的,如下:

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
    

    这时候是,self对定时器强引用,定时器对block强引用,block对self弱引用,不产生循环引用。运行代码,从当前VC返回,timer定时器不打印了,说明上面代码有效。

    这时候timer保存下来其实也没啥用,我们可以写成如下,这样写定时器也会正常工作的。

    __weak typeof(self) weakSelf = self;
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
    

    解决方案②

    换成block可以解决,我们也可以用中间对象解决。

    在没使用中间对象之前,引用关系是,self里面的timer强引用着定时器,定时器里面的target强引用着self,产生循环引用。

    添加中间对象之后,如下图:

    中间对象.png

    控制器中的timer强引用着定时器,定时器中的target强引用着中间对象,中间对象的target弱引用着控制器,这样就不会产生循环引用了。

    我们需要做的就是当定时器找到中间对象,想要调用中间对象的timerTest方法时,我们让中间对象调用控制器的timerTest方法。

    实现代码也很简单,如下:

    中间对象,MJProxy.h

    #import <Foundation/Foundation.h>
    
    @interface MJProxy : NSObject
    
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target; //用弱引用
    
    @end
    

    中间对象,MJProxy.m

    #import "MJProxy.h"
    
    @implementation MJProxy
    
    + (instancetype)proxyWithTarget:(id)target
    {
        MJProxy *proxy = [[MJProxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    //中间对象找不到timerTest方法,就通过消息转发,转发给控制器
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return self.target;
    }
    @end
    

    ViewController.m

    #import "ViewController.h"
    #import "MJProxy.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) CADisplayLink *link;
    @property (strong, nonatomic) NSTimer *timer;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
    
    - (void)timerTest
    {
        NSLog(@"%s", __func__);
    }
    
    - (void)linkTest
    {
        NSLog(@"%s", __func__);
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        [self.link invalidate];
        [self.timer invalidate];
    }
    @end
    

    上面代码,中间对象弱引用着控制器。当定时器启动后,会从中间对象中寻找timerTest方法,中间对象中找不到timerTest方法,就通过消息转发,转发给控制器,最后调用控制器的timerTest方法。

    运行代码,从当前VC返回,两个定时器都不打印了,说明使用中间对象有效。

    对于NSTimer,无论用block解决还是用中间对象解决都可以,但是对于CADisplayLink,因为它没有block的创建方式,所以只能使用中间对象。

    二. NSProxy

    以前我们说过,iOS中所有的类都继承于NSObject,但是有一个特殊的类:NSProxy(n. 代理人;委托书;代用品)

    进入NSProxy的定义:

    @interface NSProxy <NSObject> {
        Class   isa;
    }
    

    再看看NSObject的定义:

    @interface NSObject <NSObject> {
        Class isa ;
    }
    

    可以发现,NSProxy和NSObject是同一级别的,都遵守NSObject协议。

    ① NSProxy的作用

    那么NSProxy有什么用呢?
    其实,NSProxy就是专门做消息转发的。

    那么NSProxy比上面继承于NSObject的中间对象好在哪里呢?
    如果调用的是继承于NSObject某个类的方法,那么它的方法寻找流程就是先查缓存,再走消息发送、动态方法解析、消息转发,效率低。
    如果调用的是继承于NSProxy某个类的方法,那么它的方法寻找流程是,先看自己有没有这个方法,如果没有,就直接一步到位,来到methodSignatureForSelector方法,效率高。

    ② NSProxy的使用

    自定义MJProxy继承于NSProxy,使用如下:
    MJProxy.h

    #import <Foundation/Foundation.h>
    
    @interface MJProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end
    

    MJProxy.m

    #import "MJProxy.h"
    
    @implementation MJProxy
    
    + (instancetype)proxyWithTarget:(id)target
    {
        // NSProxy对象不需要调用init,因为它本来就没有init方法
        MJProxy *proxy = [MJProxy alloc];
        proxy.target = target;
        return proxy;
    }
    
    //返回方法签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
        return [self.target methodSignatureForSelector:sel];
    }
    
    //NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
    - (void)forwardInvocation:(NSInvocation *)invocation
    {
        [invocation invokeWithTarget:self.target];
    }
    @end
    

    当定时器启动时,会直接到MJProxy中寻找timerTest方法,MJProxy中没有timerTest方法,就会直接调用methodSignatureForSelector方法进行消息转发,转发给控制器后,最后调用控制器的timerTest方法。

    ③ NSProxy补充

    如下代码:

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            ViewController *vc = [[ViewController alloc] init];
            MJProxy *proxy = [MJProxy proxyWithTarget:vc]; //继承于NSProxy的类
            MJProxy1 *proxy1 = [MJProxy1 proxyWithTarget:vc]; //继承于NSObject的类
            
            NSLog(@"%d %d",
                  [proxy isKindOfClass:[ViewController class]],
                  [proxy1 isKindOfClass:[ViewController class]]);
            //打印:1 0
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    打印:1 0

    按理说,左边都不是ViewController类型或其子类,为什么第一个会打印1呢?

    在GUNstep的NSProxy.m文件中,找到isKindOfClass方法的实现:

    - (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;
    }
    

    发现,这个方法直接进行了消息转发,直接转发给ViewController了,最后通过方法寻找流程找到的是ViewController的isKindOfClass方法,所以最后就是调用ViewController的isKindOfClass方法,所以上面会打印1。

    三. GCD定时器

    主线程的RunLoop承担了大部分的工作,比如:UI界面的刷新、核心动画的执行、点击事件的处理。

    1. NSTimer不准时的原因

    NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。

    如果RunLoop专门做NSTimer的事情的话,那么NSTimer是准时的 ,如果RunLoop除了在做NSTimer的事情外还做其他事情,那么会导致NSTimer不准时。
    就比如说NSTimer是1s执行一次,可能它跑完第一圈发现才用了0.5s,这时候发现还没到1s,所以NSTimer不会执行,但是跑第二圈的时候任务就多了可能就需要0.8s,跑完两圈一共1.3s,这时候发现超过1s了,就会执行NSTimer,这时候NSTimer就不准了,晚了0.3s。

    2. GCD定时器

    而GCD的定时器会更加准时,因为GCD的定时器是直接和系统内核挂钩的。

    GCD定时器的简单使用如下:

    - (void)test
    {
        
        //传入主队列,定时器就在主线程工作
    //    dispatch_queue_t queue = dispatch_get_main_queue();
        
        //传入非主队列,定时器就在子线程工作
        dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
        
        // 创建定时器
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 设置时间
        uint64_t start = 2.0; // 2秒后开始执行
        uint64_t interval = 1.0; // 每隔1秒执行
        /**
         定时器设置
         @param  定时器
         @param  什么时候开始
         @param  定时器延迟多久
         @param  每隔几秒执行
         @param  允许多少误差
         */
        //GCD要求传入纳秒,所以要用秒乘以NSEC_PER_SEC
        dispatch_source_set_timer(timer,
                                  dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                                  interval * NSEC_PER_SEC, 0);
        
        // 设置block回调
    //    dispatch_source_set_event_handler(timer, ^{
    //        NSLog(@"1111");
    //    });
        //设置函数回调
        dispatch_source_set_event_handler_f(timer, timerFire);
        
        // 启动定时器
        dispatch_resume(timer);
        
        self.timer = timer;
    }
    
    void timerFire(void *param)
    {
        NSLog(@"2222 - %@", [NSThread currentThread]);
    }
    

    GCD的定时器是和系统内核挂钩的,所以就算界面上添加一个scrollView,滚动的时候就算RunLoop模式切换了,GCD定时器还会照常工作,因为GCD和RunLoop一点关系都没有。

    GCD虽然有使用create,但是在ARC模式下不用你管内存,因为GCD内部已经把内存管理好了。

    3. GCD定时器的封装

    GCD的定时器比较准时,推荐使用,但是GCD的定时器使用起来比较麻烦,下面封装一下GCD的定时器。

    #import "MJTimer.h"
    
    @implementation MJTimer
    
    //只初始化一次
    static NSMutableDictionary *timers_; //保存定时器的字典
    dispatch_semaphore_t semaphore_;  //信号量
    + (void)initialize
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            timers_ = [NSMutableDictionary dictionary];
            semaphore_ = dispatch_semaphore_create(1);
        });
    }
    
    /**
     封装GCD定时器
     
     @param task 任务block
     @param start 开始
     @param interval 间隔
     @param repeats 是否重复
     @param async 是否异步
     @return 返回定时器唯一标识
     */
    + (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
    {
        if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
        
        // 队列
        dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
        
        // 创建定时器
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 设置时间
        dispatch_source_set_timer(timer,
                                  dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                                  interval * NSEC_PER_SEC, 0);
        
        //对字典读写,加信号量锁,保证创建任务和取消任务同时只有一个在做
        dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
        // 定时器的唯一标识
        NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
        // 存放到字典中
        timers_[name] = timer;
        dispatch_semaphore_signal(semaphore_);
        
        // 设置回调
        dispatch_source_set_event_handler(timer, ^{
            task();
            
            if (!repeats) { // 不重复的任务
                [self cancelTask:name];
            }
        });
        
        // 启动定时器
        dispatch_resume(timer);
        
        return name;
    }
    
    /**
     封装GCD定时器
     
     @param target 消息发送者
     @param selector 消息
     @param interval 间隔
     @param repeats 是否重复
     @param async 是否异步
     @return 返回定时器唯一标识
     */
    + (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
    {
        if (!target || !selector) return nil;
        
        return [self execTask:^{
            if ([target respondsToSelector:selector]) {
    //强制消除Xcode警告
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                [target performSelector:selector];
    #pragma clang diagnostic pop
            }
        } start:start interval:interval repeats:repeats async:async];
    }
    
    /**
     取消任务
     
     @param name 根据唯一标识取消任务
     */
    + (void)cancelTask:(NSString *)name
    {
        if (name.length == 0) return;
        
        //对字典读写,加信号量锁,保证创建任务和取消任务同时只有一个在做
        dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
        
        //从字典中移除定时器
        dispatch_source_t timer = timers_[name];
        if (timer) {
            dispatch_source_cancel(timer);
            [timers_ removeObjectForKey:name];
        }
    
        dispatch_semaphore_signal(semaphore_);
    }
    @end
    

    关于GCD定时器的封装,可以看注释。

    上面代码:#pragma clang diagnostic ignored 是用来强制消除Xcode警告的,后面跟的是警告唯一标识,关于警告唯一标识的查看方法,如下:

    强制消除警告1.png 强制消除警告2.png

    第一步:点击build记录
    第二步:找到那个build
    第三步:找到那个警告
    第四步:找到警告唯一标识

    面试题:

    使用CADisplayLink、NSTimer有什么注意点?

    1. 循环引用的问题
    2. NSTimer不准时的问题

    Demo地址:定时器、NSProxy

    相关文章

      网友评论

        本文标题:iOS-内存管理1-定时器、NSProxy

        本文链接:https://www.haomeiwen.com/subject/rsibnctx.html