DSL与Runloop 分解UI渲染任务

前言

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

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

SingletonPattern(单例模式)

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

用单例模式优化本地存储

iOS最实用的13种设计模式

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

DSL与链式编程简介

  • DSL(Domain Specific Language),特定领域表达式。在OC中,如果使用Masonry会经常写出类似下面的代码。如果是Android或者是其它的什么语言,也会有相应的表达方式。如果是基于链式编程思想的话,以下代码在各个平台相似。如有雷同,纯属正常。
make.top.equalTo(superview).with.offset(10);
  • 链式编程思想:是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性提高。

  • 链式编程特点:方法的返回值是block,block必须返回对象本身(返回block时,block所在的方法调用者对象)block的参数是需要操作的值。

作为一个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 也会随着启动.
    在UIApplicationMain()方法里面完成了程序初始化,并设置程序的Delegate任务,而且随之开启主线程的 RunLoop,就开始接受事件处理.

  • RunLoop 是一个循环,在里面它接受线程的输入,通过事件处理函数来处理事件.你的代码中应该提供 while or for 循环来驱动 runloop.在你的循环中,用 runloop 对象驱动事件处理相关的内容,接受事件,并做响应的处理.

  • RunLoop 接受的事件源有两种大类: 异步的input sources, 同步的 Timer sources. 这两种事件的处理方法,系统所规定.

  • RunLoop 从以下两个不同的事件源中接受消息:
    InputSources : 用来投递异步消息,通常消息来自另外的线程或者程序.在接受到消息并调用指定的方法时,线程对应的 NSRunLoop 对象会通过执行 runUntilDate:方法来退出。

    Timer Source: 用来投递 timer 事件(Schedule 或者 Repeat)中的同步消息。在消息处理时,并不会退出 RunLoop。

    RunLoop 除了处理以上两种 Input Soruce,它也会在运行过程中生成不同的 notifications,标识 runloop 所处的状态,因此可以给 RunLoop 注册观察者 Observer,以便监控 RunLoop 的运行过程,并在 RunLoop 进入某些状态时候进行相应的操作(本文即是运用这一点)。Apple 只提供了 Core Foundation 的 API来给 RunLoop 注册观察者Observer.

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开发中,几乎用不到!!但是对于一些高级的功能,我们会涉及到!!

  • 保证程序不退出!!
  • 负责监听(处理)所有的事件: 触摸,时钟,网络事件等等...
  • 负责渲染我们的UI,Runloop一次循环渲染整个界面!!
  • 如果没有事件发生,那么"睡觉"

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是线程安全的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,458评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1 RunLoop简介 神秘的RunLoop。一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静...
    Claire_wu阅读 1,760评论 3 30
  • 一句经常反问自己又时时刻刻在对号入座的话 .可是你有想过你有对得起自己的初心么?人生的路永远无法按照自己最开始设下...
    女汉子心里的萌妹阅读 698评论 0 0
  • 最近欠下的文越积越多, 感觉心里像压了块巨石, 不清不快。 索性就从9月9日小朋友的聚会写起吧。 说是巧合吧, 本...
    采采卷耳QY阅读 1,119评论 4 3