YongSir

专业程序员伪装者

小朋友的编程5

这一篇仍然是视频部分的补充资料,主要就是介绍javaScript的

关于js代码

通过前边的知识,我们知道写在网页中script标签中的内容,就是javaScript代码,我们以前还提到过, javascript 是一种接近人类自然语言的高级编程语言。所以在网页中一个必然的事实就是,凡是看到

小朋友的编程-五

这一篇仍然是视频部分的补充资料,主要就是介绍javaScript的

关于js代码

通过前边的知识,我们知道写在网页中script标签中的内容,就是javaScript代码,我们以前还提到过, javascript 是一种接近人类自然语言的高级编程语言。所以在网页中一个必然的事实就是,凡是看到<script>标签之间包含的东西,那么它就是js代码,而js代码在现代的网页中是无处不在的,我们随意的找一些现实中的例子,比如baidu,如果我们使用开发者工具打开–还记得怎样操作吗?(想一想快捷键–F12),你就会发现大量的script标签,就像这样:
JS代码在网页中无处不在,比如baidu

但是我们为什么需要js代码呢?

因为本身html是死的,它是什么样就在你写完它的那一刻就固定了,比如一个标题元素<title>标题一</title>一开始就固定了永远是标题一,一个数字为100,那么它就会是一直是100,像这样一经创建再也不改变的现象,通常用静态这个词来概括。但现实中的网页却不是这样,大多数时候网页是可以进行各种各样的交互的。比如在我们上次视频中的那个按钮–也就是<button>,我们点击了它会触发一定的事件,我们视频中就是让它计算最终结果并显示结果,再比如你去优酷看视屏的时候,你要去点击播放按钮才会开始播放,以及在玩游戏时你常常会通过键盘让角色运动,而让它干点儿什么通常就是靠js来帮助实现的。

换一个更通俗的例子,还记得我们前边的机器人吗,我们通过html这样的元素组成了机器人的头,身体等各个部位,而js就相当于给机器人添加了各种行动和思想,让机器人可以动动胳膊抬抬腿之类的行动,这就是为什么我的网页中需要js。因为它会让原本死的元素由静态变为动态,从而变得可以被人们点击,滚动,缩放(缩放网页,你可以试试按下键盘中的Cril键,同时滚动鼠标中间的滚轮)等等交互行为。

好了,现在停下来我们总结一下:
我们会知道,通常的网页都是可以交互的,有的有可以点击的按钮,有的有可以放大的图片,大部分网页还能随着鼠标的滑动而滚动…等等这些行为,都是依靠js来实现的。

那么我们就来看看JS的具体内容,他到底有什么魔力呢?我们重新回顾一下上次视屏中的js代码:

// 表示注释,就是用来说明你在干什么的,使用时系统会忽略掉代//的行,所以这其实是给人看的,计算机是不管的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script>
// 说明: element
// 上边的右侧代码是元素,它是网页的基本组成部分,这些都是 W3C 规定好了的,所以不是重点,重点是下边的代码
// 这里用到的数学公式: 速度v * 时间t = 路程s

// ------------------
// 编程就是把复杂的事情,一步一步分解
// 目标: 创建一个网页,可以输入v和t,点击按钮,计算路程s,并显示结果
// 1> 创建一个网页,可以有输入v和t的元素 --- 已完成,并且不需要你掌握,知道了就行
// 2> 要能拿到v和t元素: 分别找到t/v input元素
// 3> a 点击按钮(事件)
// b 拿到现在的v/t的值
// c 计算
// d 拿到resultElm
// e 修改innerText


// 步骤2> 分别找到t/v input元素, 通过id去找到的 -- 规定好了的方法
var speedElm = document.getElementById("speed")
var timeElm = document.getElementById("time")
var distanceElm = document.getElementById("distance")

// 步骤3>
// a 指定点击事件 -- 也是规定好了的, 通过onClick="anXiale()"
function anXiale() {
// b 拿到现在的v/t的值
var speedValue = speedElm.value
var timeValue = timeElm.value
var distanceValue = distanceElm.value

// c 带入公式 s = v * t ????
var distance = speedValue * timeValue

// d 拿到resultElm
var resultElm = document.getElementById("result")

// e 将结果显示到 resultElm元素 -- 规定好了的
resultElm.innerText =": " + distance
}

</script>

首先,你肯定总是见到var这个单词,var是一种简写,他的全称是variable【变量】,再直白一点儿就是可以改变的量,比如在计算一个正方形的面积时,我们带入公式 S(面积) = a(边长) * a(边长),不同的a就会有不同的S, a和S都是可以改变的,因此它们都是变量,而有的量比如 圆的面积计算中 S = 2 * π * (r * r),其中的r跟S都是可以改变的是变量,而 2 和 π 都是固定的,所以我们称呼它们是常量而不是变量,对,就是这么的直白。

恭喜你,因为变量几乎就是网页可以变化的全部秘密,正是由于js中变量的存在,所以网页才可以动起来,所以变量的作用就像是提线木偶的线,一旦拉扯那些控制线,木偶就会做动作。

其次,上边代码中还有一些符号,有计算符号 + * 还有 = ,他们的意思就是他们本身的意思,只需要注意的是 =, 在编程中并不叫等号,而是赋值符号,它表示将右侧的数值或者变量 赋值给 右侧的变量,好吧看起来的确有一些相等的意思,暂时这样理解也是没问题的。

还有,这样的语句 function anXiale() {}, 这个我们称之为函数,它是最小的功能单元,我们来具体看看:

1
2
3
4
5
6
7
8
// 函数定义: 括号里数,我们叫他参数,没有参数就不写
function 函数名(参数名1, 参数2...) {
// 函数具体怎么作实在这里,我们称之为 函数实现
return 返回数值 // 有的函数有,有的没有
}

// 函数调用,就是
函数名(参数名1, 参数2...)

比如,我们刚刚提到的计算正方形的面积,如果写成函数的话并使用一下就是:

1
2
3
4
5
6
7
8
9
10
11
12

// 函数定义
function squareArea(a) {
return a * a
}

// 首先声明面积变量S
var S
// 然后调用函数,参数
S = squareArea(5)
// 输出结果
console.log("面积是" + S)

运行一下结果就是,点击这里去看代码:

所以呢,函数就是一种操作,它会有输入然后执行某种操作,在比如我们使用console.log("面积是" + S)来输出结果到控制台,其实这也是一个函数,你看不到它的实现是因为 浏览器已经默默的帮你实现了,你直接使用就可以了,还有比如document.getElementById("speed")这样的函数调用,其实我说这都是规定好的,就是因为它们跟console()一样,都由浏览器帮我们实现好了,所以浏览器跟系统,其实又点儿想你的爸爸妈妈,他们已经帮你准备好了所有的物质基础,你只需要负责好好学习跟快乐成长就可以了 :)

好了,读到这里我希望在回过去看看我们上次的视频中的代码,是不是了解更多一些了呢,是不是觉得我说的那句你们基本已经学会了编程了,其实真的不是在骗你们…

所以,总结一下,网页 = html元素 + js,html是网页这个机器人的组成零件,js是可以让机器人动起来的电能和大脑。

另外,这里我举了一个计算面积的例子,你能不能参照着,想想如何去完成一个计算器呢?
我想最起码我们要有4个函数对应 加减乘除 4个基本操作对吧?还要有数据输入的html元素?对,还有呢…?

好了,下期视屏中我们一起试试吧

小朋友的编程-四

通过前边的铺垫,我们基本上对编程有了一些了解,简短的来说,编程就是通过编程语言,给机器预设一些指令代码,让机器能够按照一定的步骤完成我们需要的功能。
然后通过视频介绍我们知道了网页的秘密,所有的网页本质上都是代码,而且是一种叫html的代码,这篇文章,会介绍一些在视频中提到的专用词汇。

首先是关于网页

一个网页就是一个html格式的文件,如果你尝试把一个网站保存下来的话,它的后缀就是文件名.html,我相信你肯定知道txt格式, 也见过mp3,mp4,exe等。
常见文件格式例子

如果你电脑里的文件是没有后缀的话,那就是系统并没有设置显示文件后缀,可以参考这里去试试https://jingyan.baidu.com/article/5d368d1e31ed903f60c057c6.html

一般来说,格式是代表着一种数据的组织方式,也指示着文件的用途。我套用个生活中的例子,需要帮助时,当你看到一个穿警服的人,即便你并不认识也从来没见过他,你也会知道他是警察,有困难就去找他帮忙。警服在这里就表示了对方警察的身份和职能,其实格式也有类似的意思,它会代表文件的某种特定的用途。比如mp4是要拿视屏播放器才能打开的,因为它是一种视屏文件格式,mp3是音乐要用音乐播放器放,txt文本格式可以用记事本打开,同理我们这里的html,它就是代表网页,是需要用浏览器来打开的。

事实上,浏览器是能够认识html中的代码,进而把这些代码渲染成漂亮的网页便于人们使用,毕竟只有专业认识才认识代码,而普通人上网只需要看到浏览器处理后的网页的样子就可以了。

– 好了,恭喜你,你了解到一个叫做文件格式的新知识了

html是有一定规则的,还记得在js.jirengu.com这个在线练习网站中,打开差不多是这样的:
这是代开网页的样子

刚进入时,左侧它就会自动给出一部分写好了的代码,我把它复制到下面:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>

</body>
</html>

视屏中并没有详细说明这些东西,但你可能也发现了一些规律,比如大部分代码都好像是这样的一种形式:<单词> 有的可能还有其他东西 </单词>, 或者简单一点儿像 <单词>,总之是由一个或者几个尖括号和单词组成的结构,这种结构我们称之为网页中的一个元素[element: 元素,单元之意]。元素就像是零件,将他们组合起来就是一个网页了,事实上,你看到的所有网页都是由各种各样的元素组合起来的,就好比扣子,拉链和布料这些基本材料,可以做成各种各样的衣服那样,(这是你小姨举的例子 (^_^)v

– 好了,恭喜你,你又知道了: 网页是由各种各样的元素组成的

除此之外,上边提到的结构中还有单词我们没有说到,这些单词不是乱写的,而是由一个叫万维网联盟(缩写W3C)的国际组织规定好了的,它们是能被各种浏览器都认识的特定单词,也称之为标签, 所以基本上:

元素 = <标签名> 可能有内容 </标签名> 或者 <标签名>

所以我们现在就来找找,上述的代码中有哪些标签呢?
有一个html标签,一个head标签,一个body标签,meta跟title也都是标签,这里说明一下,第一句<!DOCTYPE html>是个例外,它的作用是告诉浏览器下面的 HTML 是用什么版本编写的,方便浏览器认识代码用的,你可以把它理解为一种固定不变的步骤。

我们知道了术语之后,再回头看看上边的代码,其实基本上就是一个大的html元素,有2个子元素是 头(head)元素体(body)元素,而头元素中又嵌套了 meta元素这里的meta里边的charset我们称之为的属性)和 title元素, 而我们通常就会在 主体(body)元素 中利用各种标签,进行代码的编写,从而达到我们想要的网页效果,这也是我们视频中步骤一部分的内容。如果我用图表示一下,就是这样:

就像机器人拥有各种各样的零件一样,网页中w3c规定的标签目前有150多个之多,常见的还有比如视频中我们用到的 <div> (表示区域), <button>(表示按钮), <img>(表示图片), <input>(表示输入框), <script>(表示js代码)等等,以后我们还会逐渐见到其他元素,不过不要慌,这些标签都不用你去记忆,用到时去搜索查查,知道它是怎么回事儿就够了。

关于js代码

…待续

小朋友的编程-三

上两次阅读部分的意味更加浓厚一些,都是些很细小的知识,这篇文章我们将试着尝试一些更加实际的体验,去直观感受一下什么是编程,当然前半段会有一部分编程语言发展历史的介绍,耐心看完啦。

你已经知道了编程就是跟机器交互的过程,而我们告诉机器该怎么做,靠的是一种叫做编程语言的工具。你还知道机器内部所有的信息其实都是各种数字电路的信号,我们习惯使用01来代表数字信号,换言之机器能认识的其实只有0101,这种机器能直接 认识和执行的由一系列01010 组成的东西我们称之为机器语言

事实上最原始的编程,就是科学家们利用0101的组合去一步一步指示机器去完成功能的,但是只用0101又一个很大的问题,那就是虽然机器明白了,人却不懂了。具体来说如科学家A编的程序可能只有A自己知道,科学家B是无论如何都看不懂的,就算是对A自己,过了一段时间之后可能也都忘了当时是怎样编程了的。如果都是像0101这般不直观不友好的机器语言,项目就不可能让更多的人参与,机器所能完成的工作也就不可能像我们今天这样多姿多彩。怎么办呢?如果我们有一种人类能看懂而机器又能认识的语言就好了,可惜不行,因为我们前边提到了机器能也只能认识0101这样的机器语言,于是科学家从人类的实际生活中受到启发,生活中比如你想跟一个法国的小朋友交流,你会中文而她只会法语,你就会想着找个翻译就解决了你们的语言问题,于是科学家们就约定了一个总体原则,那就是所有的编程语言都最终可以被翻译成机器能认识的机器语言,这样就解决了上述问题。

按照上述的发展历程,在机器语言之上,一种更友好的编程语言被发明出来,那就是汇编语言,真实的汇编语言长得是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.section .data
#数据段,定义以ascii编码的字符串“hello, xiao ze qiu!\n”,并以output来表示内存的起始地址
output:
.ascii "hello, xiao ze qiu!\n"
.section .text
.global _start
#程序以_start:开始运行
_start:
#调用write系统,eax为4表示write,ebx表示要写入的文件描述符,
#1表示标准输出,也就是终端,ecx表示字符串开头的内存地址,edx表示长度
movl $4,%eax
movl $1,%ebx
movl $output,%ecx
movl $12,%edx
int $0x80
#调用退出系统,1表示退出,0为正常退出,这里和c语言main函数的返回值应该是一个意思
movl $1,%eax
movl $0,%ebx
int $0x80

你是不是有些蒙了,看起来好难好复杂的样子啊!其实上述程序干的事情很简答,只是在屏幕上跟你打个招呼而已 – “hello, xiao ze qiu!”,但是写起来却要那么长一串,所以在这里要心疼一下早编程的前辈们��,这个例子就暴漏了汇编语言的弱点,那就是虽然总算比0101有好一些,但实在是太麻烦了,难道编程语言就不能像我们平时说话那样,表达明确又简单吗?

其实是可以的,那就是人们终于在受不了汇编之后,又发明了很多种更接近人类语言的编程语言,我们统称它们为高级编程语言,它们有很多很多,据统计有250多种并且还在不断的被发明出来,因为它们各自要解决的问题和使用领域的不同,根据不用的设计理念就有了如此多的数量,但肯定都比汇编简单太多太多,比如上述打招呼如果用C语言来实现就是:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello, xiao ze qiu!"); // print 就是打印的意思,printf指的是打印之后自动换行而已
return 0;
}

是不是简单了很多也好懂了很多:-), 其实还有更简单比如有一门叫JavaScript的语言中只需要一句话:

1
console.log("hello, xiao ze qiu!") // console 就是控制台,就是windows系统上那个命令行的黑框框的东西,你自己先在电脑上试试,看能不能找到

如果还是觉得麻烦,还有最简单的,比如在一门叫Python的编程语言中就是:

1
print("hello, xiao ze qiu!")

是不是很简单,这样的对人友好的高级编程语言很接近于我们日常使用的语言,于是跟机器交流就变得容易多了,这才促进了整个计算机科学的发展。

所以我们稍稍总结一下,编程语言的发展基本上是这样的: 机器语言 ~> 汇编语言 ~> 高级语言, 我们通常意义的编程就是,利用接近人类自然语言的高级语言给机器规定动作。


下面就开始思考一个个具体的问题,让我们对生活中习以为常的逻辑行为做一些具体的仔细的整理。
不防就从对现实生活中模仿开始,比如我们知道周一到周五是上课时间,而周六日是休息不上课的,就这样简单的一句话,当我们试着把每个过程说清楚,用代码的方式就像是这样:

1
2
3
4
首先是一个日期
然后把它转化成星期
如果是在周一到周五,那么正常上课
如果是周六或者周日,那么不用上课

这里边蕴含着一种选择结构,如果把它画成逻辑流程图,会更直观一些:

生活中还有其他的逻辑流程,比如再考虑这样一个例子,考试的时候你需要把全部的题做完才能交卷,这样的逻辑图是怎样的呢?我可以画出来是这样的:

跟第一种不同的是这里边不光有选择结构还附带的有循环结构的逻辑,所谓循环逻辑就是直到满足一定的条件才会退出循环,否则就会一直执行的一种结构

事实上还有一种顺序结构就是按顺序一步一步来,上述的每个箭头都代表从这一步到下一步的顺序执行,它是最符合我们人类直觉最自然的结构,所以不必额外提出来。

现在,如果你能仔细在思考一下,就会发现这个世界上所有的事情,都最终可以用上边这3种基本逻辑结构通过组合来实现,有的是选择分支多一些,有的是循环多一些,比如我们让上边考试的例子在具体一些,那就加上考试是有时间的因素,一个考试的完整流程可能是这样的,先说代码:

1
2
3
4
开始
开始做题,并开始计时
如果时间到了就必须交卷,退出
如果时间没到,就把所有题做完,退出

这里边的时间逻辑是最主要的,只要时间到了就必须交卷,不管做没有做完,还有另一种情况是时间没走完但做完了也可以交卷,文字不容易说明,不防也化成逻辑流程图:

注意图中有2条不一样的线,红色的表示同时进行,因为一开始计时就代表着可以开始做题;另外一条蓝色的线,代表每次做题都会去检测时间是否到了,如果到了就马上交卷,如果没到就继续计时和做题,基本就是2个并行分支一个计时,一个检测是否做完,在每个分支内部又是选择和循环的组合。所以这就是编程全部秘密,编程就是将某个功能,按照基本步骤进行逻辑划分,拆成一个一个简单的顺序或者选择或者循环的结构,从而最终达到某种确定的结果

当然这里我们是通过文字和流程图实现的,实际的计算机编程中,我们只需要把上述,改为使用计算机能理解的编程语言实现了而已,所以只要你学会了将一个复杂过程一步一步的逻辑分解,就可以说已经学会了编程,这么一看,其实你已经无意识的使用了很多年的编程,只是你不知道罢了,所以编程不是什么神秘的事,相反它之非常平常。

那具体是怎样把这些逻辑通过编程语言来实现呢?其实我们只需要借助一点点数学就可以了,比如我下边简单的列举几个,看看你自己能不能看出来它是属于什么结构的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 一个 = 就是普通的等号," == " 这个才表示 比较数值

// -------------- 1 --------------
var x = 0 // 不要管前边的var,你可以把x当成方程中的自变量一样,下边还有几个简单的单词,你应该可以自己查到的,试试吧
if (x == 0) {
// 情况1之下做点儿什么
}else {
// 情况2之下做点儿什么
}

// -------------- 2 --------------
var x = 10
while(x > 5) { // while 单词表示 当什么什么时候
// 做点儿什么,比如输出x
console.log(" >> " + x)
x = x - 1
}

// -------------- 3 --------------
// x-- 是 x = x-1 的一种简写
for (var x = 10; x > 5; x--) {
// 做点儿什么,比如输出x
console.log(" -- " + x)
}

1很简单,就是个只有2个分支的选择结构,我们称之为 if-else语句
2通过单词也能大概猜到是个循环结构,我们称之为 while语句
3呢,其实3也是一种循环结构,这种语句在编程中称之为 for语句, 它只是比2中的 while语句 写起来更简单一些罢了, 2和3都表示一种循环结构,它们的执行结果如图

好了,这次只是让你简单的认识一下编程,至于各种语句应该怎么写,将在后续慢慢去了解,现在你只要能够知道顺序/选择/循环这3种结构,并学会把生活中的事情分解成一步一步的上述3种结构就可以了

小朋友的编程二

一切都是数字和电路

我们上一次大致对电脑以及编程的概念有了一些了解,编程就像是跟机器说话,告诉机器按照我们设计的流程去完成工作,而电脑 = 电子元件 + 程序 = 硬件 + 软件。 这一节就在具体说说为什么电脑除了需要硬件之外还需要软件。

这还得从电路开始说起,我跟你提到过电路可以分为2类。一种叫数字电路,它出现的比较晚是个小弟,它的大哥是一个叫模拟电路的家伙,这家伙从人类认识和使用电以来,基本都是它,比如作为能源供应驱动电机,灯泡等等。

我还提到过,电不光是一种能量,还可以是一种信息,所以模拟电路和数字电路的区别,最根本的来说就是2者电路中的信号不一样,一个电路板如果它里面的信号是数字信号,那它就是数字电路,否则就是模拟电路。

简单的来说,随处可见的自然信号都模拟信号,比如声音信号,灯光等等,模拟信号的特点就是在时间上和取值上都是连续的不间断的,如果在你学过的数轴上,随意画出来就是一条连续不间断的曲线,就是一个典型而自然的模拟信号。

数字信号就不一样了,在时间上和取值上都是不连续的。这些说起来有些难以理解,但是通过图就看出来的:

这是很简单的图,还有更复杂一些的,你能不能判断那些是模拟的那些是数字的呢?

其实关键就是看信号图是否连续,如果没有间断那就是模拟的,如果是一个一个柱子一样的东西那就是数字的。

既然模拟信号又自然应用的又早,那为什么现在主流都是数字信号呢?

其实,再上图中你就能猜出一些,原因很多,但最关键的是数字信号简单,规律性强,简单就意味着用电量少,有规律就意味着处理很传输方便,所以数字电路可以做到很小很强,我们接着图:


上图左侧是早期的计算机,硬件主要都是模拟电路,而右侧你就很熟悉了,就是普通的家用电脑,前者有几个屋子大小而后者桌上就能放,相对于体积差异它们的功能强弱才是更让人惊奇的,后者计算能力比前者要强好几百万倍都不止,即便你的手机的处理能力,也要比屋子一样的老电脑强很多很多。这就是为什么现在数字电路被广泛的使用在各个领域,电脑就是其中最典型的存在。


了解了这些之后,我们再回到主题,电脑 = 硬件 + 软件,其中硬件指的是数字电路,那么软件呢?

其实还是好回到数字也是信息的那个例子,回顾一下吧:
比如我给定一个标准电压5v,如果比5小的电压我们称之为低电压,我们用0表示,比它高就是高电压,我们用1表示。同时我们还规定,如果高电压就代表天气晴朗适合出门玩,低电压为雨天不能出门,文字描述对应到表格就是:

大于5V 1 天气晴朗
小于5V 0 阴雨天气

那么如果给出的是1,你一看自然就知道了:噢,今天天清气朗,是可以出去游玩,0的话就正好相反。上述就通过把电压转化成了数字信号0或1(这一步是硬件在发挥作用),从而来0表示阴雨并让1表示晴天(这个规定谁代表谁就属于软件的工作),他们共同作用就完成了表示天气的好坏的功能。

让我们在扩展一下,如果科学家发现了一种材料,它做成的电路能根据天气的好坏而发生变化,雨天低于5v,晴天就高于5v,这时候我们把这种材料做的电路放到院子里,你只用在屏幕前去看显示0或者1,就知道了天气,不用自己出去看,并且整个过程是自动化的,也就是说一旦已你第一次布置好了之后,就再也不用去干预了,你就拥有了一个能自动告诉你天气的智能设备了。

这就是软件+硬件之所以厉害的原因,它们配合起来就是自动的,智能的。

看起来也不是太难,是吧

总结一下,通过这篇文章,你对软件和硬件多了解了一些:

  • 电脑里的硬件基本都是数字电路,它们可以处理数字信号
  • 数字信号不是0就是1,电脑中的一切本质上都是0和1,不管是图片视屏还是游戏
  • 软件的作用就是为了支持和解释硬件的,它规定01代表了什么

这还是一篇概念性的材料,看过有个印象就行了,能了解到电脑中的一切都是0101很重要,因为它就是电脑工作的基本机制。预告一下:下周,我们就要开始试试最简单的编程了。

小朋友的编程(一)

第0章 引子

你一定对电脑和手机的使用非常熟悉,这些事情你几乎天天在做:

  • 打开电脑,屏幕亮起,等待一小段时间(或许你知道这是在启动系统),直到出现桌面
  • 点击游戏图标,进入游戏就可以开心的玩游戏了
  • 点击QQ,输入密码和账号,点击好友图像,就能在弹出的对话框中通过打字、语音或者视屏跟好友聊天了
  • 到超市买东西,打开手机扫一扫就完成了付款
  • 再回忆一个找动画片的例子: 打开浏览器,输入www.baidu.com就到了百度的首页,通过百度找到自己喜欢的动画片,比如“火影忍者”,在搜索结果中找到土豆网,点击它,等页面加载结束就可以看到动画片了

上述你一定非常熟练,对吧

但是你没有好奇过这一切是怎么发生的,它们背后的原理是什么呢?电脑究竟有什么魔力让它能帮你找到自己想要的信息?如果你学会了背后的东西,是不是就能自己去做自己的游戏,自己的QQ呢?

其实是可以的,在你轻描淡写的动动鼠标就完成的背后,是由大量杰出的科学家和工程师们的辛勤工作的成果,它们利用电子技术、信息网络技术和软件工程让这一切变得如此易用和普通。


那么一开始,我们就先聊聊电脑,你认为电脑是什么呢?什么才算是电脑呢?看看下边几张图吧,你猜猜图中哪些是电脑:

猜猜看这里边那些是电脑?

其实上边的都是电脑,家用台式机和笔记本很常见,但第一图的工业小型机和第四图的超级计算机就不常见了,其实远远不止这些,电脑的范围比你想象的还要广泛,比如你用的手机,每辆汽车上都有管理刹车和空调的汽车电脑,去银行取钱用到的ATM,去超市付款时收银员操作的收银机,甚至小到你玩的遥控玩具车,厨房里的电磁炉微波炉等等都可以称之为电脑,那么我们可以这样约定一下,我们所说的电脑是这样一种机器:能够自动化的实现某种或多种功能的电子设备,它们都可以称为电子计算机,也就是我们口语化的电脑。

是不是没想到原来电脑有那么多小兄弟,你的周围除了那个四四方方的带有屏幕的机器外,还有那么多设备其实都是电脑,那你在想想,除了上边我提到的之外,还有没有其他的东西也可叫做电脑呢?你可以按照我们刚才约定的那个定义去估计,试一试吧

然后我们在将注意力放到刚刚的定义上,定义里有2个关键字,一个是电子设备,一个是自动化。怎样理解呢,你可以把电脑想象成一个完整健康的人,一个人首先是有肉体躯干的,其次他还是有思想有智力的,类似的,组成电脑的各种电子器件就相当于肉体躯干,而软件就是电脑的思想智力,如果一个人只有躯干没有意识,那他就是植物人,对电脑也是一样的,如果一堆没有软件调控的硬件,也不能完成我们需要的功能。所以一个完整的电子计算机(健康的人) = 电子元件(肉体) + 程序(思想意识),更通俗一些的说法是电脑 = 硬件 + 软件

所谓编程就是编写软件的行为,是一种给器件赋予灵魂,让硬件组织起来共同完成某种功能的行为。换一个角度说来,或许更容易理解一些:我们说话用的是普通话,你跟你姥姥说话用的是山东话,你小姨跟她的学生交流需要英语,我们人类之间的交流是需要语言交流的,同样的道理,如果想让那一堆冰冷的电子器件一起干一件什么件事的话,就需要跟机器交流。跟机器交流就是编程,我们用来跟机器交流的语言就是编程语言。所以本质上编程就是通过编程语言,编写出机器能识别和执行的程序,让机器遵照先前编写好了的程序,达到完成某种功能的过程

所以简单的说编程就是告诉机器去干什么的过程

希望你通过上面的文字,能对软件,硬件,编程的概念有一个大概的印象,并且能对电脑的认识有所扩展一些就算达到目的了

给你留一个可选的小任务:上边的图片中有个叫神威的家伙,看起来很霸气,你能不能自己动手查一查它究竟有何特殊之处呢?

我的机器学习之概述

概述

  • 是什么?
    通俗来讲就是利用已有数据,得到某种模型,并利用其预测未来的计算机方法,跟模式识别,统计学习,数据挖掘,计算机视觉,语音识别,和NPL等领略息息相关,是一门交叉学科。一般可认为数据挖掘等同于机器学习,只是其数据来源不仅仅来源于结构化数据,还包括图像,音频等。下面的说法也常常被默许:
    模式识别最初来源于来源于工业界,后来扩展到语言,图像等等
    数据挖掘 = 机器学习 + 数据库
    自然语言处理 = 文本处理+机器学习
    计算机视觉=图像处理+机器学习
    语音识别=语音处理+机器学习

核心的学习问题的定义:针对某类任务T的,可以用P来衡量性能,如果P可根据经验E来自我完善,则称之为该计算机程序从经验E中学习。 而机器学习这么学科就是让机器进行学习的学科,其最原始的定义由Tom Mitchell等提出:
从样本集S(z, {xkk, y, ykk})学习/估计一个假设f(x),使得f(x)是问题世界模型F(x)的一个近似

  • 常见术语罗列,基本很容易理解
    f = aX + bY + cZ + ...

数据集: 所有可能取到的变量
示例/样本
属性/特征: a,b,c等
属性空间/样本空间/输入空间X:由全部属性而成的空间,如上述a,b,c为坐标轴形成的空间,每个点可以是一个样本
特征向量: 属性空间中的坐标向量
训练集
训练样本
假设
标记:关于示例结果的信息,如年龄20~35称之为适龄青年,适龄青年就是一个标记
样例:拥有标记的示例,会组成标记空间/输出空间
分类:对于离散型任务的别称
回归:预测结果是连续值的问题的统称
特征清洗相关: 采样,归一/离散化,降维,特征提取等
损失函数: 用以度量结果和预期的不同

  • 适用领域

收集数据 ->

  • 机器学习算法分类

js组件化演进探讨-Commom.js/AMD/CMD规范

引子

在早先在的组件化演化一文中,通过对组件封装的一步一步推演,最终得到了一个集API权限管理,节省命名空间,以及调用方便的方式,但仍然没有解决所有的问题,比如模块依赖管理,而这是模块化演进到可用级别的必要条件

如果不是把工作重点转移到公司前端的工作上,无论如何都不会想到前端为了处理命名空间冲突+组件加载及依赖管控+API接口权限管理,这种绝大多数语言几乎天然拥有的能力,而为此付出了多少,在Java一个package,C++中一个import module 在前端的工作中确实在是大费周折,本质原因就是js原本是被设计成一门脚本语言,设计者也没有料到web会发展成今天这样复杂和多样,而只是以函数作为作用域标准,如果不加处理项目稍大,命名空间的管理和组件的依赖就是灾难级别的,所以modeul化是必然并还将继续发展,正如:

规范形成的过程是痛苦的,前端的先驱在刀耕火种、茹毛饮血的阶段开始,发展到现在初具规模

模块化?

道理都说了,那就从代码来直观的展示一下模块化的历程,以下每一个🔜表示一种演进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function foo1(){
// do something
}

function foo2(){
// do something
}

// 🔜 函数 => 类
var Module = {
var1: 1,
var2: 2,
foo1: function(){ /* do something */ },
foo2: function(){ /* do something */ }
}
Module.foo2();
Module.var2 = 100; // 无权限控制

// 🔜 类 => 借助立即执行函数
var myModule = (function(){
var var1 = 1;
var var2 = 2;
foo1: function(){ /* do something */ },
foo2: function(){ /* do something */ }

return {
fn1: foo1,
fn2: foo2
};
})();
myModule.foo1()

很容易看出利用立即执行函数的好处,在以上的基础上随着服务器端NodeJS的兴起,终于推出了第一个模块化的js规范 – CommonJS

服务器端的CommonJS

具体来说就是3个基本规范:
1 作用域:单独文件,单独作用域,除非是Global的
2 输出:指定module.exports为唯一出口
3 依赖:通过require加载模块
于是上述demo套用这个规范以后就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
// --- Tony.js中 ---
var name = 'Tony Stark'
fucntion sayName() {
console.log('Iron man is' + name)
}

module.exports = {
run: sayName
}

// --- Other file ---
var tonyModule = require('./Tony.js') // 同步,并且相对路径时js需省略
tonyModule.sayName()

如果完全将CommonJS移植到浏览器,那么require同步加载势必会影响体验,但不同步呢依赖件的管理就不那么容易了,这个问题偏向的不同导致了2中浏览器端的模块化标准:AMDCMD

AMD 和 CMD

但是万变不离其宗,不管是谁都需要解决模块定义,输出和输入(加载)这3个问题

  • AMD(Asynchronous Module Definition): 通过RequireJS库,实现了浏览器端异步模块定义,具体来说:

1 模块定义和输出:使用全局函数define(id?, dependencies?, factory/exportValue)定义模块,其中的最后一个参数factory/exportValue为输出,可以是一个对象的值,更多的是初始化工厂方法
2 异步输入/加载: require([dependency, ...], function() {})函数,会自动异步加载依赖项,直到加载完成才执行对应操作

1
<script data-main="js/app.js" src="lib/require.js"></script>

页面上只出现一个<script data-main="js/app.js" src="js/require.js"></script>即可,这里的data-main是require的入口路径有的类似于main()函数一样,requirejs使用它来启动脚本的自动加载过程,这样就可以组织一个典型的页面结构:
Site页面组织结构

然后使用时借助require([dependency, ...], function() {})函数

1
2
3
4
5
6
7
8
9
10
requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
}
});

requirejs(['jquery', 'canvas', 'app/sub'], function ($, canvas, sub) {
//using jQuery, canvas and the app/sub here
});

这就是AMD规则

  • CMD(Common Module Definition): 以SeaJS为基础,国内发展出来的通用模块加载规则,其定义和输入输出如下define(id?, [dependency...], function(require, exports, module) { });,一个简单的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Tony.js
    define(function(require, exports, modules)) {
    var $ = require('jQurey.2.1.js') // 假设tony需要使用jQurey
    var name = 'Tony Stark'
    fucntion sayName() {
    console.log('Iron man is' + name)
    }

    module.exports = sayName // 输出
    }

    // ----- Other file ------
    var sayName = require('lib/Tony.js')
    var ironMan = new Tony()
    ironMan.sayName()

可看出更commondJS更相似,推崇一个文件一个模块,推崇就近加载,也就是用到就去加载

关于这两个的区别网上可以搜出一堆文章,简单总结一下

  • 最明显就是:在模块定义时,对依赖模块的执行时机处理不同(注意不是加载的时机或者方式不同)

    AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
    CMD推崇就近依赖,只有在用到某个模块的时候再去require
    这种区别各有优劣,只是语法上的差距,而且就现在阶段来说,requireJS和SeaJS都支持对方的写法,所以写法的差距都不在具有区分度

很多人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解实际上是不准确的,其实加载模块都是异步的,只不过AMD依赖前置,js可以方便知道依赖模块是谁,立即加载,而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,但实际上解析模块用的时间短到可以忽略

同样都是异步加载模块,AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的

组合兼容式

CMD 和 AMD 最初从在写法上就能区分,但是现在逐渐都兼容对方的写法,虽然对于学习来说不免会带来一些困惑,但对于实际项目的便利性确是实实在在的,其实兼容起来很容易,对于AMD/CMD检测define,对于CommonJS检查module.export,实现起来大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(function (name, definition, context) {
if (typeof module != 'undefined' && module.exports) {
// 在 CMD 规范下 (node)
module.exports = definition();
} else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd']) ) {
//在 AMD 规范下(RequireJS) 或者 CMD 规范下(SeaJS)
define(definition);
} else {
//在浏览器环境下
context[name] = definition();
}
})('sample', function () {

var sample = (function () {
"use strict";
var a = 1;
function inc(){
a++;
}
function get(){
return a;
}
return {
inc: inc,
get: get
}
})();
return sample;
}, this);

这样就实现了写法兼容,想怎么写都行了

js拾遗之面向对象

js作为一门松散的语言,如果有严格class_base面向对象语言的经验,如java,C#的,经常会有WTF的感受,不是说好了面向对象吗,怎么类不是类,继承不好好继承,多态到混乱的感觉,也写了不短时间的js,最感慨的就是:前端程序员们,竟然使用一门如此简陋的语言创造了如此异彩纷呈的互联网世界,这比js本身更WTF,不信去那个著名的诡异js的网站测测,看能做对几个题💔。好了不在吐槽引战了,本篇就来彻底说说js中的面向对象

在说明之前,特别需要指出的是,不要因为对js这种prototype_base不了解,就按照java等的标准去有色的去看js,殊不知从面向对象这个层面上来说,只要满足对操作和消息的封装,不管是class_base还是prototype_base,即便实现方式差异巨大,但无疑都是最纯正的面向对象

在js中它本身并没有明确的类的概念,这是最让很多人WTF的地方,面向对象居然没有类,但是如果从OOP的本意出发,只要可以用引用类型抽象出具有相同特性的结构,对操作和消息进行黑盒实现,那么就是面向对象,js中这个封装结构就是Object对象,在ECMA-262中这样给对象定义:无序属性的集合就是对象,其属性可以包含基本值,对象和函数,所以看起来就是一组没有特定顺序的键值对集合。对象的每个属性或者方法都有一个名字,而每个名字都会有映射的值。所以从另一角度来说,ECMAScript的对象就是散列表–一组键值对,值可以是基本值或函数。当然想所有的面向对象语言一样,对象是基于引用类型创建的

从属性说起

回顾一下对象的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Object实例创建
var person = new Object()
person.name = 'Tony stark'
person.age = 42
person.job = 'Iron man'
person.sayName = function() {
alert(this.name)
}

// 更加常见的 字面量 形式
var person = {
name: 'Tony',
age: 42,
job: 'Iron man'
sayName: function() {
alert(this.name)
}
}

当创建一个对象的时候,除了我们所定义的属性,对象本身还有为实现JavaScript引擎,还有为属性规定其特征的内部属性,也称之为特征属性,通常这些内部值是不能访问的,为了区分将其规范到2对方括号中,而特征属性可分为2种:
1 表征数据的特征属性

1
2
3
4
[[Configurable]] // 表示能否通过delete删除属性,能否修改,默认为true
[[Enmuerable]] // 表示是否可通过 for-in 遍历属性,默认为true
[[Writable]] // 可写的
[[value]] // 就是具体的属性数据值

以上这种属性的属性,大家都能嗅出权限控制的味道来,实时就是如此,并且js提供了Object.defineProperty()方法来控制,如:

1
2
3
4
5
6
7
8
var person = {}
Object.defineProperty(person, "job", {
configurable: false,
value: "Iron man"
})

delete person.job
alert(person.job) // Iron man

事实以上在严格模式之下,是会抛出异常的,这就是特征属性对属性数值的规范,并且一旦规定configurable: false,无法使用Objcet.defineProperty()方法修改除writable之外的任何特征,在使用中尤其重要的一点是,Object.defineProperty()方法,如果不指定,会默认设置enumerable,configurable,writable为false的,即便没有人真的用这个方法来创建属性,这一点也要足够审慎

2 访问器特指属性
这一种特征属性不包含数值,只包含了一对儿setter 和 getter,更具体一些就是:

1
2
3
4
[[Configurable]] // 表示能否通过delete删除属性,能否修改,默认为true
[[Enmuerable]] // 表示是否可通过 for-in 遍历属性,默认为true
[[Get]] // 默认值是undefined
[[Set]] // 默认值是undefined

对于访问器属性,也只能通过Object.defineProperty()/defineProperties()设置,用法就不多说了,绝大多数就是用以设置一些对象默认的值
而对于一个已有属性的特征属性,可以通过Object.getOwnPropertyDescriptor()得到

对象创建和扩展

看完了属性,让我们注意力转回到对象的创建上,毕竟我们的开发就是在频繁的创建或者扩展对象,先直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 工厂模式创建
function createPerson(name, age, job) {
var obj = {
name: 'Tony',
age: 42,
job: 'Iron man'
sayName: function() {
alert(this.name)
}
}
return obj
}

// 构造函数创建
function Person(name, age, job) {
this.name= name;
this.age= age;
this.job= job;
this.sayName= function() {
alert(this.name)
};
}
var tony = new Person('tony', 29, 'Iron man')

第一种方式很好理解,但一个突出的问题是,我们虽然创建了对象,但并不显示的知道对象的类型,这是不能接受的;而第二种方式,显然很明确,并且使用了 new 操作符,new实际上包含了:创建对象 -> 构造函数作用域赋给创建的对象 -> 执行构造函数 -> 返回新对象的完整过程

然后集中到构造函数中,向Java等靠拢,习惯性的大写,但它之中并没有显示的创建对象的操作,更没有直接把属性值赋给this对象,也没有return, 这是因为 new操作符都做了

对于一个对象,构造函数本身,就对应于constructor属性,可以这样验证:

1
2
3
var p1 = new Person('p1', 12, xxx)
var p2 = new Perosn('p2', 22, yyy)
p1.constructor == p2.constructor == Person // true

但使用构造函数并不是没有缺陷,如上述的p1和p2, 其sayName由于是函数,我们知道本身也是对象,所以各自实例花了自己的Function,换个角度会更明白一些:
this.sayName: new Function('alert(this.name)')

可见p1和p2的sayName的值,的确是各自分别地实现化了,那么问题随之而来:
第一:上述很明显不是我们通常意义上所理解的面向对象,虽然不排除有现实需求
第二:更严重的是在使用中,会导致不同的作用域链和标识符解析,出现了同名函数实际上不相等的事儿,eg:

1
p1.sayName == p2.sayName // false

解决方式就是避免每个实例都创建,所以使用this对象单独绑定函数就行,like this:

1
2
3
4
5
6
7
8
9
10
// 构造函数创建
function Person(name, age, job) {
this.name= name;
this.age= age;
this.job= job;
this.sayName= sayName;
}
function sayName() {
alert(this.name)
}

这明显是一种妥协的处理方式,为了让各个实例共享单独的函数对象,把函数放到了全局,且不说有很多的函数,造成全局命名是灾难,但是alert中的this只表示Person,从语义上就不脱,所以我们需要救助于原型模式

使用原型模式创建和扩展对象

接着上述,如果使用原型模式,我们这样创建对象:

1
2
3
4
5
6
7
8
9
10
11
function Person() {}
Person.prototype.name = 'tony'
Person.prototype.age = 29
Person.prototype.job = 'Iron man'
Person.prototype.sayName= function() {
alert(this.name)
}

var p1 = new Person()
var p2 = new Person()
p1.sayName == p2.sayName // true

这样所有的实例都会共享属性和方法了,同时注意到构造函数中什么也没有,但它是怎么做到的呢?这就涉及到ECMAScript中原型对象的性质

涉及到js中对象的结构,那就使用console.dir(obj)查看一下,下图分别是使用最普通的字面量创建构造函数以及原型模式的对象结构:
比较各种方式的不同之处

可见:

  • 任何时候,只要创建一个新对象,那么该对象就会有一个自动的prototype属性,这一句__proto__:Object告诉我们,这是一个指向对象的原型对象的指针 – 该对象包含了所有实例共享的属性和方法
  • 创建的对象本身的属性可以是从原型对象获取,也可以由自己创建,通过上图属性出现在不同的层能明显的看到
  • ㊙️特别注意,原型对象中才存在constructor属性,它会指向对象的构造函数。上图可见,它默认会指向Object,但一旦自定义了构造函数,就指向构造函数本身,但不管怎样,构造函数是实例本身没有的,只有实例的原型对象才有

第三种原型模式结构示意图:注意实例与构造函数间并不直接连接,而是通过原型对象实现连接

更加直接一些,我们可以通过isPrototypeOf()方法来确定对象之间是否是同一个原型对象,通过getPrototypeOf()来回去原型对象的属性值比如:

1
2
3
4
5
Person.prototype.isPrototypwOf(p1) // true

var personProtoObj = Object.getPrototypeOf(preson)
console.log(personProtoObj == Person.prototype) // true
personProtoObj.name // 'tony'

我们可以拿到了原型对象及其name属性值,从实例角度来说,获取一个属性,首先从本身的属性中去查找,如果本身没有,就去原型对象中查找,这就解释了按上述方法,不管有多少实例,最终都会向上追溯到同一个原型对象的相同属性,这就实现了共享

更进一步,如果实例拥有一个相同的属性,那么会怎样呢?使用hasOwnProperty()会检测属性是否存在于当前实例,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var p3 = new Person()
p3.name = 'hulk'

p3.hasOwnProperty('name') // true
p1.hasOwnProperty('name') // false

console.log(p3.name) // 'hulk'
console.log(p1.name) // 'tony'

// 当delete之后
delete p3.name
p3.hasOwnProperty('name') // fasle
console.log(p2.name) // 'tony' -- 重新找到原型中的属性值

同名属性如果自有,就而不会去影响原型的属性,也就是上述没有,才上溯到原型的原则。此外对于for - in循环,也只会在实例本身中去循环而不会去管原型中的属性

上边的写法很麻烦,不妨把各个属性用字面量组合写到一起:

1
2
3
4
5
6
7
8
9
10
11
function Person() {}
Person.prototype = {
name: 'tony',
age: 42,
job: 'Iron man'
sayName: function() {
alert(this.name)
}
}

var stark = new Person()

使用字面量把各个属性写到一起,如果输出stark的结构的话,还是能看到不同之处,如图:
比较之

可见比较两者,可看出这种字面量写在一起时,其原型对象并没有contructor,而是完全重写了一个新的原型对象,这个重写也切断了实例和原本prototype.constructor之间的连接关系,这是最大的不同之处,解决方式就是显示的添加一个constrictor并让其指向对象本身,这里就是在重写的原型中添加:

1
2
3
4
Person.prototype = {
...
constructor: Person
}

好吧,那到底该如何使用构造函数构造对象呢?一句话来说的就是,要共享的原型之,不共享的构造之,eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Man(name, age, job, wife) {
this.name = name
this.age = age
this.job = job
this.wife = wife
}

Man.prototype = {
constructor: Person,
sex: male,
sayName: function() {
alert(this.name)
}
eat: function() {
alert(this.name + 'is eatring!')
}
}

var tony = Man('tony', 42, 'Iron man', 'xxx')
var hulk = Man('hulk', 32, 'Hulk and scientist', 'yyy')

如上所示,tony和hulk的名字,工作,老婆是不能共享的,那就放到构造函数中,而都作为男性,性别是一样的,都能报出自己的名字,都能吃饭,所以这些共享的放到原型对象中

综上所属,可看出通过原型对象,js实现了各实例属性的共享,这就是原型对象的秘密

js对象的继承

通过上述,所有的对象都有原型对象的指针,并且原型对象本身也有一个原型对象,都是来自于Objcet这个基本对象,大致能猜到继承的影子了,对于Java等OO语言,继承可以有2种:接口继承和实现继承,接口只继承方法签名儿实现继承则继承实际方法,在js中是没有接口集成的,只有实现继承,如前边的揣测,使用过原型对象组成的链条来实现的

  • 基本的原型链式继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var SuperType = function() {
    this.property = "super"
    }
    SuperType.prototype.getSuperValue = function() {
    return this.property
    }

    var SubType = function() {
    this.subProperty = "sub"
    }
    /* 后代对象的原型是父对象的实例 */
    SubType.prototype = new SuperType()
    SubType.prototype.getSubproperty = function() {
    return this.subProperty
    }

    var father = new SuperType()
    var son = new SubType()
    console.log(instance.getSuperValue()) // 'super'

    console.log(father)
    console.dir(son)

最关键的一句就是SubType.prototype = new SuperType(),也就是重写子对象的原型对象让后代对象的原型对象 = 父对象的实例,由此实例的属性和实例能找的它原型的属性,就都可以让子对象顺着它的原型对象而收缩到,从效果上就实现继承,如上述代码他们的继承图就是:

很显然,这样单纯的让原型对象==父对象的实例以后,回引出一下几个问题:
1 如果需要为向子对象的原型对象中添加新方法,是不能写成字面量的形式的。因为这样一来就等同于重现了实例对象,会覆盖掉最初的原型对象,自然链接父对象的原型链会被切断,而失去继承的效果,这一点是尤其需要注意的
2 基于同样的道理当子对象为他的原型添加属性,这个属性会被共享到所有的子对象实例,所以不应该把子对象的构造函数留空
3 此外还有一个问题是,再也无法单独为超对象的构造函数传递参数,因为这势必会影响到继承自他的子对象
所以单纯使用原型链只能算是粗暴的继承

  • 伪经典继承(也称组合继承 combination inheritance)
    这的将构造函数 + 原型链组合的一种方式,其思路是:原型链实现对原型对象的继承,而实例属性的继承通过构造函数实现,如此便是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function SuperType(name) {
    this.name = name
    this.colors = ['red', 'green', 'bule`]
    }
    SuperType.prototype.sayName = function() {
    alert(this.name)
    }

    var SubType = function(name, age) {
    // 继承父对象的实例属性
    Supertype.call(this, arguments)
    this.age = age
    }
    // 继承父对象的原型属性
    SubType.prototype = new SuperType()
    // 可以选择纠正一下构造函数
    <!--SubType.prototype.constructor = SubType-->
    SubType.prototype.sayAge = function() {
    alert(this.age)
    }

    var son1 = new SubType('son1', 22)
    son1.colors.push('yellow')
    son1.sayName() // son1
    son1.sayAge() // 22

    var sonOther = new SubType('other', 44)
    console.log(sonOther.colors) // ['red', 'green', 'bule`]
    sonOther.sayName() // other
    sonOther.sayAge() // 44

看图更加清晰一些:
为纠正构造函数结构图

由于父对象的实例属性,在子对象的构造函数中完成继承,所以等同于子实例属性的创建和覆盖,同时只属于子实例实例如age不在被共享,而共享的只是来自于父实例的属性,这解释了son1.colors 和 sonOther.colors的不同;来自于父类原型的属性及方法以及被继承。可见没有了上面只使用原型链的问题,但这样就完美了吗?

上述这就用js实现了java类一样的继承了,但在js中绝大多数时候,是不需要这样完全模拟类的继承的,并且并不完美:至少SuperType就被调用了2次,一次在构造函数中,一次在链接原型链时,所以还需要改进

这就是js更常见的继承了,它并不模拟类的实现,虽然看起来不够模版化,数据耦合性很高,但却是见的比较多的,使用extendObj简单的封装了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var extendObj = function(superObj) {
var obj = new Object(superObj.prototype)
obj.constructor = obj
return obj
}

var fa = {
name: '老王',
surname: '王',
familyName: function() {
console.log(this.surname)
}
}
console.log(fa.familyName())
console.dir(fa)

// 不使用构造函数,不模拟类
var sequela1 = extendObj(fa)
sequela1.name = '小王'
sequela1.course = '数学'
sequela1.projectWork = function() {
console.log(this.course)
}
console.dir(sequela1)
console.log(sequela1.projectWork())

综上,就是js的继承,可见js即便他现在使用很广泛,也还有很多脚本化的身影,继承写起来并不入java等的容易,但也的确实现了继承,何况能理解到另一种形式的OOP,也很助于理解其他OOP语言

附上一个测试的demo代码:

vim拾遗之其实可以不用插件的

向所有刚接触到VIM的新手一样,笔者曾几何时也把vim搞的花里胡哨,美其名曰vim-ide,折腾了tons of plugins,直到vim被弄得越来越臃肿,直到遇到了浩哥,看到大哥信手纯vim在我面前完成了所有的那些,我原本只能借助插件完成的功能,一时惊愕不已: 原来这些VIM全都是自有的,这篇的目的并不是让大家告别插件,彻底的不用,而是结合笔者自己的使用经验,多给一钟选项罢了,好了开始吧

vim下的文件跳转和查找

不知道大家是否注意到过vim的设置选项中,是有一些默认的值得,比如:set path?,多半会输出这样一个结果:

1
path=.,/usr/include,,

通过path这个名称,大致也能猜到这是vim有关文件查找的路径,后边的就是指几个默认当前目录下,所以不出意外的话我们的查找应该以当前目录层级的相对路径,可是很可能你并不明确当前目录下的文件,这时候使用:find *通配的去查找着当前路径下的文件,可以使用tab跳转,但是如果:set wildmenu的话,找起来就更方便了,画风是这样的
wildmenu画风展示

然后enter,就进入到了想要的文件,这就是实现了简单的文件查找和变更,如果此时先要回到原来的文件,可以通过:ls命令,他会罗列出当前vim缓存所有的未关闭的文件,like this:
vim下的ls
比如这里我想回到vim拾遗的文章,输入:

1
:b vi

我甚至都不用tab出完整的文件名,直接enter就回到了原来的文件,当然你也可以语义化为back
vi…

vim自己的自动补全

用vim敲代码几乎都或多或少听过YouCompleteMe这个插件的大名,它几乎支持每种语言,让代码写起来更容易,但实际上,vim本身就提供了这样的功能,那就是一系列的Cr + n的命令,下面列举如下:

1
2
3
4
5
6
7
8
" 当前文件匹配
Cr x + Cr n " next
Cr x + Cr p " pre

" 文件夹匹配
Cr x + Cr F 再配合 n/p
" tag匹配
Cr x + Cr ]

文件名自动补全
谁用谁知道,是的我已经听到有人吐槽这个原生OnmiComplete很若,但是对多数人来说的确够用了,如果实在想做到想IDE一样索引API,那还需要CTags,对我来说完全目前还用不上,自带的足以,以后有机会在补上Ctags 及 JSTags 根vim配合的自动补全吧

Hbase面面观

引子

大数据处理和存储框架Hadoop,除了最重要的HDFS和MapReduce/Spark计算框架之外,还需要很多辅助组件,比如需要分布式协调系统zookeeper来做热备,维护元数据命名空间, Sqoop作数据导入导出等等。这些都是从整个系统上的工具,而作为数据持久化的存储系统的HDFS,自然也发展出了很多自己的辅助组件,其中HBase无疑是最耀眼的

是什么?

简单的,HBase是一种类数据库的存储层工具,其用于组织和管理结构化数据的存储,由于需要服务HDFS,自然它也是分布式的,并且是一种 列式的分布数据库 ,最早由Goolge的BigTable论文中提出。虽然依托于HDFS很容易让人想起另一个结构数据工具Hive,但二者的区别和设计目的是不同的,在生态中的所处位置也不一样,这一点在Hive初探文章中有所探讨,总体来说,Hive是MepReduce层的工具,而Hbase是在结构化存储层,直接连接hdfs,还有诸如:

  • Hive并不适用于实时数据查询处理,而这是HBase的强项
  • Hive部署不依托Zookeeper,而HBase需要
  • Hive的交互有SQL,而HBase不直接提供

相对于Hive的比较,更有趣的是HBase根传统数据库,比如mySQL这样的行式数据库的不同

HBase解决了那些HDFS的问题呢?HDFS本身就拥有良好的容错和扩展性,常常被扩展到上百节点,但仍然存在着作为文件系统,相较于结构化存储系统固有的天然劣势:如果我们只关心文件中的一部分数据,想对部分数据进行操作,文件系统只能通过对整个文件进行扫描来实现,所以HDFS:

  • 不支持数据查找
  • 不适合增量式数据处理
  • 不支持数据更新

如果是单纯的离线存储也就罢了,但现实往往有实时性的需求,所以才有了HBase对HDFS的补充,HBase可以支持:插入/删除/查询等单条/部分数据的操作,便于高效过滤和读写数据

也正是由于其设计目的限制,让HBase拥有了一些跟传统数据库完全不同的实现,这就是列式无模式稀疏的数据组织方式。

列式? 🤔因吹死停

先来看看一组数据,如果是在传统RDBMS中如mySQL是这样存储的:
| id | name | nickName | password | time |
| :–: | :——: | :——: | :——: |:——: |
| 001 | zhangsan | zzss | p11zzz | 20161212 |
| 002 | lisi | lsls | 333lll | 20161213 |

每一行的是一个完整数据单元,每一列对应于都是一个具体数据。但是在没有建立索引的情况下,读取整行,在遍历查找数据,会付出巨大IO;而索引和视图的建立,付出的IO和性能也不容忽视;最重要的是为满足多样的查询需求,数据库要不可避免的大量扩大膨胀,才能满足性能需求。而这些问题HBase是怎样处理的呢,如果上述数据在HBase中是怎样存储的呢?

数据模型




















RowKey Value (CF Qualifier Version)
001 name{“real”: “zhangshan”, “nick” : “zzss” }
info{“pwd” : “333lll”}
001 name{“real”: “lishi”, “nick” : “lsls” }
info{“pwd” : “333lll”}
RowKey CF:Column-Key Timestamp CellValue
001 name:real 123456789 zhangsan
001 name:nick 123456789 zzss
002 name:real 123456789 lishi
002 name:nick 123456789 lsls
001 info:pwd 123456789 p11zzz
002 info:pwd 123456789 333lll

上述是一个一种物理存储的形式,不太容易看明白,如果表述为逻辑存储形式:

从上述的物理和逻辑排布中,能看出在HBase中:
只有一中索引标识那就是RowKey

传统行数据库的问题

构架,为什么能做到实时?

  • 预写入日志:WALs

HDFS的物理目录结构

在Hbase的设置文件中,具体设置:
<name>hbase.rootdir</name>

常见操作

  • 2种方式

    • JDBC API
    • CLI
  • 读hdfs

  • 写hdfs

核心使用

  • 热点问题:客户点直接访问集群少数节点,导致节点机器超载,性能下降甚至region不可用,从而影响同RegionServer下的其他region
  • rowKey的设计
  • 窄表和宽表
  • 负载均衡

一个案例

jQuery拾遗之动画

jQuery中提供了一些常用的动画,拜强势的PM和UX所赐,在他们威逼利诱之下也让我逐渐熟悉了jQuery动画的某些用法。

在具体说之前,不妨先来猜测一下,如果让我自己造一个专门的Animation的轮子,我想至少要满足一下功能:
1 简单的常用动画,要足够简单–如hide,show,toggle…
2 能对简单的动画进行组合,组合出比较复杂的动画,并且对整个动画执行的各个关键帧,要能控制和截断动画等操作
3 需要一个代完整参数的仿射变换的函数,就像cocoa中的CGAffineTransform一样:

1
2
3
4
struct CGAffineTransform {
CGFloat a, b, c, d;
CGFloat tx, ty;
};

并且要映射成CSS

如果上述这些基本需求能满足,那么基本上这个地球上存在的能想到的动画都基本能实现,具体实现上由于帧动画和组合动画的要求,自然需要一个统一的动画队列,甚至是动画缓存池,要以对各个动画生命周期的控制

事实上jQuery设计者们都不是吃素的,他们为其提供了远超上以上需求的动画API,只是很多时候我们没有去仔细研究罢了

js组件化演进探讨-从简单的封装说起

先从一个slideShow的封装说去

在前端虽说已经有了一些很成熟的UI库,但是对于我这个有过客户端开发经验的人总是忍不住会对一些常见的组建进行自以为是的封装,总想着向MFC,Cocoa那样完全组件化的开发,特别是针对于UI层级,但是考虑到前端的特殊性,这两种封装思路有时候并不经相同,所以想借着一个轮播slide的Demo,浅显的梳理一下。

直接从最关键的接口设计入手:于客户端封装相比,前端的组件可以全部js化,也可以html实现,更多的是二者兼而有之,而客户端平台往往只需要管理好接口API权限和参数,没有这种选择性的问题,比如在本例中,可以这样:

1
2
<script scr='slideShow.js'></script>
<div class='slideShow'></div>

仅仅通过一个div借助js来实现,至于轮播的图像数据,以及其他元素全部通过js来添加,看起来很美妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$.fn.slide = function(opt) {
// 创建5个img
// img设置样式,src等等
// img包裹个几层
...

// 创建一个指示器
...

// 创建next/pre btn等等
}

// 调用
$('.slideShow').slide({
width: 300px;
height: 200px;
numOfCell: 5;
auto: true;
...
})

也可在更多html的基础上,如:

1
2
3
4
5
6
7
<div class='banner slideShow'>
<img class='picCell' scr='http://001.png'></img>
<img class='picCell' scr='http://001.png'></img>
<img class='picCell' scr='http://001.png'></img>
<img class='picCell' scr='http://001.png'></img>
<img class='picCell' scr='http://001.png'></img>
</div>

这一种明显给了更多的html内容,特别是img元素样式的单独分类,让变动更加直接,看起来也还不错,为了方便使用jQuery,具体例子的gits:https://github.com/13hoop/sliderShow/blob/master/index.html

1
2
3
4
5
6
// 调用方式
$('.banner').slides({
width: 480,
height: 360,
auto: true
})

以上2中共同之处都是直接封装的$.fn上,这样调用时通过$('.banner')调用,看起来的确是很方便,但遗憾的是直接从是对jQuery的扩展,但一下子让所有的页面都具有了sliderShow,太过粗暴的硬塞给所有依赖jQuery的文件,很多时候是没有必要的

困惑 😖

我自己第一个很困惑的问题是对于样式在封装中的处理
是用js创建样式,还是不使用?如果全部用js会不会引入其他问题,比如有2个同样的控件,同样的样式要js创建2遍,性能如何?如果按照web规范,结构是结构,样式是样式,那对于基本不变的样式如在移动端页面,岂不是不能做到开箱即用?在纠结以一段时间之后,很可悲的发现与其这样瞎想,不如直接动手试试,尝试几次之后,不得不接受这样的结论:没有一个一劳永逸的准则,具体控件具体实现?🤔,完全是废话根没说一样😂,看来这个问题只能探讨到这儿了

接着说封装形式,前边是封装到jQuery的fn上,如果借助js模拟类的形式呢?:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建实例
new Banner($('.banner'), function() {

})

// Banner`类`
var Banner = function($target, callback) {
this.$target = $target
this.callback = callback
this.init()
this.bind()
}
Banner.prototype.init = function() {
// 创建5个img
// img设置样式,src等等
// img包裹个几层
...

// 创建一个指示器
...

// 创建next/pre btn等等
}
Banner.prototype.bind = function() {
// 事件 here
}

很方便简洁的代码组织形式,自己偶尔写写简单基本都是按这个套路的,比如一个懒加载的图片墙,采用类型的封装之后就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var Explode = function ($target, callback) {
this.$target = $target
this.callback = callback
this.check()
this.bind()
}
Explode.prototype.bind = function () {
var _this = this
var clock // 模拟网络延伸
$(window).on('scroll', function (e) {
if (clock) {
clearTimeout(clock)
}
clock = setTimeout(function () {
console.log('hello')
_this.check()
}, 100)
})
}
Explode.prototype.isVisible = function () {
var height_win = $(window).height()
var scrolled = $(window).scrollTop()
var height = this.$target.height()
var offTop = this.$target.offset().top
if (height_win + scrolled >= offTop && offTop + height >scrolled) {
return true
} else {
return false
}
}
Explode.prototype.check = function() {
if(this.isVisible(this.$target)) {
this.callback(this.$target)
}

$('.container img').each(function (k, img) {
var $value = $(img)
new Explode($value, function ($target) {
var url = $value.attr('data-src')
if ($value.hasClass('loaded')) return
console.log(k + '~~~')
$value.attr('src', url)
$value.addClass('loaded')
})
})

最好的方式

但缺陷还是有的,最明显的就是没有实现权限管理,暴漏了太多,不妨再来一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var Explode =(function () {
var _Explode = function ($target, callback) {
this.$target = $target
this.callback = callback
this.check()
this.bind()
}
_Explode.prototype.bind = function () {
var _this = this
var clock // 模拟网络延伸
$(window).on('scroll', function (e) {
if (clock) {
clearTimeout(clock)
}
clock = setTimeout(function () {
console.log('hello')
_this.check()
}, 100)
})
}
_Explode.prototype.isVisible = function () {
var height_win = $(window).height()
var scrolled = $(window).scrollTop()
var height = this.$target.height()
var offTop = this.$target.offset().top
if (height_win + scrolled >= offTop && offTop + height > scrolled) {
return true
} else {
return false
}
}
_Explode.prototype.check = function () {
if (this.isVisible(this.$target)) {
this.callback(this.$target)
}
}

return {
init: function(targets, callback) {
$(targets).each(function(idx , target){
new _Explode($(target), callback)
})
},
// once: function(targets, callback) {
// $(targets).each(function(idx , target){
// new _Explode($(target), callback)
// })
// }
}

})()

Explode.init($('.container img'), function($elNode) {
var url = $elNode.attr('data-src')
if ($elNode.hasClass('loaded')) return
$elNode.attr('src', url)
$elNode.addClass('loaded')
})

通过一个匿名函数,实现对_Explode代码的封装,同时借助return返回需要对外开放的方法,即添加了权限开放管理,又节省了命名空间,还在调用上获得得了便利,这才是比较容易接受的好方法

总结

扯了这么多,总是觉得单单控件的实现并没有怎样的难度,在前端反倒是对这些空间站的管理才是令人头大的,好在现在的套路基本被摸索出来了,那就是模块化js的开发规则,上述的最后一种方式,其实已经有一些模块化的影子了,但并不能很好的处理组建的依赖问题,而这些将在模块化js中一并解决,具体会在js模块化管理的探索后续文章中讨论

vim拾遗之- better motion

趁着同事那儿借来《Vim使用技巧》的机会,重新梳理一下Vim的常见使用技巧,记录于此
虽说Vim平时用的很多,自认为还是比较熟练的,但直到看到这本书,才知道原来自己用了假的Vim,很多技巧性的组合可以让人耳目一新,效率提高不止一倍,尤其记住书中所言:

在Vim中所有的操作通常都会有更加便捷版本,比如在移动光标时,一旦连续按了2次以上的h,那就意味着浪费时间,停下来想想能不能更简单

当然按作者的苛刻标准绝大多数所谓vimer都是妄称,但这并不妨碍我们对工具的精益求精,配置方面,按照我自己的习惯,有2点建议供参考:
1 在vim中通常首先废弃方向键 + delete键,尤其是初学者
2 中文输入发的问题,基本上这是个无解的问题,能做到最好的就是当从中文inert下Esc时,自动切换到英文输入法,不过也仅仅是这样了

废话不多说了,下边开始记录:

更好的motion

  • 只用h/l键来解决差一错误(off-by-one error),即当目标差一两个字符时才去用
  • 启用行号,区分实际行和显示行,毕竟通常大家都是启用wrap 的,所以在行号的辅助下区分二者很重要
  • 同行多行显示下,移动用gj/g^/g$/g0/gk而不是j/^/$/0/k来操作显示行
  • 使用w,b,e,ge基于单词移动更好,借用这张图:
    基于单词移动
    并且将常见操作组合记忆,如cw就和change word很贴合,ea就是添加单词尾部等
  • 最好的移动永远是查找

    • ; + ,配合f{char}能决绝99%的问题,特别提醒 别把方向字符查找弃之不用 ,当然如果是中文就歇菜了
    • t{char}d/c合用更配噢 ,eg:

      1
      2
      3
      function foo(x: Int, y: int)
      // f,dt)操作之后,变为
      function foo(x: Int)
    • v + t/f{char} + d/c/y 比鼠标托选精准高效一万倍

      1
      2
      3
      console.log('This phrase takes time but eventually gets to the point')
      // 想删除'takes time but eventully',可以ftvfyd即可
      console.log('This phrase gets to the point')
  • 结构化文本结构化处理:
    vim既然是码农的神器,那么对于结构化文本处理能力自然是没得说,只是使用并不不及,是时候应该主注重这方面的锻炼了– 文本对象字符由2个字符组成,第一个永远是i 或者 a,通常以i开头的文本对象会选择分割符内部文本,而已a开头的文本对象会将分割符包含在内,为了记忆不妨把i读成insidea就是around/all, eg:
    结构化文本结构化处理
    熟练掌握vim结构化处理是一个vimer的基本内功,特别是在前端或者简单的脚本语言编程中,使用的尤其多,总结一下常见分割符文本对象的操作:

    1
    2
    3
    4
    5
    6
    a) 或者 ab  // 对应()
    i) 或者 ib // 对应()内部
    a} 或者 aB // 对应{}
    a> // 对应<>
    at // 对应 一对xml标签
    it // 对应 xml标签内部

    第一个例子中特意给出v可视化模式下是为了方便观察,而在操作符待决模式下才能真正展现其强大的能力,配合c / d / y + i / a + {char}才能让你键码齐飞,高效异常,eg:

    1
    2
    3
    4
    5
    <a href="#">ABOUT</a>
    <!-- 光标启示在#,ci"之后,输入http://github.com -->
    <a href="http://github.com">ABOUT</a>
    <!-- 紧接着输入cit 加上 github,就完成了编辑-->
    <a href="http://github.com">github</a>

    不少人都是单独的把ci"解读为修改双引号内部内容,把cit解读为修改标签内部内容,同样dit删除标签内部内容,其实都是文本化操作的产物,完全没有必要去死记硬背

  • 非块级问对对象的操作:按单词w-word,按句子s-sentence,按段落p-paragraph
    其实这一点在前边的按单词操作中已经有所涉及,但结合机构化的操作能有很多意想不到的高效体验,常见的如:

    1
    2
    3
    4
    5
    Improve your writing bu deleting excellent adjectives.
    // fx 光标到excellent, 然后 daw
    Improve your writing bu deleting adjectives.
    // 如果上述是 diw, 就会出现2个空格
    Improve your writing bu deleting adjectives.

    可见awiw得区别很细微,如果不好记忆,不妨想我一样只去记忆i表示inside1之意,其他的它是不管的,接着上面的例子,如果只是想把单词改变, eg:

    1
    2
    3
    Improve your writing bu deleting excellent adjectives.
    // fx ,然后 ciw 加上修改的单词 most
    Improve your writing bu deleting most adjectives.

    所以总结起来就是:
    d + aw / as / ap比较好,而ciw / is 更配

  • 善用自动标记完成移动
    vim的基本操作中其实没有必要涉及到使用m{char},系统为我们提供了很多自动的标记,它指示了一些简单的行为,如上次修改位置,上次插入位置,以及括号之间的移动,like this:

    1
    2
    3
    4
    5
    ``   // 上次跳转动作之前的位置--相当于<C-o>
    `. // 上次更改位置
    `^ // 上次插入位置
    `[/] // 上次c或y的启示/结束
    % // 括号之间的跳转

    特别提醒,括号跳转时,往往要配合标记跳转一起来使用,如下面的ruby代码要将}变为],这里就有一个小陷阱,因为%只会在同级括号键自动跳转,如果直接使用r]就不再能跳转到末尾的],所以正确的做法是:

    1
    2
    3
    4
    5
    cities = %w { london,berlin,nweYourk}
    # dt{ 然后不忙r[ 而是% 跳转到末尾的'}'
    # 然后现更改最后的} 为 ]
    # 在``会光标的上一位置,再r[
    cities = %w [ london, berlin, nweYourk]

    好吧,的确有点儿繁琐,肯定有更好的办法,不过配合光标标记还是完成了

上边的操作都很基本,可以说每个人都会,但vim不是知识而是一种技能,常常练习才能真正的实现手眼并举,编码齐飞

JS jQuery拾遗

记录一下JS中某些语法点,以及同swift,python等的对比

默认参数(Default arguments)

函数支持默认参数的必要性:从个人经历来看,在开发中特别是组建和公共API的开发中,由于JS松散的类型,特别是函数调用参数不定的特点,屡屡出现的类型检测这样的鸡肋代码让人如鲠在喉,eg:

1
2
3
4
5
6
7
var add = function(a, b) {
b = typeof b !== "undefined" ? b : 0
return a + b
}

add(1) // 1
add(1,1) // 2

如果使用默认参数,我们就省去了不必要的检测:

1
var add = function(a, b=0)

剩余/可选参数(Rest arguments)

正如前边所说,js函数调用很松散,所以可选参数在js上很容易实现,最简单的莫过于使用arguments,但对其他语言就需要特殊的支持,ES6上显然也是这样认为的,所以:

1
2
3
function(arg0, arg1, ...rest) {
alert('you pass' + rest.length + 'extra args')
}

说明: 根arguments对象不同,剩余参数数组rest是一个真实的数组

jQuery 常见方法

  • 集合类操作相关:对集合类元素的遍历操作已经见怪不怪了,几乎所有的语言都有,比如swift和java中的map,reduce等等,在jQuery中也自然存在:
    $.each(obj, function(key, value))
    $.map(obj, function(value, index):自1.4之前只对数组管用,1.6之后改为所有,按设计规则,常根return
    $.extend([deep?], target, [obj1, ...]):对象的合并扩展,默认是浅拷贝,常常用在函数参数的扩展上,这一点倒是根默认参数的设计很契合,可以扩展已有的参数 eg

    1
    2
    3
    4
    5
    6
    // 通过extend扩展已有参数
    function(obj) {
    var default = {a: 'a'};
    var opt = $.extend(default, obj);
    console.log(opt);
    }

    deep表示是否递归拷贝
    Demo 测试

jQuery非典型

当从window对象开始查找时,如果使用jQuery函数会出现问题,eg:

1
2
3
$(window).on('scroll', function (e) {
var img = $(this).find('img) // find函数只会返回[]
})

visibility: hiddendisplay: none

最重要的区别在于: 后者脱离文档流,而前者不是,所有的效果差异都是这一点造成的,如前者会在隐藏的部分保持空白,而后者不会,再如在hidden以后,前者可检测宽高offSet等位置信息,而后者没有,在作页面自动加载时遇到了这个问题,在此记录

Hive初探

Hive 初探

在hadoop生态,最重要的无疑是HDFS 和 MapReduce/Spark等运算框架,但依托于HDFS的数据库存储层工具往往是平时用到的最多的,当前2大依托于HDFS的结构化数据存储有:HBaseHive

这篇文章就来说说Hive的方方面面。

虽然Hive是hadoop生态中的一个数据结构化管理层,但要注意的是,不同于mySQL等数据库,也根Hbase不同,hive本身被设计的目的就不在于其实时查询,因此它的查询是一个耗时的过程,但他可以支持类SQL语言,可以让job不再要求我们一行一行的写代码,会根据语句自动生成MapReduce代码,从而简化操作。总之,Hive提供给我们HQL,让我们方便的MapReduce,最终用以对hdfs上数据的提取/转化/加载(ETL),对映射到数据库表中的数据进行方便的操作,其提供了3种用户接口:

  • CLI: 根mySQL的命令基本相同
  • WebUI: 默认端口9999
  • Client:通过JDBC等来远程访问

构架

借用hive - facebook爸爸的构架图:
来自facebook爸爸的hive构架图

安装及错误处理

作为Hadoop组建,安装起来很简单,关键在于配置,通常都使用mySQL存储metaStore,所以在设置中:

在安装时注意版本对应,确保各个节点机器的ssh通信正常,会省去不少麻烦
启动时需注意保证集群启动正常,一个常见的错误:
留待图片
就是因为namenode为启动成功,在hadoop生态,几乎所有的组件都是依赖与hdfs,yarn的,然后配合zookeeper热备,一旦出现启动不了的问题,首先就要去检查依赖的运行环境是否启动正常

基本操作相关

1 表的创建相关

  • 创建表基本上根普通的SQL语句一致,只是我们常常需要指定分割符号,指定排序,存储类型等等,常见的通用模型如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name [LIKE] patternTable
    [(col_name data_type [COMMENT col_comment], ...)]
    [COMMENT table_comment]
    [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)] -- 指定分区方式
    [CLUSTERED BY (col_name, col_name, ...)
    [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]
    [ROW FORMAT row_format]
    [STORED AS file_format] -- 存储类型
    [LOCATION hdfs_path]

关键字ROW FORMAT DELIMITED:

1
2
3
4
FIELDS TERMINATED BY ‘,’           //指定每行中字段分隔符为逗号
LINES TERMINATED BY ‘\n’ //指定行分隔符
COLLECTION ITEMS TERMINATED BY ‘,’ //指定集合中元素之间的分隔符
MAP KEYS TERMINATED BY ‘:’ //指定数据中Map类型的Key与Value之间的分隔符

关键字PARTITIONED BY: 就是对应于mapreduce中的分区

关键字CLUSTERED BY: 称之为分桶

关键字STORED AS:指定表在HDFS上的文件存储格式,可选的文件存储格式有:

1
2
3
4
5
TEXTFILE 		//文本,默认值
SEQUENCEFILE // 二进制序列文件
RCFILE //列式存储格式文件 Hive0.6以后开始支持
ORC //列式存储格式文件,比RCFILE有更高的压缩比和读写效率,Hive0.11以后开始支持
PARQUET //列出存储格式文件,Hive0.13以后开始支持

一个具体的🌰,eg:

1
2
3
4
5
CREATE TABLE IF NOT EXISTS tableDemo 
(sno INT,sname STRING,age INT,sex STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE;

上述创建了一个行分割符号为tab的表tableDemo,使用show tables;可查看

  • 删除– DROP
  • 修改表结构用ALTER TABLE tableName [ADD COLUMNS][RENAME TO xxx]等

2 数据操作

  • 加载数据

    1
    2
    LOAD DATA LOCAL INPATH '$local_Dir/xxxx.txt' INTO TABLE tableName; -- 加载本地数据
    LOAD DATA INPATH '$hdfs_dataDir/xxxx.txt' INTO TABLE tableName; -- 加载hdfs数据

    说明:这里只是总结操作,具体概念后文详述

    • 加载数据到分区,eg:

      1
      2
      3
      4
      LOAD DATA LOCAL INPATH
      '$loacl_dir/data.txt`
      overwrite INTO TABLE tableName
      PARTITION (dt='part001')
    • 加载数据到桶表:
      首先要开启桶:set hive.enforce.bucketing = true,eg:

      1
      2
      3
      INSERT OVERWRITE TABLE tb_bucket_shop 
      SELECT shop_id, shop_name, shopkeeper FROM tb_part_shop
      CLUSTER BY shop_id;
  • 插入操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -- 单表插入
    INSERT OVERRITE TABLE copied_table
    SELECT * FROM meta_table
    -- 多表插入
    FROM meta_table
    INSERT OVERRITE TABLE copied_table1
    SELECT *
    INSERT OVERRITE TABLE copied_table2
    SELECT *
  • 查询操作
    通用格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SELECT [DISTINCT] col_name 
    LIMIT num
    FROM table1 [table2 ...]
    [ORDER BY col_name]
    [GROUP BY col_name]
    [SORT BY col_name]
    [DISTRIBUTE BY func(col_name)]
    [CLUSTER BY col_name]
    [WHERE col_name OPERATOR [AND OPERATOR...] ]
    • GROUP BY: 分组
    • ORDER BY: 全局排序
      Hive优化常常要用到,不同于数据库的order,使 时需要指定hive.mapred.mode:

      1
      2
      hive.mapred.mode = nonstrict; // 默认
      hive.mapred.mode = strict; // 必须指 limit
    • SORT BY: 分区内排序并可以指定task数
      如task=1,那么效果等同于ORDER BY, 指定Reduc 个数:set mapred.reduce.tasks=num

    • DISTRIBUTE BY和CLUSTER BY
      distribute by:按照指定的字段或表达式对数据进行划分,输出到对应的Reduce或者文件中。
      cluster by:除了兼具distribute by的功能,还兼具sort by的排序功能。

hadoop集群搭建tip.md

hadoop + yarn + zookeeper: 5个节点搭建Demo

1 规划

  • 统一useradd:hadoop + passwd,目录~/app + ~/data + ~/tools
编号 nameNode dataNode journalnode resoucrcemanager datamanager zookeeper
node1
node2
node3
node4
node5
  • hadoop2.6.9 stable + jdk1.7 + zookeeper 3.4
  • ntpdate时钟同步 + ssh + 脚本

2 安装

  • jdk + 环境变量
  • Zookeeper+ 环境变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    zoo.cfg:
    clientPort = 2181;
    service.1=node1:2888:3888
    service.2=node1:2888:3888
    service.3=node1:2888:3888
    service.4=node1:2888:3888
    service.5=node1:2888:3888
    dataDir=/home/hadoop/data/zookeeper/zkdata
    dataLogDir=/home/hadoop/data/zookeeper/zkdatalog
    ----
    zkdata下:
    myid >> 节点编号
    启动测试:
    脚本 + ~/app/zookeeper/bin/zkServir.sh start/status + jps
  • hadoop + 环境变量

    hadoop-env.sh + core-site.xml + hdfs-site.xml
    

    首次顺序: zookeeper >> journalnode >> namebode format >> zkfc format >> 启动namenode + 备用namenode启动并同步 >> journalnode stop
    无误可使用sbin/start-dfs.sh启动

  • YARN配置: mapred-site.xml + yarn-site.xml(resourcemanager配给node1,node2,启动热备,mapReduce-shufflehandler)

3 测试

  • namenode热备测试:查看host:8088 >> active/standby >> 关闭active >> 查看standy自动active
  • recourcemanager测试:查看状态 rmadmin -getServiceStae node1/2 >> active/standby >> 关闭active >> standy自动激活
  • 官方Demo测试

跨域操作

跨域操作

浏览器出于安全方面的考虑,对于一个页面内的所有的请求做了一系列的限制,称之为:同源策略,但有时候我们的确有这方面的需求,所以可以通过一下说明一下:

  • 同源Same origin Policy: 即是指3相同–相同协议 + 相同origin(可以理解为协议后的第一个/之间的内容) + 相同端口
  • 跨域访问/不同源: 就是与不同域间接口通信,从浏览器的角度基本有4种常见形式JSONP,CROS,(满足一定条件下的)降域方式,通过window的message事件

首先演示一下正常请求操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>跨域操作</title>
<style>
.container{
width: 900px;
margin: 0 auto;
background: slategray;
}
</style>
</head>

<body>
<div class="container">
<h1>aaa域中的html</h1>
<hr> 点击去另一域bbb中加载数据
<br>
<button>点击</button>
</div>

<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var btn = $('button')[0]
var container = $('.container')[0]
btn.addEventListener('click', function() {
xhr = new XMLHttpRequest()
xhr.open('get', 'http://aaa.com:3000/info', true)
xhr.send()
var html = ""
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
result = JSON.parse(xhr.responseText)
html = document.createElement('p')
html.innerText = result['info']
container.appendChild(html)
}else {
console.log('error')
}
}
})
</script>
</body>
</html>

服务器端使用node的mock-server,在router.js中回应:

1
2
3
4
app.get('/info', function(req, res) {
var data = {info: 'Greating from router.js'}
res.send(data)
});

得到:
同源策略下请求

现在来进行简单的跨域请求,如xhr.open('get', 'http://aaa.com:3000/info', true)中的url改为http://bbb.com:3000/info',浏览器会报错:
❌错误

  • CORS
    刚才从提示中已经隐含了提示:No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://aaa.com:3000' is therefore not allowed access.如果我们在服务器端的为请求头添加权限,就能正常访问了如:
    1
    2
    3
    4
    5
    app.get('/info', function(req, res) {
    var data = {info: 'Greating from router.js'}
    res.header("Access-Control-Allow-Origin", "*");
    res.send(data)
    });

所以可以猜测实际上request是到达了,但没有响应权限,响应内容为空,有些浏览器甚至状态码都显示200,这一点挑食时注意,
由于这种处理主要是服务器端处理的资源分享方式,所以称为CORS [Cross-Origan Resource Sharing] 跨域资源共享

这样虽然解决了问题,但都是从服务器操作,下面重点介绍从浏览器端的操作

  • JSONP
    本质上就是一个script标签调用的远程脚本,利用script标签的src属性,拿到不同域的远程脚本内容实现跨域通信,其内容格式为callBack({data}),总之基本上就是js调用函数的格式,script会执行找到本执行本地方法callBack,将上诉代码更改一下:
    1
    2
    3
    4
    5
    6
    7
    <scrip>
    var callBack = function(data) {
    console.log("^^^^ . ^^^^^" + data)
    alert('本地写好了的callBack,被远程js调用,并传递数据: ' + data.info)
    }
    </script>
    <script src="http://bbb.com:3000/info.js"></script

刷新页面就能看到callBack被调用了,这就是JSONP的核心内容
但是如果仅仅是这样,就要求我们提起在本地写好callBack并通知给服务器,协商的成本太高,所以将callBack函数名带在url中,url做成?callback=funcName,后台获取到funcName,在组织JSONP文件传入数据即可
注意:虽然总是根ajax一起出现,但本质上是2中东西,前者是异步网络技术,而后者就是个普通script脚本

  • postMessage
    适用于共用一个window下不同frame的通信,即不同html间可跨域的通信,eg,这是在bbb.com域下的iframe.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
    body {
    background: palevioletred;
    }
    </style>
    </head>
    <body>
    <h3>iframe 中的html -- 来自不同域bbb.com中的html</h3>
    <div class="page">
    <form action="">
    输入:<br>
    <input class="input" type="text">
    </form>
    </div>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script>
    var input = $('.page .input')[0]
    input.addEventListener('input', function(e) {
    console.log(this.value)
    window.parent.postMessage(this.value, '*')
    })

    window.addEventListener('message', function(e) {
    input.value = e.data
    console.log(e.data)
    })
    </script>
    </body>
    </html

然后增加新代码后的index.html为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>server-mock使用说明</title>
<style>
.container{
width: 900px;
margin: 0 auto;
background: slategray;
}
.page {
background: lightseagreen;
text-align: center;
}
iframe {
width: 300px;
}
</style>
</head>
<body>
<div class="container">
<h1>aaa域中的html</h1>
<hr> 点击去另一域bbb中加载数据
<br>
<button>点击</button>
</div>
<hr>
<div class="page">
<form action="">
输入:<br>
<input class="input" type="text">
</form>
<iframe src="http://bbb.com:3000/iframe.html" frameborder="0"></iframe>
</div>

<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var btn = $('button')[0]
var container = $('.container')[0]
btn.addEventListener('click', function() {
xhr = new XMLHttpRequest()
xhr.open('get', 'http://bbb.com:3000/info', true)
xhr.send()
var html = ""
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
result = JSON.parse(xhr.responseText)
html = document.createElement('p')
html.innerText = result['info']
container.appendChild(html)
}else {
console.log('error')
}
}
})

var callBack = function(data) {
console.log("^^^^ . ^^^^^" + data)
alert('本地写好了的callBack,被远程js调用,并传递数据: ' + data.info)
}
</script>
<script src="http://bbb.com:3000/info.js"></script>
<script>
var iframe = $('.page iframe')[0]
var input = $('.page .input')[0]

input.addEventListener('input', function(e) {
console.log(this.value)
window.frames[0].postMessage(this.value, '*')
})

window.addEventListener('message', function(e) {
input.value = e.data
console.log(e.data)
})
</script>
</body>
</html>

验证结果后最终实现2不同域不同html文件之间的通信

  • 一定条件下的降域
    在主域相同情况下,通过设置document.domain为共同的自身或者更高一级的赋予,就可以通过
    window.fram[0].document.querySelecotr('.page .inpu')访问到iframe.html中的元素了,严格说来并不是一种跨域
    ,比如完全demain不用的2个域是不允许这样操作的,所以在真正需要跨域的时候并不常用

MapReduce设计思想

so上推荐了一篇通俗的介绍了MapReduce设计思想的文章,简单的做了翻译:
先从一个简单的需求开始,当我们在js中想输出这样的内容时,最直接的方法是使用alert

1
2
alert('apple!');
alert('bannaner!');

但这绝不是一个好的方式,上述代码琐碎绝并且繁琐,那么我们将它封装成一个函数:

1
2
3
4
5
6
function fruit(str) {
alert(str + '!');
}

fruit('apple'); // apple!
fruit('plum'); // plum!

通过将拥有相似功能的代码封装为函数,可以让代码简洁明了
那么如果是下面的情况呢:

1
2
3
4
5
6
7
alert('Tim like:')
fruit('apple')
fruit('plum')

alert('Jerry like:')
fruit('lemmon')
fruit('orange')

比刚才稍微复杂一点儿,但也很容易就能被包装为:

1
2
3
4
5
6
7
8
9
10
11
function someOneDo(someone, f1, f2, f) {
alert(someOne + 'like:')
f(f1)
f(f2)
}

someOneDo('Tim','apple', 'lemmon', fruit);
// 使用匿名函数:
someOneDo('Jerry','lemmon','orange', function(str) {
alert(str + '!');
});

把fruit函数作为一个参数,在someOneDo中调用,这是js中典型的回调。
接下来仿照上述来处理一个根数组有关的问题:

1
2
3
4
5
6
7
var a = [1,2,3]
for(i=0; i<a.length; i++) {
a[i] = a[i] * 2
}
for(i=0; i<a.length; i++) {
a[i] = alert(a[i])
}

分析一下,根上一个例子完全一样,代码类似,都有for循环,都再对元素进行操作,不妨这样:

1
2
3
4
5
6
map(f,arr) {
f(arr)
}

map(function(x){ return x*2}, a);
map(alert, a);

编程中中会有遇到这样的状况,就拿数组来说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sum(a)
{
var s = 0;
for (i = 0; i < a.length; i++)
s += a[i];

return s;
}

function join(a)
{
var s = "";
for (i = 0; i < a.length; i++)
s += a[i];

return s;
}

sum元素求和join拼接元素代码是如此的类似,忍不住在抽象一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function reduce(fn, a, init) {
var s = init;
for (i = 0; i < a.length; i++) {
s = fn(s, a[i])
}
return s
}

function sum(a) {
return reduce(function(a, b) { return a+b }, a, 0)
}

function jion(a) {
return reduce(function(a, b) { return a+b}, a, "")
}

这样就把里面的操作封装到了reduce上,现在不管是求和还是拼接都可以通过一个函数来完成了
很多的老旧的语言没办法像这里的js一样把函数置为first class object,比如C/C++中虽然有函数指针,但也做不到这样的简便,很多面向对象的语言干脆不提倡对函数做过多的操作,比如要在Java中这样将函数置为first class object,需要你从头创建一个functor类作为函数基础对象,特别是在数据处理领域内,现代一点的语言都不会自大到–死抱着面向对象可以解决一切问题的谬论。
上述内容多数人看到这里就觉得其实你也没说什么,不过是将函数在抽象了而已,那我们就回到map函数刚刚对数组的操作,看看我们到底能有哪些获益吧!

通过上述map发现:当你对数组元素有任何的操作时,无论数组怎样便利都不会影响结果,你可以从0..last,也可以从last…0,更可以把数组一分为二,一个线程从0…half,另一个线程从half..last,对看到了吗?map对数组的操作整整快了一倍。
现在假设你有成百个上千个分布于世界各地的数据中心,每台机器都运行着微小的map操作,处理一小个小小的数据片段的操作,组合起来的处理能力将使TP,BP级别的,这就为处理大数据问题提供了思路

By abstracting away the very concept of looping, you can implement looping any way you want, including implementing it in a way that scales nicely with extra hardware.

这就是MapReduce处理大数据的核心思想:通过抽象出循环的概念,可以以任何你想要的方式实现循环,同时支持任何程度上的随意扩展额外的硬件添加节点。

参考:
https://www.joelonsoftware.com/2006/08/01/can-your-programming-language-do-this

swift-tip004 闭包closure面面观

闭包: 自包含的函数代码块,很像OC中的Block,可以被传递,作用往往是捕获与存储上下文中的变量或引用,实现对不在同一作用域内的数据的访问,从这个意义上来讲,只要能去访问到不是自己作用域内的函数,都可以算是闭包,比如:

  • 全局函数是一个有名字但不会捕获任何值得闭包,因为位于最外层,在一些语言中当成特殊的闭包;
  • 嵌套函数是一个有名字并可以捕获其外层作用域内值的闭包
  • 闭包表达式,是可以捕获上下文变量的匿名闭包

Coredata and protocol oriented programming in Swift

首先,先给出一个稍有难度的例子,关于Core data + protocol的具体使用,如果看不下去,就不用指望能学会了

  • 使用protocol简化和规范NSManagedObject的操作

    1
    2


  • 使用protocol 分离NSFetchedResultsController 数据操作

1
2


struct? or class? 关于swift中Model的Tip

跟别人吹牛,论截止到目前为止,在实际项目中使用swfitObjective-C最大的不同,如果没有提到Model选型的问题,个人愚见:这个人要么就是沿用OC的方式在用swift,要么就是totality谎言,总之都不能算是真正的swifter

我把这个问题称之为swift中的最 WTF 问题

如一个常见的个人资料[Profile]部分的实现:在页面加载之前,开始loadData,完成以后接着装填proflieModel,然后展示model,

swift-tip之开发中的封装讨论

swift 开发经验之常见封装讨论

公司的项目终于全面swift化了,转到swift之后最大的感受在于写代码的自由性,不管是struct和enum都被大大拓展,个人感受来说确实码砖时开心了很多,代码也简洁了很多,即便是自己初级的水平,但还是想斗胆来做一下常用功能需求在swift中的实现及封装的讨论,比如对网络部分的封装,权限请求的封装等等,鉴于水平有限,诸多不足,贻笑大方,且作消遣排挤之用!

网络部分

作为正常的地球人+小公司独立开发属性,使用Matt大神的Alamofire我想是的选择,当然如果您司有自己的私有库,那就不存在这个问题的讨论了,所以也很自然的选择了它。

作为最著名的第三发框架和诸多开发者信誉加成,Alamofire本身已足够优秀,但在使用中,往往我们都会按照自己的习惯做一下封装,这也是这一部分的主题,笔者会介绍一下自己的习惯:

总体原则:

  1. 去耦: 不管怎样优秀,但Alamofire终归是第三方框架,前司升级AFNetwork 3.0的症痛还历历在目,如果刨除当时自己半路接手+经验不足的安慰理由外,对这种高耦合式的使用第三方框架的教训不够也是主要原因,吃过亏自然就知道了,所以原则上,在使用任何第三方框架时,尽量自己做一曾封装,即便是意义不大技术含量低级的封装,对第三方 API 的调用尽量集中在封装内,这也就是为何我一直推崇繁复的 CoreData 而避免所谓便利的 Rleam 等的原因。

  2. 简化: 跟后台紧密配合,单独剥离不同url请求,做到每一个请求就是对应一个方法,这样最大的好处就是请求清晰简明,别人在接手也容易

  3. 完备: 集中处理诸如权限,错误,提示的处理,集中设置请求头的属性,方法,输出请求调试内容,做到出了问题迅速定位,用户体验不割裂

以上都是常见的调调,不看也罢,主要是充分利用swift的特点,完成封装的具体过程

-------------------
🙈🙈🙈🙈🙈🙈🙈🙈 假装看不见的分割线 🙈🙈🙈🙈🙈🙈🙈🙈🙈
-------------------

避免直接使用String作为参数,借助swift的enumstruct避免 面向字符串编程

如使用枚举集中整理url和method,配合CustomStringConvertible输出descriptionMethod已在Alamofire中实现,like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Method: String, CustomStringConvertible {
case OPTIONS = "OPTIONS"
case GET = "GET"
case HEAD = "HEAD"
case POST = "POST"
case PUT = "PUT"
case PATCH = "PATCH"
case DELETE = "DELETE"
case TRACE = "TRACE"
case CONNECT = "CONNECT"

public var description: String {
return self.rawValue
}
}

加一些 “Protocal-Oriented”

从不少开源代码中看到不少这样的使用,在WWDC上也有相关内容,随着自己对swift使用经验的积累,越来越体会到“组合”比之余“继承”的方便之处

从一个常见的使用场景,performSegueWuthIdentifer(identifer: String, sender: AnyObject?)方法,根据不用的identifer执行不同的跳转,原来只能借助于switch语句,由于都是string,很容易写错,那么在swift中呢?

也许可以用enum把identifer装饰一下,避免频繁直接使用字符串,like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	enum SegueIdentifer: String {
case aaa
case bbb
}

// ...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let idf = segue.identifer, segueIdf = SegueIdentifer(raValue: idf) else {
fataError("wrong idf:\(segue.identifer)")
}

switch segueIdf {
case .aaa:
// do some thing
case .bbb
// do some thing
}
}

但是跟原先的写法没什么不通,未见的如何swift,如果在别处需要跳转这些代码仍然要重复,怎么办呢?
不如这样:

1
2
3
protocol SegueHeadlerType {
typealias SegueIdentifer: RawRepresentable
}

swift-tips003

枚举的使用tip

枚举在swift中被大大加强了,在实际使用中的一最大的感受就是真TM的灵活,基本类型基本都可以做枚举,基本都能被组合,笔者常用的一个例子:

进可当value,退可居enum

1
2
3
4
5
6
7
8
9
10
11
// 进可当value,退可居enum
enum TransStyle: String {
case car = "car"
case bicycle = "bicycle"
}

print(TransStyle.car.dynamicType) // TransStyle
print(String(TransStyle.car).dynamicType)// String

// 想回到过去...
TransStyle(rawVale: "car") // 又变成了enum

swift的协议

可能都听说过swift是面向协议的语言,从其最基本的库中就能看出,得益于开源让我们有机会能一窥一门语言是如何一步一步通过各种各样的协议组合强大起来的

这一部分内容会很多,遇到了就不断的记录在这里:

  • Hashable

此协议给你一个int型的hashValue,可以作为字典的key同时可以被比较
比如游戏中常见的单位的位置address,我们自定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Address: Hashable {
var point: CGPoint
init(point: CGPoint) {
self.point = point
}

var hashValue: Int {
get {
return "\(self.point.x),\(self.point.y)".hashValue
}
}
}

func ==(lhs: Bit, rhs: Bit) -> Bool {
return lhs.hashValue == rhs.hashValue
}

注意: hashable协议自身实现了Equatable 协议,所以还要实现其唯一的func ==(lhs: Self, rhs: Self) -> Bool方法
这样就能实现Address是否相等的比较了
Address是否相等比较

当然在此基础上,继续实现Camparable协议,还能实现<=, < …等具体的比较

具体参考

Swift Comparison Protocols

协议还能在跳脱一点儿吗?! -- 关联类型

swift 中的协议已经足够跳脱了,毕竟能随意组合,还能通过扩展extension默认带上自带实现的主,到底还能不能好好做协议了,但protocol还真的能在跳脱一些:和泛型集合,不过不叫泛型而是一个专有的名称-- 关联类型,目的是跟泛型一样提供一个占位,在使用时在确认具体类型,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protocol anyType <Element> {
public mutating func something() -> Self.Element?
}
```
遗憾的是,swift中并不支持在协议中使用这种`参数化类型代指泛型`,而是应该是使用`关联类型`,like this:
```swift
protocol anyType {
associatedType Element // 2.2版本之前用typealias
public mutating func something() -> Self.Element?
}
```

至于为什么,通过这个[proposale](https://github.com/apple/swift-evolution/blob/master/proposals/0011-replace-typealias-associated.md)能猜到一二

毕竟协议跟class, enum, struct中对泛型的要求不同,比如Array<String> ...

参考:http://www.jianshu.com/p/ef4a9b56f951




### swift中的mutable 字典

对于swift中的mutable字典,如果将其转化为`NSDictionary` 和`NSMutableDictionary`那就当我没说
如果使用元组的话,会遇到一个问题,比如:
```swift
var mutableDict = ["key1": value1, "key2": value2]

现在想添加一对数据,有可能会敲出updateValue(value: forKey: )这个方法,但是这个方法只能更新key已存在的值,如果是全新的key的话就会返回nil

所以,简单的办法是使用最直接的方式,like this :

1
mutableDict["key007"] = value007

觉得又点儿怪而已,专门记下来

Custom Presentation controller

我们都知道在iOS7之前,presentation controller是只能按系统提供的最直接的方式Modal,直到iOS7中开放了这部分的API,经过iOS8的进一步改进已经变得很好用了

An anomation controller(UIViewControllerAnimatedTransitioning) is responsible for the animation and the display of the view controller’s content, and the presentation controller(UIViewControllerTransitioningDelegate
) is responsible for the appearance of everything else.

CoreData-五

终于腾出时间了,把CoreData的其他部分补充上

一些回顾:目前为止,已经可以增删查改,也会使用NSFetchedResultsController这个方便的官方为UITableView专门服务的类了,下面进入一个使用中异常重要的话题,即CoreData数据不同app版本的变更和迁移

当我们新的版本发布时,必然会有数据的变动,对于原来没有的要重新添上,而原来已经存在的要迁移到新的文件中,这是个很危险的过程,如果发生了错误或是不兼容的情况,都将是致命的,所以了解和掌握数据的迁移是不可或缺的

CoraData的数据迁移过程--The Migration Progress

swift - tip002(later v2.2)

swift tips

可以预见今年的WWDC我们会迎来Swift3.0,刚刚发布的2.2版本,已经在逐渐提示有些应该会改动的地方了,根据自己写Demo的过程逐渐记录了一些:

  • for 循环的改动
    不在完全C一样的风格了,这一点也可以看到swift的现代之处了,摒弃沉重的历史包袱,所以大胆的拥抱吧,eg:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 数据遍历 - 推荐使用enumerate()
    for (index, item) in items.enumerate() {
    ...
    }

    // 更加灵活的指定间隔,使用stride
    for index int 2.stride(to: items.count, by: 2) {
    print(index) // 2, 4, 6 ...
    }

    // 降序 - reverse()
    for index in (0...100).reverse() {
    print(index) // 100, 99, 98, 97 ... 0
    }

但是,像复杂一些的多个组合逻辑的for,比如for(int i=0; i < sum && i / 2 == 0; ++i)这样,如何合适的更改,现在还没找到合适的方法,目前我用的是while + if

  • OC中的__FUNCTION__ 变为 #function, 不过这一累=类的变动Xcode都会提示你fix it
  • ++ and – 终于 deprecated 了: 话说这样的语意不明的语法早TM该废了,果然没有包袱就是好,swift3中要使用:

    1
    i += 1
  • 老古董Selector变动
    从”functionName” >>> #selector(XXX.func)

    1
    selectBtn.addTarget(self, action: #selector(ViewController.selectPhoto), forControlEvents: .TouchUpInside)
  • guard语法

    其实这个语法早在2.0版本就有了,但一直没有仔细使用,粗浅的涉猎并没有说服自己,直到3.0还保留看来是时候接受了,即便不使用的话完全也没什么影响,swift提供这种微妙的语法表达,很契合其简单明确的语言风格同时也保证了足够的健壮性

    是否真的是这样呢?如需要将string转化为UInt8UInt8本身已经实现了一个可接受string的初始化方法并且可以抛出错误,但并还需要足够的提示来处理上下文我们不能预知的问题,比如格式错误,或者超出了数值边界,所以实现一个新的Init(fromString string: String)可以抛出一个能够提供更具体错误信息的ConversionError

    Tip: guard并不意味着要替换所有的if..else 和 if let,切忌滥用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个字符串转化为UInt8的初始化方法,并可抛出三种Error
enum ConversionError: ErrorType {
case InvalidFormat, OutOfBounds, Unknown
}

extension UInt8 {
init(fromString string: String) throws {
if let _ = string.rangeOfString("^\\d+$", options: [.RegularExpressionSearch]) {

if string.compare("\(UInt8.max)", options: [.NumericSearch]) != NSComparisonResult.OrderedAscending {
throw ConversionError.OutOfBounds
}
if let value = UInt8(string) {
self.init(value)
} else {
throw ConversionError.Unknown
}
}
throw ConversionError.InvalidFormat
}

上边是传统的写法,我们明明只是补充一下3种可抛出错误,但这段代码很不容易看出来,至少一眼是看不出来的,如果用上了guard,效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension Unit8 {

init (fromString string: String) throws {

guard let _ = string.rangeOfString("^\\d+$", options: [.RegularExpressionSearch])
else { throw ConversionError.InvalidFormat }

guard string.compare("\(UInt8.max)", options: [.NumericSearch]) != NSComparisonResult.OrderedAscending
else { throw ConversionError.OutOfBounds }

guard let value = UInt(string)
else { throw ConversionError.Unknown }

self.init(value)
}
}

同样是想表达判断throw指定异常,使用guard语句比传统的明确很多,甚至到了一眼看出的程度

这里也能看到,原则是判断在前,最后执行self.init(value)

自swift变量的设计Optional这一特性之初,从语言层面上虽说是现代的严谨的,但其实在很多时候增加了我们的代码量,常常时不时的需要嵌套的if let才能保证后面对变量的强制解包的有效性,特别是在遇到复杂的Optional chain情况下。

在刚刚学习的很长一段时间内都是靠着编译器的提示来过日子,还常常出现各种crush,这一点也算得上是入门swift 的最大门槛了,但总学if letswift本身简洁的特质不符,好在终于有guard帮我们分担了

guard对我来说的另一个好处是,它不用让我随时操心if逻辑不全的问题了,很多时候我在写的代码时总是自以为是的认为逻辑清晰,只写if而不是保证if .. else成对出现,这在逻辑复杂时或者嵌入式开发中迟早会遇到大的问题,但平白的写很多个else { }总是显得不那么美观,感谢有了guard🙏

比如在一个典型的Alamofire请求之中:

1
2
3
4
5
6
guard response.result.isSuccess
else {
let alert = UIAlertView(title: "网络异常", message: "请检查设备网络设置", delegate: nil, cancelButtonTitle: "确定")
alert.show()
return
}

👆的写法有待商榷,实际上很多人都不建议这样去写,else里面应该很简单,其次格式也不一定要这样,但是目前来看个人还是很认同这种规范的

  • SupplementaryView的使用注意
  1. register要指定kind,自定义UICollectionReusableView
  2. dataSource代理中装填数据:
    使用switch区分装填

    1
    func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView
  3. 最重要的layout中要设置headerReferenceSize,否则代理方法not be called

  4. 最大的一个坑,再给supplyView添加subView时,使用bounds而不能是frame,否则就会出现所谓drop的现象

    1
    2
    3
    4
    5
    override init(frame: CGRect) {
    super.init(frame: frame)
    label.frame = bounds // 而不是frame
    addSubview(label)
    }
  • 接上舒,还有几个不常用的方法,是作为cell滑动以及加动画场合的使用
    1
    2
    3
    4
    5
    6
    7
    public func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath)

    public func collectionView(collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, atIndexPath indexPath: NSIndexPath)

    public func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath)

    public func collectionView(collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, atIndexPath indexPath: NSIndexPath)

想不起来就去文档中看看,这里记下来一次

collectionView - Pinterest布局实现

UICollectionView过于强大以至于到现在都没有对它深入了解,其良好的custemed特性,帮助我们可以完成各种各样的炫酷效果,比较典型的如:Pinterest的流水布局,老版paper的翻页折叠,以及系统任务切换时的滑页式效果等

当然所有的custom都是需要借助UICollectionViewLayout,在实际使用中,除非完全自定的效果之外,绝大多是时候我们使用以定义一部分的UICollectionViewFlowLayout即可,那就循序渐进,从它入手。

当使用collectionView时,初始化的时候常常个人来说会有一些‘烦恼’,特别是在swift中把collectionView当中单独的属性初始化的时候,因为需要layout,而很多时候layout是需要单独提出来设置的,所以总会带来一些烦恼,代码的区分并不明确,后来才发现的确是自己的问题,因为UICollectionView本身的属性是有可set和get的collectionViewLayout的饮用的,所以基本上是自己的问题,这里顺便记录

重点方法:

1
2
3
4
5
6
7
8
// UICollectionView calls these four methods to determine the layout information.
// Implement -layoutAttributesForElementsInRect: to return layout attributes for for supplementary or decoration views, or to perform layout in an as-needed-on-screen fashion.
// Additionally, all layout subclasses should implement -layoutAttributesForItemAtIndexPath: to return layout attributes instances on demand for specific index paths.
// If the layout supports any supplementary or decoration view types, it should also implement the respective atIndexPath: methods for those types.
public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? // return an array layout attributes instances for all the views in the given rect
public func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?
public func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?
public func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?

注释部分已经说了:上边的4个方法决定了最终的layout信息

swift - tip001

swift的tip


  • lazy的使用

lazy 几经更改,最终固定在最简单的形式,加上关键字lazy就行了,还要注意只有变量var才能拥有懒惰特性,因为只有let在实例化调用super.init()之前已经初始化好了,而var是在其后才被索引的,当我们的初始化需要复杂和高昂的代价时,在需要时赋值,就能看出lazy的意义了

同时,lazy并非是线程安全的,如果同时被多个线程同时调用,就不能保证只初始化一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//MARK: lazy
lazy var collectionView: UICollectionView = {
var collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: self.flowLayout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.registerClass(YRContentCollectionViewCell.self, forCellWithReuseIdentifier: "collection_cell")
collectionView.registerClass(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "headerView")
return collectionView
}()

lazy var flowLayout:UICollectionViewFlowLayout = {
var collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.itemSize = CGSizeMake(300, 180)
collectionViewLayout.headerReferenceSize = CGSizeMake(0, 20)
return collectionViewLayout
}()
  • switch
    其实在最开始接触编程时,学习到switch语句,虽说理解起来很简单,但是写起来和编码过程总有种“做的不够”的感觉,绝大多数的语言中switch语句都是跟C中的一样,过了这么多年终于在swift中看到了我所希望的样子

    • 首先是不要一个一个的break了
    • 其次,大大增强Matching类型,事实上可以匹配any data,还可以组合匹配,附加匹配
    • 最后,永远牢记switch必须是完备
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
            switch someValueToConsider {
      case value1:
      statements
      case value2,value3:
      statements
      case value4..<valu5:
      statements
      case (x, y) where x > y:
      statements
      default:
      statements
      }
  • Access Control访问控制权限
    2种级别:

    * 模块区分,如不同的`Framework`之间,而`App bundle`也是一个模块
    
    • 源文件级别,就是各个class的属性和方法级别

      3种访问级别:Public,InternalPrivate

      • Public: 跨模块级别,如Framework中开放的方法和属性
      • 默认访问权限是Internal - 即为同模块,例如iOS开发通常就一个boundle
      • Private: 本源文件级别
1
2
3
4
5
6
7
8
// 修饰class - 模块级别修饰
public class SomePublicClass {}
internal class SomeInternalClass {}
private class SomePrivateClass {}”

// 修饰方法 - 源文件级别
var someInternalConstant = 0 // 隐式访问级别 internal
private func somePrivateFunction() {}
  • image 和 color 是可以相互转换的
    通常在设置background 时:

    1
    2
    3
    if let patternImage = UIImage(named: "pattern-grey") {
    view.backgroundColor = UIColor(patternImage: patternImage)
    }
  • swift中网络框架Alamofir的使用
    Alamofir的基本使用参照gitHub,已经说的很详细了,这里借助几个开源项目,探求一种最合适的使用方式,先是简单的罗列,最后在分析

    • 一种较为典型的使用,url处理是mattt大神给出的,本身对框架并没有任何变动,只是将API请求和URl做了规范统一,将所有相关统一在一起,并使用枚举增强可读性,eg:

      典型使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      class ServiceApi: NSObject {
      // API 部分,统一方法一个class中,使用累方法返回完整url
      static var host:String = "http://www.swiftmi.com"

      internal class func getTopicUrl(maxId:Int,count:Int) -> String {

      return "\(host)/api/topic/list2/\(maxId)/\(count)"
      }
      }

      // 所谓的Router枚举, 遵循URLRequestConvertible 协议
      enum Router: URLRequestConvertible {

      }
      ```
      其中的`URLRequestConvertible`协议也是框架中自带的,可见是一种推荐的做法
      ```swift
      // MARK: - URLRequestConvertible

      /**
      Types adopting the `URLRequestConvertible` protocol can be used to construct URL requests.
      */
      public protocol URLRequestConvertible {
      /// The URL request.
      var URLRequest: NSMutableURLRequest { get }
      }
  • 枚举

    swift中的枚举早已不是C中那个边缘的小tip而已,而是first class类型,强大到令人发质,首先是枚举类型的扩充,从简单的基本类型到元组Tuple都是可以的,可以拥有计算型属性,可以有方法,可以嵌套子枚举,可以嵌套到class和struct中,总之枚举的概念已经被扩展到:

    枚举,用以声明可能状态的有限集,且可以具有附加值。可以通过内嵌(nesting),方法(method),关联值(associated values)和模式匹配(pattern matching),甚至可以遵循协议(protocal),从而让枚举可以分层次地定义任何有组织的数据。

    下面一次性使用Playground的展示其使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    // 1. basic Enum
    enum Movement {
    case Left
    case Right
    case Top
    case Bottom
    }

    let aMovement = Movement.Left

    // - switch
    switch aMovement {
    case .Left: print("left")
    default: ()
    }

    // - if
    if case .Left = aMovement {
    print("left")
    }

    if aMovement == .Left {
    print("left")
    }

    /* 2. Enum Values - 不止是Int型,相比C语言大大的扩展了,对于String和Int你甚至可以使用简些
    枚举中支持以下四种关联值类型:
    整型(Integer)
    浮点数(Float Point)
    字符串(String)
    布尔类型(Boolean)
    */
    enum House: String {
    case Baratheon = "Ours is the Fury"
    case Greyjoy = "We Do Not Sow"
    case Martell = "Unbowed, Unbent, Unbroken"
    case Stark = "Winter is Coming"
    case Tully = "Family, Duty, Honor"
    case Tyrell = "Growing Strong"
    }

    // float double都可以(同时注意枚举中的花式unicode)
    enum Constants: Double {
    case π = 3.14159
    case e = 2.71828
    case φ = 1.61803398874
    case λ = 1.30357
    }

    // swift2.0 的简写
    // 相当于 North = "North", ... West = "West"
    enum CompassPoint: String {
    case Noth, South, East, West
    }


    // 3. 所谓的“读”和“写”
    // - 读: 使用rawValue读取枚举值
    let bestHouse = House.Stark
    print(bestHouse.rawValue)

    // - 写:
    enum Movement002: Int {
    case Left = 0
    case Right = 1
    case Top = 2
    case Bottom = 3
    }

    let rightMovement = Movement002(rawValue: 1) // 创建一个movement.Right 用例,其raw value值为1
    print(rightMovement)

    // 3. Nesting Enums - 枚举中嵌套枚举
    enum Character {
    // 武器
    enum Weapon {
    case Bow
    case Gun
    }
    // 头盔
    enum Helmet {
    case Wooden
    case Iron
    case Diamond
    }

    // 角色
    case Thief
    case Warrior
    case Knight
    }

    let character = Character.Thief
    let weapon = Character.Weapon.Gun
    let helmet = Character.Helmet.Iron

    // 4. Containing Enums - 就是在struct和class中的内嵌枚举
    struct Characters {

    enum CharacterType {
    case Thief
    case Warrior
    case Knight
    }
    // 武器
    enum Weapon {
    case Bow
    case Gun
    }

    let type: CharacterType
    let weapon: Weapon
    }

    let warrior = Characters(type: .Warrior, weapon: .Gun)

    // 5. Associated Value
    // 前边基本都是enum的扩展,在swift中还有对case的扩展
    enum Trade {
    case Buy(stock: String, amount: Int)
    case Sell(stock: String, amount: Int)
    // 可以简写为
    case Steel(String, Int)
    }

    func trade(type: Trade) { }

    // “读”: Pattern Mathching
    let trade = Trade.Buy(stock: "APPLE", amount: 500)
    if case let Trade.Buy(stock, amount) = trade {
    print("buy \(amount) of \(stock)")
    }

    // -- 以下都是大招 --
    // 6. case 可以放元祖
    // 枚举可以有属性property - 不过是计算型属性
    // 可以有方法func
    enum Device {
    case iPad, iPhone
    case AppleWatch

    case Off, On

    // 计算型属性-- Int year
    var year: Int {
    switch self {
    case .iPhone: return 2007
    case .iPad: return 2010
    default: return 0
    }
    }

    // Static Methods - 自动转换规范用语
    static func transSlang(term: String) -> Device? {
    if term == "iWatch" {
    return AppleWatch
    }
    return nil
    }

    // mutating Methods - 可变方法:允许修改case的值
    mutating func screenSwitch() {
    switch self {
    case .On:
    self = Off
    default:
    self = On
    }
    }
    }

    var phoneScreenSwitch = Device.On
    phoneScreenSwitch.screenSwitch()
    phoneScreenSwitch.screenSwitch()
  • swift中JSON解析转Model的实践

第三方框架网络响应返回的,往往是序列化之后的数据,我们需要做的就是根据不同的key进行分割数据,找到合适的数据放到Model 中,但蛋疼的事儿就来了,如何优雅的将数据[往往都是字典]转化为对应的Model呢?
在OC中我们的做法基本上都按照一个流程:通过Model的工厂方法+ modelInitWithDict:(NSDictionay *)dict;,结合KVC实现转化。

Swift中,就不那么方便了,当前的做法是生写,特别的再用到CoreData时,更是只能一个一个的加到模版文件中,而使用ObjectMapper,Argo等第三方框架虽说也行,但在数据处理方面我是一贯持保留意见的,自己想着写一个自己用的脚本,就是根据JSON按格式输出小段重复就行了,这个留待以后再说

  • 属性Property

swift中的Property与OC相比,不单单只是类和实例与value的关联,它还负责枚举和结构体,换言之凡是first class的都是可以有属性的,所以看起来会复杂很多,但同时也做足够细化的分类

* __Stored Properties__: 仅仅存在类与结构体之中                    
* __Computed Properties__ :存在于类,结构体和枚举,功能上不仅仅只是Sotre还要Compute

当属性value发生变化时,可以定义`属性观察者`,来做出反应

C++ 那时候被吓怕了的

  • formal parameter –> destructor

    _copy constructor_

    actual parameter

  • Tempalte

值传递

1
2
3
4
template<class T>
T sum(T a, T b) {
return a + b
}

引用传递

1
2
3
4
template<class T>
T sum(T& a, T& b) {
return a + b
}

常引用 - const reference

1
2
3
4
template<class T>
T sum(const T& a, const T& b) {
return a + b
}

更加通用的版本,每个行参的类型都可以不同

1
2
3
4
template<class Ta, class Tb>
T sum(const Ta& a, const Tb& b) {
return a + b
}

  • value parameter and reference parameter

swift2 + iOS8以后的CoreData变动

try…catch

swift2以后引入了比较完整的try-catch异常的捕获机制,所以原来的error捕获全面变动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原来的
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

ReactiveCocoa初探

从构架说起

随着项目越来越大,再一开始简单MVC下的编程和维护也变得越来越难,直到现在还在赶工加新功能,秉承着1周一功能2周一版本的节奏,根本没有多少时间来维护和重构代码,这样下去终究会导致项目难以维护,特别是我司app中有大量的上一个大版本的代码,虽然老代码的确让新版的速度快了不少,但现在维护起来那个酸爽啊

所以越来越感觉到一个好的,简洁的,低耦合的并经过检验的构架对项目有多重要了,如果上天要再给我一次机会的话,我一定要…

看来看去瞅上了火热的MVVM,但很明显的也个大问题就是一旦M变化了,如何准确反映到UI层面,毕竟中间多了一层ViewModel,而FRP正式未解决这个问题而产生的
所以借着代码,来看看ReactiveCocoa的简单使用

ReactiveCocoa

什么诞生背景,好处弊端评价之类,现在说太空洞了,我对学习新东西一向的态度是“管他娘的,先上手再说”,所以还是按照本人一贯简单粗暴的风格,结合代码和Demo来说明

简单的Demo洗需求:实现一个登陆功能的界面,要求输入符合规定格式和字数的userName和password,这时logIn才可以点击,点击之后验证是否正确,正确就pushVC,错误就提示

这样的功能使用ReactiveCocoa下,我们是这样实现的:

初试RAC

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// signal创建和subscriber
- (void) test001 {
// signal流给订阅者-subscriber
[self.userNameTF.rac_textSignal subscribeNext:^(NSString *text) {
NSLog(@" --userName Input: %@", text);
}];

// signal流过滤,会产生过滤后的signal-filter
[[self.passwordTF.rac_textSignal filter:^BOOL(NSString *text) {
return text.length > 6;
}] subscribeNext:^(NSString *text) {
NSLog(@" --password Input: %@", text);
}];

// 对源signal流的更改,产生对不同值变化的新signal - map - signal的内容为对象
// ---NSString---[map]---> NSNumber(NSUInteger封装成对象)---
[[[self.passwordTF.rac_textSignal map:^id(NSString *text) {
return @(text.length);
}] filter:^BOOL(NSNumber *length) {
return [length integerValue] > 4;
}] subscribeNext:^(NSNumber *length) {
NSLog(@" - 当前PWD长度: %@", length);
}];
}

等下在看这段代码的解释,先看看效果:

看到了神奇的效果,你所输入的马上能够传递并被打印出来,可以完整的记录你输入的过程,有一种所见即所得的即视感
是不是很神奇?其实这就是FRP:Functional Reactive Programming[https://en.wikipedia.org/wiki/Functional_reactive_programming]的意思,函数式及时响应编程范式,今天使用的ReactiveCocoa,以及RxSwift,RxJava等等,都是基于这一思想的不同实现。

说会代码:

  • singnal : 信号,这是RAC下最基本的单元,它可以被组合,过滤,变更和传递
  • subscriber : 订阅者,这是信号的关注者,它承载着信号的最终的行为

现在来看看第一句代码到底干了些什么,userNameTF是一个UITextFiled,它是用来放用户名的,写成这样可能更加容易理解:

1
2
3
4
RACSignal *userNameSignal = self.userNameTF.rac_textSignal;
[userNameSignal subscriberNext:^(id x) {
NSLog(@" --userName Input: %@", x);
}];

首先产生了一个信号,是关于用户名的UITextFiled的text是否发生变化的信号,rac_textSignal是框架为UITextFiled提供的分类中的方法
然后将这个信号userNameSignal传递给了订阅者附带着输入的String,并且当发生signal的next时,就会去执行block中的操作,参数id x就是信号内容String *userNameText,它顺利输出,娥眉怎!

但是如果我们并不想每次都得到输入的内容,而仅仅想得到有效内容,比如我们需要一个至少6位数以上的密码呢?这就涉及到signal的过滤,就是第二段代码,它使用filter将输入的密码的String进行过滤,只有符合要求的大于6位的内容,才会被传递给subsriber触发block中的操作,这就是信号的过滤

在看最后一段,它解决了这样的问题,对于登陆LongIn按钮是否可以被点击来说,我们并不需要知道信号的String,而只是想知道什么时候可以able = YES,所以使用了map将信号转化为输入文本的长度的NSNumber,然后经filter筛选符合大于4位数的密码传给subsribter,真如你所猜想的,我们只能传递对象,信号的内容只能是对象,所以即便是bool也被转化为了NSNumber,这个过程使用’map’就完成了信号的转换

进一步,合并signal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void) test002 {

// map 去的只能说对象,所以bool要包装成NSNumber
RACSignal *validUserNameSG = [self.userNameTF.rac_textSignal map:^id(NSString *text) {
return @([self isValidUserName: text]);
}];
RACSignal *validPasswordSG = [self.passwordTF.rac_textSignal map:^id(NSString *text) {
return @([self isValidxPassword: text]);
}];


// 将validUserNameSG信号输出应用到输入框的backgroundColor上
[[validUserNameSG map:^id(NSNumber *userNameValid) {
return [userNameValid boolValue]? [UIColor clearColor] : [UIColor yellowColor];
}] subscribeNext:^(UIColor *color) {
self.userNameTF.backgroundColor = color;
}];

// 更优雅的写法,利用RAC宏 - 直接把信号对应输出到对象的属性上
RAC(self.passwordTF, backgroundColor) = [validPasswordSG map:^id(NSNumber *passwordValid) {
return [passwordValid boolValue]? [UIColor clearColor] : [UIColor yellowColor];
}];

// signal聚合 - combineLast: 可以聚合任意数量的信号 - userNameSG 和 passwordSG 聚合
RACSignal *logInActionSG = [RACSignal combineLatest:@[validUserNameSG, validPasswordSG] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];

// 聚合的signal - 按钮enable属性
[logInActionSG subscribeNext:^(NSNumber *logInActive) {
self.LogIn.enabled = [logInActive boolValue];
}];
}

让我们在梳理一下需求,根据输入的内容,判断是否可以开启logIn按钮,那么至少需要2个信号,当2个信号都被认定为有效的输入时,登陆按钮才会亮起,这段代码就是完成了这样的功能,并且在用户没有有效输入时,UITextFiled的背景被设置了黄色,这样将bool信号变为了UIColor,颜色对用户的交代就更加分明了,整体的过程如果用图表示的话就是:

这其中还涉及到2个信号的合并

1
2
3
RACSignal *logInActionSG = [RACSignal combineLatest:@[validUserNameSG, validPasswordSG] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];

借助combineLast[].reduce我们可以将任意的signal合并

再进一步,全部都是signal

接下来就是btn的点击,然后去请求服务器当前输入的是否是正确的,通常我们都是为btn添加事件,但别忘了这可是RAC:
在test002中,已经将用户名和密码signal聚合并绑定到LogInBtn的enable中了,但是还不够完全,事实上在响应式编程中原则尽量做到所有的一切都是signal,意味着,btn的touch事件也会被signal化 - 使用rac_signalForControlEvents,like:

1
2
3
4
5
6
- (void) test003 {
RACSignal *logInBtnClickedSG = [self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside];
[logInBtnClickedSG subscribeNext:^(id x) {
NSLog(@" - logInBtn been clicked! -");
}];
}

对signal完整的反应

现在到了去网络请求的环节了,通常异步网络回调下会有以下情形:登录成功,登录失败,其他错误
自然我们对信号的操作也要相对应,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void) test004 {
[self test002];

// 现在,尝试有异步回调下会有以下情形:登录成功,登录失败,其他错误
// 1 创建登录操作sigal - createSignal and RACDisposable
YRServer *server = [[YRServer alloc] init];
RACSignal *logInAndCallBackSG = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[server logInWithUsername:self.userNameTF.text password:self.passwordTF.text finishedCallBack:^(BOOL isSuccess) {
[subscriber sendNext:@(isSuccess)];
[subscriber sendCompleted];
}];
return nil;
}];

// 2 登录操作 - 将按钮的登录操作事件 map为 登录操作信号
// 同时 “整流/合并信号” -Flattens/merge a stream of streams
[[[self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) {
return logInAndCallBackSG; // 并不是普通转化,而是创建了又一个sigal,称之为[signal of signals]
}] subscribeNext:^(id x) {

// 这里需要注意:
// rac_signalForControlEvents 会将button本身 流给(next) subscribe
// 而map中,又create一个新的signal-logInAndCallBackSG
// 所以实际上在这里,得到了一个信号的信号,我称之为复合的signal
NSLog(@"得到一个信号的信号signal of signals,这里仅仅只能查看outer signal: %@", x);

// inner signal还需要自己的 subscribe
[x subscribeNext:^(id x) {

NSLog(@"inner signal - 是否登录成功了?: %@", x);
}];
}];
}
  • 使用createSignal创建了一个登陆操作的信号,对于网络操作结果
    1
    2
    3
    4
    [server logInWithUsername:self.userNameTF.text password:self.passwordTF.text finishedCallBack:^(BOOL isSuccess) {
    [subscriber sendNext:@(isSuccess)];
    [subscriber sendCompleted];
    }];

返回是否成功的bool,然后使命达成,到此为止需要关闭subscriber,这一点很重要

  • 然后在2中的btn点击后,map为刚刚进行的登陆请求操作--也就是把2个signal复合在了一起成为多层次的signal流,再强调一遍并不是普通转化,而是创建了又一个sigal,称之为[signal of signals]
    拆开来看:
    1
    2
    3
    rac_signalForControlEvents 会将button本身 流给(next) subscribe
    而map中,又create一个新的signal-logInAndCallBackSG
    所以实际上在这里,得到了一个信号的信号,我称之为复合的signal

所以在subscribeNext的block之中,如果想得到最内层的signal,就需要再加一个subscribeNext

在RAC中,这样的复合信号处理是如此普遍,以至于专门提供了处理的方法flattenMap,使用其替换上述代码2:

1
2
3
4
5
6
7
8

// 2 登录操作 - 将按钮的登录操作事件 map为 登录操作信号
// 复合信号成流[steam]
[[[self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^RACStream *(id value) {
return logInAndCallBackSG;
}] subscribeNext:^(id x) {
NSLog(@"inner signal - 是否登录成功了?: %@", x);
}];

就实现了完整功能

总结

综合来看,RAC彻底将传统的消息传递方式统一,不管是通知,还是UI事件,都被统一成为了signal,并且是实时响应的,这样使用MVVM,妈妈再也不用担心数据和UI状态的同步了
这里仅仅是一个小小的Demo,但是足以说服去接受RAC了,住大家玩的愉快 😄…

APNs及跳转处理

APNs

最近app加上了apns,在客户端的操作主要在于执行对应的操作,这里涉及几种不同的情况
PS: 其实配置证书方面也有一些变动,还有Xcode中项目的设置上,不过这些都是公开资料,google之

当设备接到apns发来的通知,应用处理通知有以下几种情况:

  • 接收状况

    • 应用还没有加载

      这时如果点击通知的显示按钮,会调用didFinishLaunchingWithOptions,不会调用didReceiveRemoteNotification方法。
      如果点击通知的关闭按钮,再点击应用,只会调用didFinishLaunchingWithOptions方法。

    • 应用在前台(foreground)

      这时如果收到通知,会触发didReceiveRemoteNotification方法

    • 应用在后台

      1. 此时如果收到通知,点击显示按钮,会调用didReceiveRemoteNotification方法。
      2. 点击关闭再点击应用,则上面两个方法都不会被调用这时,只能在applicationWillEnterForeground或者applicationDidBecomeActive,根据发过来通知中的badge进行判断是否有通知,然后发请求获取数据
  • 对应处理

首先是一个跳转的问题,在对应的地方如何跳转到对应的控制器呢?

其实iOS中作为控制器管理的控制器只有两种:NagVCTabVC两种,只要找到了root,跳转就很easy了,必入可以这样简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

- (void)jumpVCThroughAPNsWith: (NSString *)type {

// 先获取rootVC类型
UIViewController * root = (UIViewController*)self.window.rootViewController;
if ([root isKindOfClass:[UITabBarController class]]){
CustomTabBarController *tabbarController = (CustomTabBarController *)root;

if ([type isEqualToString:@"gift_page"]) {
tabbarController.selectedIndex = 3;
UINavigationController *navController = (UINavigationController *)[tabbarController selectedViewController];

ZXGiftManagerViewController *giftVC = [[UIStoryboard storyboardWithName:@"ZXMyProfileTableView" bundle:nil] instantiateViewControllerWithIdentifier:@"ZXGiftmanager"];

giftVC.hidesBottomBarWhenPushed = YES;
giftVC.urlStr = @"/index.php?c=salectrl&m=get_manage_card_data";
[navController pushViewController:giftVC animated:YES];
} else if([type isEqualToString:@"course_page"]){

tabbarController.selectedIndex = 2;
UINavigationController *navController = (UINavigationController *)[tabbarController selectedViewController];

LessonNewDetailViewController * lessonDCon = [[LessonNewDetailViewController alloc]init];
lessonDCon.reqUrl = [NSString stringWithFormat:@"%@/index.php?c=course_ctrl&m=course_data&id=%@",WEB_URL,self.zxMsg.urlStr];
[navController pushViewController:lessonDCon animated:YES];
} else if([type isEqualToString:@"webview_full"]) {

tabbarController.selectedIndex = 0;
UINavigationController *navController = (UINavigationController *)[tabbarController selectedViewController];
SaleViewController * webViewCon =[[SaleViewController alloc]init];
webViewCon.webNewUrl = [NSString stringWithFormat:@"http://host.../index.php?c=fast_show_ctrl&m=show_book_free&id=%@&hide_cache=1",self.zxMsg.urlStr];
[navController pushViewController:webViewCon animated:YES];
}
}

这里根据APNS发过来的typekey执行不同的跳转:

1
2
3
4
5
6
7
8
9
{
"aps": {
"alert":"Hello Push!",
"badge":1,
"sound":"default"
"type" : "xxxx"
},
"page":"home"
}

对应:

type VC
“gift_page” ZXGiftManagerViewController
“course_page” LessonNewDetailViewController
“webview_full” SaleViewController

这里只是一个最简单最方便的实现方式,前提是你知道到底跳转到哪儿

但还有更变态的情况,比如你的app基本都是用的webVC,其实是真的有在页面之间的随意非正常跳转的需求的,所以建议专门写一个RoutingManage类用来管理所有的非正常跳转,通过Runtime根据传过来的用来创建ViewControllerName创建VC,再由一个root去跳转

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)jumpVCThroughAPNsWith: (NSDictionary *)info {
{
// 拿到类名
NSString *class =[NSString stringWithFormat:@"%@", info[@"viewControllerName"]];
const char *VCClassName = [class cStringUsingEncoding:NSASCIIStringEncoding];

// 创建并注册类
Class newClass = objc_getClass(VCClassName);
if (!newClass)
{
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, VCClassName, 0);
objc_registerClassPair(newClass);
}

// 创建对象
id instance = [[newClass alloc] init];

// 获取导航控制器
UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];

// 跳转
[pushClassStance pushViewController:instance animated:YES];
}

这一点在你的app有大量的webVC,基本没什么本地VC的时候,显得尤其重要,比如我司app上一个版本就是这种情况

ok,祝你跳跳愉快了 …🤓

JS 和 OC 交互探索

在iOS中往往需要跟JS交互的需求,比如某些营销活动之类的,比如抢红包等等

前端的优势在于快速,便捷,样式灵活,这直接关乎产品的市场表现,怎样将其与本地app结合,一直是很多大厂尝试的事,甚至会给所有的开发都带来一场革命,对,我说的就是F8上的ReactNative

不过,现实情况是框架还不完善,体验稳定性都问题,眼下还没有大量的推广,何况apple真的会开放iOS这么大块饼吗?我看未必

我们能做的就是尽量在当下,完成简单的跟web的互调而已,而现实是这点儿也将将够用

本篇不聊原理不谈愿景,只介绍一款优秀的第三方框架和笔者自己使用过程中的一点儿心得

  • WebViewJavascriptBridge

这是一款老牌的框架,从11年就有了到现在已经快5年了,一直更新到现在,在github有5K+的star

借助代码看看其使用:

在使用时仅仅自然需要webView,在iOS8之前都是普通的webView,iOS8之后还有一个WKWebView

这里给VC绑定一个webView,like this:

添加属性:

1
2
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) WebViewJavascriptBridge *bridge;

然后在viewDidLoad方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 获取本地html
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"ZXDemo.html" ofType:nil];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];

NSString *str = [NSString stringWithContentsOfFile: htmlPath encoding:NSUTF8StringEncoding error:nil];

// webView 加载 html
[self.webView loadHTMLString:str baseURL: [NSURL fileURLWithPath:htmlPath]];

// 添加webView
[self.view addSubview:self.webView];

// 创建一个Btn
[self createBtn];

同时还创建了一个本地的btn

这个框架认定JS和OC之间有两种层次的交互需求:发消息传东西2种,一种仅仅只是为了通信,另一种更重量的使用是相互调方法

想想也是,很多时候并不需要一定要告诉我要怎样操作,仅仅是传递个我一个值就可以了,而有的时候最好能直接调用某一边的方法,其实稍后你就会发现其实两种都是一样的

通信

可以理解为仅仅就是为了传值,使用灵活简便

  • JS 给 OC 传值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // OC中初始化
    self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {

    NSLog(@" data: %@", data);
    responseCallback(@"back");

    }];

    ---------------------

    // JS中
    bridge.send(data, function(responseData) {
    log('[[JS中]] got response', responseData)
    })

    这样就完成了,如图:

  • OC 给 JS 传值

    还记得我们的那个btn吗,更刚才的正好相反:在btn方法中添加OC,而在JS中要加上初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // JS中初始化
    <!-- OC向JS发消息,JS中的初始化 -->
    <!-- 匿名function对应于OC中所发消息 -->
    bridge.init(function(message, responseCallback) {
    log('JS中(从OC接收的消息)', message)
    var data = { '[[JS中]]接收到消息,并回复':'hello , OC' }
    log('[[JS中]]回复', data)
    responseCallback(data)
    })

    ---------------------

    // OC中btn
    NSLog(@" -- 发送消息 --");
    [self.bridge send:@"TED is goooood!" responseCallback:^(id responseData) {
    NSLog(@"what we receve: %@ ", responseData);
    }];

    结果:

调方法

先明确一点,如果JS -> OC, 说明OC中有方法,反之也一样

  • JS -> OC
    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // OC中
    [self.bridge registerHandler:@"ZXOCHandler" handler:^(id data, WVJBResponseCallback responseCallback) {

    NSLog(@"[OC中],JS传来的消息:%@", data);

    [self sendMessage:nil];
    responseCallback(@"[OC中],已收到");
    }];

    // JS中
    <!-- JS调方法,OC中已经的初始化 -->
    bridge.callHandler('ZXOCHandler', data, function(response) {
    log('[JS中] got response', response)
    })
  • OC -> JS
    更改OC中Btn点击的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // JS中初始化方法
    <!--注册 ZXJSHandler 方法-->
    bridge.registerHandler('ZXJSHandler', function(data, responseCallback) {
    log('[JS中],ZXJSHandler be called, withPra: %@', data)
    var backData = {'[JS中]':'ZXJSHandler be called'}
    responseCallback(backData)
    log('[JS中],ZXJSHandler be called, then responding:', backData)
    })

    ---------------------

    // OC的btn中调方法
    NSLog(@"-- 调用 ZXJSHandler --");
    [self.bridge callHandler:@"ZXJSHandler" data:@"[OC中]来自OC的问候" responseCallback:^(id responseData) {
    NSLog(@"ZXJSHandler的回复: %@", responseData);
    }];

就死这样的酸爽,就是这样的写意,这些足够满足日常需求了

现在回来看,其实所谓的通信传值,不就是调用对方的init方法吗?所以说2种其实是一回事儿

一点儿吭

在博主实际使用开发app的“邀你注册,给你红包”活动功能中,遇到过一个吭。当时同样的前端代码,在android是正常的,而在ios端无论如何都不运行,或者是偶尔正常,找了所有的问题

撕了不少b

都不觉得有问题

最后发现是webView的机制问题,webView打开一个url发生跳转时,跟安卓不同,是异步的,这就意味着如果有一次性的的链接,比如活动中生命周期唯一的优惠码,如果前端框架也是异步的就会发生永远也拿不到唯一的优惠码,显示的错误为已过期的提示,最终跟前端大哥商量他修改了代码,才搞好

所以🙏前端大哥和浩哥等几位的大哥的耐心与帮助,有些问题真不是自己一个人能搞定的

记几次被rejecte的经历--apple的审核

记几次比较深刻的被拒经历:

  • 一次是使用的第三方里面包含了idfa的内容,而没有广告被拒
1
grep -r advertisingIdentifier .

某盟SDK的idf查询结果

果断活该被拒,换用之前最好自己先检测,文档历来都是屎!

  • 一次是app太简单

因为众所周知的原因,想把不该给apple看到的东西隐藏掉,结果藏多了,就悲剧了

  • 还有一次是线下服务器出问题,结果好死不死凑巧被apple看到了

对,说你呢,阿里,人家七扭可是敢赔的


当然我可不希望这个单子越来越长…

algs003-《算法》4th笔记

算法分析

如何评价一个算法?我们用什么标准来评价?具体是怎样分析的?


  • 科学的方式?

Observe some feature of the natural world, generally with precise measurements

Hypothesize a model that is consistent with the observations

Predict events using the hypothesis

Verify the predictions by making further observations

Validate by repeating until the hypothesis and observations agree

The experiments we design must be reproducible and the hypotheses that we formulate must be falsifiable

设计实验可以重复的,结果也是可以重复的
直觉告诉我们,计算性任务的困难程度可以由问题的规模来衡量,越是规模大的问题所需时间就越长,故从探讨:如何将问题规模运行时间的关系量化是很合适的起点

使用ThreeSum程序,根据输入问题的规模来实验:
猜测复合幂函数分布,并预测了跟大规模的用时,通过比较验证了程序的运行时间根问题规模基本呈幂函数分布

对于一段程序对其运行时间的数学建模往往需要一下步骤:

  • 确定输入,定义问题规模
  • 识别内循环
  • 根据内循环操作,确定成本模型
  • 对于给定的输入,判断操作的执行频率

并查集Union Find

并查集,是一种常见的数据结构方式,用以解决诸如集合合并查找类的问题,一开始并不关注其使用
还是照着老路子,我们要从基本的数据单元得到最合适的API及其实现

使用一个网络连接布线的例子,也称为动态连通性问题,如果两个触点之间已经可以连通,就不需要布线了,使用数值代表触点,并有以下约定:

  1. 使用0-9来代表10个集合编号并且每个集合内的元素就是集合号本身--也就是触点
  2. 如果假定集合4和3拥有共同的某种属性,我们就称之为集合3等同于集合4,使用3=4标记--就是3 4 触电连接
  3. 如果3=4,同时4=5,5=7,1=9的话,[1 3 4 5 7 9]可归并为同一个集合,拥有相同的集合名称 -- 触电连接可传递

约定本篇不再区分触点集合连通合并,归并等术语,设计的API如下:

1
2
3
4
5
6
public calss UF 的API:
UF(int N) 初始化0 .. N-1 共N个集合
void union(int p, int q) 在p和q连通,也叫2个集合合并
int find(int p) 查找p所在的集合的编号
boolean connected(int p, int q) p和q是否连通
int count() 连通分量数,就是集合数

使用数组id[]作为基本数据结构,一开始有N个分量,每个触电都含有一个分量,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class UF {
private int[] id;
private int count;

// 初始化:N个分量N个触点,每个触点一个分量
public UF(int N) {
count = N;
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}

public int count() {return count;}

public boolean connected(int p, int q) {
return find(p) == find(q);
}

public int find(int p) // 留待
public void union(int p, int q) // 留待

// 测试及输出说明
// 输入N个触点,通过每队整数调用find()方法:
// 若已连通,忽略;
// 若不连通,则调用union()方法连通;
// 最终输出各个连通分量对和总共的连通数
public static void main(String[] args) {
int N = StdIn.radInt();
UF uf = new NF(N);
while!(StaIn.isEmpty()) {
int p = StdIn.readInt();
int q = StdIn.readInt();
if(uf.connected(p , q)) continue; // 访问2次
uf.union(q,p);
StdOut.println(p + "连接" + q);
}
StdOut.println(uf.count() + "条连接");
}
}

剩下的就在于find()union()的实现了,而其中很多地方如connected()本质上都需要find()方法,故它的具体实现会直接决定我们算法的效率,而各种查找算法真是本书的一大章节,这篇不会详细介绍留待后边,由于我们特殊的限定(即i就是触点名)这里选用最朴素的方式直接返回即可; 然后不妨在来考虑union()的实现,由于我们使用id[i]表示触点,i为触点号,所以对于任意的两个触点连通就意味着2个触点同属一个触点的不同分量,所以对于id[x] = id [y] = SetName就完成了触点的归并,如果若B触点都拥有多个分量值,只要B中有一个分量与A触点连通,根据连通传递的特性,那么B中所有分量都与A连通,故有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void union(int q, int p) {
int pID = find(p); // 访问一次数组
int qID = find(q); // 访问一次数组

// 如果在已经连通,忽略
if(pID == qID) return;

// 合并2个触点的所有分量,即让所有分量同名
for(int i = 0; i < id.length; i++ ) { // 遍历一次
if(id[i] == pID) id[i] = qID;
}
count--;
}

// 直接返回即可
public Int find(int q) {
return id[q];
}

定性的分析以下可知,find()一次会访问数组一次,整个测试中connected()和union()中又多次find,特别是在union中有对数组的完整遍历要访问N次,如果全部触点都连通的话,在main中又会有N次union(),所以综合来看动态连通性问题的quick-find算法会是N^2级别的数组访问次数,在处理大型数据时并不能接受,是否还能提高呢?

quick-union

它是针对于上述quick-find的改进,它使用的并不是单纯的数组,而是使用结构,一开始每一个树都只有一层一个节点,自然自己跟自己是连通的;如果p和q的root节点不同,表示两者不连通,如果两者root节点相同表示连通;如果两个不连通的树,调用union之后,会将一个树作为另一个树的分支,合并为拥有一个root节点的大树,通过这些描述,我们有了实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int find(int q){
while( !id[q] == q ) { // 查找分量名
q = id[q];
}
return q;
}

public void union(int q, int p) {
int pRoot = find(p);
int qRoot = find(q);
// 如果连通,忽略;不连通,就连通
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
count--;

}

个人在理解的时候,倒是find()给我造成了些困惑,其实需要牢记先前的初始化是的一个约定,一个触点/集合的名称编号为数组的i,而集合的分量是在变动的,注意参考图示
如:

quick-union

但是,对比quick-find,quick-union真的好吗?我们来分析它们的效率

对于前者,如果需要添加一个新的连通时,如a连通b,那么由于并不直到根b连通的已经有多少,只能遍历整个数组N,保证让a,b以及各自连通的分量都使用一个名称a,这样如果添加了N个每个都要N次,那么就是N^2级别的

对于后者,在最坏的情况下,即按顺序的查找如0-1,0-2,0-3…这样的顺序,最终会得到的树没有分支,就是一整条链表,如果这时从链表最底部的开始查询,需要遍历真个数组N级别的,然后如果完成全部连通,也是N^2级别的
但在其他情形之下,由于树都是有分支的,

algs002-《算法》4th笔记

基本的数据结构及其实现

本节的主要问题是:如何用java来实现基础数据结构:背包,队列,


定容栈

首先从一个只能存储String的简单的定容栈的设计开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class FixedCapacityStackOfStrings {
private String[] a;
private int N;

public FixedCapacityStackOfStrings(int cap) {
a = new String[cap];
}

public boolean isEmpty() {
return N == 0;
}

public int size() {
return N;
}

public void push(String item) {
a[N++] = item;
}

public String pop() {
return a[--N];
}

//测试
// 输入:to be or not to - be - - that - - - is
public static void main(String[] args) {

FixedCapacityStackOfStrings s = new FixedCapacityStackOfStrings(100);

while(!StdIn.isEmpty()) {
String item = StdIn.readString();

if(!item.equals("-")) s.push(item);
else if(!s.isEmpty()) StdOut.print(s.pop() + " ");
}

StdOut.println(" ---- " + s.size() + "left on stack");
}

}

  • 合情合理的使用了数组,push就是N+1,pop就是–N

在测试中直接new了一个100的数组,很明显是欠妥的,如果压栈数超过100不够用,不到100又会太过浪费,所以开始进化为“可变容量的栈”

定容栈 -> 可变容栈

基本思路:在push时,如果超过Max则扩充;如果不到则缩减Max
但是java的数组并不支持直接的扩充和缩减,所以这里需要在节点时刻创建新的数组,这里创建一个新方法,然后对push和pop方法进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

// 将 N <= max 的栈,移动到一个大小为max的数组
private void resize(int max) {
String[] temp = (String[]) new Object[max];
for (int i = 0; I < N; i++) {
temp[i] = a[i];
}
a = temp;
}

// 如果栈满,栈就增加一倍
public void push(String item) {
if(N == a.length) resize(2 * N);
a[N++] = item;
}

// 如果弹栈之后,发现装的太少,不超过当前栈的1/4,栈缩小一倍
public String pop() {
String item = a[--N];
a[N] = nil;
if(N > 0 && N == a.length / 4) resize(a.length / 2);
return item;
}

这样就实现了栈的大小总处在最合适的大小,完成了从定容变容自适应的进化

String栈 -> T栈

已经得到了一个自适应的String栈,但利用java的范型就能将其变为万能栈,只需要将String变为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 范型化
public class FixedCapacityStackOfStrings<Item> {
// private String[] a;
private Item[] a;
private int N;

public FixedCapacityStackOfStrings(int cap) {
// a = new String[cap];
a = (Item[]) new Object[cap];
}

public boolean isEmpty() {
return N == 0;
}

public int size() {
return N;
}

public void push(Item item) {
a[N++] = item;
}

public Item pop() {
return a[--N];
}

//测试
// 输入:to be or not to - be - - that - - - is
public static void main(String[] args) {

FixedCapacityStackOfStrings s = new FixedCapacityStackOfStrings(100);

while(!StdIn.isEmpty()) {
String item = StdIn.readString();

if(!item.equals("-")) s.push(item);
else if(!s.isEmpty()) StdOut.print(s.pop() + " ");
}

StdOut.println(" ---- " + s.size() + "left on stack");
}

}

这样就完成了T栈的进化

=====================================

补充一个的具体使用:如计算(2 * (2 + 4) - (2 + 1) / 2)这样的计算,是如何按顺序得到想要的结果的?

其实,早在60年代的计算机发展之初就有前辈利用2个栈简单的解决了这个问题:一个栈用来存运算符号,一个栈用来放数值,根据)来区隔,没遇到一个),运算符栈弹出运算符,数值栈弹出数值计算得到结果,再将结果入栈,如此就完成了包含括号优先级的计算


链表

链表: 一种由node链接的数据结构,每个node含有一个范型的元素,和一个可指向另一节点的引用

回忆在上面处理可变容栈时,由于使用的数组容量不能随意变更,为此我们专门设计了一个resize()方法,而链表能够随意增删节点的特性就是专门解决这样的问题

1
2
3
4
private class Node {
Item item;
Node next;
}

对于链表的学习,最好的方式就是可视化表达,留意一些特殊节点并熟练以下操作:表头节点,表尾节点,插入节点,删除节点,遍历量表,再引申到双向链表,环行链表等等,这些是我们实现各种高级算法的基础,会逐渐都接触到

链表实现栈(Stack)

先让我们把上次设计的改为使用链表而不是数组来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
publci class StackDemo {
private int N; // 链表长度
private Node first; // 头节点

private class Node {
Item item;
Node next;
}

public boolean isEmpty() {
return first == null; // 或者 N == 0;
}

public int size() { return N; }

// 就是在头节点前添加一个node
public void push(Item item) {
Node oldFirst = first;
first = new Node();
first.item = item;
fisrt.next = oldFirst;
N++;
}

// pop就是删除首节点
public Item pop() {
Item item = first.item;
first = first.next;
N--;
return item;
}

// iterator() 留待
// 测试main根原来相似
}
  • 使用链表能保证栈的大小根链表分配的大小一致,不存在浪费和不够的问题

  • 只使用first节点就足够了,栈只有一个IO口,对于链表来说操作first节点是最容易的

链表实现队列(Quene)

然后链表来实现队列(Queue),是再合适不过了,入列就是添加last节点,出列就是first节点删除,size,isEmpty等等根栈中的一致,就不再写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class QueueDemo {
private int N;
private Node first; // first节点
private Node last; // last节点
private class Node {
Item item;
Node next;
}

...
// 入列就是向last后添加node
public void enquene(Item item) {
Node oldLast = last;
last = new Node();
last.item = item;
last.next = null;

// 如果源链表为空,那么first和last节点是同一个节点
if(isEmptyy()) {
first = last;
} else {
oldLast.next = last;
}
N++;
}

// 出队就是删除first节点
public Item dequeue() {
Item item = first.item;
first = first.next;

// 如果对空队列出队,就让链表为空
if(isEmpty()) {
last = null;
}
N--;
return item;
}

// itetator() 留待
...
}
  • 队列的实现需要first和last两个节点
  • 注意队列只有一个元素时,first = last
  • 注意队列为空就是 last = null

链表实现背包(Bag)

背包其实最简单的集合,它只支持添加和查找,不支持删除
其实现也很简单基本跟相同,只用一个first节点就足够了,所以重点不在它的add上,而在于对其中元素的查找

在java中,使用的就是迭代(Iterator),允许你使用foreach()语句来遍历集合,那迭代到底是如何实现的呢

首先需要实现Iterator接口:引入import java.util.Iteratorimplement Iterator<Item> 实现其所有方法

具体代码如:

1
public class BagDemo

tips

tips

地图效果实现,demo验证

使用scrollView,在缩放过程中,通过仿射参数(a或者d)和最大zoom值的比例,来确定percent,从而设置Interaction的进度,实现双手捏合等放地图交互。

项目框架搭建

准备阶段:

  • 主业务逻辑清楚(8成)
  • 数据组织形式(待定)
  • 数据流向

请求部分: 从业务部分,通过统一接口发送请求

响应部分: 从网络获取到原始数据(JSON),由Manager实例保管,Manager实现reformManagerData协议,直接得到最合适的view需要的具体化model,实现数据展示

  • 工具类组合:包含分类和独立协议 + 常用单利
  • 第三方框架,交由pod统一管理

尽量避免的蛋疼的JSON解析,原数据:

http://host/index.php?c=recommend_ctrl&m=get_tag_level_page_data
关系树

首先是数据的层级关系,存储字段为tag_level,只存储层级关系

  • 其中root_tag的为第一层级所有的id
  • 余下以第一层id为key,存储下一层级的科目id

关系树
具体的所有数据都在tag_meta中,共93组,每组由id为key,对应一个字典

所以需要
第一,将tag_meta所有数据加载到内存,以便于根据id去获取,并且生命周期要保持至少2个层级控制器,所以需要一个从网络获取的原始数据字典
第二,对于每一个控制器itemList中,直接可用的是每层的title

UIButton

  • 在system下,设置background无效,而要更换为custom
    另外系统并未提供setBackground:forState方法,所以自己添加一个分类实现之:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "UIButton+UIButtonColor.h"

@implementation UIButton (FillColor)

- (void)setBackgroundColor:(UIColor *)backgroundColor forState:(UIControlState)state {
[self setBackgroundImage:[UIButton imageWithColor:backgroundColor] forState:state];
}

+ (UIImage *)imageWithColor:(UIColor *)color {
CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return image;
}

@end
  • UIButton的边框和倒角,对layer操作是最方便的

AutoLayout 的时机

AutoLayout by YongSir🇨🇳🇨🇳
  • 视图布局相关
    1
    2
    3
    4
    5
    6

    // Called just before the view controller's view's layoutSubviews method is invoked. Subclasses can implement as necessary. The default is a nop.
    - (void)viewWillLayoutSubviews;

    // Called just after the view controller's view's layoutSubviews method is invoked. Subclasses can implement as necessary. The default is a nop.
    - (void)viewDidLayoutSubviews;

这两个在VC中方法,正如它的注释所述,会在VC的view的layoutSubviews被调用的之前和之后被调用,这也就是为什么,对一些添加到view的子视图布局之后,直到 viewDidLayoutSubviews才能得到正确的值!

  • PS
    1
    2
    3
    4
    5
    - (void)viewDidLoad
    |
    - (void)awakeFromNib
    |
    - (void)viewWillAppear:(BOOL)animated

可以看到viewDidLoad是最先的,它负责加载控制器,在它加载的过程中去查看view,如果从sb加载就去到awakeFromNib方法,然后再显示之类的,知道这些还不够,还需要知道:

1
2
3
4
5
111 viewDidload  -- self: <ZXMessgeCentreViewController: 0x1559a7280>, self.view: <UIView: 0x155f12cf0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x155be07a0>>

222 awake -- self: <ZXMessgeCentreViewController: 0x1559a7280>, self.view: <UIView: 0x155f12cf0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x155be07a0>>

333 viewWillAppear -- self: <ZXMessgeCentreViewController: 0x1559a7280>, self.view: <UIView: 0x155f12cf0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x155be07a0>>

亲测Xcode7 beat 下从SB来的view已经布局,和viewDidLayoutSubviews中对比,这个方法进了2次,都是:
viewDidLayoutSubviews 中的view: <UIView: 0x12697d2d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x126bd2450>>

  • PPS

当然在实际使用时,最好能跟踪一下这几个方法,确定最好的时机

AutoLayout 的一个重要概念

AutoLayout by YongSir🇨🇳🇨🇳

如果你常常为明明写好了自动布局的代码而得不到想应得效果,特别是在需要按内容自适应的时候,这时候你需要知道一个重要的概念和理解各个布局的优先级问题

  • 内容的压缩阻力内容包裹

    从StackOverFlow上的一个问题开始,还是建议先看原文,毕竟好多老外的词并不是那么好翻译

    what is the content compression resistance and content hugging of UIView?

    Taken from objc.io’s excellent Advanced Auto Layout Toolbox article:

    Intrinsic Content Size

    The intrinsic content size is the size a view prefers to have for a specific content it displays. For example, UILabel has a preferred height based on the font, and a preferred width based on the font and the text it displays. A UIProgressView only has a preferred height based on its artwork, but no preferred width. A plain UIView has neither a preferred width nor a preferred height.

    Compression Resistance and Content Hugging

    Each view has content compression resistance priorities and content hugging priorities assigned for both dimensions. These properties only take effect for views which define an intrinsic content size, otherwise there is no content size defined that could resist compression or be hugged.

    Behind the scenes, the intrinsic content size and these priority values get translated into constraints. For a label with an intrinsic content size of { 100, 30 }, horizontal/vertical compression resistance priority of 750, and horizontal/vertical content hugging priority of 250, four constraints will be generated:

    1
    2
    3
    4
    H:[label(<=100@250)]
    H:[label(>=100@750)]
    V:[label(<=30@250)]
    V:[label(>=30@750)]

    If you’re not familiar with the visual format language for the constraints used above, you can read up about it in Apple’s documentation. Keeping in mind that these additional constraints are generated implicitly helps to understand Auto Layout’s behavior and to make better sense of its error messages.

    Here’s another StackOverflow question that addresses the difference between content compression resistance & content hugging: Cocoa Autolayout: content hugging vs content compression resistance priority

    Quick summary of the concepts:

    • Hugging => content does not want to grow
    • Compression Resistance => content does not want to shrink

    and an example:

    Say you’ve got button like this:

    [ Click Me ]

    and you’ve pinned the edges to a larger superview with priority 500.

    Then, if Hugging priority > 500 it’ll look like this:

    [Click Me]

    If Hugging priority < 500 it’ll look like this:

    [ Click Me ]

    If superview now shrinks then, if the Compression Resistance priority > 500, it’ll look like this

    [Click Me]

    Else if Compression Resistance priority < 500, it could look like this:

    [Cli..]

    If it doesn’t work like this then you’ve probably got some other constraints going on that are messing up your good work!

    E.g. you could have it pinned to the superview with priority 1000. Or you could have a width priority. If so, this can be helpful:

    Editor > Size to Fit Content


其实说的已经很详细了

  • 固有内容大小

    固有内容大小是所显示内容的首选(或者保证显示完整的最小程度)的大小,比如UILabel的固有高度就是当前所用字体(包括字型和字体大小)的首选高度,固有宽度就是当前字体和内容一起决定的首选宽度。UIProgressView的固有内容大小,就只是一个首选的便与完整显示进度条形状的固有高度,而没有一个首选的宽度去决定要显示多长的进度。一个什么都不显示的空白的简易的view是没有首选宽和高的。

  • 压缩阻力内容包裹
    每一个view都分配好的:内容压缩和内容包裹这2个方面的优先级,这两个优先级属性仅仅在决定固有大小时起作用,没有内容时是不起作用的。
    在背后,固有大小和这两个优先级属性值会转化表现为约束(constrains),如对于一个显示着固有内容大小为{100,30}的UILabel来说,水平和垂直的压缩阻力的优先级值为750,内容包裹的优先级为250,所以本质上会转化为:

    1
    2
    3
    4
    H:[label(<=100@250)]
    H:[label(>=100@750)]
    V:[label(<=30@250)]
    V:[label(>=30@750)]

在形象一些了解这些概念:

  • 包裹(Hugging) => 内容不希望再增大
  • 压缩阻力(Compression Resistance) => 内容不希望再压缩

一个有很大的父视图的小btn,上下左右边距都有一定的空隙,并且优先级设为500

1
[       Click Me      ]
  • 如果包裹的优先级 > 500,就会自动包裹,变成这样:

    1
    [Click Me]
  • 如果包裹的优先级 < 500,就不会自动包裹,维持这样:

    1
    [      Click Me      ]

如果父视图现在压缩

  • 如果压缩阻力优先级 > 500,会变成:

    1
    [Click Me]
  • 压缩阻力优先级 < 500, it could look like this:

    1
    [Cli..]

如果没有这两个默认设置的优先级属性,显然在设置布局时我们要处理更多的约束!

所以,在自己添加视图的约束时,要确保子视图的内容压缩阻力和吸附属性约束没有被更高优先级的约束条件覆盖掉,这也是很多时候明明加上了约束而没有起作用的原因。

E.g. you could have it pinned to the superview with priority 1000. Or you could have a width priority. If so, this can be helpful:
Editor > Size to Fit Content

algs001-《算法》4th笔记

《算法》4th笔记

第一章 基础

利用这段时间,借助这本大作把该补的全部补上。

1
2
3
4
5
6
7
8
9
10
11
12
//  本书以Java为例,一个典型的Java类
import package(packageName);
public(optional) class ClassName extends FatherClass implement InterfaceName

构造函数;(默认自动创建)

static 类变量;
static 类方法;

成员变量;(实例变量)
成员函数;(成员方法)

  • 从一个计数器类开始 - Counter
    当我们声明一个Counter类型的变量时,发生了什么?
1
Counter heads;

对象 是能承接数据类型的值的实体,具有以下三个特征:状态 标示行为

1
2
3
对象的状态  | 数据类型中的值
对象的标识 | 能将每个对象区分,可以理解为内存中的位置
对象的行为 | 数据类型的操作

对象的表示


基础部分就不按照书上介绍的来了,有其他oop经验,快速入手分将以下问题各个击破即可开始本书的阅读,至于继承多态流程控制之类的就不废话了:

基础观感

1. 没有什么比直接上代码给合适的说明这个问题了:

1
2
3
4
5
6
7
8

public class HelloJava {

public static void main(String[] args) {

System.Out.println("Hello World");
}
}
  • 文件保存为 HelloJava.java,源文件名必须和类名一致即 ClassName.java
  • 编译:

javac HelloJava.java

编译完成,对应目录之下生成HelloJava.class文件

  • 运行:在对应的目录之下有java虚拟机运行.class文件

java HelloJava

  • 可以看到程序入口就是public static void main(String[] args)方法

  • 一个源文件可以有多个Class,但只能有一个public

2. 修饰符

Java中的修饰符根所有oop设计一样,有访问修饰符非访问修饰符2种
主要要说明的是非访问修饰符

static : 静态变量和静态方法的修饰符,也就是用于创建类方法,类变量,所声明自然是在静态区,不管多少对象,全局一份不多余分配
final : 不同情况有一些差别,但总体是有表达“这是无法改变的意思”,细微的差别常常令人困惑

三种使用final的情况:数据,方法和类

  • final 数据 - 告知编译器一块数据是恒定不变的

public + static配合作为全局常量,并且变量名要大写和初始化,eg:

static final PRICE
1
2
3
4
> 如果是对象的final了,表示引用的恒定不变,eg:
> ```java
final ClassName a = new ClassName();
a = new ClassName(); // Error: Cannot change reference

  • final方法 - 可被继承但不能修改,private默认会带上final
  • final类 - 类不可被继承

abstract : 抽象的自身都不能实现,抽象类也可以包含非抽象方法,但抽象方法有一个那这个类就必须是抽象类,一旦子类继承了就需要全部实现

还有其他线程相关的修饰符暂且搁下

  • 类的设计:如计数器Counter的设计

Stream,File及IO

基本都在Java.io

1 控制台IO - System.in System.out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.*;

public class IODemo2 {
public static void main(String[] args) throws IOException {
// read() 读取字符
// IODemo2.charRead();
// readLine() 读取字符串
IODemo2.stringRead();
}

protected static void charRead() throws IOException{
char c;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入文字,并以'q'结束");
do {
c = (char) br.read();
System.out.println(c);
} while(c != 'q');
}

protected static void stringRead() throws IOException{
String str;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入字符串,并以“q”结束");
do {
str = br.readLine();
System.out.println(str);
} while(!str.equals("q"));
}
}

虽然与read()对应的是write(),但都习惯使用更方便的print(),当然要是硬要用System.out.write('a')输出字符‘a’的话也可以的

2 文件IO

文件的io,先是Java中系统文件读写包,还是看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.io.*;

public class FileStreamDemo {
public static void main (String[] avgs) throws IOException {
// FileStreamDemo.fileWriter();
FileStreamDemo.fileReader();
}

// 写入文件
protected static void fileWriter() throws IOException {

// 1 构建FileOutputStream对象,如果没有文件会自动创建
File f = new File("HelloJava.txt");
FileOutputStream fop = new FileOutputStream(f);

// 2 构建指定编码的OutputStreamWriter
OutputStreamWriter writer = new OutputStreamWriter(fop, "UTF-8");

writer.append("输入内容:\r\n");

writer.append("hello java,this has been write to file! ");

writer.close();
fop.close();
}

// 读文件
protected static void fileReader() throws IOException {

// 1 构建FileInputStream对象,如果没有文件会自动创建
File f = new File("HelloJava.txt");
FileInputStream fip = new FileInputStream(f);

// 2 构建指定编码的InputStreamReader
InputStreamReader reader = new InputStreamReader(fip, "UTF-8");

// 3 写入buffer
StringBuffer sb = new StringBuffer();

while(reader.ready()) {
sb.append((char)reader.read());
}

// 打印
System.out.println(sb.toString());

reader.close();

fip.close();
}
}

1
2
3
4
5
6
7
8
9
                            |--
|-- OutputStream--|--
| |--

Object

| |--
|-- InputStream --| --
|--

3 本书中的IO

本书使用的是Wayne为课程专门提供的StdIn和StdOut两个包以及In``Out等类,都在algs4.jar中包含
提供的包简化了文件IO的操作,可使用命令行重定向指定文件的输入输出,还可以管道输入,eg:
命令行的重定向和管道

需要其他API,可自行查找,下面给出一个用二分法,做白名单过滤的Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.Arrays;

public class BinarySearch {

/**
* This class should not be instantiated.
*/
private BinarySearch() { }

public static int indexOf(int[] a, int key) {
int lo = 0;
int hi = a.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (key < a[mid]) hi = mid - 1;
else if (key > a[mid]) lo = mid + 1;
else return mid;
}
return -1;
}

public static int rank(int key, int[] a) {
return indexOf(a, key);
}

public static void main(String[] args) {
// 1 从文件读取整数放倒数组
In in = new In(args[0]);
int[] whitelist = in.readAllInts();

// 2 数组排序
Arrays.sort(whitelist);

// 3 从终端输入要查询的数字key,如果不在白名单中就输出
while (!StdIn.isEmpty()) {
int key = StdIn.readInt();
if (BinarySearch.indexOf(whitelist, key) < 0)
StdOut.println(key);
}

// - 打印出输入的对照白名单文件
System.out.printf(" totol length: %d \n ", whitelist.length);
for (int i = 0; i < whitelist.length; i ++) {
System.out.println(whitelist[i]);
}
}

  • 在用小数据测试时,可以打印输出,要是大文件就别作了

  • 二分法,先排序,这里使用Arrays.sort()排序

  • 查找改为使用递归,看起来会更加简洁和优雅,故将现有的rank()函数替换为:

1
2
3
4
5
6
7
8
9
10
11

public static int rank(int key, int[] a) {
return rank(key, a, 0, a.length -1);
}
public static int rank(int key, int[] a, int lo, int hi) {
if (lo > hi) return -1;
int mid = lo + (hi - lo) / 2;
if (key < a[mid]) return rank(key , a, lo, mid - 1);
else if (key > a[mid]) return rank(key, a, mid - 1, hi);
else return a[mid];
}

递归就是数学归纳法,并且:

递归总要有最简单的情况

递归总是趋近于一个更加简单的情况

递归嵌套时,各个子问题不能根父问题不能有交集

CoreData(四)

CoreData and Swift by YongSir🇨🇳🇨🇳

实际使用中,使用tableView来展示数据是如此的普遍,以至于官方针对列表中的方便的展现做了封装,这就是我们这一篇的主要目的--NSFetchedResultsController。

In fact, they saw so much potential for a close connection between UITableView and Core Data that they penned a class to formalize this bond: NSFetchedResultsController.

As the name suggests, NSFetchedResultsController is a controller, but it is not a view controller. It has no user interface. Its purpose is to make developers’ lives easier by abstracting away much of the code needed to synchronize a table view with a data source backed by Core Data.

Set up an NSFetchedResultsController correctly, and your table will “magically” mimic its data source without you have to write more than a few lines of code.

(就像 NSFetchedResultsController 的名称,它是一个控制器但不是view的控制器,只是便于数据和tableView关联的抽象的封装代码,让你短短几行代码,就能“魔法般的”让coreData数据源化)

我们借用一个WorldCup 的Demo来演练:Demo模拟球队的积分,tap一次代表加一分,得分最高的就是winner。


首先是,创建和设置NSFetchedResultsController,使用唯一的一个初始化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  // MARK:设置fetchedResultsController
// handles the coordination between Core Data and your table view
// requires at least one sort descriptor.Note that how would it know the right order for table view
func setFetchController() {
// 1 创建请求 + 指定排序
let fetchRequset = NSFetchRequest(entityName: "Team")
let sortDescriptor = NSSortDescriptor(key: "teamName", ascending: true)
fetchRequset.sortDescriptors = [sortDescriptor]

// 2 fetch控制器的实例话仍然依赖于 NSFetchRequest 和 context上下文
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequset, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)

// 3 控制器执行获取 --performFetch && 错误处理
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Error:\(error.localizedDescription)")
}
}

特别要注意的是,FetchedResultsController是依赖于普通的NSFetchRequest的,并且这个request至少需要指定一种sort,否则就会报一个错误:

1
'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'

事实上,fetchResultsController还将fetch到的结果封装在了fetchObjects属性和objectAtIndexPath中,在dataSource下是:

1
return fetchedResultsController.sections!.count

然后获取模型数据,装填到cell是这样的:

1
2
3
4
5
6
7
8
func configureCell(cell: TeamCell, indexPath: NSIndexPath) {
// - 通过fetchCtoller获取数据
let team = fetchedResultsController.objectAtIndexPath(indexPath) as! Team
// - cell设置
cell.flagImageView.image = UIImage(named: team.imageName)
cell.teamLabel.text = team.teamName
cell.scoreLabel.text = "Wins: \(team.wins)"
}

补充完成“tap加1”的功能:

1
2
3
4
5
6
7
8
9
10
11
12
// MARK: -- tableView delegat --
func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath) {
let team = fetchedResultsController.objectAtIndexPath(indexPath) as! Team
// tap加1 操作
let wins = team.wins.integerValue
team.wins = NSNumber(integer: wins + 1) // 类型转换
// 保存
coreDataStack.saveContext()
// 刷新UI
tableView.reloadData()
}

至此基本的功能就完成了,从数据展示到数据更改,倒是并没见得有什么magic的地方, 😓,接下来就是放大招的时候


  • 将各个队伍小组化

    There are six qualifying zones in the World Cup: Africa, Asia, Oceania, Europe, South America and North/Central America. The Team entity has a string attribute named qualifyingZone that stores this information.

    任务是就将各个国家放到其对应的预选赛区的section中,如果是通常情况下,这看起来是个需要首先整理model然后再改写数据源方法的繁琐步骤,但是借助fetchedResultsController就会容易很多,仅仅只需要更改2个小地方即可:

    1. 更改fetchResultsController的实例化funcsetFetchController() 中:

      1
      2
      3
      // 2 fetch控制器的实例话仍然依赖于 NSFetchRequest 和 context上下文
      // 更改使用keyPath“qualifyingZone”做实例化
      fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequset, managedObjectContext: coreDataStack.context, sectionNameKeyPath: "qualifyingZone", cacheName: nil)
    2. 实现dataSourece的方法,为section设置title:

      1
      2
      3
      4
      5
        // 设置title
      func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
      let sectionInfo = fetchedResultsController.sections![section]
      return sectionInfo.name
      }

    运行得到效果:

    视觉上差不多实现了,但是等等,阿根廷为什么成了非洲的队伍了,数据错乱了,是cell重用的的问题还是本身数据的问题?
    这里应该数据的问题,因为排序指定的仍然还是按照A~Z的顺序排列,而添加国籍之后按国籍group排列会与之冲突,所以这就引出了并列排序中的著名的“疑难杂症”--排序规则冲突

    The problem was the sort descriptor. This is another NSFetchedResultsController “gotcha” to keep in mind. If you want to separate fetched results using a section keyPath,the first sort descriptor’s attribute must match the key path’s attribute.

    The documentation for NSFetchedResultsController makes this point emphatically, and with good reason! You saw what happened when the sort descriptor doesn’t match the key path—your data ends up making no sense.

    所以更改如下

    1
    2
    3
    4
    5
    // 更改排序 - 注意排序不要冲突,否则会乱掉
    let zoneSort = NSSortDescriptor(key: "qualifyingZone", ascending: true)
    let scoreSort = NSSortDescriptor(key: "wins", ascending: true)
    let nameSort = NSSortDescriptor(key: "teamName", ascending: true)
    fetchRequset.sortDescriptors = [zoneSort, scoreSort, nameSort]

    这样对结构就按照,先地域,再积分,最后队名的排列顺序了
    在NSFetchedResultsController的协助之下,我们不必遍历所有队伍,按区域分别对应找到各个国家组成小组,然后再在小组内排序,而是很简单的代码就能实现这些功能,too maigical!

  • cache的引入
    首先考虑这样一种场景,如果这里不是32个国家,而是让你玩成我朝13亿人的人口分类,按照不同的省市地区归类的话,这样就势必带来一个性能和内存的问题。我可以自作聪明的开一个线程,like this

    “I’d just throw that on a background thread!” might be your first thought. The table view, however, can’t populate itself until all sections are available. You might save yourself from blocking the main thread, but you’d still be left looking at a spinner.

    There’s no denying that this operation is expensive. At a bare minimum, you should only pay the cost once: figure out the section grouping a single time, and reuse your result every time after that.

    除了放到后台线程之外,对于类似这样的’昂贵’操作,最好的和最必要方式就是:既然无法避免,那就尽量只做一次!这就是“Cache”的真正意义。好在苹果已经为我们考虑到了,在fetchedResultsController的实例话中,那4个参数的做后一个就是cacheName

    1
    2
    // 再次更改,指定cache的name
    fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequset, managedObjectContext: coreDataStack.context, sectionNameKeyPath: "qualifyingZone", cacheName: "worldCup")

    需要注意的是,cacheName是非常sensitive的,他标示这样一种“单例”般的机制,如果没有即表示首次加载,就加载,如果已经存在即非首次加载,就会根据这个标记取cache中去取第一次的结果,保证只加载一次。所以就可以理解cacheName的sensitive了,如果变动了使用,就用deleteCacheName或者重新指定新的name来变更。

  • 监测变动
    截止目前可见,使用NSFetchedResultesController至少带来两个好处:方便处理section和做cache处理。下面介绍第三个好处,但要说明的是,虽然功能强大但也很容易出错,就是鼬神的“越是强大的术就越有不可克服的缺陷和风险”。

    The third and last benefit is somewhat of a double-edged sword: it is powerful but also easy to misuse.

    先回忆在“Tap 加 1”的功能是,Tap一下wins+1,然后tableView.reloadData,这种方式粗暴有效,但如果考虑的更多比如Demo的下一个版本,如果并不是通过这种“tap+1”获得积分,而是从网络获取win积分,就应该考虑一种更加接近基础数据的方式,做到只要数据有变化就能及时的展示。

    Maybe there’s a detail screen for every team where you can change the score. Maybe the app calls an API endpoint and gets new score information from the web service. It would be your job to refresh the table view for every code path that updates the underlying data.

    这就需要NSFetchedResultsController的代理来帮忙了,它可以监听基础数据的变化过程,让你轻松做到只要基础数据变化就自动刷新列表显示:

    NSFetchedResultsController can listen for changes in its result set and notify its delegate, NSFetchedResultsControllerDelegate. You can use this delegate to refresh the table view as needed any time the underlying data changes.

    Note: A fetched results controller can only monitor changes made via the managed object context specified in its initializer. If you create a separate NSManagedObjectContext somewhere else in your app and start making changes there, your delegate method won’t run until those changes have been saved and merged with the fetched results controller’s context.

    操作上需要遵守协议 NSFetchedResultsControllerDelegate ,设置代理

    1
    2
    // 设置代理
    fetchedResultsController.delegate = self

    实现方法:

    1
    2
    3
    4
    5
    // MARK: -- NSFetchedResultsControllerDelegate
    func controllerDidChangeContent(controller: NSFetchedResultsController) {
    // 再次更新UI
    tableView.reloadData()
    }

    虽然改动并不大,但这样就意味着,基础数据只要有任何改动,不管是通过tap还是从网络获取从而改动,都会刷新列表数据。但仅仅是这样还是不够,虽然我并没能重复错误,倒是跳跃感很影响体验:

    The score labels update as before, but this reveals another problem. If you tap around enough times, you might get into a state where teams within a qualifying zone are not ranked by number of wins. For example, in the previous screenshot, you can see Cameroon below Algeria. However, Cameroon has 16 wins and Algeria 9. Also, when the cells do move around, it’s pretty jumpy, almost as if you were completely reloading the table every time something changed. :]

    显然我们还需要调整,为了不显得那么跳跃,需要再到代理中:

    The fetched results controller delegate can tell you if something needs to be moved, inserted or deleted due to a change in the fetched results controller’s result set.

    由于改动都是在controller的结果集合中,所以借助代理可以让你决定是否去添加,删除或者移动。换言之,能够让开发者控制到底该“如何显示,显示谁”,在这里,我们要改动代理方法中的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

    // MARK: -- NSFetchedResultsControllerDelegate
    // 按照“begin updates-make changes-end updates”的顺序变更
    func controllerWillChangeContent(controller: NSFetchedResultsController) {
    // 开始更新
    tableView.beginUpdates()
    }

    func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {

    print("move \(indexPath!) -> \(newIndexPath!)")

    /*
    根据数据的不同type,进行区分操作
    - Insert
    - Delete
    - Update
    - Move
    */
    switch type {
    case NSFetchedResultsChangeType.Insert:
    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
    case NSFetchedResultsChangeType.Delete:
    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
    case NSFetchedResultsChangeType.Update:
    tableView.cellForRowAtIndexPath(indexPath!) as! TeamCell
    case NSFetchedResultsChangeType.Move:
    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)

    // tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)

    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
    default :
    break
    }
    }

    func controllerDidChangeContent(controller: NSFetchedResultsController) {
    // 结束更新
    tableView.endUpdates()
    }

    Note that the order and nature of the methods ties in very neatly to the “begin updates-make changes-end updates” pattern used to update table views. This is not a coincidence! 这是按照官方文档指导的按照“开始-更新-结束”的顺序。注意insert时由于不存在指定index所以要newIndexPath的

    controllerWillChangeContent -> didChangeObject, atIndexPath, forChangeType type -> controllerDidChangeContent

    This delegate method is similar to didChangeObject… but notifies you of changes to sections rather than to individual objects.还有一个代理方法。类似于didChangeObject方法,只是关注的是section的改变,比如现在有一只全新的队伍在一个全新的分组(或许是火星队😛)之下,这个代理就有用了

    如果注意细节的话,在Demo中还有一个默认不启用的addItem,显然这是为我大国足准备的,因为要是等国足堂堂正正的踢进世界杯,就想殖民火星一样现实,所以我们傲娇的设置了一个后门,用来将我大国足保送到世界杯,使用摇一摇即可激活

    1
    2
    3
    4
    5
    6
    // MARK: 震动激活添加按钮 -> 中国队冲击世界杯专用
    override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) {
    if motion == UIEventSubtype.MotionShake {
    addButton.enabled = true
    }
    }

    然后添加方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @available(iOS 8.0, *)
    // 国足走后门专用方法
    @IBAction func addTeam(sender: AnyObject) {
    let alert = UIAlertController(title: "天朝特色", message: "目前看来中国足球进世界杯只能用这种手段了", preferredStyle: UIAlertControllerStyle.Alert)

    alert.addTextFieldWithConfigurationHandler { (textField) -> Void in
    textField.placeholder = "Team name"
    }
    alert.addTextFieldWithConfigurationHandler { (textField) -> Void in
    textField.placeholder = "Qualifying Zone"
    }

    alert.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.Default, handler: { (action) -> Void in
    print("--开始保送国足--")
    // 1
    let nameTextFeild = alert.textFields![0]
    let zoneTextFeild = alert.textFields![1]
    // 2 新插入一个实体--当然就是万年进不了的国足了
    let team = NSEntityDescription.insertNewObjectForEntityForName("Team", inManagedObjectContext: self.coreDataStack.context) as! Team
    team.teamName = nameTextFeild.text!
    team.qualifyingZone = zoneTextFeild.text!
    team.imageName = "wenderland-flag"
    // 3
    self.coreDataStack.saveContext()
    }))

    alert.addAction(UIAlertAction(title: "cancel", style: UIAlertActionStyle.Cancel, handler: { (action) -> Void in
    print("被cancel,果然上帝都要放弃国足了🙀")
    }))

    presentViewController(alert, animated: true, completion: nil)
    }

    代码比较简单不在赘述,总之为国足提供了一个社会主义优越性的后门,并单独开一个区就能保证晋级了

    感谢强大的代理,让我们可以做到只要原始数据更改,就自动触发刷新数据,而不管原始数据是怎样变化的

    …国足…🙏

CoreData(三)

CoreData and Swift by YongSir🇨🇳🇨🇳

NSFetchRequest is the multi-function Swiss army knife of the Core Data framework!

这一部分,将集中介绍coreData中fetch的内容,在前边所有的fetch基本都是一个套路:create an instance of NSFetchRequest, configure it and hand it over to NSManagedObjectContext,很简单,事实上有4种持有请求的方式,为了不显突兀,都列举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1
let fetchRequest1 = NSFetchRequest()
let entity = NSEntityDescription.entityForName("XXX", inManagedObjectContext: managedObjectContext)
fetchRequest1.entity = entity!

// 2
let fetchRequest2 = NSFetchRequest(entityName: "XXX")

// 3
let fetchRequest3 = managedObjectModel.fetchRequestTemplateName(“peopleFR")

// 4
let fetchRequest4 = managedObjectModel.fetchRequestFromTemplateWithName("peropleFR", substitutionVariables:["NAME" : "Ray"])

其实区别并不像想象的大和突兀,对于request来说,跟重要的在于特定的一些配置,让我们不写SQL语句也能操作数据。使用DemoBubbleTeaFinder来具体说明吧


首先是创建项目,添加coreData Stack等等的准备工作,好在已经完成了
先介绍一种最直接的request方式,就是利用Xcode的data editer添加requset,然后再通过代码关联,用fetchRequest承接:

1
2
// 关联editor的request -- 通过model + Xcode辅助
fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest")

这种方式要注意的是:

  1. 它直接从NSManagedObjectModele,通过借助Xcode辅助而来,使用异常简单;
  2. 缺点在于形式固定(A drawback of stored fetch requests is that there is no way to specify a sort order for the results.),不能多余的配置,所有常用于需要反复多次相同的request时,这种情况很少遇到。如果执意要使用,那出现这种错误就不要奇怪了:
1
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify a named fetch request in an immutable model.'

如果只是单纯的以为request只是单纯的获取数据,那就大错特错了,它的功能不止如此 :You can use it to fetch individual values, compute statistics on your data such as the average, minimum and maximum, and more.它拥有一个可以规定返回类型的属性NSManagedObjectResultType ,有四种类型:

  • NSManagedObjectResultType: Returns managed objects (default value).
  • NSCountResultType: Returns the count of the objects that match the fetch request.
  • NSDictionaryResultType: This is a catch-all return type for returning the results of different calculations.
  • NSManagedObjectIDResultType: Returns unique identifiers instead of full- fledged managed objects.

接下来就使用countResultType,Demo中的Filters界面,主要功能是按照给定的标记归档分类,给出当前最符合条件的店址,其中的PRICE部分,是根据价格分类的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  // 使用CountResultType方式,返回一个只包含总数目的数组
// 当然你也可以返回所有的[Venue],在得出数量,但当数据巨大时,
// CountResultType方式就是一种高效的选择(is more memory- efficient)
func populateCheapVenueCountLable() {
let fetchRequest = NSFetchRequest(entityName: "Venue")
fetchRequest.resultType = NSFetchRequestResultType.CountResultType
fetchRequest.predicate = cheapVenuePredicate
// 执行请求
do {
let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSNumber]
// 从数组中取出
let count = results[0].integerValue
firstPriceCategoryLabel.text = "\(count) bubbke tea places"
}catch let error as NSError {
print("未能成功获取\(error) ,\(error.userInfo)")
}
}

需要注意的是,NSCountResultType会返回一个数组“[12]”,但这个数组只含有一个元素,此外不会将所有项目都加载,在处理大量数据时,是一种高效的得到数量的方式。

在Offering a deal条目中,我们想要得到交易数目,也就是JSON中各个不同店铺的specialCount字段下的值的和,当然把所有的店铺信息全部取出来然后通过一个for循环去求和是可以的,但考虑到大量数据的情形就不行了,所以coreData为我们提了NSDictionaryResultType这种类型,会直接返回我们想要的计算后的结果, has built-in support for a number of different functions such as average, sum, min and max.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  func populateDealsCountLabel() {

// 1 使用DictionaryResultType方式--即是以NSDictionary的格式组织返回 的数据
let fetchRequest = NSFetchRequest(entityName: "Venue")
fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType

// 2 创建NSExpressionDescription,并命名
let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"

// 3 截取specialCount字段,并做sum计算
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "specialCount")])
sumExpressionDesc.expressionResultType = NSAttributeType.Integer32AttributeType

// 4 配置请求:propertiesToFetch属性赋值
fetchRequest.propertiesToFetch = [sumExpressionDesc]

// 5 执行请求 + 更新UI
do {
let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSDictionary]
// 实际上会返回一个包含一个字典的数组 [{sumDeals = 12;}]
let resultDict = results[0]
print(results)
let numDeals: AnyObject? = resultDict["sumDeals"]
numDealsLabel.text = "\(numDeals!) total deals"
}catch let error as NSError{
print("未能成功获取折扣数量\(error),\(error.userInfo)")
}
}

其中,需要注意的是,需要创建和配置NSExpressionDescription,设置propertiesToFetch属性,执行请求返回的结果是一个包含一个字典的数组[{sumDeals = 12;}]。事实上,由于代码的不直观,除非处于性能上的考虑,这样的用法并不常见:

Fetching a calculated value from Core Data requires you to follow many, often unintuitive steps, so make sure you have a good reason for using this technique— like performance considerations.

至少现在已经用过了3种方式(好吧,其实是2种),还有最后一种NSManagedObjectIDResultType,会返回一组符合符合条件的查找到的托管的数据“[ID…]”,就像数据库中的id一样。在ios5之后的并发技术引入以后,这种方式就更少见到了,所以基本可以忽略。

When you fetch with this type, the result is an array of NSManagedObjectID objects rather the actual managed objects they represent. An NSManagedObjectID is a compact, universal identifier for a managed object. It works like the primary key in the database!


在补全Files中的剩余条目之前,先来思考一个问题:

You’ve gotten a taste of all the things a fetch request can do for you. But just as important as the information a fetch request returns is the information it doesn’t return. For practical reasons, you have cap the incoming data at some point.

Why? Imagine a perfectly connected object graph, one where each Core Data object is connected to every other object through a series of relationships. If Core Data didn’t put limits on the information a fetch request returned, you’d be fetching the entire object graph every single time! That’s not memory efficient.

所以必须要对获取的请求加以限制,好在我们有很多种手段:
There are ways you can manually limit the information you get back from a fetch request. For example, NSFetchRequest supports fetching batches. You can use the properties fetchBatchSize, fetchLimit and fetchOffset to control the batching behavior.

Yet another way to limit your object graph is to use predicates, as you’ve done to populate the venue count labels above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lazy var offeringDealPredicate: NSPredicate = {
var pr = NSPredicate(format: "specialCount > 0")
return pr
}()

lazy var walkingDistancePredicate: NSPredicate = {
var pr = NSPredicate(format: "location.distance < 500")
return pr
}()

lazy var hasUserTipsPredicate: NSPredicate = {
var pr = NSPredicate(format: "stats.tipCount > 0")
return pr
}()

同时补全tableViewDelegate中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  //MARK: - UITableViewDelegate methods
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath)!
// 不同的cell,对应不用的predicate
switch cell {
// PRICE section
case cheapVenueCell:
selectedPredicate = cheapVenuePredicate
case moderateVenueCell:
selectedPredicate = moderateVenuePredicate
case expensiveVenueCell:
selectedPredicate = expensiveVenuePredicate
// POPULATE section
case offeringDealCell:
selectedPredicate = offeringDealPredicate
case walkingDistanceCell:
selectedPredicate = walkingDistancePredicate
case userTipsCell:
selectedPredicate = hasUserTipsPredicate
default:
print("default case")
} cell.accessoryType = UITableViewCellAccessoryType.Checkmark
}

借助代理传回到主控制器,控制刷新主界面的venue,这样就完成了按照指定要求获取对应数据的目的--也就是“查找”功能


接下来更进一步,在现实中,在查找之余能排序的需求更加广泛,所以coreData也为我们准备了方便的工具NSSortDescriptor,并且其用法跟单纯负责查找的NSPredicte是分类了,这里我们仍旧首先做lazy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//MARK: - lazy SortDescriptor

// 按姓名排序
lazy var nameSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "name", ascending: true, selector: "localizedStandardCompare:") // 就是要画蛇添足怎的
return sd
}()

// 按距离排序
lazy var distanceSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "location.distance", ascending: true)
return sd
}()

// 按假期排序
lazy var priceSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "piceInfo.priceCategory", ascending: true)
return sd
}()

To initialize an instance of NSSortDescriptor you need three things: a key path to specify the attribute by which you want to sort, a specification of whether the sort is ascending or descending and an optional selector.

NSSortDescriptor的实例话我们需要指明3个东西:key--表示对谁排序,布尔的ascending--指定降序还是升序,以及可选的selector--针对其他特殊要求。不使用可选的selector时,系统会默认调用localizedStandardCompare 方法做一些刚刚够用的排序,如果有特殊要求,那就自己实现selector吧!

此外额外的补充一点儿:coreData中的NSSortDescriptor和NSPredicate,都不支持在处理数组等集合中常见的的Block式的API,这是出于这样的考虑:排序和查找都是SQLite层级的处理,需要系统能快速的变成SQL语句。

The reason is related to the fact that filtering/sorting happens in the SQLite database, so the predicate/sort descriptor has to match nicely to something that can be written as an SQLite statement.

OK,还是说回来,继续完成排序功能,去添加排序实例然后并通过代理传递到主控制器。let’s go down to didSelectRowAtIndexPath and add the following cases to the end of the switch statement:

1
2
3
4
5
6
7
8
9
// SORT section
case nameAZSortCell:
selectedSortDescriptor = nameSortDescriptor
case nameZASortCell:
selectedSortDescriptor = nameSortDescriptor.reversedSortDescriptor as? NSSortDescriptor
case distanceSortCell:
selectedSortDescriptor = distanceSortDescriptor
case priceSortCell:
selectedSortDescriptor = priceSortDescriptor

截止目前为止,功能部分实现了,但是有一个坏消息是目前的request都是在主线程,这显然是一个优质应用应该避免的,现在的demo运行起来并不卡是应为过于简单,所以请求的异部化是必然的。


Asynchronous fetching

正是考虑到主线程占用的问题,所以在iOS8之后,苹果提供了一种新的请求: NSAsynchronousFetchRequest,看起来就像是对旧的NSFetchRequest一个异步的包装,因为它的实例化需要普通的NSFetchRequest,在下面的代码中就能看出,对应于异步请求,出现异步结果 NSAsynchronousFetchResult就是很自然了,同时还需要有主线程的回调,等不及了直接上代码,在主控制器的ViewDidLoad中替换原请求为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 1 创建请求
fetchRequest = NSFetchRequest(entityName: "Venue")

// 2 异步请求 + 完成回调更新UI
asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest, completionBlock: {
(results: NSAsynchronousFetchResult) -> Void in
self.venues = results.finalResult as! [Venue]
self.tableView.reloadData()
})
// 3 执行请求
do {
let results = try coreDataStack.context.executeRequest(asyncFetchRequest)
let persistentStoreResults = results
}catch let error as NSError {
print("未能成功获取\(error),\(error.userInfo)")
}

但如果此时编译运行会出现这样的错误:

1
NSConfinementConcurrencyType context <NSManagedObjectContext: 0x7f8eb3c130f0> cannot support asynchronous fetch request <NSAsynchronousFetchRequest: 0x7f8eb3e229e0> with fetch request <NSFetchRequest: 0x7f8eb3ea9580>.

实际上普通的NSManagedObjectContext并不能支持异步的请求方式,所以还有其他2个步骤需要变更

  • first,是coreDataStack 中的managedContext的实例话时,需要设置指定的并发类型:
1
2
3
4
5
6
7
8

// 指定并发类型的上下文实例
/*
NSManagedObjectContextConcurrencyType.ConfinementConcurrencyType // 默认
NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType // 多线程
NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType // 主线程并发
*/
context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)

有3种类型供选择,默认的并发类型是“.ConfinementConcurrencyType”,但 The problem is that this concurrency type is an old pattern that is all but deprecated. 还有 “.PrivateQueueConcurrencyType"类型,是在多线程中的使用,先往后放。如果此时运行,就会报这样的错误:

fatal error: unexpectedly found nil while unwrapping an Optional value

  • second,这个错误是swift编程中的老面孔,原因就是尝试强制解包一个可选的类型,如果你自认为已经可以做到很注意类型的话,可能就能猜到原因,可惜在并发场景下我没能注意到,Since the original fetch request is asynchronous, it will finish after the table view does its initial load. The table view will try to unwrap the venues property but since there are no results yet, your app will crash.看来在swift的并发之中,需要长这样一个“心眼”。更改如下:
1
var venues: [Venue]! = []

You fix this issue by initializing venues to an empty array. This way, on first load, if there are no results yet, your table view will simply be empty.


Batch Updates:no fetch request

Sometimes, the only reason you fetch objects from Core Data is to mutate an attribute. Then, after you make your changes, you have to commit the Core Data objects back to the persistent store and call it a day. This is the normal process you’ve been following all along.
通常,从coredata获取对象的唯一理由是想更改其的一个属性,并在更改之后添加到上下文,再commit固化之,这是最常见的流程。

But What if you want to update a hundred thousand records all at once? It would take a lot of time and a lot of memory to fetch all of those objects just to update one attribute. No amount of tweaking your fetch request would save your user from having to stare at a spinner for a long, long time.
但是如果考虑一下场景:如果你需要一次性更新10万条纪录,这样量级的数据会费大量的时间和内存去请求所有的对象,所以调整请求是必须的。

幸运的是在iOS8之中,苹果提供了一种新的不需要请求和加载到内存的批量更新的方式,新技术允许越过上下文(NSManagedObjectContext)直接进行固化操作,let’s see this in practice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MARK: 批量无请求

func batchNoFetch() {
// 批量请求:指明更新属性 指明作用存储 指明结果类型
let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = ["favorite": NSNumber(bool: true)]
batchUpdate.affectedStores = coreDataStack.psc.persistentStores batchUpdate.resultType = NSBatchUpdateRequestResultType.UpdatedObjectsCountResultType

// 执行 - 返回NSBatchUpdateResult
do {
let result = try coreDataStack.context.executeRequest(batchUpdate) as! NSBatchUpdateResult

print("更新记录:\(result.result)")
}catch let error as NSError {
print("未能批量更新\(error),\(error.userInfo)")
}
}

使用起来还是相当方便的,指明操作属性,作用存储和设置类型着老一套,只是结果就是 NSBatchUpdateResult了,当然如果你叫板移动端没有这么大规模的数据那我也没办法了

AutoLayout的一些经验

AutoLayout by YongSir🇨🇳🇨🇳

-----玩儿AutoLayout所得-----

  • 遇到很难布局的情况,不要忘了使用辅助视图,特别是对某些系统提供紧密的UI的时候

如想为UINavigationBar添加一个代头像和title的titleView时,因为titleView本身只有width可设置,那么如何保证正好内容剧中呢?当然给你的内容得到父视图的宽度,首先是想到设置futherVIew的Huging/Compression - Priority,但是无论如何都是不行的,原因即在于系统默认提供的navigationItem.titleView是设置Autosizingmask的,所以这时就要借助一个辅助性的containerView了,image和titleLb都加到containerView上,在设置containerView的center等于titleViewcenter,就不用去管如何自适应width的为题了,总之: 在怎么设置都不行的情况下,记的考虑添加只约束centerX 和 centerY 的辅助试图, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

private let titleView: UIView = {
let view = UIView(frame: CGRectMake(0, 0, 100, 40))
return view
}()
private let titleLb: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let userImageView: UIImageView = {
let view = UIImageView()
view.layer.cornerRadius = 20.0
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()

override func viewDidLoad() {
super.viewDidLoad()
notificationsKeyboard()

setUpNavBar()
setUpView()
}

private func setUpNavBar() {

let continarView = UIView()
continarView.translatesAutoresizingMaskIntoConstraints = false
continarView.addSubview(userImageView)
continarView.addSubview(titleLb)
titleView.addSubview(continarView)

// debuge
titleLb.text = "JASdfasfasdON"
userImageView.image = UIImage(named: "demoAlbum")?.resizeWithWidth(40.0)

let viewsDict = ["userImageView" : userImageView,
"titleLb" : titleLb,
"continarView" : continarView]
let vflDict = ["H:|[userImageView(40)]-5-[titleLb]|",
"V:|[userImageView(40)]|"]
continarView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vflDict[0] as String, options: .AlignAllCenterY, metrics: nil, views: viewsDict))
continarView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vflDict[1] as String, options: [], metrics: nil, views: viewsDict))
titleView.addConstraint(NSLayoutConstraint(item: continarView, attribute: .CenterX, relatedBy: .Equal, toItem: titleView, attribute: .CenterX, multiplier: 1.0, constant: 0))
titleView.addConstraint(NSLayoutConstraint(item: continarView, attribute: .CenterY, relatedBy: .Equal, toItem: titleView, attribute: .CenterY, multiplier: 1.0, constant: 0))

navigationItem.titleView = titleView

let item: UIBarButtonItem = UIBarButtonItem(title: "...", style: .Plain, target: self, action: #selector(settingBtnClicked))
navigationItem.rightBarButtonItem = item
}
  • 发现在设置跟父空间关系时,如果想用centterX/Y来约束,还是用系统那个繁琐的方法比较便利
    所以即便是拥有VFL,也不能忘了恶心的原装方法,比较合理的是这俩结合着用
    单单是设计高度和宽度时,选用恶心方法

  • 要注意通过VFL添加的约束本身就是NSArray,这和原装的返回NSLayoutContaint不同
    在addContaint/s 时要注意,如果对应不正确,就会有这样的错误:

    Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid parameter not satisfying: [constraint isKindOfClass:[NSLayoutConstraint class]]’

  • 注意约束的添加,绝大多数都是添加到当前的super,两个View之间有但很少,如果添加错误就可能会报
    这样一 坨错误:

    The view hierarchy is not prepared for the constraint:
    When added to a view, the constraint’s items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.

    以及

    1
    2
    3
    4
    5
    6
    View hierarchy unprepared for constraint.
    Constraint: <NSLayoutConstraint:0x7fb853f23910 YRViewOfUnlock:0x7fb853f218b0.leading == UIView:0x7fb853f12a70.leadingMargin>
    Container hierarchy:
    <YRViewOfUnlock: 0x7fb853f218b0; frame = (0 0; 0 0); layer = <CALayer: 0x7fb853f21bd0>>
    View not found in container hierarchy: <UIView: 0x7fb853f12a70; frame = (0 0; 375 667); autoresize = RM+BM; layer = <CALayer: 0x7fb853f11d70>>
    That view's superview: NO SUPERVIEW
  • 如果约束都添加了而没有任何效果,要注意检查autoSize( translatesAutoresizingMaskIntoConstraints)是否关闭了
    为了避免这样的错误,将AutoLayout的使用流程化:

    1. 创建元素,关闭 translatesAutoresizingMaskIntoConstraints
    2. 添加到父空间
    3. 设置约束
      否则就是这样的错误:
    1
    2
    3
    Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
    Unable to interpret '|' character, because the related view doesn't have a superviewV:|-top-[btn(80)]
    ^'

CoreData(二) - custom coredata stack programmlly

CoreData and Swift by YongSir🇨🇳🇨🇳

上一篇中,已经体验了CoreData的基本使用,可以通过Xcode设计数据模型,可以对数据查询,增加,更改,但还不知道它的真正工作机制,而这将是这一篇的主要内容

Core Data Stack,the stack is made up of four Core Data Classes:

1
2
3
4
* NSManagedObjectModel - 托管对象模型
* NSPersistentStore - 持久化存储
* NSPersistentStoreCoordinator - 持久化存储协调器
* NSManagedObjectContext - 托管对象上下文

我们已经和上下文NSManagedObjectContext打过交道了,可想而知其他3个是相对底层的依赖在支援者上下文,所以还是老规矩借助一个Demo开始吧。

what we do later

Rather than rely on the default starter template, you’ll build your own Core Data stack: a customizable “wrapper” around these classes that works as a unit.

一个名叫”dog-walk”的小Demo,它可以保存你遛狗的时间日期并展示到tableView。不同的是,我们不再依赖Xcode的自动模版。


不过再次之前,先要隆重寄出这4个关键类

  • the mamaged object model

    NSManagedObjectModel represents each object type in your app’s data model, the properties they can have, and the relationship between them. Other parts of the Core Data stack use the model to create objects, store properties and save data.
    NSManagedObjectModel表征应用的数据模型中每个对象的类型,以及对象包含的属性和各对象的关联关系,CoreData的其他部分使用model创建对象,保存属性和数据。

  • the persistent store

    NSPersistentStore reads and writes data to whichever storage method you’ve decided to use. Core Data provides four types of NSPersistentStore out of the box: three atomic and one non-atomic.

    NSPersistentStore读写数据的方式,coreData提供了四种方式:

    1
    2
    3
    4
    NSQLiteStoreType (default + nonatomic,backed by SQLite)
    NSXMLStoreType (readable + atomic, blacked by XML File, but have a large memory footprint, so available on OS X)
    NSBinaryStoreType (just like XML, large memory cost + atomic,blacked by a binary data file)
    NSInMemoryStoreType (in-memory not really persistent, terminate disappears)
  • the persistent store coordinator

    NSPersistentStoreCoordinator is the bridge between the managed object model and the persistent store. It is responsible for using the model and the persistent stores to do most of the hard work in Core Data. It understands the NSManagedObjectModel and knows how to send information to, and fetch information from, the NSPersistentStore.
    NSPersistentStoreCoordinator also hides the implementation details of how your persistent store or stores are configured. This is useful for two reasons:

    NSManagedObjectContext (coming next!) doesn’t have to know if it’s saving to an SQLite database, XML file or even iCloud.
    If you have multiple persistent stores, the persistent store coordinator presents a unified interface to the managed context. As far as the managed context is concerned, it always interacts with a single, aggregate persistent store.

    NSPersistentStoreCoordinator负责桥接托管对象模型和持久化储存,负责model的使用和固化存储等艰难的工作,推断出“向NSPersistentStore发送信息”或者“从NSPersistentStore获取信息”等行为。NSPersistentStoreCoordinator还隐藏了怎样持久化存储的配置的具体实施细则。这样一来

    上下文不需要关心数据固化的类型,不论是SQLite,XML或者其他类型
    如果是多种类型混合使用,协调器就为上下文提供一个便于管理的统一接口

  • the managed object context

    NSManagedObjectContext is an in-memory ‘scratchpad’, do all works within it, and any changes won’t affect the underlying data on disk until you call save() on the context.
    上下文是一个内存中的暂存器,所有的工作都要用到它,任何变动在save()到上下文之前是不会影响到硬盘上的数据的。它是如此的重要请务必注意所以下特性:

    1. The context manages the lifecycle of the objects that it creates or fetches. This lifecycle management includes powerful features such as faulting, inverse relationship handling and validation.
      上下文管理着对象创建和获取的整个生命周期 -包含一些强大的特性如错误,逆向关系以及验证。

    2. A managed object cannot exist without an associated context. In fact, a managed object and its context are so tightly coupled that every managed object keeps a reference to its context, which can be accessed like so:

      1
      let employeeContext = employee.managedObjectContext

      托管对象不能独立于其上下文而单独存在,他们是如此紧密的耦合,以致于我们约定以这样的命名和代码来获取它。

    3. Contexts are very territorial; once a managed object has associated with a particular context, it will remain associated with the same context for the duration of its lifecycle.
      上下文是非常自卫的(领土的,真不好翻译😓),一旦托管对象指定了一个上下文,那么在这个对象的整个生命周期都将和先前指定的上下文捆绑关联。

    4. An application can use more than one context—most non-trivial Core Data applications fall into this category. Since a context is an in-memory scratch pad for what’s on disk, you can actually load the same Core Data object onto two different contexts simultaneously.
      一个应用程序可以拥有不止一个上下文,许多优秀的应用都使用这一特性。因为它仅仅只是磁盘上内容在内存中的一个暂存,所以你大可以同时通过不同的上下文加载同一份CoreData对象。

    5. A context is not thread safe. The same goes for a managed object—you can only interact with contexts and managed objects on the same thread in which they were created. Apple has provided many ways to work with contexts in multithreaded applications. You’ll read all about different concurrency models in Chapter 10, “Multiple Managed Object Contexts.”
      上下文不是线程安全的,同样托管对象也不是,所以你正能在创建它们的同一线程中与他们交互。Apple也提供了很多种上下文在不同线程工作的机制,后面会介绍到。

总之NSManagedObjectContext是一个强大,牛气,专业,忠诚而又脆弱的一片内存暂存。


Now it’s time to return to Dog Walk and implement your own Core Data stack.

首先,建立模型和lazy Core Data Stack

3个主要的步骤 + 设置磁盘固化配置NSPersistentStore:

关联 .momd(也就是.xcdatamodeld)文件的NSManagedObjectModel
创建一个托管对象的协调器NSPersistentStoreCoordinator
创建上下文NSManagedObjectContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

import CoreData

class CoreDataStack {
// 四大关键类
let context: NSManagedObjectContext
let psc: NSPersistentStoreCoordinator
let model: NSManagedObjectModel
let store: NSPersistentStore?

// 初始化和配置每个组件
init() {

//1 加载“磁盘上的被托管对象的模型(.momd文件)”到“托管对象模型(NSManagedObjectModel)”
// 可以简单的比对理解为从 “.xcdatamodeld[xib]”读取“model...[视图]“
let bundle = NSBundle.mainBundle()
let modelURL = bundle.URLForResource("Dog Walk", withExtension: "momd")
model = NSManagedObjectModel(contentsOfURL: modelURL!)!

//2 使用托管对象模型,创建协调器(NSPersistentStoreCoordinator)
psc = NSPersistentStoreCoordinator(managedObjectModel: model)

//3 创建上下文(NSManagedObjectContext),关联协调器
context = NSManagedObjectContext()
context.persistentStoreCoordinator = psc

//4 创建持久化方式(NSPersistentStore),并不是直观初始化而是借助协调器配置 && 错误处理
let documentsURL = CoreDataStack.applicationDocumentsDirectory()
let storeURL = documentsURL.URLByAppendingPathComponent("Dog Walk")
let options = [NSMigratePersistentStoresAutomaticallyOption: true]
do {
store = try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options)
} catch let error as NSError {
print(__FUNCTION__ + "持久化数据出错\(error)")
abort() // C 库函数 void abort(void) 中止程序执行,直接从调用的地方跳出
}
}

// 保存添加到上下文的更改 && 错误处理
func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch let error as NSError {
print("没能保存!\(error)")
abort()
}
}
}

class func applicationDocumentsDirectory() -> NSURL {
let fileManager = NSFileManager.defaultManager()

// returns a URL to your application’s documents directory
// 返回App文件路径的url,指定数据存储的路径,不管你是否要使用CoreData都是必须的
let urls = fileManager.URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask)
return urls[0]
}

}

这是一种方法,当然最推荐的还是官方文档中的方法,对比可明显看出官方版本要严谨很多,甚至可以直接套用。

然后:

  • 在AppDelegate中lazy coreData,然后获取上下文传递到VC中以便于使用。
  • Xcode创建“.xcdatamodeld”,注意文件命名要和代码中一致。

添加entity:Dog 和 Walk,设置属性和依赖关系,It’s OK to leave this relationship as a to-one relationship. A dog can have many walks, but a walk can only belong to one dog—for the purposes of this app, at least. :]

另外可以到graph editor中去看看,like this:

其次,添加模型对象子类

还是借助Xocde在保证打开cordite editor的基础上,选择creat NSManagedOject SubClass…2个都选,发现生成4个文件,Dog 和 Dog+CoreDataProperties 和 Walk也一样(Xcode7 + swift2.0),如果是原来的话,还有很重要的一步,要将类名关联,将类名关联,将类名关联!
但是那样会出现错误,因为Xcode7会自动关联,还有添加@objc( XXX )什么的也不必了

然后,我们就开始愉快的使用啦

  • 基础功能:首先是在 viewDidLoad() 中InsertDog实体,思路就是“ 首先获取所有叫“Fido”的实体,如果得到空数组[],表示这是第一次使用App

    首次使用,需要用户添的dog命名为“Fido””,然后替换声明currentDog属性,
    在添加BTN按下之后,为currentDog添加Walk,然后reloadData:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /* 增加溜狗 */
    @IBAction func add(sender: AnyObject) {

    // 添加Walk到CoreData
    let walkEntity = NSEntityDescription.entityForName("Walk", inManagedObjectContext: managedContext)
    let walk = Walk(entity: walkEntity!, insertIntoManagedObjectContext: managedContext)
    walk.date = NSDate()

    // 添加walk到Dog的walk集合
    let walks = currentDog.walks?.mutableCopy() as! NSMutableOrderedSet
    walks.addObject(walk)
    currentDog.walks = walks.copy() as? NSOrderedSet

    // 保存上下文
    do {
    try managedContext.save()
    print("ok")
    } catch let error as NSError {
    print("添加walk失败\(error)")
    }

    // 刷新UI
    tableView.reloadData()
    }

    这样就基本完成了App的功能。这就是CoreData关联对象带来的便利之处,并没有写复杂的字典转模型之类的,而是借助系统完成

  • 删除数据。截止到目前,我们已经可以完成“增 查 改”的功能,而删除功能自然是没有理由缺失的,所以看看coreData的删操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
    }

    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

    if editingStyle == UITableViewCellEditingStyle.Delete {

    // 待删除walk
    let walkToRemove = currentDog.walks![indexPath.row] as! Walk // 从上下文删除
    managedContext.deleteObject(walkToRemove)

    // 保存上下文
    do {
    try managedContext.save()
    print("ok")
    } catch let error as NSError {
    print("添加walk失败\(error)")
    }

    // 刷新UI
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
    }

    借用tableView的左划出delete,然后commit时完成删除操作,核心方法在于 managedContext.deleteObject(walkToRemove)对上下文的操作

    至此 coreData的增删查改都已经有了,不管是利用Xcode还是手动创建stack都搞定了,如果只是简单的使用可以到此就告一段落了

CoreData(一) - 初试,人生若只如初见

CoreData and Swift by YongSir🇨🇳🇨🇳

这是这本书Core Data by Tutorials的读书笔记,大大的封面
大大的封面

section one

First,why we need coreData?It matters data persistence.

是啊,市场上有SQLRealm这些简单易用的东西,谁还需要coreData
但是coreData比你想象的强大得多,最大的优势在于是苹果的亲儿子,全平台通用;而那些动则用其效率和操作的,我就补充一段Marcus Zarra大神的回答:

First, my opinion on third-party code is well known: all code sucks. I think Realm is trying to solve a problem that is the incorrect problem to solve. They’re trying to be faster than Core Data, whereas Core Data is trying to be fast enough, but maintainable. In my playing and working with Realm, I find that the amount of code you write is about equal. … As a project leader or a developer, I want maintainability and consistency. My big concern with third-party frameworks is that they go away. It happens over and over again. We don’t know how long Realm’s going to be here. I don’t understand their business model. Core Data for me is good enough; it’s mature, it’s been around long enough, and it’s fast enough. If it’s not fast enough, I’m probably doing something wrong because I’m in object space anyway. There’s a lot of unknowns about Realm.

CoreData的意义在于数据的更灵活更高效的存储和处理,能够将数据固化时最起码的需求。干巴巴的学习没有任何意思,我们就从这个基本的需求入手,一个联系人的Demo,利用coreData实现信息从内存到本地的固化,并从本地读取。引述书中的要求:

When you tap the Home button, the app that’s currently in the foreground goes to the background. When this happens, the operating system flash-freezes everything currently in memory, including the strings in the names array. Similarly, when it’s time to wake up and return to the foreground, the operating system restores what used to be in memory as if you’d never left.

what we really want?
to persist the names you enter so they’re available for viewing after a fresh app launch.

- The first step is to create a managed object model

Since you elected to use Core Data when you created the HitList project, Xcode automatically created a data model file for you and named it HitList.xcdatamodeld.

  • An entity is a class definition in Core Data. The classic example is an Employee or a Company. In a relational database, an entity corresponds to a table.

  • An attribute is a piece of information attached to a particular entity. For example, an Employee entity could have attributes for the employee’s name, position and salary. In a database, an attribute corresponds to a particular field in a table.

  • A relationship is a link between multiple entities. In Core Data, relationships between two entities are called to-one relationships, while those between one and many entities are called to-many relationships. For example, a Manager can have a to-many relationship with a set of employees, whereas an individual Employee will have a to-one relationship with his manager.

As you’ve probably noticed, entities sound a lot like a classes. Likewise, attributes/relationships sound a lot like properties. What’s the difference? You can think of a Core Data entity as a class “definition” and the managed object as an instance of that class.

首先,需要创建托管对象模型NSManagedObjectModel,就是指定义在.mom中的所有实体,操作中将我们的数据模型添加到它之中

基本的概念:
entity实体 : 理解为数据库中的表,也就是一种类,如一个Class Employee或者Class Company
attribute属性 : 理解为数据库表中的字段,也就是特定实体的具体信息,如一个Employee实体包含name,position,salary等属性
relationship关系: 就是不同实体的关系,在coreData中,两个实体间叫有一对一关系,如果一个实体和多个实体之间,就称之为多对一关系。比如一个manager和employee之间就是对多关系,而一个employee 和manager就是对一的关系。

- Next, replace the table view’s model

1
2
3
4
// var names = [String]()
// Change “names” to “people” and [String] to [NSManagedObject]
// 使用强大的NSManagedObject实例,来替代简单的string
var people = [NSManagedObject]()

NSManagedObject represents a single object stored in Core Data—you must use it to create, edit, save and delete from your Core Data persistent store. As you’ll see shortly, NSManagedObject is a shape-shifter. It can take the form of any entity in your data model, appropriating whatever attributes and relationships you defined.

  1. Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can think of a managed object context as an in-memory “scratchpad” for working with managed objects.
  2. You create a new managed object and insert it into the managed object context. You can do this in one step with NSManagedObject’s designated initializer: init(entity:insertIntoManagedObjectContext:).
  3. With an NSManagedObject in hand, you set the name attribute using key-value coding. You have to spell the KVC key (“name” in this case) exactly as it appears on your data model, otherwise your app will crash at runtime.
  4. You commit your changes to person and save to disk by calling save on the managed object context. Note that save takes one parameter, which is a pointer to an NSError; if there is ever an error with the save operation, you can inspect the error and alert the user if necessary.
  5. Congratulations! Your new managed object is now safely ensconced in your Core Data persistent store. Insert the new managed object into the people array so that it shows up in the table view when it reloads.

其次,使用托管对象 [NSManagedObject] 集合来替换tableView数据模型(这里的names)
托管对象NSManagedObject,就是我们拿来操作数据的基本单位。对应下来:

  • 增加一条数据是在NSManagedObjectContext新建一个NSManagedObject。
  • 查找数据是查找NSManagedObject。
  • 修改是修改NSManagedObject类的属性。
  • 删除是从NSManagedObjectContext里面删除NSManagedObject类。

最后我们保存NSManagedObjectContext,然后一直向上传递到磁盘上面去,才是持久化的修改。总之,模型对象的数据被持有在NSManagedObject对象中。每一个NSManagedObject对象都对应一个实体(就像每一个对象都有一个类)。

而对于NSManagedObject的操作又引出一个叫NSManagedObjectContext的东东,这是是我们经常使用到的一个类。对于为什么要有这个类,因为数据库的IO操作是很费时的,因此把一系列的操作缓存到了一个内存区域,等待合适的实际在去写入真实的磁盘中。这样大大的提高效率。比如插入一条数据,然后修改数据,最后删除掉这条数据,如果是每次都执行commit的话是操作三次IO,如果我们把这三条合并在一起commit的话就只有一次commit。这样能有效的提高整个系统的效率。NSManagedObjectContext就是为提供IO效率而在内存中的“暂存”。同时提前指出使用Core Data需要注意的是:NSManagedObjectContext并不是线程安全的,更多相关讨论先往后放。

- Fetching from CoreData

LoadData from coreData

  1. before you can do anything with Core Data, you need a managed object context. Fetching is no different!
  2. Setting a fetch request’s entity property, or alternatively initializing it with init(entityName:), fetches all objects of a particular entity. This is what you do here to fetch all Person entities.
    NSFetchRequest is the class responsible for fetching from Core Data.
    Fetch requests have several qualifiers that refine the set of results they return.
  3. You hand the fetch request over to the managed object context to do the heavy lifting. executeFetchRequest(_:error:) returns an optional array of managed objects that meets the criteria specified by the fetch request.

获取已保存的数据:

  • 先要得到上下文
  • 设置fetchRequest
  • 根据请求从上下文获取数据并做数据处理

section two

就创建一个小小的BowTie管理的demo,就是管理你的不同颜色的领结,UI和功能如下:
segumentController - 可选颜色
name - 名称
rante - 评分,从0-5
times - 佩戴次数
lastWorn - 上次佩戴的日期
Favorite - 是否最爱
wear - 增加佩戴次数和更新佩戴日期
rate - 评分允许你重新评分

OK,What you have to do now is take this sample data, store it in Core Data and use it to implement the bow tie management functionality.

Firstly ,create your data model

在BowTies.xcdatamodeld中操作,添加Bowtie实体,同时配置属性,为属性选取对应的数据类型,便利的可视化操作问什么不呢……^^


Core Data also provides the option of storing arbitrary blobs of binary data directly in your data model. These could be anything from images to PDF files, or anything else that can be serialized into zeroes and ones.

如果注意到每个领结都是有对应image 的,事实上coreData也支持定义二进制数据,所以可以添加一个photoData属性,类型选为BinaryData,但是这样便利的结果就是会带来急剧的消耗,根据SQLite中的使用,即便只想要一个name,内存中也会全部加载database中的内容,这样的消耗是不可接受的。

Luckily, Core Data anticipates this problem.我们只需要对photoData属性进行配置Allows External Storage,如图:

The Allows External Storage option is only available for the binary data attribute type.

When you enable Allows External Storage, Core Data heuristically decides on a per-value basis if it should save the data directly in the database or store a URI that points to a separate file.当选取允许外部存储时,CoreData直接预判是否应该保存数据到database亦或是转而储存一个指向特定文件的URL。

所以,目前说来 coreData 支持常见的所有基本的数据类型:In sum, besides strings, integers, doubles, Booleans and dates, Core Data can also save binary data, and it can do so efficiently and intelligently.

但仅仅是这样还不够,因为更多时候我们需要能够存储自定义的类型,一般就是自定的对象,比如Demo中每个领结都有颜色,而基本类型中是没有UIColor的,Once again, Core Data has your back,在Attributes的Type中有个Transformable类型,CoreData允许所有遵循NSCoding协议的对象都可以存储。正如UIColor (UIColor conforms to NSSecureCoding, which inherits from NSCoding, so it can use the transformable type out of the box),添加tintColor:

New, data model is now complete. The Bowtie entity has the eight attributes it needs to store all the information in SampleData.plist.

secondly, managed object subclasses

在section1中的demo中,托管对象使用的是KVC给Person实体的name属性赋值,like this:

1
2
// 使用KVC向属性(attribute)赋值
person.setValue(name, forKey: "name")

你可以使用KVC但并不意味着应该用它,因为KVC的缺点也很明显,就是它的一切都依赖于字符串,The biggest problem with key-value coding is the fact that you’re accessing of data using strings instead of strongly-typed classes. This is often jokingly referred to as writing stringly typed code.

As you probably know from experience, “stringly typed” code is vulnerable to silly human errors such as mistyping and misspelling. Key-value coding also doesn’t take full advantage of Swift’s type-checking and Xcode’s auto-completion. :]

最好的替代方式就是在数据模型中为每个实体创建NSManagedObject子类,这就意味会存在一个着包含所有对应属性的Bowtie类,使用Xcode来生成吧

Make sure you still have Bow_Ties.xcdatamodeld open, and go to Editor\Create NSManagedObject Subclass…. Select the data model and then the Bowtie entity in the next two dialog boxes, then select Swift as the language option in the final box. If you’re asked, say No to creating an Objective-C bridging header. Click Create to save the file.

Bowtie看起来就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import CoreData
/*
Similar to @dynamic in Objective-C, the @NSManaged attribute informs
the Swift compiler that the backing store and implementation of a property
will be provided at runtime instead of at compile time.

The normal pattern is for a property to be backed by an instance variable
in memory. A property on a managed object is different: It’s backed by the
managed object context, so the source of the data is not known at compile time.
*/
class Bowtie: NSManagedObject {
// bool,double,int -> NSNumber
@NSManaged var isFavorite: NSNumber
@NSManaged var lastWorn: NSDate
@NSManaged var name: String
@NSManaged var rating: NSNumber
@NSManaged var searchKey: String
@NSManaged var timesWorn: NSNumber
@NSManaged var photoData: NSData // 二进制-> NSData
@NSManaged var tintColor: AnyObject // 可变类型-> AnyObject
}

如果不想使用NSNumber,更加紧准的类型映射需要在创建类时勾选Use scalar properties for primitive data types in the last dialog,虽然这里暂时不勾选

还有很重要的一点,你需要链接Bowtie到数据模型编辑器的Bowtie实体。(脑补StoryBoard 与 view的链接)

Next to Class, replace Bowtie with Bow_Ties.Bowtie. This is the fully specified class name for Bowtie.swift. This last step links the runtime Bowtie class to the Bowtie entity in the data model editor.

Congratulations, you’ve just made your first managed object subclass in Swift! Compared with key-value coding, this is a much better way of working with Core Data entities. There are two main benefits:

  1. Managed object subclasses unleash the syntactic power of Swift properties. By accessing attributes using properties instead of key-value coding, you again befriend Xcode and the compiler.

  2. You gain the ability to override existing methods or to add your own convenience methods. Note that there are some NSManagedObject methods you must never override. Check Apple’s documentation of NSManagedObject for a complete list.

Third ,propagating a managed context

传递上下文到控制器,毕竟老是用appdelegat是比较烦的,so pass the managed context from class to class via a property.

在控制器中,从plist中获取数据,and insert all the bow tie data you had in SampleData.plist into Core Data

1
2
// 从plist获取数据,然后插入到data
func insertSampleData()

想看看这两种特殊类型是怎么保存的,如下:

1
2
3
4
// image转变为NSData保存
let photoData = UIImagePNGRepresentation(image)
// 颜色直接用UIColor保存 - 一个专门由RGB返回颜色的方法
bowtie.tintColor = colorFromDict(tintColotDict)

这里涉及到一个类似SQL中WHERE语句的查找的类,NSPredicate,用法简单避免了你去学习火星的正则表达式,比如要在firstName中查找’Bob’:

1
[NSPredicate predicateWithFormat:@"firstName = 'Bob'"];

当然这里仅仅只是查询满足“ searchKey ! = nil”(拥有searchKey)的数量,没有用到筛选功能:

1
2
3
4
 // 获取data中已存在数据的数量
let fetchRequset = NSFetchRequest(entityName: "Bowtie")
fetchRequset.predicate = NSPredicate(format: "searchKey ! = nil")
let count = managedContest.countForFetchRequest(fetchRequset, error: nil)

ok,保存搞定了,Now you need to access the data from somewhere!

所以开始获取初始数据吧!这一步我们要完成
This is where you fetch the Bowties from Core Data and populate the UI. 在ViewDidLoad中添加获取的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 2 从coreData获取数据
let request = NSFetchRequest(entityName: "Bowtie")
let firstTitle = segmentedControl.titleForSegmentAtIndex(0)
print("要查找seg的title[0]: \(firstTitle)")
// 3 根据segment的tilte配置请求,然后执行查找相应数据
request.predicate = NSPredicate(format: "searchKey == %@", firstTitle!)
var error: NSError? = nil
var results = managedContest.executeFetchRequest(request, error: &error) as! [Bowtie]?
// 4 根据获取结果的[Bowtie]集合,展示UI & 错误处理
if let bowties = results {
populate(bowties[0])
}else {
print("未能获取\(error),\(error!.userInfo)")
}

populate方法就是将按照searchKey获取到的数据放入[Bowtie]集合后,负责展示到UI上,就是把该显示的UI的数据装进去,大部分没有技术含量,只挑选以下几点,需要注意的是数据的格式需要转化,如NSNumber到Bool:

1
2
3
4
5
6
// 图片 - 由二进制提供
imageView.image = UIImage(data: bowtie.photoData)
// 颜色依旧是颜色
view.tintColor = bowtie.tintColor as! UIColor
// NSNumber -> bool
favoriteLabel.hidden = !bowtie.isFavorite.boolValue

然后运行,看到这样:

现在充分证明我们对CoreData数据的存,取,查都是成功的,下面来完善其他功能,为了能给当前显示的领结进行评分等操作,我们需要一个属性用以纪录当前领结,so:

1
2
// 当前显示领结
var currentBowtie: Bowtie!

然后就是实现和添加wear和rate功能了,完成之后发现一个问题,就是rate我们想规定中0-5,但是直接跑到了6,通常都是考虑在代码中输入时去提示限定,但有个更简单的办法,就是利用Xcode对rate做些限定处理:

需要额外说明的是,绝对不要轻易version 模型设置,特别是在你已经发布版本之后,因为更新时很可能会带来严重的问题,但Attribute validation is one of the few exceptions.更多相关讨论先往后放。
当再次输入不再范围的数据时,就会有错误提升:

Optional(“R”)currentBowtie更新rate后,未能保存Optional(Error Domain=NSCocoaErrorDomain Code=1610
“The operation couldn’t be completed. (Cocoa error 1610.)”

根据错误对应的处理也是必须的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var error: NSError?

if !managedContest.save(&error) {
/*
如果输入的评分过大或过小,评分无效

*/
print("currentBowtie更新rate后,未能保存\(error),\(error!.userInfo)")
if error!.code == NSValidationDateTooLateError || error!.code == NSValidationDateTooSoonError {


rate(currentBowtie)

}
}else {
populate(currentBowtie)
}

这样如果输入的评分不符合预期,过大或者过小评分无效就是了

还剩下一点儿根据不同segumentTitle匹配不同数据的功能就不再赘述了,留一点儿练手的机会自己慢慢玩儿吧…

scrollView的一些经验(二)--自动布局的正确姿势

利用storyBoard尝试自动布局ScrollView

实际测试如下:

1 scrollView + imageView(Sizefit)


失败!

2 imageView修改:只有leading和TopSpace,然后再代码中添加打印:

1
2
[self.view layoutIfNeeded]; // 此行之前的layout被计算
NSLog(@"设置了imageView后 %@", self.scrollView);

失败!again

原因分析:通过打印可看出,上述两种方法都无法给出contentSize

1
2
2015-03-11 11:27:32.577 ScrollTagDemo[2393:259138] 只是设置了scrollView的约束,<UIScrollView: 0x7aaf1a00; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x788a1750>; layer = <CALayer: 0x78689bb0>; contentOffset: {0, 0}; contentSize: {0, 0}>
2015-03-11 11:27:32.579 ScrollTagDemo[2393:259138] 设置了imageView后 <UIScrollView: 0x7aaf1a00; frame = (50 130; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x788a1750>; layer = <CALayer: 0x78689bb0>; contentOffset: {0, 0}; contentSize: {0, 0}>

3 imageView再修改,先使用代码:

1
2
self.scrollView.contentSize = self.imageView.bounds.size;
NSLog(@"设置了contentSize的frame之后,%@", self.scrollView);

success!

此时的打印contentSize:

1
2015-03-11 11:52:11.289 ScrollTagDemo[2460:266559] 设置了contentSize的frame之后,<UIScrollView: 0x7b885c00; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7b06a2e0>; layer = <CALayer: 0x7b064680>; contentOffset: {0, 0}; contentSize: {759, 966}>

所以原因的确是contentSize的问题,这种方式就是官方给出的所谓混合解决方式,但在layout中设置frame可是不常见的,在我看来这有悖于layout的实质,可用但值得商榷,那么就继续探索能够使用约束给出contentSize的方法。
再次修改imageView:

失败,again!

按理说,给定img的宽高,就应该有contentSize了,但遗憾的是木有😢

1
2015-03-11 12:01:10.255 ScrollTagDemo[2525:271602] 设置了contentSize的frame之后,<UIScrollView: 0x7ca72600; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7c1926d0>; layer = <CALayer: 0x7c18cd80>; contentOffset: {0, 0}; contentSize: {0, 0}>

4 imageView再修改,这次在第二次的基础上尝试:

success!it works!

但是对比先前的

仅仅是把Trainling跟Bottom变成了0,0而已,it works!

由此可见:subView的约束是根据contentSize计算而来的,而不是scroll自己!
但是但是,打印的结果仍然是:

1
2015-03-11 12:10:35.446 ScrollTagDemo[2556:281194] 设置了contentSize的frame之后,<UIScrollView: 0x7b075600; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7aa2a2f0>; layer = <CALayer: 0x7a8795f0>; contentOffset: {0, 0}; contentSize: {0, 0}>

肯定是打印的地方不对,到底在哪儿呢?再试试,通过自定义view,猜测是laySubViews,重写之后结果验证如下:

1
2
3
4
5
6
7
8
2015-03-11 13:49:47.407 ScrollTagDemo[2846:353070] 设置了imageView后 <UIScrollView: 0x7bbaf000; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7c145be0>; layer = <CALayer: 0x7b62da80>; contentOffset: {0, 0}; contentSize: {0, 0}>
2015-03-11 13:49:47.407 ScrollTagDemo[2846:353070] 设置了contentSize的frame之后,<UIScrollView: 0x7bbaf000; frame = (190 190; 220 220); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7c145be0>; layer = <CALayer: 0x7b62da80>; contentOffset: {0, 0}; contentSize: {0, 0}>
2015-03-11 13:49:47.412 ScrollTagDemo[2846:353070] -------------->
2015-03-11 13:49:47.413 ScrollTagDemo[2846:353070] timeOfLayout 1
2015-03-11 13:49:47.413 ScrollTagDemo[2846:353070] scrollView.contentSize:{0, 0}
<!-- 2015-03-11 13:49:47.413 ScrollTagDemo[2846:353070] --------------> -->
2015-03-11 13:49:47.413 ScrollTagDemo[2846:353070] timeOfLayout 2
2015-03-11 13:49:47.413 ScrollTagDemo[2846:353070] scrollView.contentSize:{759, 966}

在整个viewDidLoad中都没有计算contentSize,而是在结束之后由系统去view的 layoutSubviews方法中计算的<在iOS8之前是要自己去加上一句的>!

5 再测试:

imageView再添加约束,让其的w和h 跟 scrollView的w和h 相等

also worked!
打印输出:

1
2
3
4
5
6
7
8
2015-03-11 15:52:42.841 ScrollTagDemo[3154:467930] -------------->
2015-03-11 15:52:42.841 ScrollTagDemo[3154:467930] timeOfLayout 1
2015-03-11 15:52:42.884 ScrollTagDemo[3154:467930] scrollView.frame:{{250, 190}, {100, 220}}
2015-03-11 15:52:42.884 ScrollTagDemo[3154:467930] scrollView.contentSize:{0, 0}
2015-03-11 15:52:42.885 ScrollTagDemo[3154:467930] -------------->
2015-03-11 15:52:42.885 ScrollTagDemo[3154:467930] timeOfLayout 2
2015-03-11 15:52:42.885 ScrollTagDemo[3154:467930] scrollView.frame:{{250, 190}, {100, 220}}
2015-03-11 15:52:42.885 ScrollTagDemo[3154:467930] scrollView.contentSize:{759, 966}

发现根本 scrollView.frame就没有变化,但是这明显与通常理解的矛盾,明明已经相等而数值不同,其实这就是系统针对scrollView做的特殊处理,其实并没有真正去加上这个约束,加的其实是:

finally

这就是scrollView的完整约束!这也就是在官方文档中所提到的“by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview”

将这个所谓相等的约束引用更改,就能结合代码更改contentSize

脑洞:既然结果显示w和h可以设置为“>=”,是否意味着可以自己来设置这样的约束对方,没实验,不过估计即便可以也不会有人去这样设置的👌!……^_^

scrollView的一些经验(一)

scrollView的一些经验(一)

scrollView,默认不会滚动

因为:

1
2
var contentOffset: CGPoint // default CGPointZero
var contentSize: CGSize // default CGSizeZero

scrollView的layout

scrollView的layout,如果只设置其subViews为leading/trailing/top/bottom的话,是没有效果的也不会滚动,因为默认是没有contentSize的,如果想用纯sb的话,你需要制定一个确定大小的辅助性view(有width和height),只有这样才能自动计算contentSize,正如:

用 UIScrollView 和它 subview 的 leading/trailing/top/bottom 来互相决定大小的时候,会出现「Has ambiguous scrollable content width/height」的 warning正确的姿势是用 UIScrollView 外部的 view 或 UIScrollView 本身的 width/height 确定 subview 的尺寸,进而确定 contentSize。因为 UIScrollView 本身的 leading/trailing/top/bottom 变得不好用,所以我习惯的做法是在 UIScrollView 和它原来的 subviews 之间增加一个 content view。

其实官方对这些问题已做了说明:

设计scrollView的核心便是对于content 的scroll方式的展示,这就意味着content size是最重要的,而自己本身的bounds与content size没有任何关系。所以理论上,只要所加的约束能最终计算出content size就没有问题。

所以常常在scrollView中率先添加一个辅助性的contentView,然后再在辅助性的视图上添加下一层级的视图,对比衍生自scrollView的tableView和collectionView,我们甚至可以推断一条这样的结论:对于scrollView 的直接子试图最好只有一个!
官方的小demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)viewDidLoad {

UIScrollView *scrollView;
UIImageView *imageView;
NSDictionary *viewsDictionary;

// Create the scroll view and the image view.
scrollView = [[UIScrollView alloc] init];
imageView = [[UIImageView alloc] init];

// Add an image to the image view.
[imageView setImage:[UIImage imageNamed:"MyReallyBigImage"]];
// Add the scroll view to our view.
[self.view addSubview:scrollView];
// Add the image view to the scroll view.
[scrollView addSubview:imageView];

// Set the translatesAutoresizingMaskIntoConstraints to NO so that the views autoresizing mask is not translated into auto layout constraints.
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.translatesAutoresizingMaskIntoConstraints = NO;

// Set the constraints for the scroll view and the image view.
viewsDictionary = NSDictionaryOfVariableBindings(scrollView, imageView);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];

/* the rest of your code here... */
}

猜测是:通过img给出了imgV的W和H,下一篇就用storyboard来试试