iOS性能优化

RunLoop应用篇

2019-11-10  本文已影响0人  SPIREJ

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

一. runloop下timer,observer,source演练

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

下面5个类的关系图。

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

一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(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

上一篇下一篇

猜你喜欢

热点阅读