一步步学R(2)

系列连载

一步步学R(1)
一步步学R(2)

If-Else

R 种的If-Else结构并没有比较特殊的地方,仍然支持两种结构:

if (condition) {
    // Do Something
} else {
   // Do Otherthing
}

或者如下:

if (condition) {
    // Do Something
} else if (condition2) {
   // Do Otherthing
} else {
   // Do Else
}

但是在R中,对于If-else有一个可以简化的地方,如:

if (x > 100) {
    y <- 10
} else if (x < 100) {
   y <- 11
} else {
   y <- 5
}

可以简化成:

y <- if (x > 100) {
    10
} else if (x < 100) {
   11
} else {
   5
}

For

For语句的语法也非常简单:

for (i in 1:10) {
    print(i)
}

seq_along 函数是for循环中可以注意的一个点。它的参数是一个vector,如x <- c('a', 'b'),调用seq_along(x)会得到一个序列,长度为2,值为1,2。因此,print(x[1]) 就等于 a

While

同样简单:

while (condition) {
   // Do studyy
}

Repeat

repeat是R中特有的一种逻辑结构,简单来理解就是死循环,想要退出的唯一方式是显式使用break

repeat {
    x <- something()

    if (A) {
        break
    } else {
        x <- x + 1
    }
}

Next

next就是其他语言中的continue

for (i in 1:100) {
     if (i < 20) {
         next
     }

     // Do other
}

函数

R中的函数可以没有显式的return,默认返回最后一句语句。

add2 <- function(x, y) {
     x + y
}

和其他语言一样,你可以给参数设置默认值,如

add2 <- function(x, y = 10) {
     x + y
}

R中的函数参数优点类似于JavaScript,可以不用赋值完全,前提是你用不到。而且,对于R中的参数,你可以打乱参数传递,只要你前面加上了行参的名称,

比如

add2 <- function(x, y) {
     x + y
}

你可以通过add2(y = 5, x = 7)来进行调用。

可变参数

在R中也是有可变参数的,即...

myplot <- function(x, y, type = 1, ...) {
    plot(x, y, type, ...)
}

同样,...也可以用在泛型函数中,后续学习中我们会说到。

**不过,与其他编程语言所不同的是,R中的可变参数可以放在函数列表的前面,如

function(..., sep = " ", collsape = NULL)

调用如上的这种函数,必须显式的通过函数参数名称来调用后续参数,如

function("haha", "heihei", sep = ",")

变量作用域

使用变量

当你使用一个R语言中的变量时,比如x,你有没有想过x究竟是存在于哪里呢?

有些人会说,我定义的呀,比如x <- 5,那么对于那些默认函数,比如vector()呢?

所以,这就涉及到R中的Symbol binding(其他语言的变量查找)了。

在R中,查找顺序是这样的。

[1] ".GlobalEnv"        "tools:rstudio"     "package:stats"     "package:graphics"  "package:grDevices"
[6] "package:utils"     "package:datasets"  "package:methods"   "Autoloads"         "package:base" 

默认查找的是.GlobalEnv,依次类推。

如果你还通过library()函数加载了其他package,如ggplot2,那么查找顺序是

 [1] ".GlobalEnv"        "package:ggplot2"   "tools:rstudio"     "package:stats"     "package:graphics" 
 [6] "package:grDevices" "package:utils"     "package:datasets"  "package:methods"   "Autoloads"        
[11] "package:base"     

也就是用户加载的package会自动加到除了.GlobalEnv之外的任意搜索顺序前。

如果要查看最新的搜索顺序,可以通过search()

作用域

R中的作用域,是Lexical Scoping,也就是静态作用域,也就是JavaScript的作用域

好了,我不多说了,如果想学习更多冠以静态作用域的话,看我的JavaScript博客部分。

当然,如果你不懂,你可以通过如下函数帮助你理解。

ls(environment(functionName))
get(variableName, environment(functionName))

一言以蔽之,lexcial scoping可以简单理解为你函数中需要的变量,是通过其定义时环境进行查找。

Data and Times

R中的时间表示,采用了一种特殊的数据结构。

Date是通过Date这一数据结构表示,而Time是通过POSIXct或者POSIXlt表示。

  • Date是不包含Time的,只显示年、月、日。
  • Date的内部储存是计算1970-01-01到当前时间之间的天数。
  • Time的内部储存是计算1979-01-01到当前时间之间的秒数。

可以采用as.Date构建Date,如as.Date("1970-01-01")

而Time相对来说比较复杂,我们首先来看看Posixlt的表现形式。

我们输入p<- Sys.time()获取当前时间,结果是"2015-12-27 00:59:18 CST",然后我们调用unclass(p)来看看其构成,结果如下:

[1] "sec"    "min"    "hour"   "mday"   "mon"    "year"   "wday"   "yday"   "isdst"  "zone"   "gmtoff"

这表明,通过Posixlt表征的Time,其内部是由一系列成分组成的集合。我们可以通过
p$wday 来查看今天是周几。

Posixct就是就算1970-01-01到当前时间的描述,是个非常大的Integer

你可以对Date或者Time进行大小比较操作,但是注意,不能讲Date和Time混合操作

Regression Model

Regression Model是什么

Regression Model是一种基本的数据分析模型,通俗点来说就是我们在中学时期学习的截距式直线方程。通过斜率和截距来定义一种诸如 y = kx + b的方程。一旦有了这样的方程,我们就可以通过我们现有的数据集,比如一堆x 来预测y。

今天就让我们来研究研究这一种数据模型

Centering

Centering是一种常用的数学用语,意为 集中化。什么意思呢?就是说如果我有从x1, x2 … xn 这n个数据构成的数据集{x},我可以求出他们的平均值为XM。我可以构建 bi = xi - XM 这样一个数列,这样的话,{b}这个数据集的平均值就为0,这一个过程就叫Centering。

Variances(方差)

方式的定义是:

求出一个数据集的平均数XM, 对于{x} 中的每一个数,求其与平均数差的平方。再这这些差的平房加在一起求和,最后用和除以n - 1,这里的n是数据集中数据的个数。

而标准差就是方差的平方根。 通过构建bi = xi / 标准差s 可以得到{b}数据集,它的标准差为1,这一过程也叫做Scaling

Normalization

将数据集先Centering再Scaling的过程叫做Normalization。

Covariance(协方差)

协方差的定义其实和方差类似,只不过针对的是一对数据集{x, y}。

求出数据集{x}的平均数M,求出数据集{y}的平均数N,对于每一个i,求和(xi - M) * (yi - N),最后和除以n - 1,其中n是数据集中数据的个数。

Corrleation(两个数据集的关联)

Correlation就是将两个数据集的协方差除以{x}的标准差和{y}的标准差的乘积。
Correlation的值域从-1到1,越接近两端表示两个数据集关联度越大,越靠近0表示越小。

重点

对于 y = kx + b 来说,可以通过如下公式求解方程:

k = cor(y, x) * s(y) / s(x) 以及 b = mean(y) - k * mean(x)

一步步学R(1)

系列连载

一步步学R(1)
一步步学R(2)

嘿嘿,作为一名天才,当然要掌握一定的数据分析能力啦,所以从今天起,我们来一步步学习R这门有意思的语言。

这一系列的文章的开发环境都是基于Mac的。

安装

  1. 首先你要安装R语言的环境 安装点我
  2. 可选:安装RStudio(类似于Matlab),安装之前必须已经安装了R 安装点我

基础知识

R里面的基础类型

  • Character,如 “a”,”haha”
  • Number(实数),如 5, 5.5
  • Integer(整数),如 5,6

    要注意的是,默认情况,所有的整数类型都为实数,如果要特定为整数,需要加L,如5L

  • Logical,如TRUE, FALSE

    FALSE = 0,TRUE为一切非0数,可以简写为F/T

  • Complex(复数),如 5 + 7i

操作

x <- 5 表示将5赋值给x
x 表示输出x,效果等同于print(x)

向量

虽然向量不是一个基本类型,但是却是R语言中非常关键的一种数据结构

x <- 5 这句语句虽然表面上看起来是定义了一个Number类型的x,值为5。但是实质上却是构建了一个大小为1的向量,通过x语句我们可以得到一下结果:

> x
[1] 5

所以x 等同于语句 x[1],会输出5。

注意:R语言里面的数据索引从1开始

除了默认构造的向量以外,我们可以通过vector(class, size)来构建指定类型和大小的向量,如:

> k <- vector('logical', 5L)
> z <- vector("complex", 7)

以上两句我们分别构建了一个名为k的logical向量,大小5以及一个复数向量,大小为7。

注意:在R的向量里,同一个向量只能包含相同类型的对象,如果包含了不相同,会进行隐式转换,如果不能转换就会报错。

如:

> k <- c(T, "a")
> k
[1] "TRUE" "a" 

当然也可以强制转换:

as.characteras.logicalas.numericas.complexas.integer

list

list是一种可以同时保存多个类型对象的数据结构,通过list(a, b, c, d)进行构建。

Matrix

matrix就是多维向量,通过函数matrix(data, nrows, ncols) 构建,默认是按列排列数据,可以通过更改byrow=T来按照行来构建。

Factor

简单理解,就是分类定义的数组,比如,x <- factor(c('yes', 'no', 'yes', 'no', 'yes')) ,虽然看起来构建了一个和Character类型的向量差不多,但是实质上,我们可以通过

> x <- factor(c('yes', 'no', 'yes'))
> levels(x)
[1] "no"  "yes"

这个向量代表了几种分类。

Missing Values

Na和NaN, NaN仅代表数值计算的不存在,而Na是代表一切的不存在,即Na包含NaN
我们可以通过is.Nais.NaN来进行判断。

Data Frames

Data Frames简单理解就是一个表格,但是与Matrix不同的是,她可以包含不同类型的data,也就是说,DataFrames的每一列都是一个list

> k <- data.frame(foo = 1:3, w = c(T, F, F), row.names = c('1', 'a', 'k'))
> k
  foo     w
1   1  TRUE
a   2 FALSE
k   3 FALSE

需要注意的是,每一个list的长度,都必须完全一样

Names(别名)

对于一个向量、list或者matrix等等,我们都可以为里面的元素加上别名,如:

> x <- 1:3
> names(x) <- c(T, F, "ha")
> x
 TRUE FALSE    ha 
    1     2     3 

其中,这些别名都会以字符串的形式存在,因此,你可以通过x[“TRUE”]来访问元素1。


进阶的R 哈哈哈

Partical Matching

R里面有个很酷的特性叫做Partial Matching。比如你定义了如下的list:x <- list(caonikljslkdfj = 5),每次都要输入caonikljslkdfj势必很麻烦,因此你可以使用$来获取,如x$c 即可以得到5了。

Subset

在R里面,获取数据或者构建数据简直就是a piece of cake。(哈哈哈,英语好就是屌啊)

比如有这样一个向量x <- c(1, 2, 3, 4, 5)

你可以x[1] 来获取元素1,也可以通过x[1:3]来获取元素1到元素3。同样,你也可以通过x[x > 2] 来获取元素中值比2大的元素。

你也可以通过logical来构建另外的向量,如:

> u <- x > 2
> u
[1] FALSE FALSE  TRUE  TRUE  TRUE

而对于Matrix类型来说,c <- matrix(1:3, nrows = 3, ncols = 1),你可以使用c[1, ]来获取第一行所有列的数据,或者c[,1]来获取第一列所有行的数据。

[] vs [[]] vs $

首先,这三个符号都可以获取元素,但是区别在于:

  • []返回的是和变量本身类型相同的东西,如果对list使用[],那么返回的就是list,如果对向量使用,返回的就是向量,无关取出的元素本身的类型。
  • []可以获取多个元素,而[[]]和$不行。
  • $可以模糊匹配(Partial Matching),而[[]]不行,如果要启用模糊匹配,得使用[[name, exact = F]]
  • $不可以使用计算值,[[]]可以

是不是很拗口?让我们来看几个例子,在看例子之前强调一点,

元素并非特指一个,而是一个基本的数据结构,比如对于向量 x <- 1:5来说,里面的元素1到元素5分别都是一个元素。而对于c<- list(foo = 1:3, bar = 0.5)来说,foo和bar都分别是一个元素,尽管foo本身仍然是个向量。

例子

对于c <- list(foo = 1:3, bar = 0.6)定义的一个list,我们分别使用:

> c["foo"]
$foo
[1] 1 2 3

> c[["foo"]]
[1] 1 2 3
> c$foo
[1] 1 2 3

可以看到,通过[]取出的foo元素仍然是list,而[[]]和$取出的都是向量元素本身了。

再比如计算变量的差别:

> name <- "foo"
> c[[name]]
[1] 1 2 3
> c$name
NULL

可以看到,[[]]可以使用计算变量,而$不可以。这点其实和JavaScript里面的dot和[]操作符很类似的

模糊匹配差别:

> c$f
[1] 1 2 3
> c[["f"]]
NULL
> c[["f", exact = F]]
[1] 1 2 3

向量化操作

R里面的所有计算操作都是并行的

因此,如果要计算两个Matrix的乘积,需要使用%*%,否则如果直接使用*就是对应位置的元素相乘而已。

浅入浅出LLDB(1)

这周开始好好钻研一下LLDB相关的知识,这是一系列的文章。有些初级知识可能大家都有所涉猎,嘿嘿,懂得自然懂,看我的博客,什么时候会收获小。

基础语法

1.help 有啥不会,就直接输入help,你会得到如下的一系列信息,

command           -- A set of commands for managing or customizing the
                   debugger commands.
disassemble       -- Disassemble bytes in the current function, or elsewhere
                   in the executable program as specified by the user.

2.print 输出变量值
比如,对于如下的程序语句 let haha = 5; 你只要在LLDB里面输入 print haha 就可以得到如下输出结果。

(lldb) p haha
(Int) $R0 = 5

请注意,在与调试器共舞- LLDB 的华尔兹一文中曾经提出会输出类似于 $0 = 5,并指出$0是当前的输出值。这个说话其实是不严谨的,正确的说法是,应该是当前的haha的值5存在了R0寄存器里。

比如,当我们构建如下的程序语句时,

let haha = 5;
let object = "jkjksdjf";

我们在分别print haha以及 print object就会分别得到(Int) $R0 = 5(String) $R1 = "jkjksdjf"

这说明haha的值和object的值分别以int型和string型存在了R0和R1寄存器之中。

当然,图快速的话,可以像我上面一样将print简写成p。

3.po
输出变量值

哎,有人奇怪了,po也是输出变量值,那和p有啥区别啊?本质上没啥区别,如果真要说,就是po = e -O –,具体我们后续再说啦。

4.breakpoubt l 输出所有的断点,可以得到如下的结果

Current breakpoints:
1: file = 'xxx/ViewController.swift', line = 19, locations = 1, resolved = 1, hit count = 1

1.1: where = xxx.ViewController.viewDidLoad (xxx.ViewController)() -> () + 131 at ViewController.swift:19, address = 0x0000000108d0a443, resolved, hit count = 1 

其中 1: file的这个1就是ID号。
那这里的1.1是什么鬼?嘿嘿,当里使用Symbolic Breakpoint的时候,你一个断点很有可能截获了多个地方,比如AViewController和BViewController的viewDidLoad都被加上了断点,这个时候就需要靠诸如1.1和1.2之类的细分ID来进行区别了。

当然,有人会问,输出这个断点有什么用啊。嘿嘿,当你使用Xcode Symbolic Breakpoint的时候,你就会发现究竟在多少个地方下了断点了。

同样的,你可以将breakpoint简写成br。

5.br delete ID 这里的ID就是之前的断点的ID号
通过这个命令,可以删除ID对应的断点

6.br e ID 启用一个ID号对应的断点

7.br di ID 禁用一个ID号对应的断点

8.b xxx.swift:lineY 在xxx文件的第lineY行设置一个断点
b ViewController.swift:10 就是在ViewController的第10号下了一个断点。

需要注意的是,通过b命令设置的断点,无法直观的在Xcode界面上显示出来,而br delete删除一个断点可以直接在Xcode上看出效果。

9.br set -n functionName 对functionName设置Symbolic Breakpoint
br set -n viewDidLoad 就是对所有的viewDidLoad设置了Symboloc Breakpoint

10.br mod -C "Condition" ID 对ID号对应的breakpoint添加条件触发
假设我们有下面这样的一段代码

1. //ViewController.swift
2. for var value in money {
3.     totalValue += value
4. }

我们首先先使用b ViewController.swift:3设置一个断点,然后使用br l查询到对应的ID为3。
然后我们使用br mod -C "totalValue > 50" 3对这个断点设置条件触发,条件为当totalValue 大于50时候才触发

当然,可能有些人会问,如果我不想删除断点,只是想移除条件触发怎么办?很简单,只要输入br mod -C "" ID,将其中的Condition部分设置为空即可。

11.continue 继续运行程序
12.n step over单步调试
13.s step in进行函数
14.finish step out退出函数

大杀器

上面的命令是不是很多,一个个敲实在是太麻烦,那如果我想对一个断点执行多条语句怎么办?
嘿嘿,大杀器来了。

br com add ID 对ID对应的断点进入交互式指定。如:

br com add 2
> bt
> continue
> DONE

上面的语句指的是,对2号断点进行交互式指定,当这个断点触发的时候,首先执行bt(具体bt命令的意思我们后续再说,粗略理解就是backtrace输出调用栈,可以简单看下面的例子),然后执行continue,最后通过关键字Done退出指定,这里的Done类似于shell里面的exit。

frame #0: xxx`xxx.ViewController.viewDidLoad (self=0x00007faf52439c20)() -> () + 470 at ViewController.swift:27
frame #1: xxx`@objc xxx.ViewController.viewDidLoad (xxx.ViewController)() -> () + 34 at ViewController.swift:0
frame #2: UIKit`-[UIViewController loadViewIfRequired] + 1198
frame #3: UIKit`-[UIViewController view] + 27
frame #4: UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
frame #5: UIKit`-[UIWindow _setHidden:forced:] + 282
frame #6: UIKit`-[UIWindow makeKeyAndVisible] + 42
frame #7: UIKit`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131
frame #8: UIKit`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1760
frame #9: UIKit`-[UIApplication workspaceDidEndTransaction:] + 188
frame #10: FrontBoardServices`-[FBSSerialQueue _performNext] + 192
frame #11: FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 45
frame #12: CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #13: CoreFoundation`__CFRunLoopDoSources0 + 556
frame #14: CoreFoundation`__CFRunLoopRun + 867
frame #15: CoreFoundation`CFRunLoopRunSpecific + 488
frame #16: UIKit`-[UIApplication _run] + 402
frame #17: UIKit`UIApplicationMain + 171
frame #18: xxx`main + 109 at AppDelegate.swift:12
frame #19: libdyld.dylib`start + 1
frame #20: libdyld.dylib`start + 1

TBAnnotationClustering源码解析

毕设这两天飞一般的加速,终于可以写写源码解析了,嘿嘿,这周读两个跟性能相关的源码,首先是一个跟地图相关的,今天来看看一个数据结构在iOS开发中的妙用。

TBAnnotationClustering

我们都知道,MapView的实现机制其实和UITableView类似,首先他们都是基于UIScrollView支持滑动的,此外,他们都采用了循环服用的机制了来保持内存的开销。

但是,两者之间有个很大的区别就是数量级的差距,UITableView就算充满整个屏幕,充其量也就是10多个同时可见的VisibleCell,因此就多维护一个大小是VisibleCells数量 + 2的这样一个循环队列,进行复用。但是MapView就不一样了,地图上同时展示一段范围内几千个Point of Interest是非常有可能的,这一下子的内存开销和性能卡顿就非常不得了,因此我们需要一种合理的手段来避免,这就是我们今天要讲解的TBAnnotationClustering的由来。

Github地址

Level of Detail

首先,让我们先介绍一下相关背景知识。

根据图像渲染的理论我们可以知道,人的视野存在焦点区域和盲点区域,总是更倾向于关注处于视线左上角到视线中心部分的。因此,在现实应用中,如游戏场景,当场景需要展现的模型距离视线焦点非常近时,就采用高精度的模型来进行展示;而到模型处于较远位置时,比如体育游戏的场外观众,就可以采用低精度模型进行替换,减少渲染时候的计算量;而到模型所处位置基本可以考虑成为背景时,则会采用基本图元进行展示。通过这种方法,即保证了场景的真实观感,同时又大大减少了不必要的计算量。这也就是通常计算机图形学领域所谓的Level Of Detail技术。

QuadTree

QuadTree可能很多人会比较陌生,但是一提到他的哥哥 - 二叉树,想必大家不会陌生,所以QuadTree又被称为四叉树,关于四叉树的定义,


A quadtree is a tree data structure in which each internal node has exactly four children.

四叉树被广泛的运用于空间划分。通过将空间递归划分不同层次的子结构,可以达到较高的空间数据插入和查询效果。

下面就是一张比较经典的四叉树构造,首先先将一个大空间划分为四个字空间 a b c d。然后根据每一个子空间内的节点个数再进行细分。这里要强调一点,四叉树的细分没有具体要求,你可以按你的需求划分成每个节点能只包含一个,也可以根据平衡减少划分次数。

TBAnnotationClustering源码讲解

打开这个项目,粗略过一下项目结构,大致需要关注的代码如下:

  • TBQuadTree.h/.m
  • TBCoordinateQuadTree.h/.m

让我们一个个来分析

TBQuadTree

毫无疑问,从文件名称来看,我们就知道,这个类就代表基础的四叉树数据结构,首先让我们来看看数据结构的定义

typedef struct TBQuadTreeNodeData {
    double x;
    double y;
    void* data;
} TBQuadTreeNodeData;
TBQuadTreeNodeData TBQuadTreeNodeDataMake(double x, double y, void* data);

这个毫无疑问,就是代表的坐标系的数据节点。 (x, y)表征坐标点,void *data自由的指向附加的数据。

typedef struct TBBoundingBox {
    double x0; double y0;
    double xf; double yf;
} TBBoundingBox;
TBBoundingBox TBBoundingBoxMake(double x0, double y0, double xf, double yf);

这个同样很简单,用两个对角点限定了一个长方形区域,也就是一个四叉树的节点究竟包含哪些范围。

typedef struct quadTreeNode {
    struct quadTreeNode* northWest;
    struct quadTreeNode* northEast;
    struct quadTreeNode* southWest;
    struct quadTreeNode* southEast;
    TBBoundingBox boundingBox;
    int bucketCapacity;
    TBQuadTreeNodeData *points;
    int count;
} TBQuadTreeNode;
TBQuadTreeNode* TBQuadTreeNodeMake(TBBoundingBox boundary, int bucketCapacity);

这个稍微复杂点,是四叉树的树节点,其中

  • northWest, northEast, southWest, southEast分别代表四叉树的四个子细分区域。
  • bondingBox代表的当前这个树节点的涵盖区域。
  • bucketCapacity表示这个树节点最大容纳的数据节点个数
  • points 数据节点数组
  • count 当前包含了数据节点。

再次强调,千万不要把树节点和数据节点搞混。树节点指的是四叉树上的数据结构,每个树节点最多有四个子树节点,但是可以有bucketCapacity大小的数据节点,数据节点仅仅是用来封装坐标系和其相关的数据的一个数据结构,非四叉树特有。

看完了数据定义,我们再来看看其实现部分。

#pragma mark - Constructors

TBQuadTreeNodeData TBQuadTreeNodeDataMake(double x, double y, void* data)
{
    TBQuadTreeNodeData d; d.x = x; d.y = y; d.data = data;
    return d;
}

TBBoundingBox TBBoundingBoxMake(double x0, double y0, double xf, double yf)
{
    TBBoundingBox bb; bb.x0 = x0; bb.y0 = y0; bb.xf = xf; bb.yf = yf;
    return bb;
}

TBQuadTreeNode* TBQuadTreeNodeMake(TBBoundingBox boundary, int bucketCapacity)
{
    TBQuadTreeNode* node = malloc(sizeof(TBQuadTreeNode));
    node->northWest = NULL;
    node->northEast = NULL;
    node->southWest = NULL;
    node->southEast = NULL;

    node->boundingBox = boundary;
    node->bucketCapacity = bucketCapacity;
    node->count = 0;
    node->points = malloc(sizeof(TBQuadTreeNodeData) * bucketCapacity);

    return node;
}

这三个构造函数,分别是构造数据节点、长方形以及四叉树节点,默认情况下四叉树的节点并非满构造,而是初始化为空,根据需要插入新节点。

#pragma mark - Bounding Box Functions

bool TBBoundingBoxContainsData(TBBoundingBox box, TBQuadTreeNodeData data)
{
    bool containsX = box.x0 <= data.x && data.x <= box.xf;
    bool containsY = box.y0 <= data.y && data.y <= box.yf;

    return containsX && containsY;
}

bool TBBoundingBoxIntersectsBoundingBox(TBBoundingBox b1, TBBoundingBox b2)
{
    return (b1.x0 <= b2.xf && b1.xf >= b2.x0 && b1.y0 <= b2.yf && b1.yf >= b2.y0);
}

随后就是上面两个判断长方形包含和相交的方法了,包含自然是整个包围。而相交的补集是不相交,即在横坐标上一个长方形的xf另一个长方形的x0抑或是一个长方形的x0完全大于另一个长方形的xf,当然在y轴上也是同理,因此通过补集很容易就理解TBBoundingBoxIntersectsBoundingBox的实现了。

然后来看看非常重要的几个函数,首先是TBQuadTreeNodeSubdivide

void TBQuadTreeNodeSubdivide(TBQuadTreeNode* node)
{
    TBBoundingBox box = node->boundingBox;

    double xMid = (box.xf + box.x0) / 2.0;
    double yMid = (box.yf + box.y0) / 2.0;

    TBBoundingBox northWest = TBBoundingBoxMake(box.x0, box.y0, xMid, yMid);
    node->northWest = TBQuadTreeNodeMake(northWest, node->bucketCapacity);

    TBBoundingBox northEast = TBBoundingBoxMake(xMid, box.y0, box.xf, yMid);
    node->northEast = TBQuadTreeNodeMake(northEast, node->bucketCapacity);

    TBBoundingBox southWest = TBBoundingBoxMake(box.x0, yMid, xMid, box.yf);
    node->southWest = TBQuadTreeNodeMake(southWest, node->bucketCapacity);

    TBBoundingBox southEast = TBBoundingBoxMake(xMid, yMid, box.xf, box.yf);
    node->southEast = TBQuadTreeNodeMake(southEast, node->bucketCapacity);
}

这个函数负责将四叉树节点进行细分。首先获取当前节点负责的长方形区域的中点,然后根据中点到原有长方形的四个顶点,分成四个象限,进行划分。这个时候请注意,还只是进行四叉树节点的细分,还没重新更改数据节点的分布。

bool TBQuadTreeNodeInsertData(TBQuadTreeNode* node, TBQuadTreeNodeData data)
{
    if (!TBBoundingBoxContainsData(node->boundingBox, data)) {
        return false;
    }

    if (node->count < node->bucketCapacity) {
        node->points[node->count++] = data;
        return true;
    }

    if (node->northWest == NULL) {
        TBQuadTreeNodeSubdivide(node);
    }

    if (TBQuadTreeNodeInsertData(node->northWest, data)) return true;
    if (TBQuadTreeNodeInsertData(node->northEast, data)) return true;
    if (TBQuadTreeNodeInsertData(node->southWest, data)) return true;
    if (TBQuadTreeNodeInsertData(node->southEast, data)) return true;

    return false;
}

这个函数则是真正的将数据插入到节点中。

  • 首先先判断这个数据是否落在该长方形中,不是直接滚蛋。
  • 如果当前包含的数据节点个数没有超过最大数目,直接应用在其中。
  • 如果四个子节点为空,就先创建
  • 然后再递归插入
void TBQuadTreeGatherDataInRange(TBQuadTreeNode* node, TBBoundingBox range, TBDataReturnBlock block)
{
    if (!TBBoundingBoxIntersectsBoundingBox(node->boundingBox, range)) {
        return;
    }

    for (int i = 0; i < node->count; i++) {
        if (TBBoundingBoxContainsData(range, node->points[i])) {
            block(node->points[i]);
        }
    }

    if (node->northWest == NULL) {
        return;
    }

    TBQuadTreeGatherDataInRange(node->northWest, range, block);
    TBQuadTreeGatherDataInRange(node->northEast, range, block);
    TBQuadTreeGatherDataInRange(node->southWest, range, block);
    TBQuadTreeGatherDataInRange(node->southEast, range, block);
}

这个就是通过DFS进行节点的遍历,一旦有落在range内的数据节点,就进行回调。

综上所述,就是一个基本的四叉树,可以很明显的看到,在四叉树的构建、遍历中,都用了树的递归,也就是俗称的DFS算法。

TBCoordinateQuadTree

这个类呢,和实质上的四叉树或者性能优化并无太大关系,只是一层简单的封装,我们大致来了解一下就好。

TBBoundingBox TBBoundingBoxForMapRect(MKMapRect mapRect)
{
    CLLocationCoordinate2D topLeft = MKCoordinateForMapPoint(mapRect.origin);
    CLLocationCoordinate2D botRight = MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMaxX(mapRect), MKMapRectGetMaxY(mapRect)));

    CLLocationDegrees minLat = botRight.latitude;
    CLLocationDegrees maxLat = topLeft.latitude;

    CLLocationDegrees minLon = topLeft.longitude;
    CLLocationDegrees maxLon = botRight.longitude;

    return TBBoundingBoxMake(minLat, minLon, maxLat, maxLon);
}

MKMapRect TBMapRectForBoundingBox(TBBoundingBox boundingBox)
{
    MKMapPoint topLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake(boundingBox.x0, boundingBox.y0));
    MKMapPoint botRight = MKMapPointForCoordinate(CLLocationCoordinate2DMake(boundingBox.xf, boundingBox.yf));

    return MKMapRectMake(topLeft.x, botRight.y, fabs(botRight.x - topLeft.x), fabs(botRight.y - topLeft.y));
}

这两个函数就是MKMapRect和我们的BoundingBox之间的转换,难度很小,但是很有意思啊。从中我们可以一窥MapView的一些实现。比如MapView不仅仅是传统的ContentView和ContainerView,更重要的其坐标系和传统的CGRect之间的无法换算,简而言之,就是,在MapView中,所有的东西都要拿经度纬度来谈。

- (NSArray *)clusteredAnnotationsWithinMapRect:(MKMapRect)rect withZoomScale:(double)zoomScale
{
     // 1.
    double TBCellSize = TBCellSizeForZoomScale(zoomScale);
    double scaleFactor = zoomScale / TBCellSize;

     // 2.
    NSInteger minX = floor(MKMapRectGetMinX(rect) * scaleFactor);
    NSInteger maxX = floor(MKMapRectGetMaxX(rect) * scaleFactor);
    NSInteger minY = floor(MKMapRectGetMinY(rect) * scaleFactor);
    NSInteger maxY = floor(MKMapRectGetMaxY(rect) * scaleFactor);

    NSMutableArray *clusteredAnnotations = [[NSMutableArray alloc] init];
    for (NSInteger x = minX; x <= maxX; x++) {
        for (NSInteger y = minY; y <= maxY; y++) {
            MKMapRect mapRect = MKMapRectMake(x / scaleFactor, y / scaleFactor, 1.0 / scaleFactor, 1.0 / scaleFactor);

            __block double totalX = 0;
            __block double totalY = 0;
            __block int count = 0;

            NSMutableArray *names = [[NSMutableArray alloc] init];
            NSMutableArray *phoneNumbers = [[NSMutableArray alloc] init];

              // 3.
            TBQuadTreeGatherDataInRange(self.root, TBBoundingBoxForMapRect(mapRect), ^(TBQuadTreeNodeData data) {
                totalX += data.x;
                totalY += data.y;
                count++;

                TBHotelInfo hotelInfo = *(TBHotelInfo *)data.data;
                [names addObject:[NSString stringWithFormat:@"%s", hotelInfo.hotelName]];
                [phoneNumbers addObject:[NSString stringWithFormat:@"%s", hotelInfo.hotelPhoneNumber]];
            });

              // 4.
            if (count == 1) {
                CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(totalX, totalY);
                TBClusterAnnotation *annotation = [[TBClusterAnnotation alloc] initWithCoordinate:coordinate count:count];
                annotation.title = [names lastObject];
                annotation.subtitle = [phoneNumbers lastObject];
                [clusteredAnnotations addObject:annotation];
            }

           // 5.
            if (count > 1) {
                CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(totalX / count, totalY / count);
                TBClusterAnnotation *annotation = [[TBClusterAnnotation alloc] initWithCoordinate:coordinate count:count];
                [clusteredAnnotations addObject:annotation];
            }
        }
    }

    return [NSArray arrayWithArray:clusteredAnnotations];
}

而上述的最后一个函数,就是根据传入的MKMapRect,返回簇类数组的。

  • 1.首先根据放缩比例,或者Cell大小。
  • 2.根据cell大小计算当前地图区域的范围所对应的minX - maxX,minY - maxY对应的网格。

什么是网格?就是根据Cell大小将地图划分成了一块块区域,通过minX, maxX, minY - maxY找到对应的网格。类似于array[1][2]找到第二行第三列的网格(从0开始索引)。

  • 3.遍历每一个网格,获取当前网格对应的四叉树节点中的数据信息,并记录个数。
  • 4.如果个数是1,那么直接显示,包含数据节点的附加信息,比如在这里就是酒店名称和酒店电话。
  • 5.如果个数大于1的话,利用均值计算中心点,中心点是所有包含的数据节点平均值,同时信息只简单的显示个数。

至此,整个代码就解读完整啦。

FDFullScreenPopGesture源码解析

嘿嘿,花了三天时间把自己的代码从700行缩减成了200行,终于有时间可以拜读新源码了,好开心。

好了,废话不多话,今天我们要读了百度出品的一个开源项目FDFullScreenPopGesture。

项目介绍

FDFullScreenPopGesture是一款无需改动即可整合进入现有项目的全局手势操作,使用这个即可以在左侧边缘拖拽的时候返回上一级的效果。

看到这,有人会问,我们这个直接用UIPanGestureRecognizer不也能达到吗?
没错,仅仅是返回上一级这个需求确实很简单。但是iOS7上返回上一级时候,UINavigationBar的切换效果你能实现吗?

而且,我要说的重点是FDFullscreenPopGesture实现思路很赞!!!

源码分析

结构分析

整个FDFullscreenPopGesture其实可以主要拆分成三块:

-- _FDFullscreenPopGestureRecognizerDelegate
-- UIViewController (FDFullscreenPopGesturePrivate)
-- UINavigationController (FDFullscreenPopGesture)
  • _FDFullscreenPopGestureRecognizerDelegate虽然名字看起来像一个Protocol,但是它实质上是一个NSObject子类,同时实现了UIGestureRecognizerDelegate。这么做的好处是什么呢?不知道大家有没有经历过ViewController重构,以前很多时候,比如我们写UIScrollView,UITableView,他们的Delegate,DataSource都耦合进了ViewController,常常导致MassViewController灾难的发生。单独构建一个专门负责的Delgeate“处理器”是非常有效的手段

  • UIViewController (FDFullscreenPopGesturePrivate)是一个Category,我们在这个分类里面主要进行viewWillAppear的Hook,具体做什么,后面章节我们细细道来。

  • UINavigationController (FDFullscreenPopGesture)也是一个分类,是进行pushViewController:animated:方法的hook,实现部分我们后续再看。

源码分析

_FDFullscreenPopGestureRecognizerDelegate

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    // 1.
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }

    // 2.
    UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
    if (topViewController.fd_interactivePopDisabled) {
        return NO;
    }

    // 3. 
    CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
    if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
        return NO;
    }

    // 4.
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }

    // 5.
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    if (translation.x <= 0) {
        return NO;
    }

    return YES;
}

整个类特别精简,它的职责就是维护一个UINavigationController然后根据一系列的状态判断该手势是否生效。这些状态包括

  1. 当前UINavigationController的栈是否只剩最后一个ViewController了
  2. 当前即将出栈的topViewController是否禁用了fd_interactivePopDisabled,该变量我们稍后会说
  3. 当前手势的启动点是不是离左侧边缘太远了,毕竟我们是要模拟iOS原生的手势操作,原生的不支持全屏,我们为啥要支持!
  4. 当前是否已经处在转场过程中。在这里,可以看到它使用了valueForKey这一Key-Value-Coding技术,它可以访问私有变量_isTransitioning哦!
  5. 方向相反的滑动滚粗。

UIViewController (FDFullscreenPopGesturePrivate)

整个这个类也非常简单,就是通过 fd_viewWillAppear hook了 viewWillAppear 这个方法,然后插入了自己一段回调的block。

- (void)fd_viewWillAppear:(BOOL)animated
{
    // Forward to primary implementation.
    [self fd_viewWillAppear:animated];

    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }
}

UINavigationController (FDFullscreenPopGesture)

这个分类是整个项目的逻辑控制核心。它干了这么几件事:

  • fd_pushViewController:animated: hook pushViewController:animated:
  • 禁用UINavgationController的interactivePopGestureRecognizer
  • 构建了属于自己UIPanGestureRecognizer替换interactivePopGestureRecognizer,同时把手势的delegate赋值给了_FDFullscreenPopGestureRecognizerDelegate

它的主要核心代码如下:

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {

        // 1.
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // 2.
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];

        // 3.
        self.interactivePopGestureRecognizer.enabled = NO;
    }

    // 4.
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];

    // Forward to primary implementation.
    if (![self.viewControllers containsObject:viewController]) {
        [self fd_pushViewController:viewController animated:animated];
    }
}

整体来看这段源码,无非做了如下这些事:

  1. 将UIPanGestureRecognizer添加到本来interactivePopGestureRecognizer所在的view上
  2. 这段是重点的重点,一定要往下看!!!

    将PanGesture的target设置为internalTarget,action设置为
    handleNavigationTransition.
  3. 禁用interactivePopGestureRecognizer
  4. 根据是否需要隐藏UINavigationBar来调用fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController进行之前提到过的fdwillAppearInjectBlock设置,代码如下:

    - (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
    {
        if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
            return;
        }
    
        __weak typeof(self) weakSelf = self;
        _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            if (strongSelf) {
                [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
            }
        };
    
        // Setup will appear inject block to appearing view controller.
        // Setup disappearing view controller as well, because not every view controller is added into
        // stack by pushing, maybe by "-setViewControllers:".
        appearingViewController.fd_willAppearInjectBlock = block;
        UIViewController *disappearingViewController = self.viewControllers.lastObject;
        if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
            disappearingViewController.fd_willAppearInjectBlock = block;
        }
    }
    

这段代码就是将即将消失和展现的ViewController在viewWillAppear设置了一个自定义UINavigationBar的回调,用以根据进入的方式来展现NaviagtionBar,而不会出现突兀的“镂空”。

到这,源码就结束了,可以回家收衣服喽!

重点

源码是不是很简单?有什么好分析的呢?
如果你读到这,哈哈,恭喜啦,重点分析来啦。

首先感谢@J_雨的天才思路,大家可以阅读轻松学习之二——iOS利用Runtime自定义控制器POP手势动画这篇文章,真的很赞

之前我们在上文用红色标注了一段内容:


将PanGesture的target设置为internalTarget,action设置为
handleNavigationTransition

看起来很容易理解,可是大家有没有想过为什么action的名称是handleNavigationTransition呢?

首先我们先打印看看NavigationController的interactivePopGestureRecognizer究竟是个什么玩意?

<UIScreenEdgePanGestureRecognizer: 0x7fea78ec5950; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7fea78f77960>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7fea78c1c640>)>

这是什么玩意?让我们分别来看看它答应出来的这些属性。

  1. state = Possible,很简单,就是一个UIGestureRecognizerState = UIGestureRecognizerStatePossible。
  2. view = UILayoutContainerView,不太懂,暂时也没觉得有需要,不管他。
  3. target =<(action=handleNavigationTransition:, target=<_uinavigationinteractivetransition 0x7fea78c1c640="">),这个看起来很有用,因为我们都知道,Gesture就是通过Target-Action的方式进行动作触发的。

所以我们赶紧看看这个target是个啥玩意,使用如下命令:

[self.navigationController.interactivePopGestureRecognizer valueForKey:@"target"];

卧槽,一运行,Crash了,报找不到这个Key。咋回事,难道我记错了KVC的用户,赶紧换成valueForKey:@”View”试试。

哎!没错啊!成功得到了如下输出:

-[UILayoutContainerView objectAtIndexedSubscript:]

那咋回事,看来必须祭出屠龙刀Runtime了,嘿嘿,Objective-C面前,一切私有变量都是纸老虎。

unsigned int count = 0;
Ivar *var = class_copyIvarList([UIGestureRecognizer class], &count);
for (unsigned int i = 0; i < count; i++) {
     Ivar _var = *(var + i);
     NSLog(@"%s", ivar_getTypeEncoding(_var));
     NSLog(@"%s", ivar_getName(_var));
}

输出太长了,我们找我们想看的,

2015-11-27 02:10:03.873 SamplePhotosApp[85305:2664323] _targets
2015-11-27 02:10:03.873 SamplePhotosApp[85305:2664323] @"NSMutableArray"

卧槽,这丫叫_targets,好吧,赶紧改成valueForKey:@”targets”再试试。
哎,等等,不是_targets吗,怎么能用targets呢?

咳咳,吴老师又要来讲课了!对于KVC来说,它的查找顺序是key -> property -> ivar,也就是说,它会先按照是否有targets这个名称的key,然后targets这个property,最后再找_targets这个ivar。

通过输出log,我们可以发现_targets是个数组,维护了一个个自定义结构维护的target-action配对。

因此,我们现在只要找到这个自定义结构是啥,里面包含了啥就可以了是吧。

当头一棒,很遗憾,苹果太阴了,直接重载了这个自定义结构的debugDescription,特喵的什么都看不到。

事情到了这咋办呢?其实我也没想到,还好上述的参考文章告诉了我们可以依靠断点,通过断点,我们发发现了该自定义结构叫UIGestureRecognizerTarget,我们通过KVC获取其target和action即可。

补充:关于Method Swizzling

Class class = [self class];

SEL originalSelector = @selector(pushViewController:animated:);
SEL swizzledSelector = @selector(fd_pushViewController:animated:);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
    class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

有很多人都了解Method Swizzling,但是不知道为什么这里需要进行BOOL success = class_addMethod判断。

其主要原因就是如果直接通过method_exchangeImplementations来进行的话,可能子类里并没有originalSelector所代表的方法,你直接和父类进行了交换,这是我们不希望看到的。

因此通过addMethod来判断,如果加成功了,说明原先这个函数在子类中并不存在,我们现在添加了,只要再把swizzleSelector指向旧函数即可;而如果没成功,说明这个函数在子类中存在了,我们直接替换也不会影响父类。

Swift每日一练:妙用CAReplicationLayer

今天我们来学习一下一个在iOS常常被忽视的类 - CAReplicationLayer。我们要使用这个类来完成两个开起来很炫酷的加载效果。

老样子,先上具体的效果一看

怎么样,效果还不错吧。一个是看起来有渐变效果的圆形加载,另外一个则是模仿Apple Music的柱状图加载。

接下来,就让我们一起揭开实现的面纱吧!

CAReplicationLayer

CAReplicationLayer是CALayer的子类,它与传统的CALayer系列不同,它基本不直接承担诸多效果,而是更多的承担一种“容器”的职责。

怎么理解呢?大家可以把CAReplicationLayer看成一个工厂,你提供给他一个产品的模型,它就可以为了源源不断的复制出纺织品。回到iOS中来说就是,你提供一个CALayer给CAReplicationLayer,并告诉它你希望它复制几份,就打造出多个具有效果样式的Layer。不仅如此,动画效果也会被一同复制

因此,对于CAReplicationLayer,我们有几个特别需要关注的参数。

  • instanceCount: 代表你希望将你提供它的Layer复制几份
  • instanceDelay: 这个参数在编写动画的前提下特别有用,它表明对每一个复制(或者原生)出来的Layer,启动动画之间的时间差在多少。
  • instanceTransform: 这个参数表示对于每一个Layer,它们之间的形变差距是多少。比如,每个Layer需要有相同的间隔。

渐变效果的圆形加载

了解完CAReplicationLayer的基本知识以后,我们首先来看看圆形加载效果怎么实现。

第一步,毫无疑问的,需要构建一个CAReplicationLayer。

let replicationLayer2 = CAReplicatorLayer()

第二部,需要一个样品,让我们的CAReplicationLayer复制生产

let circle = CAShapeLayer()
circle.path = UIBezierPath(ovalInRect: CGRect(x: 0, y: 0, width: 10, height: 10)).CGPath
circle.lineWidth = 1
circle.fillColor = UIColor.whiteColor().CGColor
circle.transform = CATransform3DMakeScale(0.1, 0.1, 0.1)

在这里,我们构建了一个白色小球,把初始大小设置为了0.1。

接下去,我们需要然CAReplicationLayer开始复制了,

replicationLayer2.instanceCount = 12
replicationLayer2.instanceTransform = CATransform3DMakeRotation(CGFloat(2 * M_PI/12.0), 0.0, 0.0, 1.0)

从上述代码我们可以看出,我们首先构建了12个复制体,然后将这12个小球通过instanceTransform绕Z轴均匀的分度在圆周上。

现在赶快run一下你的app,看看效果是不是正如我所说的!

现在万事具备,只欠动画了!

首先我们来定义如例子中的放缩动画

func scaleAnimation() -> CABasicAnimation {
    let animation = CABasicAnimation(keyPath: "transform.scale")
    animation.duration = 1.5
    animation.fromValue = 1.0
    animation.toValue = 0.1
    animation.repeatCount = Float.infinity

    return animation
}

我们定义了一个针对scale属性、时常1.5s的动画,然后我们将这个动画添加到小球上。

circle.addAnimation(scaleAnimation(), forKey: "scale")

现在如果你跑一下代码的话,你会发现,所有的小球都同时启动了动画,和例子的效果不一致,怎么回事呢?

嘿嘿,还记得我们之前提及的属性instanceDelay吗?没错,我们需要借助它的力量。

replicationLayer2.instanceDelay = 1.5/12

现在再看看?哈哈,效果拔群!

Apple Music加载效果

下面的代码,很清晰易懂,我就不逐条解析啦,有问题欢迎大家在下方评论!

// 1. 创建容器ReplicationLayer
let replicationLayer = CAReplicatorLayer()

// 2. 创建柱状图
let bar = CALayer()
bar.frame = CGRect(x: 0, y: 0, width: 8, height: 40)
bar.position = CGPoint(x: 10, y: 75)
bar.backgroundColor = UIColor.purpleColor().CGColor

// 3. 复制生产3个,并添加水平间隔
replicationLayer.addSublayer(bar)
replicationLayer.instanceCount = 3
replicationLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)

// 4. 创建动画,构造时间差
func jumpAnimation(bar:CALayer) -> CABasicAnimation {
    let animation = CABasicAnimation(keyPath: "position.y")
    animation.toValue = bar.position.y - 35.0
    animation.duration = 0.45
    animation.autoreverses = true
    animation.repeatCount = Float.infinity

    return animation
}

bar.addAnimation(jumpAnimation(bar), forKey: "jump")
replicationLayer.instanceDelay = 0.3

Swift每日一练:自定义转场在iOS8中的那些坑

之前因为面试的缘故发现了自己在自定义转场这块有点欠缺,今天拿Swift练下手,实现一个自定义转场的效果。没耐心的话请直接翻到最后吧,我前面都是铺垫呢。

首先让我们先来看看最后实现的效果:

下面就让我们一步步来看看是如何实现这个效果的。

自定义转场

要实现自定义转场动画,比较重要的就是三个部分。

  • UIViewControllerContextTransition

    这个接口主要用来提供切换上下文给开发者使用,包含了从哪个VC到哪个VC等各类信息,一般不需要开发者自己实现。

    本文关注的包含了如下一些内容:

    1. - (UIView *)containerView; 
    // VC切换所发生的view容器    
    
    2. - (UIViewController *)viewControllerForKey:(NSString *)key;
    // 根据UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey两种,分别返回将要切出和切入的ViewController。
    
    3. - (void)completeTransition:(BOOL)didComplete; 
    // 报告切换已经完成。
    
  • UIViewControllerAnimatedTransition

    这个接口主要用来定义如何完成转场动画,同时定义转场动画的持续时间。(在本文中我们不考虑交互式的转场)

    1. - (NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext; 
    // 返回转场动画持续的时间
    
    2. - (void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext; 
    // 我们自定义的转场要在这里完成
    
  • UIViewControllerTransitionDelegate

    这个接口主要用于指定,我们希望采用哪种转场效果(比如你可以根据不同的状态,切换不同的自定义专场效果)

    1. - (id< UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
    // 当弹出模态窗口的时候,使用什么转场效果
    
    2. - (id< UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed;
    // 当关闭模态窗口的时候,使用什么转场效果
    

实现

知道了转场动画需要的必要条件,我们可以很轻松分别实现三个部分。

第一部分UIViewControllerContextTransition在本文中并没有特殊定制化的地方,直接完成。

第二部分关于UIViewControllerAnimatedTransition的代码如下:

class SwipeAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    enum SwipeTo {
        case Main
        case Modal
    };

    var transitonTo:SwipeTo = .Main

    // 0. 返回动画时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 1.0
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // 1. 获取相关资源
        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!

        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)

        // 2. 弹出模态
        if (self.transitonTo == .Modal) {
            if toView != nil {
                transitionContext.containerView().addSubview(toView!)

                toView!.alpha = 0.0

                // 2.1 以左上角为锚点旋转
                fromVC.view.layer.anchorPoint = CGPoint(x: 0, y: 0)
                fromVC.view.layer.position = CGPointMake(0, 0)

                UIView.animateWithDuration(1.0, animations: { () -> Void in
                    fromVC.view.transform = CGAffineTransformMakeRotation(CGFloat(-M_PI/2))
                    toView!.alpha = 1.0
                }, completion: { (completion:Bool) -> Void in
                    // 2.2 报告转场动画完成
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
                })
            }
        } else {
            // 3. 关闭模态
            if fromView != nil {
                fromView!.alpha = 1.0
                UIView.animateWithDuration(1.0, animations: { () -> Void in
                    toVC.view.transform = CGAffineTransformMakeRotation(0)
                    fromView!.alpha = 0.0
                }, completion: { (completion:Bool) -> Void in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
                })
            }
        }
    }
}

实现非常简单,我们来一步步看下。

    1. 根据UIViewControllerAnimatedTransition协议返回动画时间
    1. 根据UIViewControllerContextTransition获取我们需要操作的即将切出的ViewController(fromVC)以及即将切入的页面(toView),为什么要用这种获取方式,稍微在重点分析会指出
    1. 当遇到是弹出模态窗口转场的时候,我们首先将toView加入到转场过程提供的一个containView中,然后改变fromVC的锚点,进行旋转,同时我们对toView进行了一个淡入淡出。当转场动画完成以后,在回调的closure中报告转场动画已经完成
    1. 当关闭模态转场的时候,这个时候转场的出和入就正好和弹出的时候截然相反。原先的fromVC成了现在的toVC,因此我们在这里将toVC旋转回原来的位置。当然别忘了我们的淡入淡出啦,原理是一样的,只要改变fromView的alpha即可。动画完成后,依然要报告我们的转场完成了。

第三部分,UIViewControllerTransitionDelegate的实现依然非常简单,我们仅仅需要告知转场发生时,我们具体要采用哪种转场效果就好了。

func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    self.animator.transitonTo = .Modal
    return self.animator
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    self.animator.transitonTo = .Main
    return self.animator
}

在这里,我们复用了同一个转场效果,通过不同的transitionTo参数进行控制。你当然也可以给两个转场分别生成对应不同的转场效果。

iOS8的坑

嘿嘿,重头戏来了,千万别错过。

一开始实现这个转场效果的时候,压根没想到这么复杂,但是,突然发现了一个很大的问题:

当弹出模态窗口的时候,转场效果正常,最后成功显示模态界面。但是当关闭模态窗口的时候,转场效果依然正确,但是转场结束后,整个屏幕都黑了。

What the F*ck!!!

我以为是我自己实现有问题,但是我去Github上找了几个著名的转场效果跑了下,都存在这个问题,那我就百思不得其解了呀!!!

误打误撞

从网上搜寻了很久之后,我还是没有头绪,于是我首先尝试将如下代码中

var modalVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ModalViewController") as! UIViewController
modalVC.transitioningDelegate = self
modalVC.modalPresentationStyle = .Custom
presentViewController(modalVC, animated: true, completion: nil)

modalVC.modalPresentationStyle = .Custom中的.Custom改成了.FullScreen。
这一下子就给我整好了!

所以,给大家提个醒,如果遇到相似的问题,解决方法很简单,就是.FullScreen即可。

深层原因

作为一个站在红旗下的三好学生,弄懂问题的深层原因才是最主要的,通过不过的debug,我终于弄懂了。

首先,我们要强调一个基本知识。一个UIView的superview最多只能由一个。当一个本身处于别的UIView下的subView被添加到另一个UIView上的时候,它就自动被从前一个UIView的Hierarchy中移除了。

还记得我们之前一个很奇怪的写法吗?
根据UIViewControllerContextTransition获取我们需要操作的即将切出的ViewController(fromVC)以及即将切入的页面(toView)

我们这么写的原因就是因为我们刚刚强调的基本知识,别急,让我们一步步来解析。

在iOS8中,苹果提供了一个新的API:

@availability(iOS, introduced=8.0)
func viewForKey(key: String) -> UIView?

并且在文档中明确强调了一点:

// Currently only two keys are defined by the
// system - UITransitionContextToViewControllerKey, and
// UITransitionContextFromViewControllerKey. 
// Animators should not directly manipulate a view controller's views and should
// use viewForKey: to get views instead.
func viewControllerForKey(key: String) -> UIViewController?

什么意思呢?
之前在iOS7中,开发者需要通过viewControllerForKey这个方法获取切入切出的ViewController,并直接操作ViewController对应的View来编写转场动画。而在iOS8以后,苹果规定必须使用viewForKey来获取fromView和toView来进行转场动画的操作。

那这个API的更改和黑屏有什么关联呢?

  • 在整个转场过程中,我们都依赖于转场上下文transitonContext提供的containerView容器进行view动画的操作。这是因为在转场完成前,即将切入的viewcontroller都不存在于当前的可视界面的视图层级内(View Hierarchy)。因此,苹果提供了一个过渡的容器给我们使用(如果大家debug下的话,就会发现在转场过程之中,UIWindow上多了一个UITransitionView,就是切换上下文的containerView)。
  • 在iOS7的实现中,我们需要将fromVC.view和toVC.view都通过addSubView的方式添加到容器View上进行动画展示。由于是直接操作了ViewController的view,因此,fromVC的view会被从当前的视图层级中移除
  • 但是,iOS7中,会在转场动画完成后,自动将fromVC的view添加回原先fromVC从属的父视图中
  • iOS8中不会
  • 在本文的初版实现中,在转场过程中当判断transitionTo == .Main的时候,将此时toViewController.view (也就是原先的主窗口) 添加到了containerView上。因此,当转场结束的时候,containerView从window可见视图层级中移除了,因此就变得不可见,从而变成黑屏了。

那么为什么模态窗口在转场过程后可见呢?

  • 因为这个特性依然正确。

containerView到底是啥?

  • 就是一个过渡的UITransitionView,一个转场效果对应生成一个(会复用)。
  • 在转场结束后自动从视图层级中移除,因此不需要大家手动进行removeFromSuperView。

那么viewForKey和viewControlelrForKey直接操纵view的区别呢?

  • viewForKey很可能返回的是一个完全克隆VC的view的对象。

到这,相信大家都弄懂了吧,我真是太佩服我自己了。

浅入浅出VIPER设计架构(1)

之前和豌豆荚的同学聊天,发现他们的App从MVVM切换到VIPER设计架构,于是这两天花了点时间研读了一下 相关资料,浅谈一下。

VIPER是什么

VIPER是View, Interactor, Presenter, Entity, Routing5个词语的缩写,这5个词语也是VIPER架构的核心。

View - 视图,接收传递的内容,然后进行内容展示。

Presenter - 视图逻辑控制器,这个模块只用来执行和UI相关的逻辑。什么叫和UI相关的逻辑呢?比如:接收用户交互、将展示的内容分发给不同的UI界面进行展示。

Interactor - 纯业务逻辑控制器,这个模块完全脱离于UI,只用来做业务相关的逻辑控制。举例来说,比如下载逻辑、计算逻辑等等可以划分为单一Use Case的逻辑。

Entity - 基础的数据模型,比方说大家经常定义的XXXModel之类的,但是,这里的XXXModel可以认为基本就是纯数据结构了,不包含轻量级业务处理逻辑

Routing - 路由器,视图切换的逻辑。比如包含如何从一个界面切换到另一个界面,切换的顺序是如何等等。

除了五大核心之外,我们还经常使用了Data Store这个模块。因为我们之前提过,我们的Entity基本就是纯数据结构,不包含轻量级的业务逻辑,因此,Entity进行持久化这个职责是要单独划分出来,而这个职能就落到了Data Store身上。

下面是网上一张比较经典的架构设计图:

为什么要用VIPER

在软件工程领域,有一个很重要的观点,就是测试、测试、测试。(重要的话说三遍!)

之前在BMW Group实习的时候从事Java开发,每一个函数都要经过JUnit的测试,也就是我们所谓的Test-Driven Development。但是iOS开发由于其逻辑和视图的强耦合关系,常常导致业务逻辑不能完全独立于界面,进行单元测试十分困难。比如,臭名昭著的”Massive View Controller” 就是因为将大量的业务逻辑和视图逻辑耦合进了Controller,导致Controller非常臃肿,也不容易剥离进行测试。

而VIPER架构引入了Interactor和Presentator两个概念,将业务逻辑和视图逻辑独立开来,从而可以单独测试业务逻辑的代码。

例子分析

说了好多虚的概念,有些人一定已经被弄晕了,还是赶紧看两个典型的例子看深入了解下吧.

1. Counter

第一个例子来源于Counter,是一个非常简单了计数应用。麻雀虽小,却五脏俱全。

打开项目,我们可以快速的过一下项目结构:

-- CNTAppDelegate.h/.m
-- CNTCountInteractorIO.h
-- CNTCountInteractor.h/.m
-- CNTCountPresenter.h/.m
-- CNTCountView.h
-- CNTCountViewController.h/.m/.xib

首先我们来看一下最简单的模块:CNTCountView

// CNTCountView.h
@protocol CNTCountView <NSObject>
- (void)setCountText:(NSString*)countText;
- (void)setDecrementEnabled:(BOOL)enabled;
@end

咦?为什么这个View只包含一个Protocol呢?说好的View用来接收传递的内容然后进行内容展示呢?

原因是这样:在iOS应用中,一个无法避免的模块就是ViewController(无论如何每个应用都必须存在一个RootViewController)。每个ViewController包含一个根View用来进行视图展示。因此,在将VIPER架构应用到iOS领域的过程中,实际上起到View作用的是ViewController!(当然,ViewController不仅仅起到了视图展示的作用)

题外话:我个人认为这个例子的取名不太好,让人迷惑,如果换个名字,如CNTCountViewOperation就没有这个问题了

既然如此,让我们赶快去看看相关的ViewController:CNTCountViewController中的实现。

// CNTCountViewController.h
@interface CNTCountViewController : UIViewController <CNTCountView>
@property (nonatomic, weak) IBOutlet    UILabel*    countLabel;
@property (nonatomic, weak) IBOutlet    UIButton*   decrementButton;
@property (nonatomic, weak) IBOutlet    UIButton*   incrementButton;

@property (nonatomic, strong)   CNTCountPresenter*  presenter;
@end

在上述CNTCountViewController的头文件中,我们可以看到该类遵从了CNTCountView协议。其次,他包含了真正的View:两个UIButton和一个UILabel。说明这和我们之前说的ViewController起到了真正的视图展示作用是吻合的。

同时,我们还看到了一个非常显眼的类:CNTCountPresenter。如果这个类的命名没错,那这个类就是所谓的Presenter了,用来进行界面逻辑控制的类。那么是不是这样呢?我们Command + 左击这个类一探究竟!

// CNTCountPresenter.h
@interface CNTCountPresenter : NSObject <CNTCountInteractorOutput>
@property (nonatomic, weak)     id<CNTCountView>            view;
@property (nonatomic, strong)   id<CNTCountInteractorInput> interactor;

- (void)updateView;
- (void)increment;
- (void)decrement;
@end

从CNTCountPresenter的头文件中,我们能看出很多逻辑关系。Presenter包含了一个View和一个Interactor。这和文首我们对于VIPER的介绍相吻合:Presenter从Interactor获取数据,经过界面逻辑处理后,将数据发送给View进行展示。

从Interactor请求获取数据的部分代码如下:

// CNTCountPresenter.m
- (void)updateView
{
    [self.interactor requestCount];
}

- (void)increment
{
    [self.interactor increment];
}

- (void)decrement
{
    [self.interactor decrement];
}

由于Presenter从Interactor获取数据,那么势必Presenter是Interactor的一个输出,而Interactor是Presenter的一个输入。

// CNTCountInteractorIO.h
@protocol CNTCountInteractorInput <NSObject>
- (void)requestCount;
- (void)increment;
- (void)decrement;
@end

@protocol CNTCountInteractorOutput <NSObject>
- (void)updateCount:(NSUInteger)count;
@end

这里各位可以先不关注Protocol协议的设计本身,只要理解逻辑关系即可。

通过这样的职责划分,落入到Interactor类中的职责就是最基本的业务逻辑:计数的增加和减少。

// CNTCountInteractor.h
- (void)requestCount
{
    [self sendCount];
}

- (void)increment
{
    ++self.count;
    [self sendCount];
}

- (void)decrement
{
    if ([self canDecrement])
    {
        --self.count;
        [self sendCount];
    }
}

至此,我们可以将Counter的项目架构进行如下表示:

Interactor <——–> Presenter

  • InteractorPresenter输入
  • PresenterInteractor输出
  • Presenter 通过某些事件触发,去向其输入Interactor请求数据

Presenter <——–> View(ViewController)

  • ViewPresenter输出
  • ViewController 触发了 PresenterInteractor中请求新的业务结果,从而更新View

Presenter更新View的代码如下:

// CNTCountPresenter.m
- (void)updateCount:(NSUInteger)count
{
    [self.view setCountText:[self formattedCount:count]];
    [self.view setDecrementEnabled:[self canDecrementCount:count]];
}

到这里,Counter这个例子的解读基本就完成了。细心的读者一定会发现,EntityRoute到哪里去了?

是的,这个例子由于过于简单,压根不需要Entity。并且由于它是单视图应用,压根不涉及到页面切换,因此也无须Route功能。不过我们在后续的浅入浅出VIPER架构(2)中解读一个更细致更负责的例子,敬请期待!

Object-Path 源码解读

Object-Path

今天看到了一个非常有意思的JavaScript库,乍一看有点iOS keypath chain的意思,名字叫Object-Path,地址点我,主要的功能包含了:

  • Set
  • Get
  • Del
  • Empty
  • Insert
  • EnsureExist
  • Push

从它的Example中我们可以先一览它的功能:

var obj = {
  a: {
    b: "d",
    c: ["e", "f"],
    '\u1200': 'unicode key',
    'dot.dot': 'key'
  }
};

var objectPath = require("object-path");

//get deep property
objectPath.get(obj, "a.b");  //returns "d"
objectPath.get(obj, ["a", "dot.dot"]);  //returns "key"
objectPath.get(obj, 'a.\u1200');  //returns "unicode key"

从这个例子中,我们可以发现对于一个Object obc,除了默认的直接通过点 .方式访问一个属性外, 如obj.a。我们还可以通过点 . 来进行“链式”访问其属性的属性(如果存在的话),如obj.a.b。这看起来真的和iOS中的keypath coding非常相似,所以,今天就让我们来分析下其实现吧!

源码解析

Set方法

例子:objectPath.set(obj, "a.h", "m"); 

Set方法是用来给obj的某个“链式”属性进行赋值的,源码如下:

function set(obj, path, value, doNotReplace){
     // 1. 
    if (isNumber(path)) {
      path = [path];
    }

    // 2.
    if (isEmpty(path)) {
      return obj;
    }

    // 3.
    if (isString(path)) {
      return set(obj, path.split('.').map(getKey), value, doNotReplace);
    }

    // 4.
    var currentPath = path[0];

    if (path.length === 1) {
      var oldVal = obj[currentPath];
      if (oldVal === void 0 || !doNotReplace) {
        obj[currentPath] = value;
      }
      return oldVal;
    }

     // 5.
    if (obj[currentPath] === void 0) {
      //check if we assume an array
      if(isNumber(path[1])) {
        obj[currentPath] = [];
      } else {
        obj[currentPath] = {};
      }
    }

    return set(obj[currentPath], path.slice(1), value, doNotReplace);
  }
    1. 判断传入的path是否是Number类型,如果不是的话,构建一个包含这个pathde数组,这里为什么要这样处理,在下文解释。注:JavaScript中不存在浮点数、整数等等不同数值类型,统一为Number
    1. 判断传入的path是不是“空”,空的情况包含:undefined,空数组,没有任何自身属性的对象。下文我们会详细查看isEmpty的实现。
    1. 判断是否是字符串,如果是字符串,就通过“.”进行分割,分割完成构建数组,然后进行set方法的重新调用。
    1. 判断是否当前path深度只为一层,var currentPath = path[0],如果是一层的话,根据是否要doNotReplace进行值的替换。

这里有个很有意思的实现 oldVal === void 0,void 0是什么鬼,我们经常会看见JavaScript::void(0)代表网页的死链接,那么oldVal == void 0又是什么意思呢?

其实,void 0就是undefined!在现代的浏览器中,它们两已经完全的等同了。而写成void 0的形式,是为了兼容。在过去的浏览器中,undefined不是一个关键字,它是个全局变量。因此,你完全可以徒手改变它的含义,

var undefined = 1;
console.log(undefined);

这样的改变,就会undefined原本的含义错乱。而由于void是一个操作符,你无法改写它的含义,因此void 0是一种更安全的写法!

在这里的含义就是,判断oldVal === void 0是不是未定义罢了。

    1. 如果不是一层深度的path,并且当前的obj[currentPath]是未定义的。那么就需要判断path[1],也就是path数组中的第二个属性是什么了。如果是数字,就将obj[currentPath]构建为数组[],否则构建一个对象{}。

为什么要分开处理呢?这里就需要提及JavaScript访问属性的一个特别的地方。对于如下这样一个对象,

var obj = {a:5}

来说,我们实现obj.a 或者 obj[“a”]都可以获得正确的结果5,但是对于另一个对象,

var obj = {1: 5}

我们只能使用obj[“1”]的方式来访问结果5,而不能使用obj.1。如果使用obj.1,就能报错误:

Uncaught SyntaxError: Unexpected number(…)

究其原因,就在于JavaScript在处理对象的key的时候,都是将key当成字符串处理的。

所以,在这里构建完相对应的下一层级obj[currrentPath],递归调用set方法即可。

Del方法

function del(obj, path) {
    if (isNumber(path)) {
     path = [path];
    }

    if (isEmpty(obj)) {
     return void 0;
    }

    if (isEmpty(path)) {
     return obj;
    }

    if(isString(path)) {
     return del(obj, path.split('.'));
    }

    // 重点
    var currentPath = getKey(path[0]);
    var oldVal = obj[currentPath];

    if(path.length === 1) {
     if (oldVal !== void 0) {
       if (isArray(obj)) {
         obj.splice(currentPath, 1);
       } else {
         delete obj[currentPath];
       }
     }
    } else {
     if (obj[currentPath] !== void 0) {
       return del(obj[currentPath], path.slice(1));
     }
    }

    return obj;
}

让我们再来看看Del方法,这个方法用于“链式”删除一些属性对应的值。由于之前的逻辑和Set类似,我们直接从重点开始看起。

  • 首先先从path[0]中获取key,然后取出对应的oldVal。
  • 如果path深度为1,并且oldVal不是未定义的话,究进行删除,删除分为两种:

    (1)如果是数组的话,通过splice删除数组中currentPath位置上的属性值
    (2)如果是object,直接进行删除即可

  • 如果深度不为1,并且oldValue不是未定义,递归调用del函数即可。

这里有一点需要注意,我们看到在获取currentPath的过程中,调用了getKey这个函数,让我们赶紧去看看这个getKey的实现。

function getKey(key){
    var intKey = parseInt(key);
    if (intKey.toString() === key) {
      return intKey;
    }
    return key;
} 

这里的逻辑不难理解,通过parseInt将key转成整型,如果转换后的结果通过toString函数和原有的key一致,就直接返回整形,否则返回原有的key。通过===(强等于号)不难理解,这里if的满足条件当且仅当key本身是string类型,同时其值是个整数才可以满足,如”5”等,像类似”a.2”, “5.5”就不会满足。

剩下的诸如Get, Has方法,实现都大同小异,我们就不再一一解读了,大家有兴趣可以自行阅读。

补充知识:JavaScript类型判断

function isNumber(value){
  return typeof value === 'number' || toString(value) === "[object Number]";
}

function isString(obj){
  return typeof obj === 'string' || toString(obj) === "[object String]";
}

function isObject(obj){
  return typeof obj === 'object' && toString(obj) === "[object Object]";
}

function isArray(obj){
  return typeof obj === 'object' && typeof obj.length === 'number' && toString(obj) === '[object Array]';
}

function isBoolean(obj){
  return typeof obj === 'boolean' || toString(obj) === '[object Boolean]';
}

在Object-Path的实现中,大量的类型判断工作都是通过如上一些函数来搞定的。有人会问了,判断一个类型为啥要这么麻烦,直接用instanceOf或者typeof不可以吗?

  • 用instanceOf是肯定错误的,如果被判断的对象不处于同一个页面,那么instanceOf就肯定失效了。

比如,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个array,并将其赋值给父页面的一个变量,这时判断该变量是否是array类型,就是失败
  • 用typeof的缺陷在于,JavaScript中存在基本类型和包装类型,对于数值5来说,它是基本类型Number,通过typeof可以准备的判断出。但是如果是包装对象var k = new Number(5),在意义上其仍然应该是属于数值类型,但是通过typeof获得的结果是object,因此就不正确。

所以,在这里的实现,我们采用了Object.prototype.toString.call(obj)的方式来获取正确的类型。

嘿嘿,今天就到这里啦。

大菜比学安卓

本人新手,学习安卓,这里用来记录一下阅读《第一行代码》的笔试和体会。

  1. 在XML引入一个id,就使用@id/id_name,如果要添加一个id,就使用@+id/id_name
  2. 给主活动指定的Label不仅会成为标题栏中的内容,还会成为Launcher中应用程序显示的名称。
  3. Intent 调用StartActivityForResult用来给上一层Activity返回数据
  4. 活动的生存期
  • 运行状态,当一个活动处于返回栈的栈顶时,就处于运行状态,系统最不愿意回收的就是处于运行状态的活动。
  • 暂停状态,当一个活动不处于栈顶位置,但是仍然可见的时候,就进入了暂停状态(因为有些活动,如对话框等,就不会占据整个屏幕,因此其他活动仍然可见)
  • 停止状态,当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。
  • 销毁状态,当一个活动从返回栈中被移除后,就进入了销毁状态。
  1. 活动生命周期回调函数
  • onCreate在活动第一次创建的时候调用
  • onStart在活动由不可见变成可见的时候调用
  • onResume在活动准备好和用户进行交互的时候调用,此时的活动一定位于栈的栈顶,且处于运行状态。
  • onPause在系统准备去启动或者恢复另一个活动的时候调用。
  • onStop在活动完全不可见的时候调用。
  • onDestory在活动被销毁之前调用,之后的活动变为销毁状态。
  • onRestart在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。

Swift每日一练:重写UICountingLabel

今天Swift练习的是准备尝试把UICountingLabel这个Github Star数超过500的库用Swift重写一遍。

UICountingLabel源码解析

整个uICountingLabel的本质就是基于一个NSTimer来计算当前时间内应该显示什么值,

- (CGFloat)currentValue {

    if (self.progress >= self.totalTime) {
        return self.destinationValue;
    }

    CGFloat percent = self.progress / self.totalTime;
    CGFloat updateVal = [self.counter update:percent];
    return self.startingValue + (updateVal * (self.destinationValue - self.startingValue));
}

这个值通过不同的插值方式得到,这个插值方式的具体实现时通过self.counter的不同子类进行实现。

@implementation UILabelCounterLinear

-(CGFloat)update:(CGFloat)t
{
    return t;
}

@end

@implementation UILabelCounterEaseIn

-(CGFloat)update:(CGFloat)t
{
    return powf(t, kUILabelCounterRate);
}

@end

@implementation UILabelCounterEaseOut

-(CGFloat)update:(CGFloat)t{
    return 1.0-powf((1.0-t), kUILabelCounterRate);
}

@end

@implementation UILabelCounterEaseInOut

-(CGFloat) update: (CGFloat) t
{
    int sign =1;
    int r = (int) kUILabelCounterRate;
    if (r % 2 == 0)
        sign = -1;
    t *= 2;
    if (t < 1)
        return 0.5f * powf(t, kUILabelCounterRate);
    else
        return sign * 0.5f * (powf(t-2, kUILabelCounterRate) + sign * 2);
}

这里用子类化这么说是不严谨的,因为这其实就类似于Java或者C#是面向了接口编程了而已,因为这里的每个类都只是实现了update这个函数接口而已。

然后实现里相对比较直观,

-(void)countFrom:(CGFloat)startValue to:(CGFloat)endValue withDuration:(NSTimeInterval)duration {
    if (duration == 0.0) {
        // No animation
        [self setTextValue:endValue];
        [self runCompletionBlock];
        return;
    }

    self.lastUpdate = [NSDate timeIntervalSinceReferenceDate];

    switch(self.method)
    {
        case UILabelCountingMethodLinear:
            self.counter = [[UILabelCounterLinear alloc] init];
            break;
        case UILabelCountingMethodEaseIn:
            self.counter = [[UILabelCounterEaseIn alloc] init];
            break;
        case UILabelCountingMethodEaseOut:
            self.counter = [[UILabelCounterEaseOut alloc] init];
            break;
        case UILabelCountingMethodEaseInOut:
            self.counter = [[UILabelCounterEaseInOut alloc] init];
            break;
    }

    NSTimer *timer = [NSTimer timerWithTimeInterval:(1.0f/30.0f) target:self selector:@selector(updateValue:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    self.timer = timer;
}

首先如果动画的时间是0,就默认直接回调。但是,我想说duration == 0.0,浮点数这么判断,真的没问题吗?

由于NSTimer无法得知确切得知道执行了多少,所以这里要记录上一步回调的lastUpdate。
根据不同的方法选择不同的Counter进行插值,然后创建NSTimer,这里我们要特别注意两句话:

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

这表示NSTimer默认加入的是RunLoop的Default Mode,但是Default Mode在进入Tracking Mode的时候(也就是当用户滑动的时候)会被阻塞,影响动画的执行。因此要特别加入TrackingMode。

但是我又想说了,NSRunLoopCommonModes难道不是包含UITrackingRunLoopMode吗?天呐,还是我理解的不对,重复添加的意义呢!!!!

然后在NSTimer的回调函数中,

NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
self.progress += now - self.lastUpdate;
self.lastUpdate = now;

if (self.progress >= self.totalTime) {
    [self.timer invalidate];
    self.timer = nil;
    self.progress = self.totalTime;
}

[self setTextValue:[self currentValue]];

if (self.progress == self.totalTime) {
    [self runCompletionBlock];
}

就是计算当前的progress进度,更新相应的值而已,没有难度。

Swift 重写

先来效果:

首先我们来回忆下Objective-C版本中的一些逻辑,使用NSTimer动态根据进度更新时间。但是我们知道,NSTimer有一个很大的问题就是他的“刷新率”不够准确。如果NSTimer设置了每两秒更新一次,那么如果RunLoop中有个耗时的任务,就会讲这个更新任务推迟,用时间轴来理解:

0 -> 2s (NSTimer回调) -> 3s (耗时的任务) -> 2s (NSTimer回调)。

那么这个会导致什么问题呢?NSTimer的回调和屏幕刷新不是同步的。因此,在Swift版本的重写中,我采用了CADisplayLink来改写,它的好处我们同样用时间轴来理解:

0 -> 2s(CADisplayLink回调) -> 3s(耗时的任务) -> 1s(空闲) -> 2s(CADisplayLink回调)

可以看出,CADisplayLink始终是保持和屏幕刷新率一样。

这里强调一下,不论是NSTimer抑或是CADisplayLink,既然他们都是基于RunLoop的,就无法脱离被所处同一个RunLoop里的其他任务所影响的宿命,所以网上那些说CADisplayLink不会被阻塞的说法都是错误的。

Swift 面向接口编程

之前我们提过“子类化“这个说法是不准确的,因此,我们在Swift中,可以基于protocol-oriented进行编程。

let WZCountRate:Float = 3.0

protocol Interpolation {
    func update(val:Float) -> Float;
}

struct Linear:Interpolation {
    func update(val: Float) -> Float {
        return val
    }
}

struct EaseIn:Interpolation {
    func update(val: Float) -> Float {
        return powf(val, WZCountRate)
    }
}

struct EaseOut:Interpolation {
    func update(val: Float) -> Float {
        return 1 - powf((1 - val), WZCountRate)
    }
}

struct EaseInOut:Interpolation {
    func update(val: Float) -> Float {
        var sign:Float = 1
        let r = Int(WZCountRate)

        if (r % 2 == 0) {
           sign = -1
        }

        var t = val * 2
        if (t < 1) {
            return 0.5 * powf(t, WZCountRate)
        } else {
            return 0.5 * (powf(t - 2.0, WZCountRate) + sign * 2) * sign
        }
    }
}

可以看到,我们用四个Struct结构体遵从了Interpolation这个接口,实现了四种不同的插值方法。

别的方面没什么特别难的,

Access Control 对Selector Callback的影响

我们都知道,Swift对类引入了Access Control这一机制,默认情况下是internal的权限。由于CADisplayLink还是基于Selector设置回调的,如下:

let displayLink = CADisplayLink(
   target: self,
   selector: Selector("displayTick:")
)

当这个displayTick函数处于public或者internal权限的时候,没有任何问题。而当你想声明如

private func displayTick()

的时候,就会产生doesn’t recognize selector的runtime error。

原因如下:
基于Selector的回调方式还是采用了Objective-C传统的runtime 查函数表的方式,但是在Swift中声明为private的函数,对于Objective-C Runtime是不可见的。因此,如果你需要让私有函数可以被查询到,你需要添加@objc关键词。

@objc private func displayTick()

当然啦, 对于IBOutlets, IBActions 以及 Core Data 相关属性来说,默认是可以被Objective-C Runtime查询到的。

最后,附上项目地址

Swift每日一练:圆形滑动条

最近遇到了好多人问我会不会Swift,虽然我很早就进行了Swift的学习,但是苹果对于Swift的更新真是日新月异,和我当时beta版本学习的时候大大不同了,所以,从今天起准备督促自己进行每日一练。

今天想要做的是一个360度的圆型滑动条。

预览效果

细节方面还有欠缺,比如,没有进行旋转圈数的控制,同时也没有进行拖拽范围的控制。

效果实现

我们首先来大致看看怎么做分解:

- 一个圆环
- 一个渐变色
- 一个控制球
- 一个显示文字

首先看看圆环怎么实现的,很简单,我们利用Core Graphic使用如下代码即可以画出一个圆圈,只要加上描边宽度,就可以形成圆环效果。

CGContextAddArc(
    ctx,
    self.frame.size.width/2,
    self.frame.size.height/2,
    self.radius,
    0,
    2.0 * M_PI,
    0
)
UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).set()
CGContextSetLineWidth(ctx, 72)
CGContextSetLineCap(ctx, kCGLineCapButt)
CGContextDrawPath(ctx, kCGPathStroke)

然后我们的圆环背景色是渐变的,这可怎么办?我们直接使用CAGradientLayer吗?当然那是其中之一的方法,不过这次我们试试用Mask + 背景色的方式实现。什么是Mask呢?简单来理解就是,如果你想做出非常复杂的形状,你可以将构造一个包含这种形状的图片,需要显示形状的地方用非白色的颜色来填充,不显示的地方用白色填充。因此,我们构建一个从上到下的渐变色背景,然后配合黑色的圆环进行Mask,就可以得到带有渐变色背景的。

CGContextSaveGState(ctx)     
CGContextClipToMask(ctx, self.bounds, mask)

let startColorComponents = CGColorGetComponents(startColor.CGColor)
let endColorComponents = CGColorGetComponents(endColor.CGColor)
let components = [
  startColorComponents[0], startColorComponents[1], startColorComponents[2], 1.0,
  endColorComponents[0], endColorComponents[1], endColorComponents[2], 1.0
]
let gradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(), components, nil, 2)

CGContextDrawLinearGradient(
    ctx,
    gradient,
    CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect)),
    CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect)),
    0
);

CGContextRestoreGState(ctx);

到此,绘画的工作基本就结束了,画白色小球的原理很简单,也不赘述了。我们下面来说一下根据手势来控制白色小球旋转位置,主要的思路还是控制位置来计算弧度。

override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent)     {    
    //println("Touches Moved")

    if let touch = touches.first as? UITouch {
        var location = touch.locationInView(self)
        moveHandle(location)
    }
}

func computeAngle(p1:CGPoint , p2:CGPoint) -> Double {
    var delta = CGPoint(x: p2.x - p1.x, y: p1.y - p2.y)

    let radians = Double(atan2(delta.y, delta.x))
    let result = toDegree(radians)
    return result >= 0 ? result : result + 360
}

其中,有一点我们要特别注意
var delta = CGPoint(x: p2.x - p1.x, y: p1.y - p2.y),在Y轴的计算上,我们需要进行上下翻转的计算,原因就在于我们通过UITouch拿到的location的Y坐标,是UIKit系的坐标体系,Y轴原点在上方,而我们通过绘图的时候需要进行进行Y轴的上下颠倒。

此外,根据计算的弧度,来更新小球的位置,由于我们是逆时针的slider,所以我们需要用 360 减去我们实际计算得到的弧度。

一些感受

真的,Swift中的类型安全太恶心了,不能忍,于是只好玩起了操作符重载

func -(lhs:CGFloat, rhs:Double) -> CGFloat {
    return lhs - CGFloat(rhs)
}

func +(lhs:Double, rhs:CGFloat) -> CGFloat {
    return CGFloat(lhs) + rhs
}

func +(lhs:CGFloat, rhs:Double) -> CGFloat {
    return lhs + CGFloat(rhs)
}

func *(lhs:Double, rhs:CGFloat) -> CGFloat {
    return CGFloat(lhs) * rhs
}

func *(lhs:CGFloat, rhs:Double) -> CGFloat {
    return lhs * CGFloat(rhs)
}

func /(lhs:Double, rhs:CGFloat) -> CGFloat {
    return CGFloat(lhs) / rhs
}

func /(lhs:CGFloat, rhs:Double) -> CGFloat {
    return lhs / CGFloat(rhs)
}

最后附上项目地址

好了,不多说了,我要升级El Captain了。

GLCaledarView 源码解读 & Glow面经

今天选择了GLCalendarView来进行源码解读,一方面是因为它的实现效果简单清晰,第二方面,是我同学让我把Glow的面经给写了。

Glow面经

可能对于大多数来说,Glow相对来说还算一个比较神秘的公司,它主要致力于基于大数据分析女性健康的app的研发。

好吧,其实我也不是很懂,但是我觉得懂一点也挺好,毕竟我是有女朋友的人。

先说说我为什么会投Glow,两点原因:

  • 主要原因我是想去参加他们的开放日吃东西,然后他们的登记页面有个投简历的选项,我就投了,但是后来开放日那天我被导师拉出去开项目会议了结果就没去成。
  • 他们的团队很厉害,学历上都是清华、复旦、交大、同济(其中交大和复旦排名不分先后,你们别喷我)。 初创团队都是Google出身,董事还是Paypal的创始人之一。

后来9月中下旬,Glow给我安排了一次电面,商量了一个时间,电话如约而至。

电面

电面的内容我不记得了,有一个问题是围绕Core Data展开的,问我多线程之间的Core Data搞过没,我说我搞过。后来说到在多个Context之间传输object的问题,一般情况下都是直接mergeChangeFromNotification就可以了,然后你就可以在private context进行数据获取,当时面试官问的一个问题是你是直接merge完了就使用了,还是[context save]提交到persistentStoreCoordinator以后通过objectID去取呢。当时我说了一个我的做法,后来我再打开我自己代码的时候,发现是因为我的app支持删后保存数据,造成了objectID不一致,所以我当时提出了刚启动的时候一定要拉去objectID,现在想想好像应该是答错了。

现场面试

地狱般的6小时连续面试,累晕了。

第一轮 iOS面试

第一轮是iOS面试,同时来了两位iOS面试官, 先上来让自我介绍下。我每次自我介绍都比较喜欢节约时间,把自己做过的所有app都装在手机上带到现场演示。于是第一轮的很多问题都是穿插着我的项目提问,具体问题我就不说了,因人而异的,所以我就说下我没答上来的知识面:

  1. UITransition
  2. UIView的仿射变换对于界面层级显示的一个效果
  3. 他们自家app中一个动画效果制作方式,我应该是答错了

这一轮给自己打个70分吧。

第二轮 数学

第二轮一上来我以为是产品面,结果是服务器端的一位面试官,先聊了聊自己对于做app有没有什么促进app增长的方式。然后,就开始做一道很奇怪的算法题,为什么说奇怪,是因为这题没有正确答案。然后说完思路以后就在白纸上写下来,我用C++写了。面试官问我为啥不用Objective-C写,我说Objective-C手写代码太长了,顺便我还想证明下我还会别的技术。

这一轮给自己打个80分。

第三轮 英语面试

第三轮是他们的产品的创始人来面我,他一上来问说他中文不好,我选择英语面试还是中文面试,我说的英语没问题,于是我们就开始了至少50分钟了全英语面试。

这一轮可以打个95分。

第四轮 CTO面

面试的时候我不知道他就是CTO,他还带我去外出吃了顿中饭,吃完回来就开始了面试。
这轮一开始先问了我怎么修一些关于多线程的Bug。然后问了我道算法题,就是求矩形相交。当时太诚实了,直接说了我做过这题,然后给出了我自己非投影的解法,于是,悲剧就从这里开始了。

然后CTO就给我出了一道在我看来比较奇特的题目,具体题目保密我不说了,反正就是一开始他出难的,我没答上来,他说你先简化下,做简单版本的。我做出来了,然后又切换成了复杂的,在他的提示下我还是没做出来。

这轮只能打30分。

第五轮 CEO面

CEO进来的时候我还在想上一轮的题目,于是悲剧又来了。我在一边想题目一边和CEO进行了握手,然后就被xx了。然后CEO提了一个让我至今非常难忘的面试题:前面面你的所有面试官他们叫什么?这下是真傻眼了。
可能最后比较好的是我用纯英文跟CEO聊了一段时间,估计挽回了一点分数。

这轮打个50分。

反正面试感觉有点晕,尤其是下午,战线拖的太长了,自己表现的不是很理想。具体技术题目不说了,说出来我认为是对以后面试者的一种不尊重,所以就略过了。总体面试的体验是硅谷风格,英语 + 技术 + 智力。

GLCalendarView 源码解析

先来张效果图

效果还挺酷炫的,这里是它的Github地址

从Github上的介绍来说,这款Calendar和别的同类库之间有个比较大的区别就是可以选择Date Range。从上面的效果我们也可以看出,用户可以通过手势选择一段连续日期。

源码解析

图层分解

既然是个UI的开源库,抛开内部逻辑实现来看,我们首先需要剖析它的图层结构,这样才有助于我们理解整个设计思路。

在看真正的源代码之前,我们首先来猜测下这玩意怎么实现的。从我个人角度出发,我一开始是这么猜测的:

  • 整体的结构应该是个UICollectionView,每个日期都是一个UICollectionViewCell。
  • 每个Cell包含一个圆的背景色(如果是今天),包含一个方形的背景色(连续),左右尽头是两个半圆的,基于这个,猜测在每个日期Cell里面包含一个背景View,填充颜色,设置圆角。
  • 最上面有个悬浮的sunday - saturday的表示星期几的UIView,应该是个独立图层,添加了阴影。国外人为什么喜欢把星期日当成一周的开始呢,真蛋疼。
  • 滚动整个过程中,会出现一个显示对应月份的View,可能是个独立的UIView。
  • 拖动过程中出现的放大镜效果,没猜出来。这个放大镜怎么做的?

带着这些疑问我们下面进入图层分解,来一个个验证我们的猜测正确不正确。

当然,首先我们先大致浏览下结构,整个项目的文件结构大致如下:

-- GLCalendarDateRange.h/.m
-- GLCalendarDayCell
   -- GLCalendarDayCell.h/.m/.xib
   -- GLCalendarDayCellBackgroundCover.h/.m
-- GLCalendarMonthCoverView.h/.m
-- GLCalendarView.h/.m/.xib
-- GLDateUtils.h/.m

GLCalendarDayCell

全局搜索下CollectionViewCell关键字,我们发现,果然在GLCalendarDayCell这个类里面,包含了一个UICollectionView成员。

GLCalendarDayCell采用xib式的图形化开发,只要找到对应的xib文件,对于一个界面的布局很轻松得就了然于胸了.

然后我们打开这个对应的xib文件,发现这个cell的布局包括了:

  • 底层的Cell
  • backgroundCover 的UIView
  • 显示日期的UILabel
  • 显示月份的UILabel

这个类很简单,具体就是做了一大堆细节配置的东西,我们来说一个有意思的地方,在GLCalendarDayCell.h里面,有这个一段定义:

@property (nonatomic, strong) UIColor *evenMonthBackgroundColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *oddMonthBackgroundColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) NSDictionary *dayLabelAttributes UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) NSDictionary *futureDayLabelAttributes UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) NSDictionary *todayLabelAttributes UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) NSDictionary *monthLabelAttributes UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *todayBackgroundColor UI_APPEARANCE_SELECTOR;

其中UIAPPEARANCESELECTOR可能对于很多人不熟悉,我们来看看的定义,

#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))

这个其实是一个宏,那么这个东西的作用是什么呢?是为了全局配置样式!

不知道大家是否还记得我们经常会写[UINavigationBar appearance].tintColor来修改导航栏的颜色,那么这个宏的作用简单来理解就是让你的类拥有可以全局修改样式的能力,如:

[GLCalendarDayCell appearance].todayBackgroundColor = [UIColol redColor];

这个类代码虽然简单,却在实现上有个很好的地方,他把这个Cell的样式(简单来说的就是颜色,边界)交由了下面我们要提到的GLCalendarDayCellBackgroundCover类去做,也就是说,这个Cell仅仅维护状态变更的逻辑,样式有单独的样式类完成绘制。

GLCalendarDayCellBackgroundCover

这个类的作用,无它,就是画。看下他的头文件定义,大家就能瞬间明白:

@interface GLCalendarDayCellBackgroundCover : UIView
@property (nonatomic) RANGE_POSITION rangePosition;
@property (nonatomic) CGFloat paddingLeft;
@property (nonatomic) CGFloat paddingRight;
@property (nonatomic) CGFloat paddingTop;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, strong) UIImage *backgroundImage;
@property (nonatomic) CGFloat borderWidth;
@property (nonatomic) BOOL inEdit;
@property (nonatomic) BOOL isToday;
@property (nonatomic) BOOL continuousRangeDisplay;
@property (nonatomic) CGFloat pointSize;
@property (nonatomic) CGFloat pointScale;
- (void)enlargeBeginPoint:(BOOL)enlarge;
- (void)enlargeEndPoint:(BOOL)enlarge;
@end

当然,这里要提一点,画圆的几种方法:

  • cornerRadius -> GPU offscreen rendering 低性能
  • CAShaperLayer 当成mask -> GPU offscreen rendering 低性能
  • drawRect + UIBeizerPath -> CPU offerscreen rendering,快,但是内存开销大

这里在实现圆角的时候,就用了第三种:

path = [UIBezierPath bezierPathWithOvalInRect:rect];
[path closePath];
[self.fillColor setFill];
[path fill];

别的也没什么,都是具体的数学求画的位置。以如下代码为例子说明下

if (self.rangePosition == RANGE_POSITION_BEGIN) {
    [path moveToPoint:CGPointMake(radius + borderWidth + paddingLeft, paddingTop + borderWidth)];
    [path addArcWithCenter:CGPointMake(radius + borderWidth + paddingLeft, midY) radius:radius startAngle: - M_PI / 2 endAngle: M_PI / 2 clockwise:NO];

    [path addLineToPoint:CGPointMake(width, height - borderWidth - paddingTop)];
    [path addLineToPoint:CGPointMake(width, borderWidth + paddingTop)];

    [path closePath];
}

RANGEPOSITIONBEGIN表示为左侧开头的cell,所以有个左弧度。画法过程如下:

  • 左弧度的上部顶点作为贝塞尔曲线的初始点
  • 画左弧度
  • 画左弧度下方的一条水平横线
  • 画水平横线右端顶点向上的竖直线
  • 封闭,自动会在竖直线和上部初始顶点间添加一条水平横线。

GLCalendarView

最后来说下对外暴露的类,GLCalendarView。
这个类的实现就是一对控制逻辑,主要说下我特别感兴趣的拖动range的过程中放大镜的实现。

  • 首先这个放大镜,是个中心区域透明的图片。我一开始是纯代码编程的,真蛋疼。
  • 这个放大镜底下偷偷埋了个UIImageView,每次拖动的更新的时候,都自动去截图,把截图放到这个UIImageView里面,就完成了放大镜的效果。

具体的实现如下:

- (void)showMagnifierAboveDate:(NSDate *)date
{
    if (!self.showMagnifier) {
        return;
    }
    GLCalendarDayCell *cell = (GLCalendarDayCell *)[self collectionView:self.collectionView cellForItemAtIndexPath:[self indexPathForDate:date]];
    CGFloat delta = self.cellWidth / 2;
    if (self.draggingBeginDate) {
        delta = delta;
    } else {
        delta = -delta;
    }
    UIGraphicsBeginImageContextWithOptions(self.maginifierContentView.frame.size, YES, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextFillRect(context, self.maginifierContentView.bounds);
    CGContextTranslateCTM(context, -cell.center.x + delta, -cell.center.y);
    CGContextTranslateCTM(context, self.maginifierContentView.frame.size.width / 2, self.maginifierContentView.frame.size.height / 2);
    [self.collectionView.layer renderInContext:context];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    self.maginifierContentView.image = image;
    self.magnifierContainer.center = [self convertPoint:CGPointMake(cell.center.x - delta - 58, cell.center.y - 90) fromView:self.collectionView];
    self.magnifierContainer.hidden = NO;
}

主要说下CGContext那块,因为发现好多人还不是特别能理解。

首先,坐标系和UIKit是反过来的!
然后这段代码做了啥呢?

  • 获取当前拖拽日期的对应的Cell。

  • 创建一个画布,画布大小就是放大镜的大小。

    UIGraphicsBeginImageContextWithOptions(self.maginifierContentView.frame.size, YES, 0.0);
    
  • 位移到cell的位置处进行截图,因为是CGContext画图,所以向上是正的,向下负的。

    CGContextTranslateCTM(context, -cell.center.x + delta, -cell.center.y);
    
  • 向上移到放大镜的中心截图,否则因为放大镜的大小是cell的高度,会包含上cell的下半部分,和下cell的上半部分。

    CGContextTranslateCTM(context, self.maginifierContentView.frame.size.width / 2, self.maginifierContentView.frame.size.height / 2);
    

Thread Local 变量

在GLCalendarView里面,有一个比较有亮点的实现:

+ (NSCalendar *)calendar {
    NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];

    NSCalendar *cal = [threadDictionary objectForKey:@"GLCalendar"];
    if (!cal) {
        cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
        cal.locale = [NSLocale currentLocale];
        [threadDictionary setObject:cal forKey:@"GLCalendar"];
    }
    return cal;
}

我们都知道,NSCalendar的初始化是比较耗时的,所以经验上来说一般会构建一个单例子使用。但是在这里可以看到,这里用了threadDictionary去获取thread local variable。相当于在每个使用到这个函数的线程里都构建一个thread local的单例子,那么thread local的好处有啥呢?

  • 线程安全
  • thread local cache,访问快,避免flush cache line。

但是其实我个人不是很理解这么做的优势究竟有多大,请大家指点一下。

实现iOS中的函数节流和函数防抖

函数防抖与节流

今天来和大家谈论一个非常有意思的话题,就是函数节流和函数防抖。
可能大家还不是非常了解这两个术语的意思,让我们先来看下他们的含义吧。


Throttling enforces a maximum number of times a function can be called over time. As in “execute this function at most once every 100 milliseconds.”

首先是函数节流(Throttling),意思就是说一个函数在一定时间内,只能执行有限次数。比如,一个函数在100毫秒呢,最多只能执行一次。当然,也可以不执行!

看完了节流,我们再来看看函数防抖(Debouncing)。


Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. As in “execute this function only if 100 milliseconds have passed without it being called.”

函数防抖的意思就是,一个函数被执行过一次以后,在一段时间内不能再次执行。比如,一个函数执行完了之后,100毫秒之内不能第二次执行。

咦?有人看到这,肯定就有疑惑了,这两玩意有啥区别啊?这么解释吧,函数防抖可能会被无限延迟。用现实乘坐公交车中的例子来说,Throttle就是准点就发车(比如15分钟一班公交车);Debounce就是黑车,上了一个人以后,司机说,再等一个人,等不到,咱么10分钟后出发。但是呢,如果在10分钟内又有一个人上车,这个10分钟自动延后直到等待的10分钟内没人上车了。换句话说,Debounce可以理解成merge一段时间的一系列相同函数调用。

如果还不能理解,这里有个很好玩的在线演示 (PS:你必须用电脑上)

JavaScript中的函数节流和防抖

说到防抖和节流,我们就不得不先来提一提JavaScript中坑爹的DOM操作。我们直接看underscore.js中的源码:
首先是Throttle:

_.throttle = function(func, wait) {
  var context, args, result;
  var timeout = null;

  var previous = 0;
  var later = function() {
    // 若设定了开始边界不执行选项,上次执行时间始终为0
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = _.now();
    // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
    if (!previous && options.leading === false) previous = now;
    // 延迟执行时间间隔
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
    // remaining大于时间窗口wait,表示客户端系统时间被调整过
    if (remaining <= 0 || remaining > wait) {
      clearTimeout(timeout);
      timeout = null;
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    //如果延迟执行不存在,且没有设定结尾边界不执行选项
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};

代码逻辑非常好理解,

var remaining = wait - (now - previous);

就是计算下上一次调用的时间和现在所处的时间间隔是不是超过了wait的时间(这个时间只有可能会大于等于wait,因为runloop是很繁忙的,前面一个任务很耗时,那你就多等一会呗)
可能会有人不理解option是啥意思,我们看下代码中涉及了以下两个参数:

options.leadingoptions.trailing

这两个参数的意思就是禁止第一次调用和最后一次调用,简单吧。

理解了函数节流,我们再来看看防抖是怎么做的。
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;

  var later = function() {
    // 据上一次触发时间间隔
    var last = _.now() - timestamp;

    // 上次被包装函数被调用时间间隔last小于设定时间间隔wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function() {
    context = this;
    args = arguments;
    timestamp = _.now();
    var callNow = immediate && !timeout;
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
};

可以看到,在防抖的函数中包含了一个timestamp,这个参数用来记录了当前这个函数最后一次被调用是什么时候,每次调用的时候,就更新了timestamp。有人可能会问,这个timestamp在多次调用的过程中还能保留吗?答案是肯定的,debounce函数返回的实际是一个匿名函数,这个匿名函数就是一个闭包环境,可以捕捉timestap进行持久话访问,所以多次调用这个匿名函数实际上访问的都是同一个timestamp。

实现iOS中的函数节流

重头戏来啦!!!!。
在iOS中,我们经常会遇到这样的需求,比如说你要根据UIScrollView的contentOffset进行某些计算。这些计算有的很简单不耗时,比如根据offset动态的更改UINavigationBar的alpha值。但是有些就很复杂,比如你要根据某些offset启动某些动画,或者进行大规模的运算,还有很多时候时候会发送很多异步的API请求,由于很多用户会不停的用指尖在完美的iPhone屏幕上来回滑动,你一定不想你的App甚至是服务器这些操作给玩崩了,所以,函数节流是必不可少的。

初级版本

第一个版本的函数节流如下:

#import "PerformSelectorWithDebounce.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (Throttle)

const char * WZThrottledSelectorKey;

- (void) wz_performSelector:(SEL)aSelector withThrottle:(NSTimeInterval)duration
{
    NSMutableDictionary *blockedSelectors = objc_getAssociatedObject(self, WZThrottledSelectorKey);

    if(!blockedSelectors)
    {
        blockedSelectors = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, kDictionaryOfSelectorsToBlock, blockedSelectors, OBJC_ASSOCIATION_RETAIN);
    }

    NSString * selectorName = NSStringFromSelector(aSelector);
    if(![blockedSelectors objectForKey:aSelectorAsStr])
    {
        [blockedSelectors setObject:selectorName selectorName];

        objc_msgSend(self, aSelector);
        [self performSelector:@selector(unlockSelector:) withObject:aSelectorAsStr afterDelay:duration];

    }
}

-(void)unlockSelector:(NSString*)selectorAsString
{
    NSMutableDictionary *blockedSelectors = objc_getAssociatedObject(self, WZThrottledSelectorKey);
    [blockedSelectors removeObjectForKey:selectorAsString];
}

初看这个代码逻辑,这个代码基本实现了在一定时间内只调用有限次数的函数。但是,这哪是函数节流啊,这是函数断流!不信你们在[UIScrollView scrollViewDidScroll]试试看,你们就知道啥叫断流了。原因也很简单,我们的unlockSelector是基于performSelector解锁的,而performSelector是基于runloop的,我们在不停的滚动的时候就会导致整个主runloop被我们占用,因此,unlock的函数一直没有得到调用,结果就导致了断流。

高级版本

因此,我又实现了一个高级版本,代码如下,需要测试用例和源码的可以上我的github自取,地址戳我。代码逻辑很简单同时也解决了上述版本所提及的问题。

#import "NSObject+Throttle.h"
#import <objc/runtime.h>
#import <objc/message.h>

static char WZThrottledSelectorKey;
static char WZThrottledSerialQueue;

@implementation NSObject (Throttle)

- (void)wz_performSelector:(SEL)aSelector withThrottle:(NSTimeInterval)inteval
{
    dispatch_async([self getSerialQueue], ^{
        NSMutableDictionary *blockedSelectors = [objc_getAssociatedObject(self, &WZThrottledSelectorKey) mutableCopy];

        if (!blockedSelectors) {
            blockedSelectors = [NSMutableDictionary dictionary];
            objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);
        }

        NSString *selectorName = NSStringFromSelector(aSelector);
        if (![blockedSelectors objectForKey:selectorName]) {
            [blockedSelectors setObject:selectorName forKey:selectorName];
            objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);

            dispatch_async(dispatch_get_main_queue(), ^{
                objc_msgSend(self, aSelector);

                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(inteval * NSEC_PER_SEC)), [self getSerialQueue], ^{
                    [self unlockSelector:selectorName];
                });
            });
        }
    });
}

#pragma mark - Private
- (void)unlockSelector:(NSString *)selectorName
{
    dispatch_async([self getSerialQueue], ^{
        NSMutableDictionary *blockedSelectors = [objc_getAssociatedObject(self, &WZThrottledSelectorKey) mutableCopy];

        if ([blockedSelectors objectForKey:selectorName]) {
            [blockedSelectors removeObjectForKey:selectorName];
        }

        objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);
    });
}

- (dispatch_queue_t)getSerialQueue
{
    dispatch_queue_t serialQueur = objc_getAssociatedObject(self, &WZThrottledSerialQueue);
    if (!serialQueur) {
        serialQueur = dispatch_queue_create("com.satanwoo.throttle", NULL);
        objc_setAssociatedObject(self, &WZThrottledSerialQueue, serialQueur, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return serialQueur;
}

@end

Flux源码解析(一)

Flux源码解析 第一章

Flux是Facebook推出的一个新的Web架构,用来构建新一代的客户端的Web程序,今天我们来解析下其中的源码:Dispatcher.js和Invariant.js

准备工作

建议大家先了解一下什么是Flux架构,Facebook的官网上有非常详细的解释:链接点我

Invariant.js

首先让我们来看下Invariant.js的代码内容,非常短:

"use strict";

var invariant = function(condition, format, a, b, c, d, e, f) {
  1. 
  if (__DEV__) {
    if (format === undefined) {
      throw new Error('invariant requires an error message argument');
    }
  }

  2. 
  if (!condition) {
    var error;
    if (format === undefined) {
      error = new Error(
        'Minified exception occurred; use the non-minified dev environment ' +
        'for the full error message and additional helpful warnings.'
      );
    } else {
      var args = [a, b, c, d, e, f];
      var argIndex = 0;
      error = new Error(
        'Invariant Violation: ' +
        format.replace(/%s/g, function() { return args[argIndex++]; })
      );
    }

    error.framesToPop = 1; // we don't care about invariant's own frame
    throw error;
  }
};

module.exports = invariant;

‘use strict’ 不必多说,就是构建了一个严格的JS的执行环境。
整个Invariant.js其实就是定义了一个函数, 名字叫Invariant, 他的参数包含一个condition(判断条件),一个自定义的报错格式,以及一系列的自定义参数。

第一次读Flux.js源码的时候,这里采用的还是arguments可变参数的定义方式,今天写这篇文章的时候竟然就改写了,晕!

然后我们来分两个段落查看下其中的代码含义。

第一部分,定义了一个dev变量,这个变量就是一种类似于配置的环境变量。玩过JavaScript或者ruby的人可能都有所了解,比如非常著名的’development’, ‘product’, ‘test’就是与之类似的。
如果没有format是个未定义的变量(在这里就是没有传入第二个参数),那么就保个错。

第二部分也很简单,如果condition为假值,说明这个条件没被满足,那么执行报错的功能,报错的时候使用的提示格式就是传入的format, 参数则为自定义的a-f。

总之,这个Invariant.js的作用就是构建了一个类似iOS中NSAssert的东西。

哦,这里要指出一下 /%s/g这个正则表达式,这玩意啥意思呢。其实就类似于我们常常用的printf函数,可以简单理解为,把format中所有的%s, 替换成自定义的参数,g表示为global。

重点:Dispatcher.js

再让我们来看一看Dispatcher.js,闲话不多话,上代码。

'use strict';

var invariant = require('./invariant');

export type DispatchToken = string;

var _prefix = 'ID_';

class Dispatcher<TPayload> {
  _callbacks: {[key: DispatchToken]: (payload: TPayload) => void};
  _isDispatching: boolean;
  _isHandled: {[key: DispatchToken]: boolean};
  _isPending: {[key: DispatchToken]: boolean};
  _lastID: number;
  _pendingPayload: TPayload;

  constructor() {
    this._callbacks = {};
    this._isDispatching = false;
    this._isHandled = {};
    this._isPending = {};
    this._lastID = 1;
  }


  unregister(id: DispatchToken): void {
    invariant(
      this._callbacks[id],
      'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
      id
    );
    delete this._callbacks[id];
  }

  waitFor(ids: Array<DispatchToken>): void {
    invariant(
      this._isDispatching,
      'Dispatcher.waitFor(...): Must be invoked while dispatching.'
    );
    for (var ii = 0; ii < ids.length; ii++) {
      var id = ids[ii];
      if (this._isPending[id]) {
        invariant(
          this._isHandled[id],
          'Dispatcher.waitFor(...): Circular dependency detected while ' +
          'waiting for `%s`.',
          id
        );
        continue;
      }
      invariant(
        this._callbacks[id],
        'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
        id
      );
      this._invokeCallback(id);
    }
  }

  dispatch(payload: TPayload): void {
    invariant(
      !this._isDispatching,
      'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
    );
    this._startDispatching(payload);
    try {
      for (var id in this._callbacks) {
        if (this._isPending[id]) {
          continue;
        }
        this._invokeCallback(id);
      }
    } finally {
      this._stopDispatching();
    }
  }

  isDispatching(): boolean {
    return this._isDispatching;
  }

  _invokeCallback(id: DispatchToken): void {
    this._isPending[id] = true;
    this._callbacks[id](this._pendingPayload);
    this._isHandled[id] = true;
  }

  _startDispatching(payload: TPayload): void {
    for (var id in this._callbacks) {
      this._isPending[id] = false;
      this._isHandled[id] = false;
    }
    this._pendingPayload = payload;
    this._isDispatching = true;
  }

  _stopDispatching(): void {
    delete this._pendingPayload;
    this._isDispatching = false;
  }
}

module.exports = Dispatcher;

首先我们来看一下这句话:

export type DispatchToken = string;

这其实就是一个typedef,那么DispatchToken是什么意思呢?如果你本文最上方的Flux解答的话,你就可以了解到,其实一个Flux的回调代码块有唯一的一个token,这个token就是用来让别人Invokoe你代码的。

那么这个DispatchToken怎么确保唯一呢?看下面这句话:

var id = _prefix + this._lastID++;

第一感觉是不是想,我了个擦,怎么那么简单。后来想想,JavaScript在浏览器中执行就是单线程,这种自增ID的做法又快又安全呐。

下面我们来解释一下Dispatcher这个类,这个类的目的很简单,六个字归纳一下:唯一,中控,分配。
可以把他看成一个单例对象,所有的任务分发都必须由这个类统一控制。

那么,这个类中包含了哪些玩意呢?让我们赶快来一探究竟!

_callbacks: {[key: DispatchToken]: (payload: TPayload) => void};
_isDispatching: boolean;
_isHandled: {[key: DispatchToken]: boolean};
_isPending: {[key: DispatchToken]: boolean};
_lastID: number;
_pendingPayload: TPayload;
  • callbacks,就是DispatchToken和函数回调的一个Dictionary。
  • isDispatching,体现当前Dispatcher是否处于dispatch状态。
  • isHandled,通过token去检测一个函数是否被处理过了。
  • isPending,通过token去检测一个函数是否被提交Dispatcher了。
  • lastID,最近一次被加入Dispatcher的函数体的UniqueID,即DispatchToken。
  • pendingPayload,需要传递给调用函数的参数,iOS开发者可以理解为userInfo

下面我们一个个函数来看看。

register(callback: (payload: TPayload) => void): DispatchToken {
    var id = _prefix + this._lastID++;
    this._callbacks[id] = callback;
    return id;
}

这函数就是注册一个callback进入dispatcher,同时为这个callback生成DispatchToken,加入字典。

unregister(id: DispatchToken): void {
    invariant(
      this._callbacks[id],
      'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
      id
    );
    delete this._callbacks[id];
}

有注册就有取消注册,unregister就是通过DispatchToken将注册的callback从字典中删除。当然了,这里使用了我们在上文中提过的Invariant来进行一个判断:即字典中必须包含对应这个DispatchToken的函数。

dispatch函数是Dispatcher用来分发payload的函数。

dispatch(payload: TPayload): void {
    invariant(
      !this._isDispatching,
      'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
    );
    this._startDispatching(payload);
    try {
      for (var id in this._callbacks) {
        if (this._isPending[id]) {
          continue;
        }
        this._invokeCallback(id);
      }
    } finally {
      this._stopDispatching();
    }
  }

整个函数的含义就是,首先是判断当前Dispatcher是否已经处于Dispatching状态中了,如果是,不能打断。然后通过_startDispatching更新状态。更新状态结束以后,将非pending状态的callback进通过_invokeCallback执行(pending在这里的含义可以简单理解为,还没准备好或者被卡住了)。所有执行完了以后,通过_stopDispatching恢复状态。

接下来便让我一一来看看其中涉及的几个函数,首先是_startDispatching函数。

_startDispatching(payload: TPayload): void {
    for (var id in this._callbacks) {
      this._isPending[id] = false;
      this._isHandled[id] = false;
    }
    this._pendingPayload = payload;
    this._isDispatching = true;
  }

首先该函数将所有注册的callback的状态都清空,并标记Dispatcher的状态进入dispatching。

同样,与之对应的有的_stopDispatching,函数很简单,不具体解释了。

_stopDispatching(): void {
    delete this._pendingPayload;
    this._isDispatching = false;
}

而_invokeCallback函数也很简单,当真正调用callback之前将其状态设置为pending,执行完成之后设置为handled。
_invokeCallback(id: DispatchToken): void {
this._isPending[id] = true;
this._callbacksid;
this._isHandled[id] = true;
}

读到这里,有些人可能有些疑问,这个pending到底是用来做什么的呢?别着急,看了下面这个最重要的函数waitFor,你就会了解。

waitFor(ids: Array<DispatchToken>): void {
     1. 
    invariant(
      this._isDispatching,
      'Dispatcher.waitFor(...): Must be invoked while dispatching.'
    );

     2.
    for (var ii = 0; ii < ids.length; ii++) {
      var id = ids[ii];
      if (this._isPending[id]) {
        3. 
        invariant(
          this._isHandled[id],
          'Dispatcher.waitFor(...): Circular dependency detected while ' +
          'waiting for `%s`.',
          id
        );
        continue;
      }
       4.
      invariant(
        this._callbacks[id],
        'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
        id
      );

      5.
      this._invokeCallback(id);
    }
  }

这个waitFor啥意思呢?我们首先看下他的参数,一个数组,里面全是DispatchToken。再看下实现。

  • 首先是一个Invariant判断当前必须处于Dispatching状态。一开始比较难理解,简单来说就是,如果不处于Dispatching状态中,那么说明压根没有函数在执行,那你等谁呢?
  • 然后该函数从DispatchToken的数组中进行遍历,如果遍历到的DispatchToken处于pending状态,就暂时跳过他。
  • 但是,在这有个必要的循环以来的检查,试想如下情况,如果A函数以来B的Token, B函数依赖A的Token,就会造成“死锁”。所以,当一个函数依赖的对象处于pending,说明这个函数已经被开始执行了,但是如果同时该函数没有进入handled状态,说明该函数也被卡死了。
  • 检查token对应的callback是否存在
  • 调用这个token对应的函数。

从上诉步骤中,我们可以看到,waitFor就是一个等待函数,当B执行,执行到某些条件不满足的时候(我们称之为依赖没解决),就是等待依赖去完成,简单来说,就是如下函数代表的意思:

while (!satisfied) ; doSomething

到这里,差不多Dispatcher.js就解释完了,下次继续。

JSONModel源码解析

背景介绍

现在的移动App基本上免不了和网络传输打交道,我们经常需要和服务器进行数据的传输,常用的数据格式无外乎XML或者JSON。这也就引出了一个新的话题,如何将获得的数据实际应用到我们的App中?答案很简单,建立Model。我们拿到的网络传输数据,无非就是一种以JSON格式标准划分的字符串罢了,我们需要将字符串解析到相应的字段中来,否则如果每次我们都需要直接和网络字符串打交道,那也太复杂了。在刚学习iOS开发的时候,那个时候看了Stanford老爷爷讲的开发教程,里面在涉及Core Data的章节曾经写过这样的一段代码:

+ (XXXModel *)insertModelWithDictionary:(NSDictionary *)dict inContext:(NSManagedObjectContext *)context
{
    if (dict == nil || ![dict isKindOf[NSDictionary class]]) return nil;

    XXXModel *model = [XXXModel getByIdentifier:[dict objectForKey:@"id"] inContext:context];
    if (!model) {
        model = [NSEntityDescription insertNewObjectForEntityForName:@"XXXModel" 
                                              inManagedObjectContext:context];
    }

    model.identifier = [dict objectForKey:@"id"];
    model.name       = [dict objectForKey:@"name"];
    model.age        = [dict objectForKey:@"age"];

    // 许多许多属性 
    ......
    ......
    ......

    return model;
}

+ (XXXModel *)getByIdentifier:(NSString *)identifier inContext:(NSManagedObjectContext *)context
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"XXXModel"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"identifier == %@", identifier]];

    XXXModel *model = [[context executeFetchRequest:request error:nil] lastObject];
    return model;
}

当时一直以为这个是圣经式的写法,一直坚持着,直到后来我接触到了Mantle。当然,今天这篇文章说的不是Mantle,而是另外一个类似的开源库:JSONModel

JSONModel

既然要解析它,首先先放上他的代码地址以示尊重:点我点我!

从Github上的介绍来看,利用JSONModel,我们可以节省大量的时间,它会利用类似于反射(Introspect)的机制,帮助我们自动将JSON的数据解析成Model的字段。

它的用法也很简单,比如有如下这样的JSON数据:

{"id":"10", "country":"Germany", "dialCode": 49, "isInEurope":true}

你只需要建立如下这样的Model:

#import "JSONModel.h"

@interface CountryModel : JSONModel

@property (assign, nonatomic) int id;
@property (strong, nonatomic) NSString* country;
@property (strong, nonatomic) NSString* dialCode;
@property (assign, nonatomic) BOOL isInEurope;

@end

然后像这样:

NSString* json = (fetch here JSON from Internet) ... 
NSError* err = nil;
CountryModel* country = [[CountryModel alloc] initWithString:json error:&err];

嘿嘿,JSON的数据就成功得被解析到country这个变量的对应字段上了!

是不是很神奇?让我们赶快来一探背后的原理吧!!!

源码分析

下载源代码,我们可以看到如下格式的包内容:

-- JSONModel
   -- JSONModel.h/.m
   -- JSONModelArray.h/.m
   -- JSONModelClassProperty.h/.m
   -- JSONModelError.h/.m
-- JSONModelCategories
   -- NSArray+JSONModel.h/.m
-- JSONModelNetworking
   -- 略
-- JSONModelTransformations
   -- JSONKeyMapper.h/.m
   -- JSONValueTransformer.h/.m

我们把JSONModelNetworking中的内容略过,因为基本就是网络传输的东西,不是我们了解的重点。

JSONModel文件

JSONModel是我们要讲解的重点,我们首先从它的初始化方法谈起。

-(instancetype)initWithString:(NSString*)string error:(JSONModelError**)err;
-(instancetype)initWithString:(NSString *)string usingEncoding:(NSStringEncoding)encoding error:(JSONModelError**)err;
-(instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err;
-(instancetype)initWithData:(NSData *)data error:(NSError **)error;

粗略一看,四个初始化方法,太可怕了。但是我们知道在iOS的设计理念里,有一种designated initializer的说法,因此,我们挑一个initWithDictionary看起。

initWithDictionary
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
{
    1. //check for nil input
    if (!dict) {
        if (err) *err = [JSONModelError errorInputIsNil];
        return nil;
    }

    2. //invalid input, just create empty instance
    if (![dict isKindOfClass:[NSDictionary class]]) {
        if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
        return nil;
    }

    3. //create a class instance
    self = [self init];
    if (!self) {

        //super init didn't succeed
        if (err) *err = [JSONModelError errorModelIsInvalid];
        return nil;
    }
    // Class infoClass = NSClassFromString([NSString stringWithFormat:@"%@Info", NSStringFromClass(class)]);
    4. //check incoming data structure
    if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
        return nil;
    }

    5. //import the data from a dictionary
    if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
        return nil;
    }

    6. //run any custom model validation
    if (![self validate:err]) {
        return nil;
    }

    7. //model is valid! yay!
    return self;
}

我们分七步来解析这个函数:

  • 检查参数dict是不是空,是空直接解析失败
  • 检查参数dict是不是NSDictionary或其子类的实例,是空直接解析失败
  • 关键步骤:创建JSONModel,创建失败直接滚粗,千万别小看这一步哦,具体我们下文说
  • 关键步骤:这是一个通过检查是不是有没被映射到的property的过程,如果有存在疏漏的property,那么会导致解析失败,触发Crash
  • 关键步骤:这是真正将解析的值对应的赋予Model的property的阶段
  • 进行自定义的检查工作,比如避免直接触发Crash之类的
  • 得到了正确的model,返回给程序使用。

下面我们来着重分析最重要的三个步骤

重点1: 第三步 [self init]

[self init]的代码调用了init初始化函数,实现如下,我们主要关注的是其中[self setup]

- (id)init
{
    self = [super init];
    if (self) {
        //do initial class setup
        [self __setup__];
    }
    return self;
}

来看看setup的实现,

- (void)__setup__
{
    //if first instance of this model, generate the property list
    if (!objc_getAssociatedObject(self.class, &kClassPropertiesKey)) {
        [self __inspectProperties];
    }

    //if there's a custom key mapper, store it in the associated object
    id mapper = [[self class] keyMapper];
    if ( mapper && !objc_getAssociatedObject(self.class, &kMapperObjectKey) ) {
        objc_setAssociatedObject(
                                 self.class,
                                 &kMapperObjectKey,
                                 mapper,
                                 OBJC_ASSOCIATION_RETAIN // This is atomic
                                 );
    }
}

WoW! What are 这个函数弄啥类?看起来很吓人?别怕,让我来一个个解释。

objc_getAssociatedObject想必大家都不会陌生,这就是一个Associate Object,我们经常用这种方法在Category给一个类添加property。

所以,第一个if的意思就是,我根据kClassPropertiesKey这个key,去当前类的Associate Object找property list,如果没找到,就说明是第一次执行解析,所以我要自己建立一个。所以我们赶紧进入
__inspectProperties一探究竟!

- (void)__inspectProperties
{
    //JMLog(@"Inspect class: %@", [self class]);

    NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary];

    //temp variables for the loops
    Class class = [self class];
    NSScanner* scanner = nil;
    NSString* propertyType = nil;

    // inspect inherited properties up to the JSONModel class
    while (class != [JSONModel class]) {
        //JMLog(@"inspecting: %@", NSStringFromClass(class));

        unsigned int propertyCount;
        objc_property_t *properties = class_copyPropertyList(class, &propertyCount);

        //loop over the class properties
        for (unsigned int i = 0; i < propertyCount; i++) {

            JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];
            //get property name
            objc_property_t property = properties[i];
            const char *propertyName = property_getName(property);
            p.name = @(propertyName);

            //JMLog(@"property: %@", p.name);

            //get property attributes
            const char *attrs = property_getAttributes(property);
            NSString* propertyAttributes = @(attrs);
            NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","];

            //ignore read-only properties
            if ([attributeItems containsObject:@"R"]) {
                continue; //to next property
            }

            //check for 64b BOOLs
            if ([propertyAttributes hasPrefix:@"Tc,"]) {
                //mask BOOLs as structs so they can have custom convertors
                p.structName = @"BOOL";
            }

            scanner = [NSScanner scannerWithString: propertyAttributes];

            //JMLog(@"attr: %@", [NSString stringWithCString:attrs encoding:NSUTF8StringEncoding]);
            [scanner scanUpToString:@"T" intoString: nil];
            [scanner scanString:@"T" intoString:nil];

            //check if the property is an instance of a class
            if ([scanner scanString:@"@\"" intoString: &propertyType]) {

                [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"]
                                        intoString:&propertyType];

                //JMLog(@"type: %@", propertyClassName);
                p.type = NSClassFromString(propertyType);
                p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound);
                p.isStandardJSONType = [allowedJSONTypes containsObject:p.type];

                //read through the property protocols
                while ([scanner scanString:@"<" intoString:NULL]) {

                    NSString* protocolName = nil;

                    [scanner scanUpToString:@">" intoString: &protocolName];

                    if ([protocolName isEqualToString:@"Optional"]) {
                        p.isOptional = YES;
                    } else if([protocolName isEqualToString:@"Index"]) {
                        p.isIndex = YES;
                        objc_setAssociatedObject(
                                                 self.class,
                                                 &kIndexPropertyNameKey,
                                                 p.name,
                                                 OBJC_ASSOCIATION_RETAIN // This is atomic
                                                 );
                    } else if([protocolName isEqualToString:@"ConvertOnDemand"]) {
                        p.convertsOnDemand = YES;
                    } else if([protocolName isEqualToString:@"Ignore"]) {
                        p = nil;
                    } else {
                        p.protocol = protocolName;
                    }

                    [scanner scanString:@">" intoString:NULL];
                }

            }
            //check if the property is a structure
            else if ([scanner scanString:@"{" intoString: &propertyType]) {
                [scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
                                    intoString:&propertyType];

                p.isStandardJSONType = NO;
                p.structName = propertyType;

            }
            //the property must be a primitive
            else {

                //the property contains a primitive data type
                [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","]
                                        intoString:&propertyType];

                //get the full name of the primitive type
                propertyType = valueTransformer.primitivesNames[propertyType];

                if (![allowedPrimitiveTypes containsObject:propertyType]) {

                    //type not allowed - programmer mistaked -> exception
                    @throw [NSException exceptionWithName:@"JSONModelProperty type not allowed"
                                                   reason:[NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", self.class, p.name]
                                                 userInfo:nil];
                }

            }

            NSString *nsPropertyName = @(propertyName);
            if([[self class] propertyIsOptional:nsPropertyName]){
                p.isOptional = YES;
            }

            if([[self class] propertyIsIgnored:nsPropertyName]){
                p = nil;
            }

            //few cases where JSONModel will ignore properties automatically
            if ([propertyType isEqualToString:@"Block"]) {
                p = nil;
            }

            //add the property object to the temp index
            if (p && ![propertyIndex objectForKey:p.name]) {
                [propertyIndex setValue:p forKey:p.name];
            }
        }

        free(properties);

        //ascend to the super of the class
        //(will do that until it reaches the root class - JSONModel)
        class = [class superclass];
    }

    //finally store the property index in the static property index
    objc_setAssociatedObject(
                             self.class,
                             &kClassPropertiesKey,
                             [propertyIndex copy],
                             OBJC_ASSOCIATION_RETAIN // This is atomic
                             );
}

这么多代码,其实总结起来就几个步骤:

  • 获取当前类的的property list,通过class_copyPropertyList runtime的方法
  • 遍历每一个propery,解析他们的属性,这里的属性包括是否只读、类型、是否是weak类型,是否是原子性的等等,如果不了解,可以看如下的表格:

    | Code | Meaning |
    | :————-: |:————-
    | R | The property is read-only (readonly).
    | C | The property is a copy of the value last assigned (copy).
    | & | The property is a reference to the value last assigned (retain).
    | N | The property is non-atomic (nonatomic).
    | G | The property defines a custom getter selector name. The name follows the G (for example, GcustomGetter,).
    | S | The property defines a custom setter selector name. The name follows the S (for example, ScustomSetter:,).
    | D | The property is dynamic (@dynamic).
    | W | The property is a weak reference (__weak).
    | P | The property is eligible for garbage collection.
    | t | Specifies the type using old-style encoding.

  • 根据解析结果,检测是不是合法,如果合法创建对应的JSONModelClassProperty并赋值相应的属性值。

  • 然后重复执行,查看完当前的类就去查询其父类,直到没有为止。

  • 最后将解析出来的property list通过Associate Object给赋值,这和我们刚刚在setup中看到的相呼应。

同样,附上关于property属性的苹果的官方文档链接

这一步基本就解释完了,我们来看看下一步。

重点2: 第四步 __doesDictionary:(NSDictionary)dict matchModelWithKeyMapper:(JSONKeyMapper)keyMapper error:(NSError**)err

老样子,我们先来看看他的实现:

-(BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMapper:(JSONKeyMapper*)keyMapper error:(NSError**)err
{
    //check if all required properties are present
    NSArray* incomingKeysArray = [dict allKeys];
    NSMutableSet* requiredProperties = [self __requiredPropertyNames].mutableCopy;
    NSSet* incomingKeys = [NSSet setWithArray: incomingKeysArray];

    //transform the key names, if neccessary
    if (keyMapper || globalKeyMapper) {

        NSMutableSet* transformedIncomingKeys = [NSMutableSet setWithCapacity: requiredProperties.count];
        NSString* transformedName = nil;

        //loop over the required properties list
        for (JSONModelClassProperty* property in [self __properties__]) {

            transformedName = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name;

            //chek if exists and if so, add to incoming keys
            id value;
            @try {
                value = [dict valueForKeyPath:transformedName];
            }
            @catch (NSException *exception) {
                value = dict[transformedName];
            }

            if (value) {
                [transformedIncomingKeys addObject: property.name];
            }
        }

        //overwrite the raw incoming list with the mapped key names
        incomingKeys = transformedIncomingKeys;
    }

    //check for missing input keys
    if (![requiredProperties isSubsetOfSet:incomingKeys]) {

        //get a list of the missing properties
        [requiredProperties minusSet:incomingKeys];

        //not all required properties are in - invalid input
        JMLog(@"Incoming data was invalid [%@ initWithDictionary:]. Keys missing: %@", self.class, requiredProperties);

        if (err) *err = [JSONModelError errorInvalidDataWithMissingKeys:requiredProperties];
        return NO;
    }

    //not needed anymore
    incomingKeys= nil;
    requiredProperties= nil;

    return YES;
}

抛开我们暂时还不熟悉的keyMapper(用过Mantle的人估计有一定了解。),整个函数非常容易理解。我们首先获取我们传入的代表JSON数据的Dictionary,然后和我们解析出来的property list进行对比(基于NSSet),如果得到property list室Dictionary的超集,意味着JSON中的数据不能完全覆盖我们生命的属性,说明我们有属性得不到赋值,因此会判断出错。在默认的实现中,如果出现没匹配的实现,就是导致Crash

重点2: 第五步 __importDictionary:(NSDictionary)dict withKeyMapper:(JSONKeyMapper)keyMapper validation:(BOOL)validation error:(NSError**)err

继续看实现:

-(BOOL)__importDictionary:(NSDictionary*)dict withKeyMapper:(JSONKeyMapper*)keyMapper validation:(BOOL)validation error:(NSError**)err
{
    //loop over the incoming keys and set self's properties
    for (JSONModelClassProperty* property in [self __properties__]) {

        //convert key name ot model keys, if a mapper is provided
        NSString* jsonKeyPath = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name;
        //JMLog(@"keyPath: %@", jsonKeyPath);

        //general check for data type compliance
        id jsonValue;
        @try {
            jsonValue = [dict valueForKeyPath: jsonKeyPath];
        }
        @catch (NSException *exception) {
            jsonValue = dict[jsonKeyPath];
        }

        //check for Optional properties
        if (isNull(jsonValue)) {
            //skip this property, continue with next property
            if (property.isOptional || !validation) continue;

            if ([property.type isSubclassOfClass:[JSONModel class]]) {
                NSMutableDictionary *infoKey = [NSMutableDictionary new];
                for (NSString *name in dict.allKeys) {
                    id value = [dict objectForKey:name];
                    NSMutableString *ignoredCaseName = [NSMutableString stringWithString:[name lowercaseString]];
                    NSString *ignoredCaseKey  = [property.name lowercaseString];

                    NSRange range = [ignoredCaseName rangeOfString:ignoredCaseKey];
                    if (range.location != NSNotFound) {
                        [ignoredCaseName deleteCharactersInRange:range];
                        NSString *newPropertyName = [ignoredCaseName copy];
                        if (!isNull(value)) {
                            [infoKey setObject:value forKey:newPropertyName];
                        }
                    }
                }

                jsonValue = [infoKey copy];
            }

            if (err && isNull(jsonValue)) {
                //null value for required property
                NSString* msg = [NSString stringWithFormat:@"Value of required model key %@ is null", property.name];
                JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
                *err = [dataErr errorByPrependingKeyPathComponent:property.name];

                return NO;
            }
        }

        Class jsonValueClass = [jsonValue class];
        BOOL isValueOfAllowedType = NO;

        for (Class allowedType in allowedJSONTypes) {
            if ( [jsonValueClass isSubclassOfClass: allowedType] ) {
                isValueOfAllowedType = YES;
                break;
            }
        }

        if (isValueOfAllowedType==NO) {
            //type not allowed
            JMLog(@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass));

            if (err) {
                NSString* msg = [NSString stringWithFormat:@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass)];
                JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
                *err = [dataErr errorByPrependingKeyPathComponent:property.name];
            }
            return NO;
        }

        //check if there's matching property in the model
        if (property) {

            // check for custom setter, than the model doesn't need to do any guessing
            // how to read the property's value from JSON
            if ([self __customSetValue:jsonValue forProperty:property]) {
                //skip to next JSON key
                continue;
            };

            // 0) handle primitives
            if (property.type == nil && property.structName==nil) {

                //generic setter
                if (jsonValue != [self valueForKey:property.name]) {
                    [self setValue:jsonValue forKey: property.name];
                }

                //skip directly to the next key
                continue;
            }

            // 0.5) handle nils
            if (isNull(jsonValue)) {
                if ([self valueForKey:property.name] != nil) {
                    [self setValue:nil forKey: property.name];
                }
                continue;
            }


            // 1) check if property is itself a JSONModel
            if ([self __isJSONModelSubClass:property.type]) {

                //initialize the property's model, store it
                JSONModelError* initErr = nil;
                id value = [[property.type alloc] initWithDictionary: jsonValue error:&initErr];

                if (!value) {
                    //skip this property, continue with next property
                    if (property.isOptional || !validation) continue;

                    // Propagate the error, including the property name as the key-path component
                    if((err != nil) && (initErr != nil))
                    {
                        *err = [initErr errorByPrependingKeyPathComponent:property.name];
                    }
                    return NO;
                }
                if (![value isEqual:[self valueForKey:property.name]]) {
                    [self setValue:value forKey: property.name];
                }

                //for clarity, does the same without continue
                continue;

            } else {

                // 2) check if there's a protocol to the property
                //  ) might or not be the case there's a built in transofrm for it
                if (property.protocol) {

                    //JMLog(@"proto: %@", p.protocol);
                    jsonValue = [self __transform:jsonValue forProperty:property error:err];
                    if (!jsonValue) {
                        if ((err != nil) && (*err == nil)) {
                            NSString* msg = [NSString stringWithFormat:@"Failed to transform value, but no error was set during transformation. (%@)", property];
                            JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
                            *err = [dataErr errorByPrependingKeyPathComponent:property.name];
                        }
                        return NO;
                    }
                }

                // 3.1) handle matching standard JSON types
                if (property.isStandardJSONType && [jsonValue isKindOfClass: property.type]) {

                    //mutable properties
                    if (property.isMutable) {
                        jsonValue = [jsonValue mutableCopy];
                    }

                    //set the property value
                    if (![jsonValue isEqual:[self valueForKey:property.name]]) {
                        [self setValue:jsonValue forKey: property.name];
                    }
                    continue;
                }

                // 3.3) handle values to transform
                if (
                    (![jsonValue isKindOfClass:property.type] && !isNull(jsonValue))
                    ||
                    //the property is mutable
                    property.isMutable
                    ||
                    //custom struct property
                    property.structName
                    ) {

                    // searched around the web how to do this better
                    // but did not find any solution, maybe that's the best idea? (hardly)
                    Class sourceClass = [JSONValueTransformer classByResolvingClusterClasses:[jsonValue class]];

                    //JMLog(@"to type: [%@] from type: [%@] transformer: [%@]", p.type, sourceClass, selectorName);

                    //build a method selector for the property and json object classes
                    NSString* selectorName = [NSString stringWithFormat:@"%@From%@:",
                                              (property.structName? property.structName : property.type), //target name
                                              sourceClass]; //source name
                    SEL selector = NSSelectorFromString(selectorName);

                    //check for custom transformer
                    BOOL foundCustomTransformer = NO;
                    if ([valueTransformer respondsToSelector:selector]) {
                        foundCustomTransformer = YES;
                    } else {
                        //try for hidden custom transformer
                        selectorName = [NSString stringWithFormat:@"__%@",selectorName];
                        selector = NSSelectorFromString(selectorName);
                        if ([valueTransformer respondsToSelector:selector]) {
                            foundCustomTransformer = YES;
                        }
                    }

                    //check if there's a transformer with that name
                    if (foundCustomTransformer) {

                        //it's OK, believe me...
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                        //transform the value
                        jsonValue = [valueTransformer performSelector:selector withObject:jsonValue];
#pragma clang diagnostic pop

                        if (![jsonValue isEqual:[self valueForKey:property.name]]) {
                            [self setValue:jsonValue forKey: property.name];
                        }

                    } else {

                        // it's not a JSON data type, and there's no transformer for it
                        // if property type is not supported - that's a programmer mistaked -> exception
                        @throw [NSException exceptionWithName:@"Type not allowed"
                                                       reason:[NSString stringWithFormat:@"%@ type not supported for %@.%@", property.type, [self class], property.name]
                                                     userInfo:nil];
                        return NO;
                    }

                } else {
                    // 3.4) handle "all other" cases (if any)
                    if (![jsonValue isEqual:[self valueForKey:property.name]]) {
                        [self setValue:jsonValue forKey: property.name];
                    }
                }
            }
        }
    }

    return YES;
}

这一个函数看着吓人,其实非常容易理解。根据我们刚刚得到的property list,我们一个个取出来,用property name作为key,来查询在对应的JSON字典中的value。然后分为如下几个情况:

  • 检查是不是空值。如果该属性是optional,那么无所谓。如果不能为空,那么抛出错误。
  • 检查这个值是不是合法的JSON类型,如果不是,抛出错误。
  • 如果property是非自定义JSONModel子类的字段,基于Key-Value赋值,当然,你可以自己override setter。
  • 如果是自定义的JSONModel子类是,创建一个对应的新类,解析对应的value。
  • 如果再不行,进行一系列的判断和利用JSONValueTransformer进行类型转换进行解析。

为什么要进行JSONValueTransformer的转换呢,是因为在iOS的视线中,由于抽象工厂的存在,构建了大量的簇类,比如NSArray, NSNumber, NSDictionary等等,他们只是对外暴露的一层皮,实质上底层对于真正的类。比如NSArrayI <=> NSArray等等。因此,我们需要通过JSONValueTransformer得到真正的Class Type,同时通过class Type找到最合适的转换方法,在JSONValueTransformer.m的文件中,我们能找到一大堆xxxFromYYY的函数。

好了,到此为止,JSONModel的源码解读就差不多了,下周带来SDWebImageCache的解读。

iOS下载模块的实现

这是我在实习的时候对于一个下载模块的实现,在这里记录一下心得体会。这个模块完全独立实现,且不依赖于公司任何的代码,不会涉及任何隐私。

下载模块的需求

在实现一个新模块之前,我们需要理清楚这个模块需要支持最小的功能集,并逐步扩展。并且由于下载模块是存在潜在的可能性被多个模块调用,因此,我们特别要注意模块的封装。

功能需求

支持下载

下载模块最基本的自然就是可以正确的将需要下载的东西下载到指定路径,这里下载的东西包括但是不局限于图片、文本文档、压缩文件等等。因此,这些下载东西的大小是不等的,在设计功能的时候,需要讲这点考虑进去。

支持取消下载

处于控制角度的考虑,甚至是出于当流量和资源节省的角度考虑,我们都已经对下载任务有控制权。可以随时根据我们业务处于的状态,对下载任务进行暂停或者完整的停止取消。

支持并发下载

下载任务的个数肯定不会只有一个,而是可能同时存在多个。因此,我们就需要考虑如何去支持多个下载任务同时并发的下载。其外,我们还要考虑到潜在的任务之间的关联性和依赖性(比如B下载任务需要依赖于A下载任务的完成)

初步的任务我们可以认为就是如上三个就可以(初期设计模块真的不要考虑过多功能,先开始做起来,一点点看着自己造的轮子可以运转起来,成就感会促使你会不停的去完善。当然,你的代码一定要写的整洁易读!

Read More