不会开机的男孩

iOS APP 架构漫谈

| Comments

最近看了一些有关server的东西,一些很简单的东西,不外乎是一些文档规范,另外结合最近看的wwdc的一些video,觉得对软件架构(software architecture)认识又清楚了一些,这里记录下来。

software architecture 听上去是一个很大的概念,实际上也包括很多东西,里面的争议也很多。在我看来软件架构最好放在小的场景中理解。

问题1

我们有2个页面。

  • 页面A:主页面
  • 页面B:详情页面

demo code 1.0.0

2个页面分别显示一个数字,这个数字应该相同。详情会修改这个数字,这里我们发现,详情页面和主页面数字不一样。

数据不一致

问题1 解决方法A

这里首先的感觉就是,详情页面返回,主页面数据没有刷新,导致数据不一致。 那么Fix这个Bug的方法,就是在主页面出现的时候刷新界面

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.displayLabel.text = [[CUDataDAO selectData].data stringValue];
}

现在来看,还不错。但是,我们调用selectData的次数则变得非常非常多。数据不是经常变化的。

demo code 1.0.1

问题1 解决方法B

我们发现既然数据的改变是在页面B进行的,那么页面B修改这个数据的时候,应该把数据变化”通知”给页面A,那么我们写了一个Delegate

@protocol CUDetailViewControllerDelegate <NSObject>

- (void)detailVC:(CUDetailViewController *)vc dataChanged:(NSNumber *)data;

@end

在页面B修改数据之后,通过delegate 通知给页面A。

- (IBAction)changeButtonClicked:(id)sender {
    int value = arc4random() % 100;
    [CUDataDAO setData:value];

    self.displayLabel.text = [@(value) stringValue];

    if ([self.delegate respondsToSelector:@selector(detailVC:dataChanged:)]) {
        [self.delegate detailVC:self dataChanged:@(value)];
    }
}

到此场景1得到了不错的解决。

demo code 1.0.2

问题2

这时我们增加了另一个页面C。这个场景会稍微抽象一点,我们定义了3个数据

  • 页面A的数据dataA
  • 页面B的数据dataB
  • 页面C的数据dataC

问题1中 dataA = dataB。在问题2中dataA = dataB + dataC;

问题2 解决方法C

也就是说页面C的修改,也会影响页面A的数据,那么我们是不是也要写一个XXXXDelegate呢?

这时我们的大脑嗅出了一些不好的味道,如果再来个什么dataD,dataE,我们要写这么多的Delegate么?对于多对一”通知”这种味道,很自然的想到了不用Delegate,而是用NSNotification来做。让我们未雨绸缪一下,定义一个Notificaiton

NSString *const kCUDataChangedNotification = @"CUDataChangedNotification";

[[NSNotificationCenter defaultCenter] postNotificationName:kCUDataChangedNotification
                                                  object:nil
                                                userInfo:nil];

那这个变化broadcast到listener,看上去是一个很赞的idea。

demo code 1.0.3

问题3

过了一段时间,我们发现问题2的方法有一个Bug,当界面停在页面B的时候,切换到页面C,修改数据,B中再返回时,数据和页面A的数据不一致。

数据不一致

那也可以类比解决方法B,得到了下面的方法

解决方法D

既然A和B的数据不一致,而A的数据比B的新,那么保留一个B的指针,然后A变化的时候,更新B就好了。

- (void)handleDataChangedNotification {
    [self updateLabel];
    [self.vc updateLabel];
}

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"push"]) {
        CUDetailViewController *vc = [segue destinationViewController];
        if ([vc isKindOfClass:[CUDetailViewController class]]) {
            self.vc = vc;
        }
    }
}

demo code 1.0.4

问题4

页面C实在是太简单了,这次我们希望在页面C中显示页面A的数据。因为上次我们就产生了一个数据不一致的问题,这次我们注意到了,那么怎么修改呢?

解决方法E

在看了看整个APP各种通知之后,觉得挺麻烦,准备用一个取巧的方法。可以类比解决方法A。在页面C出现的时候,刷新数据,至于什么性能问题,不管了,先fix bug。

- (void)viewWillAppear:(BOOL)animated {
    [self updateLabel];
}

- (void)updateLabel {
    int dataB = [[CUDataDAO selectData].data intValue];
    int dataC = [[CUDataDAO selectOtherData].data intValue];

    self.dataLabel.text = [@(dataB + dataC) stringValue];
}

demo code 1.0.5

问题5

这时的数据需要不断的变化,我们在CUDataDAO加了一个timer 模拟数据变化,数据变化的原因可能是server push 一些数据。client 本地数据库更新了数据,需要在页面A、B、C中显示。

页面C的数据又不一致了。。。。

问题到底在哪里呢

走到这里,我们需要重新思考为什么这个问题会不断的重复出现呢?software architecture就是来解决这个问题的。但是在提出一个合理的方案之前,先思考一个概念。

我们把数据库中的数据,显示到屏幕上,或是传递给View时,这个过程其实是对data 做了一次copy。而且只要不是通过引用或是指针这些方式,通过值传递的方式都是对data做了一次copy。而这个copy的过程,非常类似Cache

通常建立一个Cache会遇到2种问题。

  • Cache情况A: 与original Data 数据不一致,没有及时更新
  • Cache情况B: 重复建立Cache

让我们用这个思路来看我们的解决方案

解决方法A

这是一个非常典型的Cache情况B。数据库的数据并没有变化,但我们却多次重复计算cache

解决方法B

页面之间的关系可以用下面来描述

这里我们隐隐能够感觉到问题,A的数据变化依赖于2个地方。不急,再往后看

解决方法C

解决方法D

事情变得更糟了

解决方法E

和解决方法A类似,同样的重复计算Cache问题。

实际上问题还会更糟

现在还是一个简单的Model,如果project变得很大,那么就会变成这个样子

每一个X都可能是一个Bug。

我们似乎已经找到问题了

《Advanced iOS Application Architecture and Patterns》 中,把这个图叫做information flow。我们的直觉会告诉我们,这个信息的传递,应该是自上而下的树或是森林,而且最好是一个层次平衡结构,要清晰,每一个位置都有相对于的职责。那我们就需要制定一个规则。

在想这个规则之前,如果把上面的图背后的数据忘记,我们感觉这很类似内存模型。当然内存模型会比较复杂。但是我们可以借鉴很多”内存管理中的规则”,比如谁创建,谁销毁。同样,在我们的information flow中,我们希望谁创建Cache,谁更新Cache变化

DAO的数据库似乎很难做这件事情,我们引入了一个新的元素dataSource(当然他本身又是DAO的一个Cache)。其中A、B、C3个都会显示数据,那么他们应该在一个层级,其中B、C会修改数据,他们会把这个数据返回给dataSource,而通过dataSource来把这个变化通知到A、B、C。

这样带来的好处很明显,我们再添加一个D,也不会对其他地方的数据产生任何影响,我们的Unit Test、Mock也更加好写。

我们之前的思路错在哪里呢?

从局部来看,我们之前的思路都没有任何问题,但是整体来看却把问题隐藏化。关键的问题是在于没有找到Truth,找到问题真正的地方。而找到真正的地方,需要我们在大脑中有一个清晰的information flow或是data flow。了解之间元素的相互关系,才能建立一个个的层。才能坐到真正的解耦,解耦并不是仅仅一个个的Manager,更重要的是建立一套清晰的flow机制,或是消息机制,如果没有一套flow,中间引入的各种各样的方法,即便使用了各种设计模式,整个software 依然是深度耦合

疑问

这个APP看上去交互非常复杂

上面的model,有些同学还可能觉得这是交互上面的问题,这个交互看上去非常的复杂,不是一个好设计。

我这里列举一个实际的例子:

A页面要创建动画,动画背后包括很多数据,这些数据会在B,C甚至更多的页面,或是后台被修改。动画本身实际上体现在View,而这些view可能不仅仅在A中有,B,C可能也会有部分的View。

单例怎么样

当然我们可以用单例的法子。单例是个魔鬼,被很多滥用,这个场景用单例,其实仅仅是把全局变量合理的封装在了单例下,因为这份数据,并没有任何理由要一定是一份copy。

recap

在了解这个概念后,再看一些server的架构,规则时,也会更容易理解这些层之间的关系。包括

  • 为什么要规定那些层之间,不能相互调用,不能有静态方法。
  • 一个层之间的model,不能有重叠功能,不能连表查询。
  • 在哪个层才能调用另一个服务,而调用这个服务还必须要通过统一的接口

software architecture 涵盖的东西非常多。这篇只是一个引子,介绍了设计之前的准备工作。但是在实际过程中,我们的模型可能要比我这里写的还要复杂很多。下一篇会介绍一种策略用来处理更加复杂模型的情况。

最后附上一个完整功能的 demo code

参考

《Advanced iOS Application Architecture and Patterns》

机器学习(二) 线性回归、梯度下降实现

| Comments

了解一个算法最好的方法就是实现它,不过在开始实现算法之前,有一些额外的概念需要理解。

Vectorization

这是上一篇提到的hypothesis的计算公式。

当计算这个表达式值的时候,往往第一个感觉是写一个for loop 然后累加求和

prediction = 0;
for (int i = 0; i < n; ++i) {
    prediction += theta[j] * x[j];
}

但是在machine learning中更倾向于使用矩阵的方式。 比如同样的公式,会看成矩阵相乘。

其中theta和X分别是

这里通过矩阵或是向量来代替之前的loop。

这是上一篇提到的算法

image

计算function J如果用octave来实现则是这个样子

function J = computeCost(X, y, theta)
%COMPUTECOST Compute cost for linear regression
%   J = COMPUTECOST(X, y, theta) computes the cost of using theta as the
%   parameter for linear regression to fit the data points in X and y

% Initialize some useful values
m = length(y); % number of training examples

% You need to return the following variables correctly 
J = 0;

% ====================== YOUR CODE HERE ======================
% Instructions: Compute the cost of a particular choice of theta
%               You should set J to the cost.


t = (X * theta) - y;
J = (sum(t .* t)) / (2 * m);

% =========================================================================

end

image

而求偏导数迭代更新theta的代码则是这个样子

function [theta, J_history] = gradientDescent(X, y, theta, alpha, num_iters)
%GRADIENTDESCENT Performs gradient descent to learn theta
%   theta = GRADIENTDESENT(X, y, theta, alpha, num_iters) updates theta by 
%   taking num_iters gradient steps with learning rate alpha

% Initialize some useful values
m = length(y); % number of training examples
J_history = zeros(num_iters, 1);

for iter = 1:num_iters

    % ====================== YOUR CODE HERE ======================
    % Instructions: Perform a single gradient step on the parameter vector
    %               theta. 
    %
    % Hint: While debugging, it can be useful to print out the values
    %       of the cost function (computeCost) and gradient here.
    %


    s = sum(bsxfun(@times, X * theta - y, X));
    theta = theta - (alpha / m) * s';

    % ============================================================

    % Save the cost J in every iteration    
    J_history(iter) = computeCost(X, y, theta);

end

上面的2部分代码如果做一些合并分别可以简化成1行代码。说到这里自己还是相当羞愧的。今天早上花了3个小时才搞定这2行代码…主要时间花在了 2个地方。

  1. 算好theta去predict的上面,和normal equations的方式计算的答案总是对不上,不得不怀疑人生了。。。后面才发现是因为函数没有完全收敛,在调整learning rate之后误差明显变小了。
  2. 让大脑适应矩阵还是有点难,很多东西看上去很简单,反应很长时间,不过后面会好一些。

为什么用矩阵

在费了老半天力气搞定Vectorization的转变之后,不得不想想为什么要用这个方式做。obviously有2个好处,Andrew课上也提到了好多次。

  1. 增加一个feature很简单,只要把输入增加一列就好,而算法不需要改动。
  2. 矩阵的运算更容易优化,性能比循环更快。实际我们往往处理上百万个Example和N多的features

第一个很好理解,而且把循环的一大堆代码写成一行,显得逼格很高。 第二个会比较麻烦,涉及到了并行计算优化。

其他

在之前的算法中,我们看到了每一次调整theta都需要iterate整个所有的example,但实际中往往需要处理上百万个examples,而这样的iteration显然是不能接受的。实际上会随机选取一部分examples然后去迭代theta,最后得到一个较为可靠的theta向量。

最后附上Andrew作业的图片,虽然Andrew 不希望把答案放在网上或是论坛什么的,不过我觉得都过去2年多了,应该没关系了。

最后的预测效果图

cost function & theta

cost function & theta 等高线

learning rate

《中国古代历史与人物——秦始皇》笔记

| Comments

最近在coursera学完了台湾国立大学的公开课——《中国古代历史与人物——秦始皇》。虽然自己看过不少课程,看过不少历史书,但是这门课程对我的影响超过了任何一本文史书。我自己完整的看过3遍,并做了作业(可惜没有认真做好)。这门课从某种角度来看,彻底改变了我对文史课程的认知,对自己之前的无知和误解感到深深的愧疚,并再次讥讽一下国内的文史课程。下面是自己整理的东东,方便自己日后review。

  • 读历史需要思辨
  • 每一个决定的结果是否成功是通过时间衡量的,做决定的关键是在于是否得到了自己想要的结果。得到就要付出,除非有其他人替自己付出
  • 社会就是需要和有用,说服别人是因为知道别人需要什么
  • 人和人的差别不在于遇到的问题,而是面对问题时的态度
  • 看书要学会“沙金”,从沙子里面掏出金子,而不是一味的埋怨,批判。任何事情都有值得自己学习的东西。放下自己的成见,很多事情没有自己想象的那么简单。

2-5

秦国百战百胜,不在于它真能变法,而在于列国不能真正变法 历史说明了一个道理,不能适应时代的,只有被淘汰消失

2-6

历史上的所谓的成功和失败,就看你站在什么位置,用什么尺度来衡量。而判断成功和失败的标准,最后还是问自己

3-4

想要成功,就要懂得把理智放在感情之上。

谋事在人这是真得有用,有人用很多的时间去提高效率,但是节约的时间,并没有被真正利用起来。那么这个办法也很一般。 自己需要自己不断的努力,才能让之前的办法真正变成好办法。。。

方法和人关系太大了

这个社会不外乎需要和有用。

你需要知道别人需要什么,你才能变得有用

5-3

富家子弟处逆境难 穷家子弟处顺境难

6-1

人生总会有赌博的时候,对错往往是命运差别巨大。 百折不挠的民族

人生:运气 ,自我要求。好好准备,让自己成为那样的条件。等待机会。

理想是,你知道你下一步应该做什么 妄想是,你只有目标却不知道从何做起

自强的第一步,不自欺

6-8

在中国文化中,最高的道德和最高的智慧必然是合一的

7-3

学会认错,改过。

8-1

改变环境,需要改变自己,让自己适应那个自己想要的环境的要求。 如何改变,通过学习

9-1

做事之前,为虑胜,先虑败,方能考虑周全

9-9

历史启发智慧,所有的学问都能带来智慧

审时度势:

时:你所处的环境。 势:环境变动的方向

变的是时间和环境。 不变的是人性和良知

不仅仅是自己改变,需要改变周围的人。

历史用真,去伪。。。 改动人心

机器学习(一) 简单的背景介绍、线性回归、梯度下降

| Comments

Introduction

机器学习很久之前就已经热得不行了,直到最近这几个星期,自己才打算了解一些这方面的东西。原因大概有这么3点。

  1. 自从Andrew Ng 加入我厂之后(虽然和我毛关系也没有),总觉得还是需要围观一下这个令他兴奋的领域。
  2. 在听了IDL的有关手环算法分享后(其实毛也没有听懂), 在知道了一大堆的名词如最小二乘、梯度下降、SVM。以及里面很多的线性代数,微积分的概念,让我觉得这是一个很好的回收自己大学时期的沉默成本(微积分、现代是我在学校里面不多的用心学过的课程)的好机会。总之就是对这些很感兴趣。
  3. 前一段时间受组里高工分享睡眠算法影响,对这种阅读paper,然后优化算法的过程感到很开心。

有了这3条,足够我忙活好几个月了 : )

Background

在机器学习中,有2个很大的思路监督学习(supervised learning)非监督学习(unsupervised learning)

监督学习,用通俗的话来说就是你知道问题的答案,需要计算机给出一个更标准的答案

非监督学习,用通俗的话来说就是物以类聚,人以群分。我们拿到了很多数据,但是不知道问题的答案,希望计算机给我们提供思路。

在生产环境中,往往采用混合模式。比如图片搜索,如何能够查找网页中判断那个图片是老虎,那个是狗。就有2个思路。

  1. 根据图片周围的文字。
  2. 图片的图像数据分析。

2个角度相互校验,稳定之后,就可以产生足够的标注信息了。

线性回归(Linear regression)

线性回归主要用于手环的里程部分的计算,涉及到更细节的是 最小二乘,梯度下降。这里从先从最简单的一元线性回归开始。

一元线性回归(Linear regression with one variable)

Regression Problem : Predict real-valued output

1-1 算法运行的过程

最关键的在于如何描述hypothesis。

1-2 一元线性回归中的hypothesis函数

那么应该如何选取参数呢?直觉告诉我们这个直线需要尽可能的拟合我们的数据集。

1-3 线性回归的目标函数

通过下面的cost function 来评估参数的好坏。算法的目标也很清晰,让函数越小越好。

1-4 cost function

那个这个cost function 到底是个什么样子呢?

1-5 图形化的cost function

当然这个图还是看起来比较麻烦,Andrew 用了更为简单绘制的图来表示(有点类似等高线)。 相同的圆圈上,有着相同的cost function value。这里可以看到和上面的图一样,有一个极值。

1-6 一个比较差的选择

1-7 一个很接近极值的选择

梯度下降 (Gradient descent)

梯度下降,不仅仅是用于线性回归,也可以用在其他机器学习的场景下。

1-8 梯度下降的思路(2个参数的情况)

1-9 梯度下降函数图形(2个参数的情况)

我们的目标是寻找这个图形中的最小值,也就是靠近蓝色的地方。直觉告诉我们,我们先随机一个点,然后沿着最大的坡度向下走最后就可以走到一个极值里。

1-10 一条算法路径,全局最优

这个算法也有问题,随着第一个点的位置不同,我们可能找到一个局部最优的解,而不是全局最优。

1-11 另一条算法路径,局部最优

好在在很多实际问题中,我们遇到的情况要好很多,往往只有一个极值

那么梯度下降的算法就可以简单的描述出来,分别计算2个维度的偏导数,直到函数收敛

1-12

通过分别计算偏导数,a 为learning rate,决定每一步的步长,太小函数收敛很慢,太大则可能无法找到极值,甚至函数无法收敛。

这里Andrew 着重指出了一个叫做同步更新的概念

1-13

如果不同步更新,最后也可以得到极致,但是Andrew 更推荐计算完成所有的参数之后,再一起同步更新。

梯度下降和一元线性回归

将图1-4分别偏导后

1-14 算法公式

其他

  1. 根据上面的算法,如果我们的cost function 在一些地方不可导,那算法不就没法继续了?
  2. 有其他的方法,可以不去循环计算而是直接根据工具计算

梯度下降和一般化的线性回归

很多时候我们不仅仅满足2个参数,决定事情的因素很多,我们需要更一般化的公式。

1-15

算法

1-16

分别求偏导后

1-17

梯度下降生产环境中的一些技巧

Feature Scaling

思路: 希望所有的feature在相同或是类似的范围之内,这样梯度下降会更快收敛。

下图是feature的范围不在一起的运算过程,可以看出来不是圆形,2个维度调整的步长不一样,导致很多反复

1-18 红色箭头表示算法的一次迭代

下图则是调整过的feature,好了很多

1-19 红色箭头表示算法的一次迭代

更一般的,Andrew 推荐每一个feature放在[-1, 1]区间范围内

Learning Rate

说到Learning Rate 就不能不提收敛(convergence)。一般应该定义多大的阀值来判断是否收敛呢?

1-20 Andrew 并不推荐使用一个阀值来判断是否收敛

Andrew 更推荐用图表的形式,因为这个不仅仅可以看到是否马上收敛,而且还能看到算法是否运行正常,是不是一些参数的问题,导致算法无法收敛。

1-21

下图是2个出了问题的J函数,通常来说是Learning Rate 过大。

1-22 一些过大的Learning Rate 导致的图形

最后Andrew 还提供了一些practice的Learning Rate 选取方法,比如一些0.001, 0.003, 0.01, 0.03, 0.1, …

参考

Coursera 《Machine Learning》 Stanford Andrew Ng

Core Animation基本概念和Additive Animation

| Comments

上一篇《AutoLayout 相关概念介绍和动画demo》提到了一些Core Aniamtion的基础知识,这篇依然介绍一些基本概念,最后提到一点iOS8的动画改动。

一些基本概念

说到Core Animation 不能不说Layer, 一个个Layer通过tree的结构组织起来,在Display的过程中实际上有3种Layer tree。

  • model layer tree
  • presentation tree
  • render tree

model Layer tree 中的Layer是我们通常意义说的Layer。当我们修改layer中的属性时,就会立刻修改model layer tree。

layer.position = CGPointMake(0,0); //这里的修改会直接影响model layer tree

presentation tree 是Layer在屏幕中的真实位置。比如我们创建一个动画

1
2
3
4
5
6
7
8
9
   [UIView animateWithDuration:5.0f
                   animations:^{
                     self.animationLabel.center = CGPointMake(200, 400);
                   }];

  //这里用一个Timer print presentLayer的位置。
  CALayer *layer = self.animationLabel.layer.presentationLayer;

  NSLog(@"model:%@, presentLayer%@", NSStringFromCGPoint(self.animationLabel.layer.position), NSStringFromCGPoint(layer.position));

下面是屏幕输出结果

model:{73.5, 155.5}, presentLayer{73.5, 155.5}
model:{200, 400}, presentLayer{73.559769, 155.61552}//开始动画
model:{200, 400}, presentLayer{73.814095, 156.10709}
model:{200, 400}, presentLayer{74.267357, 156.98315}
...
...
...
model:{200, 400}, presentLayer{199.99576, 399.99182}
model:{200, 400}, presentLayer{200, 400}

Note: render tree 在apple的render server进程中,是真正处理动画的地方。而且线程的优先级也比我们主线程优先级高。所以有时候即使我们的App主线程busy,依然不会影响到手机屏幕的绘制工作。

CADisplayLink

了解cocos2dx对CADisplayLink一点也不陌生,对APP开发者可能就有一点远,但是facebook的Pop一下子拉近了我们和CADisplayLink的距离。通过设置callback函数,当屏幕刷新的时候,就可以执行我们的代码。当然,我们也可以利用NSTimer 或是GCD来实现类似的功能。但是CADisplayLink是最优的,因为不管是哪种类型的Timer,即使我们的刷新间隔和屏幕刷新保持一致。我们都无法知道系统什么时候刷新屏幕。

1-1 NSTimer中每一帧其实只有8ms的时间,如果大于8ms,那么就会丢帧

facebook的Pop非常类似UIDynamic,但是我们需要注意一点,相对于传统的model动画来说,CADisplayLink导致部分绘制工作放在了我们APP的地址空间中,也就是说,增大了APP内存,CPU的开销。也更容易遇到性能瓶颈。

Note: model layer的这部分绘制是完全在render server,而render server运行在比APP更高优先级的进程中,而这个也意味着会有进程间通讯的开销。传递的数据包括整个render tree还有动画,所以,Apple 并不推荐我们手动commit transaction, Core Animation 默认会在run loop 中提交transaction。

UIView animation

Apple 最近在推荐一些Modern APP的设计,其中有一条是希望responsive。比如下面的场景,启动一个动画之后,在动画还没有完成之前取消这个动画。

下图的相关代码

这里我们看到了3种情况。

  • 红色的2个动画之间有一个很大的跳动。
  • 绿色的比红色的好一点,没有跳动,但是就像撞到了墙一样,完全丧失了一开始动画的速度。
  • 蓝色的的运动更加平滑,有更真实的物理效果。

UIKit创建的动画,系统是如何理解的

UIKit的动画最后都会通过Core Animation 来实现, 那么当我们修改layer(model layer)的数值时,系统是如何理解并创建动画呢? 比如这里有一个线性的动画,将animationView的坐标从(0,0)移动到(0,500)

1
2
3
4
5
6
7
8
9
   animationView.center = CGPointMake(0, 0);
    [UIView animateWithDuration:1.0f
                        delay:0
                      options:UIViewAnimationOptionCurveLinear
                   animations:^{
                     animationView.center = CGPointMake(0, 500);
                   } completion:^(BOOL finished) {

                   }];

下面是当我们创建一个UIKit的动画时发生的事情

  • Model:在animationView.center = CGPointMake(0, 500);之后会立刻修改animationView的model Layer中的position的值为(0, 500)。
  • Animation:系统的理解就是从原来的model layer的值(0,0)到新的model layer的值(0, 500)创建一个动画。
  • Presentation: Presentation就像上面提到的,是表示animationView当前在屏幕的真实位置(渲染位置),因为还没有”动”起来,所以还是(0,0)

Note: Animation的部分如果没有明白,可以结合后面的回头再看

当我们看到屏幕上面的View移动的时候,发生了下面的事情

这是在0.4s时刻之前的状态。Model Layer的数值没有变化,而Presentation则在变化,和真正的屏幕动画保持一致。

在一个animation并没有完成的情况下,再创建一个动画系统是如何理解的呢?

如果我们在0.5时刻创建一个reverse动画,animationView.center = CGPointMake(0, 0);

1
2
3
4
5
6
7
8
   [UIView animateWithDuration:1.0f
                        delay:0
                      options:UIViewAnimationOptionCurveLinear
                   animations:^{
                     animationView.center = CGPointMake(0, 0);
                   } completion:^(BOOL finished) {

                   }];

  • Model:的数值会被立刻修改成目标数值(0, 0)
  • Animation: 系统的理解是从原来的(0, 500),创建一个去(0,0)的动画
  • Presentation: 基于系统的理解,Presentation layer的数值变成了(0, 500)。1秒中的时间内递减到(0, 0)

到目前为止,我们可以清楚的理解为什么红色的view会有一个大的跳跃,在我们这里的理解就是presentation layer的一个不连续的修改。

绿色的动画效果原因

在上面的基础之前,绿色的就可以简单说一些

  • Model 这里还是和之前一样,表示目标值
  • Animation:系统的理解是从当前的动画位置开始,也就是 (0, 150)开始创建一个1秒的动画到(0,0)
  • Presentation 和我们的预期一样。

linear animation 图中的颜色和本文的颜色无关,只是表示2个动画的stage EseInOut animation 图中的颜色和本文的颜色无关,只是表示2个动画的stage

可以看出来2个动画相接的曲线不平滑,而造成这个不平滑的原因在于把之前的动画覆盖了, 丢掉了之前动画的速度,如果要实现一个更一般化的解决方案,我们很自然的想到了动画合成。

蓝色的动画原因

蓝色的动画比较复杂,使用了Core Animation中的additive属性,动画被设置成相对的,那么就和动画具体的位置无关。最后还合成了2个动画。

首先,解释一下什么是相对的动画。

这里很容易看到,view的真实位置是Animation 的值 + Model的值。系统的理解就是相对目标值(0, 500)来说,创建一个从-500 到 0 的动画。

其次,相比之前的动画,在0.6时刻(为了方便计算,把之前的0.5时刻移动到了0.6时刻)并没有删除掉之前的动画,而是添加了一个新的动画Animation2。也就是一个相对目标值(0,0)来说,创建一个从500到0的动画。整个运动变成了2个动画的合成。

Note: Animation2的duration修改了,在demo code里面并没有修改 :)

这里,我们就得到了一个一般化的解决方案。

图中的颜色和本文的颜色无关,只是表示2个动画的stage

iOS8的改动

Core Animation 有一个additive的属性实际上已经存在很久了,但是却很少被大家知道(我自己也是)。在iOS8 之前,UIKit创建的动画默认是不使用additive的,而在iOS8之后,默认是Additive的。有兴趣的同学可以试一试download demo code用Xcode6(这会还是beta)并打开macro#define USING_UIKIT 1看一下新的UIKit animation效果。

在了解背后的机制之后,其中的变化也很容易理解。

  1. completion block 的调用变了。之前在创建一个UIKit的动画时候,会覆盖掉上一个动画,也就是删除再添加一个新动画,而现在前一个动画会在真正执行完毕才会执行completion block。
  2. 不是所有的动画都支持additive

……

参考