不会开机的男孩

翻译 并发编程:API 和 挑战

| Comments

并发(Concurrency)描述了一个同时运行多个任务的概念。他可以在单核设备上通过分时的方式实现,或者如果多个CPU则会真正的并行。

OSX和iOS提供了多种不同的API来实现并发编程。每一个API都有不同的功能和局限,适用于不同的任务。他们有不同的抽象层次。我们可以操作底层API,但是这也意味着我们需要做很多事情来保证正确。

并发编程是非常困难的,里面有很多陷阱。而且很容易被忘记,特别是我们使用API,比如 GCD 或者 NSOperationQueue。这篇文章来会总体介绍在iOS和OSX上面的不同的并发API,然后深入一点并发编程里面的固有的,每个API所独有的挑战。

OSX和iOS上面的并发API

Apple为移动和桌面操作系统提供了相同的并发编程API。这里我们会看一下pthread和NSThread。Grand Central Dispatch, NSOperationQueue 和 NSRunLoop。技术上来说,run loops 有一点奇怪。因为他们并不是真正的并发。但是他和这篇文章的主旨很接近,所以还是值得深入了解的。

我们从底层API开始,然后到上层API。我们这样做,因为上层的API是通过底层的API实现的。当然,在实际中,我们需要反过来考虑:用最上面的一层抽象搞定事情会简单很多。

你可能想知道为什么我们这个坚持推荐使用高抽象层会让并发编程代码简单。你可以阅读这篇文章的第二部分。也可以看 Peter Steinberger 的 线程安全文章。

线程

线程是进程的子单元,被操作系统独立调度执行。事实上,所有的并发API都基于线程。背后实际上的确是真的,不管是GCD还是operation queues。

多线程可以在同一个时间执行,即时在单核的CPU上。(或是至少看上去是在同时)。操作系统赋予每个线程一个时间片,用户看上去多个任务就像是在同时执行一样。如果CPU是多核的,那么多个线程就可以真正的并发执行,因此总的执行时间就会减少。

你可以使用CPU strategy viewInstruments中的几个工具来深入了解一下你的代码或者是framework代码在多核下是如何调用和执行的。

这里面有一个非常重要的需要记住的是你对什么时候执行代码,执行那段代码无能为力,执行多长时间,什么时候会被暂停去执行别的任务,也是不知道的。这个线程调度的技术非常强大。然而,也带来了非常大的复杂性,后面会提到。

让我们把这个复杂的情况暂时放在一边。你可以使用pthread API,或是其他的Objective-C封装的代码,NSThread去创建自己的线程。这里有一个简单的代码,查找100W和数中的最小和最大的数。使用了4个线程并发执行。这就是一个非常好的例子,告你你最好不要直接使用pThread。

struct threadInfo {
    uint32_t * inputValues;
    size_t count;
};

struct threadResult {
    uint32_t min;
    uint32_t max;
};

void * findMinAndMax(void *arg)
{
    struct threadInfo const * const info = (struct threadInfo *) arg;
    uint32_t min = UINT32_MAX;
    uint32_t max = 0;
    for (size_t i = 0; i < info->count; ++i) {
        uint32_t v = info->inputValues[i];
        min = MIN(min, v);
        max = MAX(max, v);
    }
    free(arg);
    struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
    result->min = min;
    result->max = max;
    return result;
}

int main(int argc, const char * argv[])
{
    size_t const count = 1000000;
    uint32_t inputValues[count];

    // Fill input values with random numbers:
    for (size_t i = 0; i < count; ++i) {
        inputValues[i] = arc4random();
    }

    // Spawn 4 threads to find the minimum and maximum:
    size_t const threadCount = 4;
    pthread_t tid[threadCount];
    for (size_t i = 0; i < threadCount; ++i) {
        struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
        size_t offset = (count / threadCount) * i;
        info->inputValues = inputValues + offset;
        info->count = MIN(count - offset, count / threadCount);
        int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
        NSCAssert(err == 0, @"pthread_create() failed: %d", err);
    }
    // Wait for the threads to exit:
    struct threadResult * results[threadCount];
    for (size_t i = 0; i < threadCount; ++i) {
        int err = pthread_join(tid[i], (void **) &(results[i]));
        NSCAssert(err == 0, @"pthread_join() failed: %d", err);
    }
    // Find the min and max:
    uint32_t min = UINT32_MAX;
    uint32_t max = 0;
    for (size_t i = 0; i < threadCount; ++i) {
        min = MIN(min, results[i]->min);
        max = MAX(max, results[i]->max);
        free(results[i]);
        results[i] = NULL;
    }

    NSLog(@"min = %u", min);
    NSLog(@"max = %u", max);
    return 0;
}

NSThread是

2013年度总结

| Comments

过去的一年

人生的第25个年头就要过去了,现在是14号,离25岁的终点还有一个星期。这个意味着我马上就要26岁了。25岁的人生马上就要结束了。为什么这么害怕25岁。25岁到底意味着什么?身体开始衰老而且入行3年。身体衰老意味着如果希望有一个好身材,那么需要付出比之前大的多的时间和毅力,甚至保持都是一件越来越难的事情。入行3年意味着你完成1W小时体验,科学上意味着你应该是一个专家且小有成就了。你已经不是一个小孩子,那种看一篇文章,听一个演讲就马上行动起来的年轻人。你需要能够制定适合自己的计划,而且稳定靠谱的执行下去,没有为什么,因为这个就是这个时候应该做到的。你的试错的成本会变得越来越高,而且你不得不面对一些自己在年轻的时候不愿意面对的问题。那么看看这1年来我做的事情吧。

我养成了2个好习惯,一个健身一个学英语。而且这2个方面都有了不少的进步,之前已经写得太多,这里不多说了。旅行过一次,一次接力马拉松,另外看了52本书。下面是具体的统计

image

image

其中小说最多16篇,心理学6本,计算机5本。20篇读书笔记,5个书评。

翻翻去年给自己定得目标,除了雅思没有搞定以外,其他的还算过去了。

我是谁

现在的社交工具越来越多,会有越来越多人的生活不经意之间闯进我们的生活里。你需要学会辨别他们。就拿一个跑马拉松来说,有人会说敢于参加就好了。但是实际上你不会知道她背后做了什么,各种准备,训练,以及比赛当天的赛程详细计划,多少米需要做什么,以及自己的目标,多少是基本目标,多少是超额的。我这么说并不说一些人会刻意隐瞒什么,而是这些一切的小事情上面所积累的习惯,造就了一个人的做事态度,价值观。

1年前的这会,我写下了这样的一段话。

“I would not be able to find the most difficult task in my life that is changing myself. 我将失去,或是更晚的意识到, 这个人生中最困难的任务,就是改变自己, 因为有太多的人, 别说改变自己, 面对自己都做不到, 因为自己可以骗得了任何人,但唯独骗不了自己。”

今天和一个朋友聊天,也正好聊到了这个。人们最大的敌人是自己,真的真的只有极少数的人可以做到面对自己,可以自己揭开自己心中的伤疤。今天我清晰的感觉到了我心脏的跳动,那种砰砰砰跳动的感觉,我很害怕。非常害怕,害怕到想哭却哭不出来。因为很简单,这一年来不管我做过哪些改变以及努力,其实可以用8个字概况“看似忙碌,实则焦虑”——纤细的胳膊,蹩脚的英语,以及居无定所没有稳定收入来源的生活。

不管是工作上面的总结,还是人际交往的总结,还是我做的一切一切的东西的源动力在于我过的生活和我想的不一样。胸围长5cm腰围小12cm绝对不会是“我希望我的身体强壮一点”一句话搞定的,同样那个钢筋铁骨的30多岁大姐姐也不是一句“我希望更漂亮健康”搞定的,有谁会理解从一个相夫教子的全职太太到孤身一人的心理落差呢?

这就是现实的生活,生活中永远不缺努力的人,也不缺焦虑。虽然我现在要比1年前做得要好很多。我不会去抱怨。学会把用来抱怨的时间去做事情。人总会慢慢的发现自己似乎强壮起来了,就像温水煮青蛙,看上去不错,但实际上根本没有变化,我只是学得更会掩饰自己的内心,更会假装而已。

越忙碌,学得越多,做的越多,只是在给自己掩饰自己的焦虑,暗示自己过的还好,但其实一点也不好。不得不承认自己处在一个下降的电梯,这就是我心中的“坑”。

当然也有好消息。好消息就是不是我才有焦虑,这个几乎只要是一个有想法的年轻人都有的(好阴暗的感觉…)

要到那里去

当我们回头看自己走过来的路时,看到的似乎只是依稀莫辩的“或许”。我们所能明确认知的仅仅是现在这一瞬间,而这也只是与我们擦肩而过。看看自己,除了青春,什么都没有。青春就是时间,时间不像钱,可以存,可以攒,时间只能用于消费或是投资。不得不承认,过去的日子里面,我浪费了太多的时间,有太多的遗憾,小时候有太多的不懂事,让自己现在变成这个样子,但是我不后悔。我认定的事情我都全力去做,有部分做到了,有部分延期做到了,也有一部分最后失败了。

2014年,把2013年的一些遗憾弥补上之后,给自己一个新的平台。到了找一个新电梯的时候了。多年以前做的决定,现在也是搏一把的时候了。

后记

这篇是在14号写的,实际上,在14号之前我已经想了1个多月了。如果一件事情还有遗憾,那么就还没有到放弃的时候。我从来没有这么长时间的思考过自己,也第一次帮别人思考自己。回头看自己思路的时候,发现还是有很多坑和不足,但我也看到了我在慢慢的变好。即将的26岁这一年,真的将是我非常重要的一年。鸡血又开始打上了。

翻译 《Getting Pixels Onto the Screen》

| Comments

这篇提交到伯乐在线

原文

绘制像素到屏幕

像素是如何绘制到屏幕上面的?有非常多的方式输出数据到屏幕,通过调用很多不同的framework和不同的函数。这里我们讲一下这个过程背后的东西。希望能够帮助大家了解什么时候该使用什么API,特别是当遇到性能问题需要调试的时候。当然,我们这里主要讲iOS,但是事实上,很多东西也是可以应用到OSX上面的。

Graphics Stack

绘制屏幕的过程中又很多都是不被人了解的。但是一旦像素被绘制到屏幕上面,那么像素就是有3种颜色组成:红绿蓝。这3个颜色单元通过特定的强弱组合形成一个特定的颜色。对于iPhone5 IPS_LCD 的分辨率是1,136×640 = 727,040个像素,也就是有2,181,120个颜色单元。对于一个15寸高清屏幕的MacBook Pro来说,这个数字差不多是1500万。Graphics Stack 就是确保每一个单元的强弱都正确。当滑动整个屏幕的时候,上百万的颜色单元需要在每秒60次的更新。

The Software Components

下面是一个简单的例子,整个软件看起来是这个样子

image

显示器上面的就是GPU,图像处理单元。GPU是一个高度并发计算的硬件单元,特别是处理图形图像的并行计算。这就是为什么可以这么快的更新像素并输出到屏幕的原因。并行计算的设计让GPU可以高效的混合图像纹理。我们会在后面详细解释混合图像纹理这个过程。现在需要知道的就是GPU是被高度优化设计的,因此非常适合计算图像这种类型的工作。他比CPU计算的更快,更节约能耗。因为CPU是为了更一般的计算设计的硬件。CPU虽然可以做很多事情,但是在图像这方面还是远远慢于GPU。

GPU驱动是一些直接操作GPU的代码,由于各个GPU是不同的,驱动在他们之上创建一个层,这个层通常是OpenGL/OpenGL ES。

OpenGL(Open Graphics Library))是用来做2D和3G图形图像渲染的API。由于GPU是一个非常定制化的硬件,OpenGL和GPU紧密合作充分发挥GPU的能力来实现图形图像渲染硬件加速。对大多数情况,OpenGL太底层了。但是当1992年第一个版本发布后(20多年前),它就成为主流的操作GPU的方式,并且前进了一大步。因为程序员再也不用为了每一个GPU编写不同的应用程序。

在OpenGL上面,分开了几个。iOS设备几乎所有的东西变成了Core Animation,但是在OSX,绕过Core Animation而使用Core Graphic 并不是不常见。有一些特别的应用程序,特别是游戏,可能直接使用OpenGL/OpenGL ES. 然后事情变得让人疑惑起来,因为有些渲染Core Animation 使用 Core Graphic。类似AVFoundation, Core Image 这样的框架,或是其他的一些混合的方式。

这里提醒一件事情, GPU是一个强有力的图形图像硬件,在显示像素方面起着核心作用。它也连接着CPU。从硬件方面讲就是有一些总线把他们连接了起来。也有一些框架比如 OpenGL, Core Animation。Core Graphic控制GPU和CPU之间的数据传输。为了让像素能够显示到屏幕上面,有一些工作是需要CPU的。然后数据会被传给GPU,然后数据再被处理,最后显示到屏幕上面。

每一个过程中都有自己的挑战,在这个过程中也存在很多权衡。

硬件层

image

这是一个很简单的图表用来描述一个挑战。GPU有纹理(位图)合成为一帧(比如1秒60帧)每一个纹理占用VRAM(显卡)因此GPU一次处理的纹理有大小限制。GPU处理合成方面非常高效,但是有一些合成任务比其他要复杂,所以GPU对处理能力有一个不能超过16.7ms的限制(1秒60帧)。

另一个挑战是把数据传给GPU。为了让GPU能够访问数据,我们需要把数据从内存复制到显存。这个过程叫做上传到GPU。这个可能看上去不重要,但是对于一个大的纹理来说,会非常耗时。

最后CPU运行程序。你可能告诉CPU从资源文件夹中加载一个PNG图片,并解压。这些过程都发生在CPU。当需要显示这些解压的图片时,就需要上传数据到GPU。一些事情看似非常简单,比如显示一段文字,对CPU来说是一个非常复杂的任务。需要调用Core Text 和 Core Graphic框架去根据文字生成一个位图。完成后,以纹理的方式上传到GPU,然后准备显示。当你滑动或是移动一段屏幕上面的文字时,同样的纹理会被重用,CPU会简单的告诉GPU只是需要一个新的位置,所以GPU可以重新利用现有的纹理。CPU不需要重新绘制文字,位图也不需要重新上传到GPU。

上面的有一点复杂,在有一个整体概念之后,我们会开始解释里面的技术细节。

图像合成

图像合成的字面意思就是把不同的位图放到一起创建成最后的图像然后显示到屏幕上面。在很多方面来看,这个过程都是显而易见的,所以很容易忽视其中的复杂性和运算量。

让我们忽视一些特殊情况,假设屏幕上面都是纹理。纹理就是一个RGBA值的矩形区域。每一个像素包括红,绿,蓝,透明度。在Core Animation世界里面,基本上相当于CALayer。

在这个简单的假设中,每一个层是一个纹理,所有的纹理通过栈的方式排列起来。屏幕上的每一个像素,CPU都需要明白应该如何混合这些纹理,从而得到相对应的RGB值。这就是合成的过程。

如果我们只有一个纹理,而且这个纹理和屏幕大小一致。每一个像素就和纹理中得一个像素对应起来。也就是说这个纹理的像素就是最后屏幕显示的样子。

如果我们有另一个纹理,这个纹理覆盖在之前的纹理上面。GPU需要首先把第二个纹理和第一个纹理合成。这里面有不同的覆盖模式,但是如果我们假设所有的纹理都是像素对齐且我们使用普通的覆盖模式。那么最后的颜色就是通过下面的公式计算出来的。

R = S + D * (1 - Sa)

最后的结果是通过源的颜色(最上面的纹理)加目标颜色(下面的纹理) 乘以(1 - 源颜色的透明度)公式里面所有的颜色就假定已经预先乘以了他们的透明度。

很显然,这里面很麻烦。让我们再假设所有的颜色都是不透明的,也就是alpha = 1. 如果目标纹理(下面的纹理)是蓝色的(RGB = 0,0,1)源纹理(上面的纹理)是红色(RGB = 1,0,0)。因为Sa = 1, 那么这个公式就简化为

R = S

结果就是源的红色,这个和你预期一致。

如果源(上面的)层50%透明,比如 alpha = 0,5. 那么 S 的RGB值需要乘以alpha会变成 (0.5,0,0)。这个公式会变成这个样子

                   0.5   0               0.5

R = S + D * (1 - Sa) = 0 + 0 * (1 - 0.5) = 0

                   0     1               0.5

我们最后得到的RGB颜色是紫色(0.5, 0, 0.5) 。这个和我们的直觉预期一致。透明和红色和蓝色背景混合后成为紫色。

要记住,这个只是把一个纹理中的一个像素和另一个纹理中的一个像素合成起来。GPU需要把2个纹理之间覆盖的部分中的像素都合成起来。大家都知道,大多数的app都有多层,因此很多纹理需要被合成起来。这个对GPU的开销很大,即便GPU已经是被高度硬件优化的设备。

不透明 VS 透明

当源纹理是完全不透明,最终的颜色和源纹理一样。这就可以节省GPU的很多工作,因为GPU可以简单的复制源纹理而不用合成所有像素值。但是GPU没有办法区别纹理中的像素是不透明的还是透明。只有程序员才能知道CALayer里面的到底是什么。这也就是CAlayer有opaque属性的原因。如果opaque = YES, 那么GPU将不会做任何合成计算,而是直接直接简单的复制颜色,不管下面还有什么东西。GPU可以减少大量的工作。这就是Instruments(Xcode 的性能测试工具)中 color blended layers 选项做的事情。(这个选项也在模拟器菜单里面)。它可以让你了解哪一个层(纹理)被标记成透明,也就是说,GPU需要做合成工作。合成不透明层要比透明的层工作量少很多,因为没有那么多的数学运算在里面。

如果你知道哪一个层是不透明的,那么一定确保opaque = YES。如果你载入一个没有alpha通道的image,而且在UIImageView显示,那么UIImageView会自动帮你设置opaque = YES。但是需要注意一个没有alpha通道的图片和每个地方的alpha都是100%的图片区别很大。后面的情况,Core Animation 需要假定所有像素的alpha都不是100%。在Finder中,你可以使用Get Info并且检查More Info部分。它将告诉你这张图片是否拥有alpha通道。

像素对齐和不对齐

到目前为止,我们考虑的层都是完美的像素对齐的。当所有的像素都对齐时,我们有一个相对简单的公式。当GPU判断屏幕上面的一个像素应该是什么时,只需要看一下覆盖在屏幕上面的所有层中的单个像素,然后把这些像素合成起来,或者如果最上面的纹理是不透明的,GPU只需要简单的复制最上面的像素就好了。

当一个层上面的所有像素和屏幕上面的像素完美对应,我们就说这个层是像素对齐的。主要有2个原因导致可能不对齐。第一个是放大缩小;当放大或是缩小是,纹理的像素和屏幕像素不对齐。另一个原因是当纹理的起点不在一个像素边界上。

这2种情况,GPU不得不做额外的计算。这个需要从源纹理中混合很多像素来创建一个像素用来合成。当所有像素对齐时,GPU就可以少做很多工作。

注意,Core Animation Instrument和模拟器都有color misaligned images 选项,当CALayer中存在像素不对齐的时候,把问题显示出来。

遮罩(mask)

一个层可以有一个和它相关联的遮罩。遮罩是一个有alpha值的位图,而且在合成像素之前需要被应用到层的contents属性上。当你这顶一个层为圆角时,一就在设置一个遮罩在这个层上面。然而,我们也可以指定一个任意的遮罩。比如我们有一个形状像字母A的遮罩。只有CALayer的contents中的和字母A重合的一部分被会被绘制到屏幕。

离屏渲染(Offscreen rendering)

离屏渲染可以被Core Animation 自动触发或是应用程序手动触发。离屏渲染绘制layer tree中的一部分到一个新的缓存里面(这个缓存不是屏幕,是另一个地方),然后再把这个缓存渲染到屏幕上面。

你可能希望强制离屏渲染,特别是计算很复杂的时候。这是一种缓存合成好的纹理或是层的方式。如果你的呈现树(render tree)是复杂的。那么就希望强制离屏渲染到缓存这些层,然后再使用缓存合成到屏幕。

如果你的APP有很多层,而且希望增加动画。GPU一般来说不得不重新合成所有的层在1秒60帧的速度下。当使用离屏渲染时,GPU需要合成这些层到一个新的位图纹理缓存里面,然后再用这个纹理绘制到屏幕上面。当这些层一起移动时,GPU可以重复利用这个位图缓存,这样就可以提高效率。当然,如果这些层没有修改的化,才能有效。如果这些层被修改了,GPU就不得不重新创建这个位图缓存。你可以触发这个行为,通过设置shouldRasterize = YES

这是一个权衡,如果只是绘制一次,那么这样做反而会更慢。创建一个额外的缓存对GPU来说是一个额外的工作,特别是如果这个位图永远没有被复用。这个实在是太浪费了。然而,如果这个位图缓存可以被重用,GPU也可能把缓存删掉了。所以你需要计算GPU的利用率和帧的速率来判断这个位图是否有用

离屏渲染也可以在一些其他场景发生。如果你直接或是间接的给一个层增加了遮罩。Core Animation 会为了实现遮罩强制做离屏渲染。这个增加了GPU的负担,因为一般上来,这些都是直接在屏幕上面渲染的。

Instrument的Core Animation 有一个叫做Color Offscreen-Rendered Yellow的选项。它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。同时确保勾选Color Hits Green and Misses Red选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。

一般来说,你需要避免离屏渲染。因为这个开销很大。在屏幕上面直接合成层要比先创建一个离屏缓存然后在缓存上面绘制,最后再绘制缓存到屏幕上面快很多。这里面有2个上下文环境的切换(切换到屏幕外缓存环境,和屏幕环境)。

所以当你打开Color Offscreen-Rendered Yellow后看到黄色,这便是一个警告,但这不一定是不好的。如果Core Animation能够复用屏幕外渲染的结果,这便能够提升性能,当绘制到缓存上面的层没有被修改的时候,就可以被复用了。

注意,缓存位图的尺寸大小是有限制的。Apple 提示大约是2倍屏幕的大小。

如果你使用的层引发了离屏渲染,那么你最好避免这种方式。增加遮罩,设置圆角,设置阴影都造成离屏渲染。

对于遮罩来说,圆角只是一个特殊的遮罩。clipsToBounds 和 masksToBounds 2个属性而已。你可以简单的创建一个已经设置好遮罩的层创建内容。比如,使用已经设置了遮罩的图片。当然,这个也是一种权衡。如果你希望在层的contents属性这只一个矩形的遮罩,那你更应该使用contentsRect而不是使用遮罩。

如果你最后这是shouldRasterize = YES,记住还要设置rasterizationScale = contentsScale

更多的关于合成

通常,维基百科上面有许多关于图像合成的背景知识。我们这里简单的拓展一下像素中的红、绿、蓝以及alpha是如何呈现在内存中的。

OSX

如果你在OSX上面工作,你会发现大部分的这些调试选项在一个独立的叫做“Quartz Debug”的程序里面。而并不在 Instruments 中。Quartz Debug是Graphics Tools中的一部分,这可以在苹果的developer portal中下载到

Core Animation & OpenGL ES

就像名字所建议的那样,Core Animation 让我们可以创建屏幕动画。我们将跳过大部分的动画,关注于绘制部分。重要的是,Core Animation允许你坐高效的渲染。这就是为什么你可以通过Core Animation 实现每秒60帧的动画。

Core Animation 的核心就是基于OpenGL ES的抽象。简单说,它让你使用OpenGL ES的强大能力而不需要知道OpenGL ES的复杂性。当我讨论像素合成的时候,我们提到的层(layer)和 纹理(texture)是等价的。他们准确来说不是一个东西,但是缺非常类似。

Core Animation的层可以有多个子层。所以最后形成了一个layer tree。Core Animation做的最复杂的事情就是判断出那些层需要被绘制或重新绘制,那些层需要OpenGL ES 去合成到屏幕上面。

例如,当你这是一个layer的contents属性是一个CGImageRef时,Core Animation创建一个OpenGL 纹理,然后确保这个图片中的位图上传到指定的纹理中。或者,你重写了-drawInContext方法,Core Animation 会分配一个纹理,确保你的Core Graphics的调用将会被作用到这个纹理中。层的 性质和CALayer的子类会影响OpenGL渲染方式的效率。很多底层的OpenGL ES行为被简单的封装到容易理解的CALayer的概念中去。

Core Animation通过Core Graphics和OpenGL ES,精心策划基于CPU的位图绘制。因为Core Animation在渲染过程中处于非常重要的地位,所以如何使用Core Animation,将会对性能产生极大影响。

CPU限制 vs GPU限制(CPU bound vs. GPU bound)

当在屏幕上面显示的时候,有很多组件都参与其中。这里面有2个主要的硬件分别是CPU和GPU。P和U的意思就是处理单元。当东西被显示到屏幕上面是,CPU和GPU都需要处理计算。他们也都受到限制。

为了能够达到每秒60帧的效果,你需要确保CPU和GPU都不能过载。也就是说,即使你当前能达到60fps,你还是要尽可能多的绘制工作交给GPU做。CPU需要做其他的应用程序代码,而不是渲染。通常,GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些。

因为绘制的性能是基于GPU和CPU的。你需要去分辨哪一个是你绘制的瓶颈。如果你用尽的GPU的资源,GPU是性能的瓶颈,也就是绘制是GPU的瓶颈,反之就是CPU的瓶颈。

如果你是GPU的瓶颈,你需要为GPU减负(比如把一些工作交给CPU),反之亦然。

如果是GPU瓶颈,可以使用OpenGL ES Driver instrument,然后点击 i 按钮。配置一下,同时注意查看Device Utilization % 是否被选中。然后运行app。你会看到GPU的负荷。如果这个数字接近100%,那么你交给GPU的工作太多了。

CPU瓶颈是更加通常的问题。可以通过Time Profiler instrument,找到问题所在。

Core Graphics / Quartz 2D

通过Core Graphics这个框架名字,Quartz 2D更被人所知。

Quartz 2D 有很多小功能,我们不会在这里提及。我们不会讲有关PDF创建,绘制,解析或打印。只需要了解答应PDF和创建PDF和在屏幕上面绘制位图原理几乎一致,因为他们都是基于Quartz 2D。

让我们简单了解一下Quartz 2D的概念。更多细节可以参考Apple的 官方文档

Quartz 2D是一个处理2D绘制的非常强大的工具。有基于路径的绘制,反锯齿渲染,透明图层,分辨率,并且设备独立等很多特性。因为是更为底层的基于C的API,所以看上去会有一点让人恐惧。

主要概念是非常简单的。UIKit和AppKit都封装了Quartz 2D的一些简单API,一旦你熟练了,一些简单C的API也是很容易理解的。最后你可以做一个引擎,它的功能和Photoshop一样。Apple提到的一个 app,就是一个很好的Quartz 2D例子。

当你的程序进行位图绘制时,不管使用哪种方式,都是基于Quartz 2D的。也就是说,CPU通过Quartz 2D绘制。尽管Quartz可以做其他事情,但是我们这里还是集中于位图绘制,比如在缓存(一块内存)绘制位图会包括RGBA数据。

比方说,我们要画一个八角形,我们通过UIKit能做到这一点

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

Core Graphics 的代码差不多

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);

问题就是,绘制到哪里呢? 这就是CGContext做的事情。我们传递的ctx这个参数。这个context定义了我们绘制的地方。如果我们实现了CALayer的-drawInContext:方法。我们传递了一个参数context。在context上面绘制,最后会在layer的一个缓存里面。我们也可以创建我们自己的context,比如 CGBitmapContextCreate()。这个函数返回一个context,然后我们可以传递这个context,然后在刚刚创建的这个context上面绘制。

这里我们发现,UIKit的代码并没有传递context。这是因为UIKit或AppKit的context是隐形的。UIKit和UIKit维护着一个context栈。这些UIKit的方法始终在最上面的context绘制。你可以使用UIGraphicsPushContext()和 UIGraphicsPopContext()来push和pop对应的context。

UIKit有一个简单的方式,通过 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext()来创建一个位图context,和 CGBitmapContextCreate()一样。混合UIKit和 Core Graphics调用很简单。

UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
...
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();

或其他方式

CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
...
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);

通过 Core Graphics可以做很多有趣的事情。苹果的文档有很多例子,我们这里就不太细说他们了。但是Core Graphics有一个非常接近Adobe Illustrator和Adobe Photoshop如何工作的绘图模型,并且大多数工具的理念翻译成Core Graphics了。毕竟这就是NextStep一开始做的。

CGLayer

一件非常值得提起的事,便是CGLayer。它经常被忽视,并且它的名字有时会造成困惑。他不是Photoshop中的图层的意思,也不是Core Animation中的层的意思。

把CGLayer想象成一个子context。它共用父context的所有特性。你可以独立于父context,在它自己的缓存中绘制。并且因为它跟context紧密的联系在一起,CGLayer可以被高效的绘制到context中。

什么时候这将变得有用呢?如果你用Core Graphics来绘制一些相当复杂的,并且部分内容需要被重新绘制的,你只需将那部分内容绘制到CGLayer一次,然后便可绘制这个CGLayer到父context中。这是一个非常优雅的性能窍门。这和我们前面提到的离屏绘制概念有点类似。你需要做出权衡,是否需要为CGLayer的缓存申请额外的内存,确定这是否对你有所帮助。

像素(Pixels)

屏幕上面的像素是通过3个颜色组成的:红,绿,蓝。因此位图数据有时候也被成为RGB数据。你可能想知道这个数据在内存中是什么样子。但是实际上,有非常非常多的方式。

后面我们会提到压缩,这个和下面讲得完全不一样。现在我们看一下RGB位图数据。RGB位图数据的每一个值有3个组成部分,红,绿,蓝。更多的时候,我们有4个组成部分,红,绿,蓝,alpha。这里我们讲4个组成部分的情况。

默认的像素布局

iOS和OS X上面的最通常的文件格式是32 bits-per-pixel (bpp),8 bits-per-component (bpc),alpha会被预先计算进去。在内存里面像这个样子

  A   R   G   B   A   R   G   B   A   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

这个格式经常被叫做ARGB。每一个像素使用4个字节,每一个颜色组件1个字节。每一个像素有一个alpha值在R,G,B前面。最后RGB分别预先乘以alpha。如果我们有一个橘黄的颜色。那么看上去就是 240,99,24. ARGB就是 255,240,99,24 如果我们有一个同样的颜色,但是alpha是0.33,那么最后的就是 ARGB就是 84,80,33,8

另一个常见的格式是32bpp,8bpc,alpha被跳过了。

x   R   G   B   x   R   G   B   x   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
0   1   2   3   4   5   6   7   8   9   10  11 ...

这个也被称为xRGB。像素并没有alpha(也就是100%不透明),但是内存结构是相同的。你可能奇怪为什么这个格式很流行。因为如果我们把这个没用的字节从像素中去掉,我们可以节省25%的空间。实际上,这个格式更适合现代的CPU和图像算法。因为每一个独立的像素和32字节对齐。现代CPU不喜欢读取不对其的数据。算法会处理大量的位移,特别是这个格式和ARGB混合在一起的时候。

当处理xRGB时,Core Graphic也需要支持把alpha放到最后的格式,比如RGBA,RGBx(RGB已经预先乘以alpha的格式)

深奥的布局

大多数时候,当我们处理位图数据时,我们就在使用Core Graphic 或是 Quartz 2D。有一个列表包括了所有的支持的文件格式。让我们先看一下剩余的RGB格式。

有16bpp,5bpc,不包括alpha。这个格式比之前节省50%空间(2个字节一个像素)。但是如果解压成RGB数据在内存里面或是磁盘上面就有用了。但是,因为只有5个字节一个像素,图像特别是一些平滑的渐变,可能就混合到一起了。(图像质量下降)。

还有一个是64bpp,16bpc,最终为128bpp,32bpc,浮点数组件(有或没有alpha值)。它们分别使用8字节和16字节,并且允许更高的精度。当然,这会造成更多的内存和更复杂的计算。

最后,Core Graphics 也支持一些其他格式,比如CMYK,还有一些只有alpha的格式,比如之前提到的遮罩。

平面数据 (Planar Data color plane)

大多数的框架(包括 Core Graphics)使用的像素格式是混合起来的。这就是所谓的 planar components, or component planes。每一个颜色组件都在内存中的一个区域。比如,对于RGB数据。我们有3个独立的内存空间,分别保存红色,绿色,和蓝色的数值。

在某些情况下,一些视频框架会使用 Planar Data。

YCbCr

YCbCr 是一个常见的视频格式。同样有3个部分组成(Y,Cb,Cr)。但是它更倾向于人眼识别的颜色。人眼是很难精确识别出来Cb和Cr的色彩度。但是却能很容易识别出来Y的亮度。在相同的质量下,Cb和Cr要比Y压缩的更多。

JPEG有时候把RGB格式转换为YCbCr格式。JPEG单独压缩每一个color plane。当压缩YCbCr格式时,Cb和Cr比Y压缩得更好。

图片格式

iOS和OSX上面的大多数图片都是JPEG和PNG格式。下面我们再了解一下

JPEG

每个人都知道JPEG,他来自相机。他代表了图片是图和存储在电脑里,即时是你的妈妈也听过JPEG。

大家都认为JPEG就是一个像素格式。就像我们之前提到的RGB格式一样,但是实际上并不是这样。

真正的JPEG数据变成像素是一个非常复杂的过程。一个星期都没有办法讲清楚,或是更久。对于一个color plane, JPEG使用一种离散余弦变换的算法。讲空间信息转换为频率(convert spatial information into the frequency domain)。然后通过哈夫曼编码的变种来压缩。一开始会把RGB转换成YCbCr,解压缩的时候,再反过来。

这就是为什么从一个JPEG文件创建一个UIImage然后会知道屏幕上面会有一点点延迟的原因。因为CPU正在忙于解压图片。如果每个TableViewCell都需要解压图片的话,那么你的滚动效果就不会平滑。

那么,为什么使用JPEG文件呢?因为JPEG可以把图片压缩的非常非常好。一个没有压缩过的IPhone5拍照的图片差不多24MB。使用默认的压缩设置,这个只有2-3MB。JPEG压缩效果非常好,因为几乎没有损失。他把那些人眼不能识别的部分去掉了。这样做可以远远的超过gzip这样的压缩算法。但是,这个仅仅在图片上面有效。因为,JPEG依赖于丢掉那些人眼无法识别的数据。如果你从一个基本是文本的网页截取一张图片,JPEG就不会那么高效,压缩效率会变得低下。你甚至都可以看出图片已经变形了。

PNG

PNG读作“ping”,和JPEG相反,他是无损压缩的。当你保存图片成PNG时,然后再打开。所有的像素数据和之前的完全一样。因为有这个限制,所有PNG压缩图片的效果没有JPEG那么好。但是对于app中的设计来说,比如按钮,icon,PNG就非常适合。而且PNG的解码工作要比JPEG简单很多。

在真实的世界里面,事情没有这么简单。有很多不同的PNG格式。维基百科上面有很多细节。但是简单说,PNG支持压缩有alpha或是没有alpha通道的RGB像素,这也就是为什么他适合app上面的原因。

格式挑选

当在app中使用颜色是,你需要使用者2种格式中得一个,PNG和JPEG。他们的解码和压缩算法都是被高度硬件优化的。有些情况甚至支持并行计算。同时Apple也在不断地提高解码的能力在未来的操作系统版本中。如果使用其他格式,这可能会对你的程序性能产生影响,而且可能会产生漏洞,因为图像解码的算法是黑客们最喜欢攻击的目标。

已经讲了好多有关PNG的优化了,你可以在互联网上面自己查找。这里需要注意一点,Xcode的压缩算法和大部分的压缩引擎不一样。

当Xcode压缩png时,技术上来说,已经不是一个有效的PNG文件了。但是iOS系统可以读取这个文件,然后比通常的PNG图片处理速度更快。Xcode这样做,是为了更好地利用解码算法,而这些解码算法不能在一般的PNG文件上面适用。就像上面提到的,有非常多的方法去表示RGB数据。而且如果这个格式不是iOS图形图像系统需要的,那么就需要增加额外的计算。这样就不会有性能上的提高了。

再抢到一次,如果你可以,你需要设置 resizable images。你的文件会变得更小,因此,这样就会有更小的文件需要从文件系统里面读取,然后在解码。

UIKit and Pixels

UIKit中得每一个view都有自己的CALayer,一般都有一个缓存,也就是位图,有一点类似图片。这个缓存最后会被绘制到屏幕上面。

-drawRect:

如果你的自定义view的类实现了-drawRest:,那么就是这样子工作的:

当你调用-setNeedsDisplay时,UIKit会调用这个view的层的 -setNeedsDisplay方法。这个设置一个标记,表明这个层已经脏了(dirty,被修改了)。实际上,并没有做任何事情,所以,调用多次-setNeedsDisplay 没有任何问题。

当渲染系统准备好后,会调用层的-display方法。这时,层会设置缓存。然后设置缓存的Core Graphics的上下文环境(CGContextRef)。后面的绘制会通过这个CGContextRef绘制到缓存中。

当你调用UIKit中的函数,比如UIRectFill() 或者 -[UIBezierPath fill]时,会通过这个CGContextRef调用你的drawRect方法。他们是通过把上面的CGContextRef push 到 图形图像堆栈中,也就是设置成当前的上下文环境。UIGraphicsGetCurrent()会返回刚才push的那个context。由于UIKit绘制方法使用UIGraphicsGetCurrent(),所以这些绘制会被绘制到缓存中。如果你希望直接使用 Core Graphics 方法,那么你需要调用UIGraphicsGetCurrent()方法,然后自己手动传递context参数到Core Graphics的绘制函数中去。

那么,一个个层的缓存都会被绘制到屏幕上面,知道下一次设置-setNeedsDisplay,然后再重新更新缓存,再重复上面的过程。

不使用 drawRect

当你使用UIImageView的时候,有一点点的不同。这个view依然包含一个CALayer,但是这个层并不会分配一个缓存空间。而是使用CGImageRef作为CALayer的contents属性,渲染系统会把这个图片绘制到帧的缓存,比如屏幕。

这个情况下,就没有继续绘制的过程了。我们就是简单的通过传递位图这种方式把图片传递给UIImageView,然后传递给Core Animation,然后传递给渲染系统。

使用drawRect 还是不使用drawRect

听上去不怎么样,但是,最快速的方法,就是不使用。

大多数情况,你可以通过自定义view或是组合其他层来实现。可以看一下Chris的文章,有关自定义控件。这个方法是推荐的,因为UIKit非常高效。

当你需要自定义绘制的时候 WWDC2012 session 506 Optimizing 2D Graphics and Animation Performance是一个非常好的例子 。

另一个地方需要自定义绘制的是iOS的股票软件。这个股票图是通过Core Graphics实现的。注意,这个只是你需要自定义绘制,并不是一定要实现drawRect函数,有时候通过UIGraphicsBeginImageContextWithOptions()或是 CGBitmapContextCreate()创建一个额外的位图,然后再上面绘制图片,然后传递给CALayer的contents会更容易。下面有一个测试例子

单色

这是一个简单的例子

// Don't do this
- (void)drawRect:(CGRect)rect
{
    [[UIColor redColor] setFill];
    UIRectFill([self bounds]);
}

我们知道为什么这样做很烂,我们让Core Animation创建了一个额外的缓存,然后我们让Core Graphics 在缓存上面填充了一个颜色。然后上传给了GPU。

我们可以不实现-drawRect:函数来省去这些步骤。只是简单的设置view的backgroundColor就好了。如果这个view有CAGradientLayer,那么同样的方法也可以设置成渐变的颜色。

可变大小的图片(resizable image)

你可以简单的通过可变大小的图片减少图形系统的工作压力。如果你原图上面的按钮大小是300*50。那么就有 600 * 100 = 60k 像素 * 4 = 240KB的内存数据需要传递给GPU。传递给显存。如果我们使用resizable image。我们可以使用一个 52 * 12 大小的图片,这样可以节省10kb的内存。这样会更快。

Core Animation 通过 contentsCenter 来resize图片,但是,更简单的是通过 -[UIImage resizableImageWithCapInsets:resizingMode:]。

而且,在第一次绘制的时候,我们并不需要从文件系统读取60K像素的PNG文件,然后解码。越小的图片解码越快。这样,我们的app就可以启动的更快。

并发绘制

上一个我们讲到了并发。UIKit的线程模型非常简单,你只能在主线程使用UIKit。所以,这里面还能有并发的概念?

如果你不得不实现-drawRect:,并且你必须绘制大量的东西,而这个会花费不少时间。而且你希望动画变得更平滑,除了在主线程中,你还希望在其他线程中做一些工作。并发的绘图是复杂的,但是除了几个警告,并发的绘图还是比较容易实现的。

你不能在CAlayer的缓存里面做任何事情出了主线程,否则不好的事情会发生。但是你可以在一个独立的位图上面绘制。

所有的Core Graphics的绘制方法需要一个context参数,指定这个绘制到那里去。UIKit有一个概念是绘制到当前的context上。而这个当前的context是线程独立的。

为了实现异步绘制,我们做下面的事情。我们在其他队列(queue,GCD中的概念)中创建一个图片,然后我们切换到主队列中把结果传递给UIImageView。这个技术被WWDC 2012 session 211中提到

- (UIImage *)renderInImageOfSize:(CGSize)size;
{
    UIGraphicsBeginImageContextWithOptions(size, NO, 0);

    // do drawing here

    UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return result;
}

这个函数通过UIGraphicsBeginImageContextWithOptions 创建一个新的CGContextRef。这个函数也修改了当前的UIKit的context。然后可以铜鼓UIKit的方法绘制,然后通过UIGraphicsGetImageFromCurrentImageContext()根据位图数据生成一个UIImage。然后关闭掉创建的这个context。

保证线程安全是非常重要的,比如你访问UIKit的属性,必须线程安全。如果你在其他队列调用这个方法,而这个方法在你的view类里面,这个事情就可能古怪了。更简单的方法是创建一个独立的渲染类,然后当触发绘制这个图片的时候才设置这些必须得属性。

但是UIKit的绘制函数是可以在其他队列中调用的,只需要保证这些操作在 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext ()之前就好。

你可以通过下面的方法触发绘制

UIImageView *view; // assume we have this
NSOperationQueue *renderQueue; // assume we have this
CGSize size = view.bounds.size;
[renderQueue addOperationWithBlock:^(){
    UIImage *image = [renderer renderInImageOfSize:size];
    [[NSOperationQueue mainQueue] addOperationWithBlock:^(){
        view.image = image;
    }];
}];

注意view.image = image; 必须在主队列调用。这是非常重要的细节。你不能在其他队列中调用。

通常来说,异步绘制会带来很多复杂度。你需要实现取消绘制的过程。你还需要限制异步操作的最大数目。

所以,最简单的就是通过NSOperation的子类来实现renderInImageOfSize方法。

最后,有一点非常重要的就是异步设置UITableViewCell 的content有时候很诡异。因为当异步绘制结束的时候,这个Cell很可能已经被重用到其他地方了。

CALayer的奇怪和最后

现在你是到了CALayer某种程度上很像GPU中的纹理。层有自己的缓存,缓存就是一个会被绘制到屏幕上的位图。 大多数情况,当你使用CALayer时,你会设置contents属性给一个图片。这个意思就是告诉 Core Animation,使用这个图片的位图数据作为纹理。 如果这个图片是PNG或JPEG,Core Animation 会解码,然后上传到GPU。

当然,还有其他种类的层,如果你使用CALayer,不设置contents,而是这事background color, Core Animation不会上传任何数据给GPU,当然这些工作还是要被GPU运算的,只是不需要具体的像素数据,同理,渐变也是一个道理,不需要把像素上传给GPU。

图层和自定义绘制

如果CALayer或是子类实现了 -drawInContext 或是-drawLayer:inContext delegate。Core Animation会为这个layer创建一个缓存,用来保存这些函数中绘制的结果。这些代码是在CPU上面运行的,结果会被传递给GPU。

形状和文本层(Shape and Text Layers)

形状和文本层会有一点不同。首先,Core Animation 会为每一个层生成一个位图文件用来保存这些数据。然后Core Animation 会绘制到layer的缓存上面。如果你实现了-drawInContext方法,结果和上面提到的一样。最后性能会受到很大影响。

当你修改形状层或是文本层导致需要更新layer的缓存时,Core Animation会重新渲染缓存,比如。当实现shape layer的大小动画时,Core Animation会在动画的每一帧中重新绘制形状。

异步绘制

CALayer 有一个属性是 drawsAsynchronously。这个似乎看上去很不错,可以解决所有问题。实际上虽然可能会提高效率,但是可能会让事情更慢。

当你设置 drawsAsynchronously = YES 后,-drawRect: 和 -drawInContext: 函数依然实在主线程调用的。但是所有的Core Graphics函数(包括UIKit的绘制API,最后其实还是Core Graphics的调用)不会做任何事情,而是所有的绘制命令会被在后台线程处理。

这种方式就是先记录绘制命令,然后在后台线程执行。为了实现这个过程,更多的事情不得不做,更多的内存开销。最后只是把一些工作从主线程移动出来。这个过程是需要权衡,测试的。

这个可能是代价最昂贵的的提高绘制性能的方法,也不会节省很多资源。

翻译吐槽

这篇文章实在是太长了,而且太啰嗦了。真是累觉不爱。觉得侮辱读者智商,不过内容还是非常值得学习的。

翻译:《iOS7 新功能 视图控制器API》

| Comments

这篇文章投稿在 伯乐在线

原文出处: objc

View Controller Transitions

Issue #5 iOS 7, October 2013

作者 Chris Eidhof

自定义动画

iOS7对我来说最激动人心的特性就是新的 View Controller Transitioning API。 iOS7之前,View Controller之间切换,我需要创建自定义的transitions。 而且这些方法都支持不完整,让人头疼。在transitions中增加交互功能就更难了。

在开始这篇文章之前,我要提醒一下:这是一个新的API,我们尽最大努力让他可以实用,但是并不能保证是最佳。可能需要至少一个月后才能确定,这篇文章是不是最佳的实用方案,这里只是一个对新功能的探索。如果有更好的使用这个API的方法,请联系我们,这样就可以修正这篇文章。

在开始介绍这个API之前,我们需要知道导航控制器的默认行为在iOS7下已经改变了:导航控制器下,切换2个view controller的动画有一点细微的改变,变得更有交互性。例如,当你希望弹出一个view controller时,可以从屏幕左边开始拖动,把整个内容拖动到屏幕右边。

让我们仔细看一下这个API,我发现这个被重度使用的接口是协议并不是一个实体。虽然一上来看上去有一点怪,但是我喜欢这个API,它给了我们更多的灵活性。我们从简单开始:用自定义动画代替原有的view controller的push动画(这里是sample project 在github)。我们首先需要实现这个新的 UINavigationControllerDelegate 方法:

- (id<UIViewControllerAnimatedTransitioning>)
               navigationController:(UINavigationController *)navigationController
    animationControllerForOperation:(UINavigationControllerOperation)operation
                 fromViewController:(UIViewController*)fromVC
                   toViewController:(UIViewController*)toVC
{
    if (operation == UINavigationControllerOperationPush) {
        return self.animator;
    }
    return nil;
}

我们可以观察一下这种类型的操作(push 和 pop)返回一个不同的 animator。如果我们分享代码的话,这个可能是一个对象。我们可能需要把这个变量通过property保存下来。我们也可以为不同的操作创建不同的对象,这里有很高的灵活性。

让这个动画运行起来,我们创建一个自定义对象实现 UIViewControllerContextTransitioning 协议。

@interface Animator : NSObject <UIViewControllerAnimatedTransitioning>

@end

这个协议要求我们实现2个方法,其中一个是描述动画的执行时间

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.25;
}

另一个是描述动画的执行。

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1);
        toViewController.view.alpha = 1;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];

    }];

}

这里你可以看到这个协议是怎么用的:没有提供实体的对象参数,而是通过这个类型 id 得到transitionContext 唯一的最重要的东西就是在完成动画之后要调用 completeTransition 这个告诉 transitionContext 我们已经完成动画并且相应的更新了 view controller的状态。其他代码是标准的,我们通过transitionContext得到2个UIViewController,然后使用简单的 UIView 动画,这里我们很简单的做了一个zooming的动画

注意,我们只是写了push的自定义动画,当view controller pop时,iOS系统还是会使用默认的滑动动画。而且,实现这个方法后。导航栏也不能交互了(就是从左到右拖动实现pop view controller)。下面完善他

交互动画

让之前的动画变得能够交互起来非常简单。我们需要实现另一个UINavigationControllerDelegate

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
    return self.interactionController;
}

注意,如果在一个不能交互的动画中,这里会返回nil。(译注:当不能交互时 self.interactionController 为 nil)

interactionController是UIPercentDrivenInteractionTransition的实例,没有必要更多的设置。我们通过创建拖动手势(UIPanGestureRecognizer)来实现:

if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
    if (location.x >  CGRectGetMidX(view.bounds)) {
        navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self performSegueWithIdentifier:PushSegueIdentifier sender:self];
    }
} 

只有当用户在屏幕右边操作时,我们才设置动画是可以交互的(通过设置interactionController 属性)。然后我们调用performSegueWithIdentifier(或是不用storyboards,直接push view controller) 在这个手势变化中,我们调用interactionController 的一个方法 updateInteractiveTransition:

else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
    CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1;
    [interactionController updateInteractiveTransition:d];
} 

这里根据拖动的距离设置百分比,非常cool的事情是交互控制器(interactionController)和 动画控制器(animation controller)相互协作。而且因为是普通的 UIView 动画,它控制着动画的进程。我们不需要处理他们之前的事情, 所有的事情都在背后默默的自动搞定了。

最后,当手势停止或是取消掉,我们需要调用interaction controller相应的方法

else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
    if ([panGestureRecognizer velocityInView:view].x < 0) {
        [interactionController finishInteractiveTransition];
    } else {
        [interactionController cancelInteractiveTransition];
    }
    navigationControllerDelegate.interactionController = nil;
}

当切换动画完毕时,设定interactionController为nil非常重要。如果下一个动画是非交互的,我们不希望得到一个奇怪的 interactionController

现在我们已经有一个完整的自定义的可交互的过度变换(transition)了。通过普通的拖动手势和一个UIKit提供的实体对象,几行代码就搞定了。对于大多数的自定义交互过度变换,你可以在这里停下来,用上面提到的方法做任何你想做得动画 或是交互。

GPUImage自定义动画

我们现在已经能够实现一个完整的自定义动画了,可以不用UIView 甚至Core Animation,做自己喜欢的动画。一开始,我用Core Image实现了一个项目Letterpress-style。但是在我的旧iPhone4上面只能跑到大约9FPS,这个和我所期望的60FPS差距太大了。

但是当我使用GPUImage后,实现一个非常漂亮的自定义动画效果变得非常简单。我们希望这个动画能够做到像素级的消融在2个view controller切换的时候。这个是通过分别对2个view controller 截屏,然后应用GPUImage的图片滤镜实现的。

首先,我们创建一个自定义类,实现animation 和 interactive transition 协议。

@interface GPUImageAnimator : NSObject
  <UIViewControllerAnimatedTransitioning,
   UIViewControllerInteractiveTransitioning>

@property (nonatomic) BOOL interactive;
@property (nonatomic) CGFloat progress;

- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;

@end

为了让这个动画跑的飞快,我们只把图片传给GPU一次,然后把所有的图像处理绘制交给GPU,而不是传给CPU(GPU和CPU之间的数据传输非常慢)。通过GPUImageView,我们可以用OpenGL绘制动画效果(不需要手动编写底层的OpenGL代码,我们可以继续编写上层代码)

创建这样的滤镜链非常方便。这里可以看一下下面的例子。有一点挑战的是实现动态的滤镜。GPUImage不能给我们直接提供动画效果。这里我们通过在每一帧的时候更新滤镜来实现动画的绘制。我们使用CADisplayLink类来做这个。

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

在frame:方法中,我们根据时间更新动画进度,然后更新滤镜

- (void)frame:(CADisplayLink*)link
{
    self.progress = MAX(0, MIN((link.timestamp - self.startTime) / duration, 1));
    self.blend.mix = self.progress;
    self.sourcePixellateFilter.fractionalWidthOfAPixel = self.progress *0.1;
    self.targetPixellateFilter.fractionalWidthOfAPixel = (1- self.progress)*0.1;
    [self triggerRenderOfNextFrame];
}

以上就是我们所有要讲得了。在交互变换中,我们需要确保我们的进度是根据手势识别设置的,而不是根据时间。但是剩下的代码几乎都一样了。

这个真的太强大了,你可以使用GPUImage提供的任何滤镜或是自己写的OpenGL代码来实现上面的效果。

小结

我们这里仅仅提到了导航控制器下面的2个 view controller 之间的动画,事实上你可以做相同的事情在tabbar controller 或是自定义的container view controller。而且 UICollectionViewController 现在已经可以在layout上面自动实现交互动画了。他们都是使用相同的机制。这个真的太强大了。

当我和Orta提到这个API时,他指出他已经使用这个功能创建了一些轻量级的view controller。不要在每一个view controller 保存管理动画的代码,而是创建一个新的view controller,然后实现2个view controlller视图切换时的自定义的动画效果。

更多

第一次马拉松,也是这段时间健身的总结

| Comments

第一次接力马拉松

11月16日是个特别的日子,健身满16个月,更重要的是参加了人生第一次马拉松,香山接力赛,平均坡度35,一次往返3km,一个队伍4个人,每人4圈,每人总共12km。时间从上午11点,跑到下午4点。16个月前,当我开始准备减肥的时候,我从来没有想过可以一口气做6个引体向上,没有想过可以一口气600多个仰卧起坐,也没有想过我的胸围可以长5cm,没有想过我的腰围可以减少12cm,没有想到我可以减40斤,更没有想过,我居然有了参加山地马拉松的能力,虽然仅仅是接力赛。

说道马拉松,不得不多说几句,跑一次全程马拉松是我30岁前必须完成的人生目标之一。为了方便去香山,15日我暂住一个朋友家,在14日在家的时候,我开心的一晚上睡不着觉,弄得我不得不担心我会不会因为过于兴奋而导致体力消耗太大最后不能完赛。好在16日的时候,我已经完全调整过来,而且老天也特别配合,PM2.5出奇的低,给我一个风和日丽的早上。整个比赛还算轻松,或是对自己要求不够高吧,除了第一圈比较吃力以外,后面越来越好,特别是最后一圈,如果再给我一次机会,我会更努力的跑。

16个月运动统计

说了这么多虚的,这是我运动的统计,由于没有想到过自己能够坚持这么长时间,统计只是从2013年4月5日开始到今天。总共226天的时间其中健身160.73小时,下面是每个月的运动时间统计,纵坐标单位分钟。

image

一个月的维度已经很大了。这个充分说明,即便我已经有了这样大的运动效果,16个月,我的健身习惯还没有真正的养成,不管是计划制定,还是执行层面,因为一个习惯的正常曲线应该是这样子的。

image

这是我的英语听写计划时间统计,英语计划相对健身更简单可控。记得有一本书中写道,一个成熟的人制定的计划是稳步递增的,而那些年轻人的计划,往往都是大起大落。好吧,这里就不嘲笑自己了。

很多人都问我,坚持下来的原因是什么,这个真的很难说,在一开始的时候,想法总是很简单的,和很多人一样,希望更好看一点,更健壮一点。但是如果一直是这么简单的想法,必然不能长久,很多东西会在这个改变的过程中加入进来,甚至变成最重要的东西。反之也会有很多东西变得不那么重要最后离我而去。现在来说,最大的原因在于我的性格上的变化。这半年来,我性格上面最大的变化就是变得越来越goal oriented, 在健身的过程中,最能体现的就是一个个目标达成的幸福感,这个让我非常开心,因为就目前我能做的事情来看,像健身这种只要做,就比不做强的事情,而且还有不少附加价值的实在是少之又少。

意志力

健身的过程的确是很苦的,当然这个世界上被认为有价值的习惯都是很难达到,需要花费大量的时间,不仅是健身,还是英语我实际花费的时间都比上面统计的要多的多。比如你可能需要学习做饭,你可能需要控制饮食,我在减脂的3个月中,几乎不吃肉,把一切零食,甜食,饮料当成毒药。忌口对于我这种意志力薄弱的人来说,真的是相当的难。不管是再好吃的东西,要做到能够停下来,或是坚决不吃。一个朋友说过这样子的话“我常常把吃得放在一边,人必须学会控制自己的嘴巴,这是一种训练意志力的方式,因为很简单,如果自己的嘴巴都控制不了,还指望她能做点什么”。这个怎么说呢,话粗理不粗吧。我反正是不同意这个观点,但是喜欢这个做事态度。

说道意志力,让我想起了施瓦辛格说的一句话“健身的精髓在于力竭后的最后一个”,同样还有很多类似的话“马拉松比赛在20km之后才开始”。对现在的我来说,健身在跑完5km,或是300个仰卧起坐,或是300个深蹲又或是10组pull ups,俯卧撑,推胸之后才真正开始,之前做得一切运动都只是为了积累身体疲劳到一定程度,这样才能有意义,因为力竭之后的动作,才是给身体最好的信号“我要变得更强壮”。

讲锻炼意志力的书有很多很多,方法也很多很多,在健身的方面,我最喜欢的就是P90X里面教练提到的方法,另外我增加了最后面一条。

1、设定目标 2、达成目标 3、记录下来 4、忘掉它

设定目标

设定目标的动机很明白,跑马拉松的时候,大家不会设定一个终点这样的目标,而是将这个过程分解成一个弯角,一段斜坡。同样力量训练也是分成25个一组的小重量,8-12个的大重量。设定目标最大得作用在于clear your mind and focus on the things。

达成目标

达成目标,这个毋庸置疑,目标就是要被达成的。

记录下来

前2个一般人都可以做到,但是后面的2个才是真正的精髓。在任何过程中都会产生迷茫,哪怕是改变一个小小的习惯,因为不管怎样你都无法逃避掉一个问题“How to be better XXX”。记录下来就是为了这个准备的。我是那种容易消极的人,数据让我不会把一些局部失败看得太重,把局部成功看得太重。这一点对我帮助很大,我看到了我最近3个月器械力量增大一倍,但是整体时间依然波动很大等等这些都是帮助我抵抗不良情绪最好的良药。

忘掉它

忘掉它是一个很神奇的东西,在跳出舒适区的过程中,特别有用。比如你已经做了500多个仰卧起坐,每一个都已经burning,如果你脑子里面想得是我已经做了500个了,一般人200个都做不了,那么就很难做到600+。忘掉它是一种归零的心态,每一组动作都是一个新的起点,新的开始。也是对抗懒惰最好的良药,特别是我这种对自己要求比较低,喜欢放纵自己的人。我是真的对自己要求低,特别是在健身上面。我见过非常多的人一起跑步的时候吐过,一起踢球的时候抽筋过,也亲眼见过跑步训练跑到眼底角膜充血的,也见过做yoga把腿都拉伤的,很多很多的例子。而我则是被教练说“你就是对自己要求太低”,“你之前炼过吧,额,你怎么不跑了?还有5圈呢?”,从小到大一直都是。

后记

健身已经从一开始的甩掉大肚子,变成了我生活的一部分或是他本身就是我生活的缩影。在香山认识了也见识了很多牛人,有骑行川藏的,海拔5000km,有跑戈壁滩的,一天30km,连续跑4天的。而他们有的甚至平均年龄高达45岁。除了这些已经几乎bt的人以外,也有30大几的已婚姐姐锻炼成“钢筋铁骨”的,相比而言我这25岁的“小娃娃”,记得当时我开始健身的时候,最大的原因就在于,我已经25岁了,老了,而且我之前2次减肥失败,这次再不减肥肯定就完蛋了的心态。呵呵,现在想起来还是蛮好笑的。虽然人的确在经过25岁之后,心肺,肌肉等等身体各项机能都会开始退化开始衰老,更容易产生脂肪,但是我很开心,在我25岁的时候,我所做的一切。身体和灵魂,至少有一个需要在路上。