iOS精英班iOS学习移动开发技术前沿

DSL与Runloop 分解UI渲染任务

2017-08-10  本文已影响257人  _onePiece

前言

标题里每一个单词都可以用来长篇阔论一篇文章,我自己是参考了一些资料也才着笔。所以,本文对于一些编程思想或者是底层知识只是浅尝辄止,反而更加着重于应用。通常我们会将耗时操作放到子线程,但是更新UI只能在主线程操作,那么UI耗时操作怎么办?

本文着重讲解通过DSL将编程过程中一个“大”的任务(比如当cell的图片加载过多过大)细分成一个个小任务然后装到runloop中,解决更新UI的耗时操作问题,在一定程度能够有效的解决卡顿。

SingletonPattern(单例模式)

demo里面用的单例模式,这里不再赘述单例模式。如果想详细了解的话,可以参考我之前写过的文章。

用单例模式优化本地存储

iOS最实用的13种设计模式

DSL(本文简单使用链式编程思想)

DSL与链式编程简介

make.top.equalTo(superview).with.offset(10);

作为一个iOS程序员基本上都应该接触过Masonry这个自动布局库。这个库能够极大程度地简化自动布局的代码。使用这个库让我感到惊叹的不是如何能够将较为复杂的传统自动布局写法精简到如此程度,而是精简后的代码的书写方式。本文的目的之一便是想将细分任务的代码更加优雅。

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

优雅的编写自己的DSL

如何优雅地编写自己的DSL,本文不赘述。不过给大家找到一遍很好的文章,强烈推荐美团iOS技术专家臧成威《如何利用 Objective-C 写一个精美的 DSL》

本文用到的链式调用

WPRunloopTasks.h

typedef void(^RunloopBlock) (void);

/**
 最大任务数
 */
@property (nonatomic, assign) NSInteger numOfRunloops;

/**
 链式调用添加任务
 */
@property (nonatomic, copy, readonly) WPRunloopTasks * (^addTask) (RunloopBlock runloopTask);

WPRunloopTasks.m

(具体的实现细节可以忽略,知道这个格式,或者参考相应的格式即可)

/**
 链式调用添加task
 */
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
    __weak __typeof(&*self)weakSelf = self;
    return ^(RunloopBlock runloopTask) {
        [weakSelf.numOfRunloopTasks addObject:runloopTask];
        //保证之前没有显示出来的任务,不再浪费时间加载
        if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
            [weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
        }
        return weakSelf;
    };
}

Runloop

RunLoop 的概念

在新建 xcode 生产的工程中有如下代码块:

int main(int argc, char * argv[]) {
     @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
    }
}

Runloop的mode

apple暴露的只有以下两种模式

kCFRunLoopDefaultMode 默认模式,一般用于处理timer

kCFRunLoopCommonModes 占位模式(既是默认模式又是交互模式,这一点很重要,使用这种模式在默认模式和交互模式都可以触发。)
注:交互模式默认是处理UI事件的。

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // set,非内核事件,比如点击按钮/屏幕
    CFMutableSetRef _sources1;    // set,系统内核事件
    CFMutableArrayRef _observers; // Array,观察者
    CFMutableArrayRef _timers;    // Array,时钟
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

Runloop深入理解

有关runloop的深入理解,推荐ibireme的《深入理解RunLoop》

Runloop总结

在目前iOS开发中,几乎用不到!!但是对于一些高级的功能,我们会涉及到!!

DSL+Runloop

在init方法中创建观察者,在观察者的回调中执行任务并删除已经执行的任务

/**
 添加观察者
 */
- (void)addRunloopObserver {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    static CFRunLoopObserverRef defaultModeServer;
    
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL,
    };
    
    defaultModeServer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    //运行循环 观察者 Runloop占位模式
    CFRunLoopAddObserver(runloop, defaultModeServer, kCFRunLoopCommonModes);
    
    CFRelease(defaultModeServer);
}

/**
 回调函数,一次runloop运行一次
 
 @param observer 观察者
 @param activity 活动
 @param info info
 */
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//这里的info经过打印知道是self,所以可以通过info拿到property
    WPRunloopTasks *runloop = (__bridge WPRunloopTasks *)info;
    if(runloop.numOfRunloopTasks.count) {
        //取出任务
        RunloopBlock task = runloop.numOfRunloopTasks.firstObject;
        //执行任务
        task();
        //干掉第一个任务
        [runloop.numOfRunloopTasks removeObjectAtIndex:0];
    }
}

链式调用添加任务

/**
 链式调用添加task
 */
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
    __weak __typeof(&*self)weakSelf = self;
    return ^(RunloopBlock runloopTask) {
        [weakSelf.numOfRunloopTasks addObject:runloopTask];
        //保证之前没有显示出来的任务,不再浪费时间加载
        if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
            [weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
        }
        return weakSelf;
    };
}

模拟卡顿

demo中的图片是3072*2304高清大图。在渲染的时候,为了更加直观感受效果,用了0.3s的动画。每一个cell有3张图片,屏幕上至少会出现6个cell。先来看一下最后的调用代码:

[WPRunloopTasks shareRunloop].addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView1];
        } completion:nil];
    }).addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView2];
        } completion:nil];
    }).addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView3];
        } completion:nil];
    });

效果对比

没有runloop优化.gif runloop优化.gif

源码

runloop demo

总结

  1. 如果连更新UI耗时的操作都可以优化,我想只要是不涉及到更加底层的东西,都是可以优化的很好的。本问在“外功”方面已经做的可以了,至于“内功”比如图片的解码问题等等就不是本文的范畴了。
  2. runloop功能比较强大,设计到高级功能的应该是会用到的。
  3. NSRunloop是对CFRunLoop的封装,是线程不安全的,而CFRunLoop是线程安全的。
上一篇下一篇

猜你喜欢

热点阅读