RunLoop应用篇

作者: SPIREJ | 来源:发表于2019-11-10 15:04 被阅读0次

    基础理论请移步这两篇:
    RunLoop介绍篇
    RunLoop内部调用过程

    一. runloop下timer,observer,source演练

    我们在 RunLoop介绍篇 中介绍了Core Foundation框架下关于RunLoop的5个类:

    • CFRunLoopRef:代表RunLoop的对象
    • CFRunLoopModeRef:RunLoop的运行模式
    • CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源
    • CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源
    • CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变

    下面5个类的关系图。

    接着来讲解这5个类的相互关系。

    一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。

    • 每次RunLoop启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作CurrentMode。
    • 如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
    • 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响。

    用一张图来总结它们就是这种关系:

    1.1 timer

    - (void)cfTimerDemo {
        // 定义runloop timer上下文
        CFRunLoopTimerContext context = {
            0,
            ((__bridge void *)self),
            NULL,
            NULL,
            NULL
        };
        // 获取当前的runloop
        CFRunLoopRef rlp = CFRunLoopGetCurrent();
        /**
        参数一:用于分配对象的内存
        参数二:在什么是触发 (距离现在)
        参数三:每隔多少时间触发一次
        参数四:未来参数
        参数五:CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
        参数六:回调,比如触发事件,我就会来到这里
        参数七:上下文记录信息
        */
        // 创建runloop timer
        CFRunLoopTimerRef timerRef = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 1, 0, 0, sp_RunLoopTimerCallBack, &context);
        // 添加到当前的runloop
        CFRunLoopAddTimer(rlp, timerRef, kCFRunLoopDefaultMode);
    }
    
    void sp_RunLoopTimerCallBack(CFRunLoopTimerRef timer, void *info){
        NSLog(@"%@---%@",timer,info);
    }
    

    运行结果每秒打印一次。

    1.2 observer

    - (void)cfObserverDemo {
        CFRunLoopObserverContext context = {
                0,
                ((__bridge void *)self),
                NULL,
                NULL,
                NULL
            };
        CFRunLoopRef rlp = CFRunLoopGetCurrent();
        /**
         参数一:用于分配对象的内存
         参数二:你关注的事件
              kCFRunLoopEntry=(1<<0),
              kCFRunLoopBeforeTimers=(1<<1),
              kCFRunLoopBeforeSources=(1<<2),
              kCFRunLoopBeforeWaiting=(1<<5),
              kCFRunLoopAfterWaiting=(1<<6),
              kCFRunLoopExit=(1<<7),
              kCFRunLoopAllActivities=0x0FFFFFFFU
         参数三:CFRunLoopObserver是否循环调用
         参数四:CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
         参数五:回调,比如触发事件,我就会来到这里
         参数六:上下文记录信息
         */
        CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, sp_RunLoopObserverCallBack, &context);
        CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
    }
    
    void sp_RunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
        NSLog(@"%lu-%@",activity,info);
    }
    

    我发送一个通知来测试以下observer,发现observer观察到runloop的状态变化,打印结果如下:

    RunLoopTest[6149:679231] 64-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] gotNotification = NSConcreteNotification 0x6000024acc30 {name = helloMyNotification; object = cooci}
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 32-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 64-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
    RunLoopTest[6149:679231] 32-<ModeSourceTimerViewController: 0x7f8807c036d0>
    

    1.3 source

    source0:事件源非基于Port
    source1:基于Port,通过内核和其他线程通信,接收、分发系统事件

    1.3.1 source0

    - (void)source0Demo {
        //初始runloopSource上下文(点进去看知道是结构体对象)
        CFRunLoopSourceContext context = {
            0,
            NULL,
            NULL,
            NULL,
            NULL,
            NULL,
            NULL,
            schedule,
            cancel,
            perform,
        };
        /**
        参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
        参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
        参数三:为运行循环源保存上下文信息的结构
        */
        CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
        CFRunLoopRef rlp = CFRunLoopGetCurrent();
        CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
        // 发送一个执行信号
        CFRunLoopSourceSignal(source0);
        // 唤醒 runloop 防止沉睡状态
        CFRunLoopWakeUp(rlp);
        // 取消,移除
    //    CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
    //    CFRelease(rlp);
    }
    
    void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
        NSLog(@"准备代发");
    }
    
    void perform(void *info){
        NSLog(@"代发ing...");
    }
    
    void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
        NSLog(@"取消了,终止了!!!!");
    }
    

    1.3.2 source1

    source1 port线程之间的通讯演示

    @property (nonatomic, strong) NSPort* subThreadPort;
    @property (nonatomic, strong) NSPort* mainThreadPort;
    
    - (void)source1Demo {
        NSMutableArray* components = [NSMutableArray array];
        NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];
        // 子线程向主线程发送数据
        [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
    }
    
    #pragma mark - NSPortDelegate
    - (void)handlePortMessage:(id)message {
        NSLog(@"%@", [NSThread currentThread]); // 子线程 - 主线程
    
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([message class], &count);
        for (int i = 0; i<count; i++) {
            
            NSString *name = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
            NSLog(@"%@",name); // -- components
        }
        
        sleep(1);
        if (![[NSThread currentThread] isMainThread]) {
    
            NSMutableArray* components = [NSMutableArray array];
            NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
            [components addObject:data];
    
            // 主线程向子线程发送数据
            [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
        }
    }
    
    /*
    配置
    */
    - (void)setupPort{
        self.mainThreadPort = [NSPort port];
        self.mainThreadPort.delegate = self;
        // port - source1 -- runloop
        [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
    
        [self task];
    }
    
    - (void)task {
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            self.subThreadPort = [NSPort port];
            self.subThreadPort.delegate = self;
            
            [[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        }];
        
        [thread start];
    }
    

    二、runloop在开发中的应用

    上面rooploop的timer,observer,source演练完毕之后,下面讲一下RunLoop的几种应用。

    1. NSTimer的使用

    NSTimer的使用方法在 runloop介绍篇 讲解CFRunLoopTimerRef类的时候详细讲解过,具体参考文章的 2.3 CFRunLoopTimerRef

    很简单,发现NSTimer不准的问题就是在runloopModel切换时产生的问题,两种解决办法:

    (1)timer的runloopModel改为NSRunLoopCommonModes

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    (2)在子线程运行timer(即子线程处理耗时操作且常驻线程)

    - (void)timerAddSubThreadTest {
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(displayCount) object:nil];
        [self.thread start];
    }
    
    - (void)displayCount {
        
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(log) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];
    }
    
    - (void)log {
        NSLog(@"hello world");
    }
    

    或者用block方法这样写,但都别忘了子线程的runloop需要手动开启:

    [NSThread detachNewThreadWithBlock:^{
            NSLog(@"%@", [NSThread currentThread]);
            [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
                NSLog(@"hello world");
            }];
            [[NSRunLoop currentRunLoop] run];
        }];
    

    2. 后台常驻线程(很常用)

    我们在开发应用程序的时候,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好让这条线程永远常驻内存。

    那么怎么做呢?

    添加一条用于常驻内存强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。

    具体实现过程如下:

    1. 创建子线程并分配任务
    @property (nonatomic, strong) NSThread *thread;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 创建线程,并调用run1方法执行任务
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
        // 开启线程
        [self.thread start];    
    }
    
    - (void)run1
    {
        // 这里写任务
        NSLog(@"----run1-----");
    
        // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    
        // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
        NSLog(@"未开启RunLoop");
    }
    
    1. 运行之后发现打印了 ----run1----,而 未开启RunLoop 则未打印。

    这时,我们就开启了一条常驻线程,下面我们来试着添加其他任务,除了之前创建的时候调用了run1方法,我们另外在点击的时候调用run2方法

    那么,我们在touchesBegan中调用PerformSelector,从而实现在点击屏幕的时候调用run2方法

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {   
        // 利用performSelector,在self.thread的线程中调用run2方法执行任务
        [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (void)run2
    {
        NSLog(@"----run2------");
    }
    

    经过运行测试,除了之前打印的----run1----,每当我们点击屏幕,都能调用----run2----
    这样我们就实现了常驻线程的需求。

    3. 加载大量图片的性能优化

    有时候,我们会遇到这种情况:
    当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象,我们需要保持流畅度和加载速度。

    怎么解决这个问题呢?做一个简单的分析:

    1,因为这里用到了Runloop循环,那么我们可以监听到runloop的每次循环,在每一次循环当中我们考虑去进行一次图片下载和布局。
    2,既然要在每次循环执行一次任务,我们可以先把所有图片加载的任务代码块添加到一个数组当中,每次循环取出第一个任务进行执行。
    3,因为runloop在闲置的时候会自动休眠,所以我们要想办法让runloop始终处于循环中的状态。

    好了,下面开始考虑代码实现

    第一步,UITableView的创建和基本效果

    创建UITableView 并实现必要的代理,代码略

    第二步,初始化可变数组用来存储任务

    typedef void(^SaveFuncBlock)(void);
    // 存放任务的数组
    @property (nonatomic, strong) NSMutableArray *saveTaskMarr;
    // 最大任务数(超过最大任务数的任务就停止执行)
    @property (nonatomic, assign) NSInteger maxTaskNumber;
    // 任务执行的代码块
    @property (nonatomic, copy) SaveFuncBlock saveFuncBlock;
    
    - (NSMutableArray *)saveTaskMarr {
        if (!_saveTaskMarr) {
            _saveTaskMarr = [NSMutableArray array];
        }
        return _saveTaskMarr;
    }
    

    第三步,将任务添加到数组保存

    // 添加任务进数组保存
    - (void)addTasks:(SaveFuncBlock)taskBlock {
        [self.saveTaskMarr addObject:taskBlock];
        // 超过每次最多执行的任务数就移除当前数组
        if (self.saveTaskMarr.count > self.maxTaskNumber) {
            [self.saveTaskMarr removeObjectAtIndex:0];
        }
    }
    

    第四步,在cellForRow方法中,添加方法

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        
        MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
        
        // 添加任务到数组
        [self addTasks:^{
            // 下载图片的任务
            [cell.icon1 setImage:[UIImage imageNamed:@"1.jpg"]];
            [cell.icon2 setImage:[UIImage imageNamed:@"2.jpeg"]];
            [cell.icon3 setImage:[UIImage imageNamed:@"3.jpg"]];
        }];
    
        return cell;
    }
    

    第五步,监听runloop

    //添加一个监听者RunloopObserver
    -(void)addRunloopObserver{
        //获取当前的RunLoop
        CFRunLoopRef runloop = CFRunLoopGetCurrent();
        //定义一个centext
        CFRunLoopObserverContext context = {
            0,
            ( __bridge void *)(self),
            &CFRetain,
            &CFRelease,
            NULL
        };
        //定义一个观察者
        static CFRunLoopObserverRef defaultModeObsever;
        //创建观察者
        defaultModeObsever = CFRunLoopObserverCreate(NULL,
                                                     kCFRunLoopBeforeWaiting,
                                                     YES,
                                                     0,
                                                     &Callback,
                                                     &context
                                                     );
        
        //添加当前RunLoop的观察者
        CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
        //c语言有creat 就需要release
        CFRelease(defaultModeObsever);
    }
    

    第六步,使用定时器,保持runloop处于循环中

    @property (nonatomic, weak) NSTimer *timer;
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:self block:^(NSTimer * _Nonnull timer) {
           // 此方法主要是利用计时器事件保持runloop处于循环中,不用做任何处理
        }];
    

    第七步,在runloop循环中去处理事件

    //定义一个回调函数  一次RunLoop来一次
    static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
        DelayLoadImageViewController * vcSelf = (__bridge DelayLoadImageViewController *)(info);
        
        if (vcSelf.saveTaskMarr.count > 0) {
            
            //获取一次数组里面的任务并执行
            SaveFuncBlock funcBlock = vcSelf.saveTaskMarr.firstObject;
            funcBlock();
            [vcSelf.saveTaskMarr removeObjectAtIndex:0];
        }
    }
    

    写在最后
    本文中所有的示例都可以在这里下载,如果您喜欢,可以动动手指给个☆哦
    https://github.com/SPIREJ/RunLoopTest

    相关文章

      网友评论

        本文标题:RunLoop应用篇

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