工程师的懒惰

懒惰被视为七宗罪之一,是个贬义词,但在一定场景下,懒惰变成了褒义词:在我看来,懒惰才是人类进步的关键,正是因为懒,才创造出各种各样的工具来提升效率。人们懒得走路,发明了自行车,后来懒得蹬车,就发明了汽车,最近连开车都懒得开了,于是出现了自动驾驶汽车。对于工程师而言,懒惰也分两种,这两种类型的懒惰会使工程师的成长出现截然不同的道路。

有利的懒惰

有利的懒惰是指讨厌重复而低效的任务,自己懒得做,就让工具做,将重复任务自动化。有利的懒惰能够极大地提高效率,节约时间。下面举几个例子:

CocoaPods

CocoaPods 是开发 OS X 和 iOS 应用程序时一个第三方库的依赖管理工具。在 CocoaPods 出现之前,需要添加一个第三方库需要以下操作:

  1. 下载第三方库的代码。
  2. 将第三方库的代码引入工程,并添加第三方库所需的 Framework。
  3. 解决库与库之间、库与工程之间的依赖关系,检测重复添加的 Framework。
  4. 如果第三方库有更新,需要将库从工程中删除,并重复上面的步骤。

哦买噶,这些重复繁琐的工作能把人烦死,有些“懒惰”的工程师无法忍受这种情况,于是 CocoaPods 出现了,它能够自动下载配置文件中指定的第三方库,处理库与库之间的依赖关系,并通过新建一个 xcworkspace 的方式将第三方库同工程连接起来。哈利路亚,感觉整个世界清净了。

ARC 与 Block

ARC 为什么会出现呢?因为在 MRC 下每次都要 retain/release 真是太麻烦了,而且还容易不配对导致内存泄露,估计苹果的工程师都写烦了,既然编译器能够识别出对象的生命周期,那就让编译器去做内存管理吧,简单省事。有人可能不放心把内存管理交给编译器,你放心,在识别对象生命周期这件事上,编译器比你厉害,再厉害的开发者也有可能因为一时疏忽而遗漏,但编译器不会。另外,会有人认为 ARC 会影响性能,这其实是不理解 ARC 的原理:ARC 不是垃圾回收,只是自动帮你写 retain/release,而且写 retain/relese 时不再经过消息传递,是直接调用对应的 C 函数,这会提升性能的。另外,对于工厂方法返回值,ARC 也会做优化,不再将返回的对象放入 AutoReleasePool 了,而是直接返回,相当于调 alloc + init。所以放心的使用 ARC 吧,这种提高效率的东西为什么不用?

Block 为什么会出现呢?在我看来,是因为在使用回调函数时,每次使用变量都要将变量整合到一个结构体中,用 void * 指针的形式传递给回调函数的 context 参数,真是太麻烦了。编译器既然能识别出在回调函数里使用了哪些变量,就自动地跟回调函数整合成为一个对象吧,这样在回调函数中就能直接使用了。

《iOS 开发进阶》中的脚本

唐巧《iOS 开发进阶》中让我印象最深的是实战技巧里的一些脚本,例如删除未使用的图片资源、检查图片长宽是否是偶数等,虽然都是些简单操作,但是能提升效率,感觉很棒。

我写过的一个自动部署工具

在中科院实习的时候,曾经负责开发维护一个嵌入式系统,代码是跑在一块 ARM 开发板上的,因此每次交叉编译过后需要通过 FTP 将包传到开发板上解压,并配置一下 rcS 启动脚本。开发阶段只是一块 ARM 板,手动部署就还好,后来变成了十五块 ARM 板,这下我不干了,手动部署会死人的,而且一旦程序有 Bug,就要重新部署一遍。于是“懒惰”的我写了一个自动部署工具,思路就是轮询目标 ARM 板的 IP 地址,针对每个 IP,先通过 FTP 将程序包上传,再通过 Telnet 输入解压程序包以及覆盖 rcS 启动脚本的指令,将整个过程自动化。由于懒得每次 IP 地址改变就重新编译程序,因此将 IP 地址、FTP 账户密码等信息从程序中抽出来,放到一个配置文件中,每次启动时读取(也算是一种依赖注入了)。同时也懒得每次 Telnet 输入的命令改变就重新编译程序,将 Telnet 要输入的命令也写到一个文本文件中,动态读取。写好了之后,手动部署估计要两个小时的活,一行命令搞定,感觉生活顿时美好了许多。

如果仔细观察,还有非常多的例子,其实要做到这一点与能力无关,与方向无关,与规模无关,只跟态度有关,包括个人与团队的态度。对于个人而言,遇到重复工作,是就这样低效地重复下去,还是思考用自动化提高效率?对于团队而言,是否给成员时间来完成一些能够提高效率的工具?工程师文化越是浓厚的团队,各种工具就越多,效率就越高。总之,人并不擅长做重复枯燥的工作,而这些工作恰恰是机器擅长的,想办法交给机器去做吧,遵循 DRY:

Don’t Repeat Yourself!

不利的懒惰

下面这些不利的懒惰会极大地妨碍我们成为优秀的工程师(在写下面的内容时,我也在不断反思自己,发现其实自己许多地方依然犯了懒惰的错误,边写边出汗,膝盖各种中箭。。。):

懒得搜索

我记得微博上有过一张亚一程 Laruence 一段群对话的截图,里面是这样说的:“不是我说你,这么简单的问题,你不 Google,不百度,来群里问,简直是舍近求远”。其实真正原因就是懒。在现在这个时代,搜索是无比强大的工具,想想看,世界那么大,就去搜一搜,你不会是第一个遇到问题的,也不会是最后一个遇到的,我觉得,Google + StackOverflow + Github + Dash 基本上能解决99%的问题。

我们经常会遇到搜索不到答案的过程,于是很多人就放弃了,回到了到处去问的老路上。其实搜索不到答案的原因有2点:1,我们没有正确描述以及抽象问题,找对关键字。2,我们没有用搜索引擎的思维思考。遇到搜索不到的情况时,不要放弃,努力思考如何修改关键字与描述,多试几次,虽然很痛苦,但痛苦说明我们的大脑在形成新的思维模式,一旦形成,我们的搜索会越来越准确,效率也会越来越高。

哦,对了,最后提醒一下,对于技术问题,还是避免使用百度吧,真的搜不到什么有用的东西。有人会说用 Google 还要科学上网,多麻烦,相比搜索到有效答案带来的收益,翻墙这点工作真不算什么,我们可是工程师啊,反思下是不是因为懒所以不愿意用 Google?

懒得思考

我们在学习一个知识的时候,要积极思考,不能死记硬背。一种框架/特性出现时,一定有它的原因,多想想为什么会出现?解决了什么样的问题?为什么要这样做?这样做的好处是什么?原理是什么?到底是如何实现的?保持强烈的好奇心,这会使我们不断发问,在回答问题时会不断思考,而只有不断的思考才能真正理解一个知识,从而能够更好的使用。

另外,我们在遇到问题后,往往会搜到解决方案只是简单的拷贝,不分析背后的原因,不分析解决方案会造成哪些影响。Bug 是那磨人的小妖精,这次不彻底搞清楚原因,下次它还会来烦我们,我们就会成为传说中的救火队长,哪里着火灭哪里,疲于奔命,但火却越灭越多。

懒得阅读

现在不是知识匮乏,而是知识爆炸,如果想学习,有太多的东西可以学了:

  • 书:iOS 的经典书籍,随便一本都能让人受益匪浅。
  • 博客:有太多优秀的博客了,那都是别人深思熟虑的精华,花了数个小时写出来的。
  • 文档:很多时候,StackOverflow 回答问题的方式就是贴上一段官方开发文档上的文字,或者接口 API 的说明,在看不到源码的情况下,官方文档可以看出源码的接口说明,很值得一读。用 Dash 或 Xcode 自带文档工具,遇到不清楚的点就去看一看。
  • 源码。Reading the Fucking SOURCE CODE 不是一句空话,源码之下无秘密。有些效果不知道怎么做,到 GitHub 上搜一搜,看懂了自己不就会了。

总之,Stay Hungry,Stay Foolish!

懒得动手尝试

看看这篇《Leveling Up》,纸上得来终觉浅,绝知此事要躬行,动手才是学习最有效的方法:

  • 在看别人教程时,把 Demo 下载,自己跑一跑,改改参数,或者自己尝试重新写一遍,效果绝对比只看要好。自己有疑问时或者有想法时,都可以写个 Demo 实验一下。
  • 在看 Objective-C Runtime 原理时,亲自用 clang -rewrite-objc file.m 将 .m 文件转成 .cpp 文件看一看。用 Associated Object 给 Category 加属性时都自己写段代码试一试。
  • 想看系统函数的调用情况,可以用 Method Swizzle 给一些系统方法加一些“装饰”,或者还可以用符号断点。没事干找台越狱手机用 Reveal 看看别人家的 App。

懒得改进优化

唯一不变的就是变化。代码在最初时由于业务简单一般都很不错,但往往在增加新需求/需求变化时开始出现坏味道,因为需求的变化经常导致大环境变化,而不同环境下的实现是不同的,例如网站支持100人访问与支持100000人访问是两种实现方式,控件只支持一行显示与支持多行显示也是两种实现方式。PM 有时候意识不到需求变化背后隐含的环境变化对技术实现的影响,觉得不就是简单的改一下,有什么难的?对啊,把大象装冰箱里也只需要三步,有什么难的?为了应对这些变化,工程师有时需要对结构进行调整,保证结构的灵活,在下次变化来临时更从容,这种调整就是重构。重构不是洪水猛兽,重构可以很大,也可以很小,一切在于时间点,修改的时间点越早,成本就越低,不然就会欠下技术债。在逻辑的世界里,只分对错,欠下的一定会还。不要为了一时便利而忽略了可持续性,切记技术债是高利贷,利滚利,拖得时间越久,成本就越高,到最后一定会连本带利让欠债者赔个精光。

因此工程师在实现需求时一定要留出 Buffer 来处理结构变化引起的重构与遗留代码带来的技术债,不能懒,这样以后的需求才会更好做。而团队在每个迭代中也应考虑将一些技术债与优化作为需求加入到需求池中,不然代码的坏味道就开始在工程中弥漫,需求越做越慢,Bug 越做越多,为了速度,开始拼命加班招人,效率却越来越低,进入恶性循环。

懒得总结

在我看来,经验从来不是比拼总时间,而是比拼效果。有些人多年经验却不如有些人一年经验,这是为何?关键在于总结。就拿圣斗士星矢来说,如果单论时间,他能当上青铜圣斗士都很勉强了,为什么他能打败黄金圣斗士,因为他说过:圣斗士不会败给同一招两次!犯错掉进坑里不要紧,谁没有犯过错?但掉进同一个坑两次就不太好了,而有效的经验能让我们以后不再犯相同或类似的错误。

如何克服这些问题

我仔细观察过一些优秀的 iOS 工程师,发现:

  • 每年都有 WWDC,大家都能看,但喵神 onevcat 总能写出高质量的笔记与总结
  • 同样是学 Objective-C,阳神孙源能玩出花来,挖掘出各种特性与原理。(我曾经在阳神的博客上问过一个问题,阳神告诉我他是通过反解汇编代码得出的,我就意识到自己犯了懒于尝试的错误了)
  • 动画狂魔叶孤城_与动画小王子 KITTEN-YANG 的动画屌炸天,不用问他们为何如此屌,去他们的 GitHub 上看看他们各种尝试动画的 Demo 就知道了。
  • 唐巧《iOS 开发进阶》让我收获最多的不是里面的知识,而是他学习与总结的方式,我不断的反思自己,我平时学习时,是否能像他一样总结出一本自己的 iOS 学习笔记。

还有许多优秀的 iOS 工程师这里就不一一举例了,我认为,这些优秀的 iOS 工程师并没有比你我聪明,跟我们一样只是普通人,但他们在上面这些事情上不懒惰,积极思考、尝试、总结,在同样的条件下收获多一点点,日积月累,于是他们变得优秀。不要小看这一点点,我们要相信积累的力量,水滴石穿啊,tinyfool 说过,这种积累所达到的层次,很难被人短时间追赶上,需要别人同样去积累,是非常具有竞争力的。

面对这些优秀的 iOS 工程师,我们经常会犯另一种懒惰的错误:我们总想加好友,攀交情,甚至用拉低姿态的方式,总觉得自己抱上大腿就能迅速成长,迅速变牛。这其实是种假象,是自己不自信不独立的表现。即使加了好友,他们能够答疑解惑,甚至手把手教,亲自帮忙解决问题又怎样,那还是别人的东西,自己没有任何成长,自己不就犯了懒惰的错误吗?

学习与成长从来没有捷径,也只能靠自己!

除了欣赏与钦佩这些优秀的人外,我觉得更重要的是默默地观察与努力,观察他们是如何成长的,学习他们的好习惯,努力提升自己,向他们看齐,当有一天达到了他们的水平之后,无需刻意培养,同他们自会相熟,因为优秀的人总是互相吸引互相欣赏,“臭味相投”,不是吗?

总之,借用《学 iOS 开发的一些经验》里的一段话来激励自己,努力成为一名“懒惰”而不懒惰的优秀工程师:

我觉得支撑我们不断探索和前进的动力不是兴趣,而是永不满足的好奇心,和对优雅代码的追求。

与技术无关的懒惰

最后提一下工程师非常常见的懒惰:懒得锻炼,不注意自己身体。在我看来,我们可以热爱编程,热爱自己的工作,热爱自己的事业,热爱自己的公司,但这些不是最重要的,最重要的是我们自己的身体与我们的家庭。为什么呢?因为对于公司而言,我们是可以替代的,无论我们多么牛,多么重要,少了我们,公司依然可以运作。但对于身体与家庭而言,我们是不可替代的!身体不是程序,不能重置,一旦身体坏了,就很难恢复,甚至继续恶化,伴随一生。一旦我们走了,我们的父母、配偶、子女就失去了唯一的我们,这带来的伤害与损失对于家庭而言是无法估量的,甚至为持续一辈子(看看那些失孤的老人,让人心碎啊)。孰重孰轻,我相信理智的工程师会做出自己的决定。

我这里不是说我们不要奋斗不要拼搏,这跟锻炼身体完全是互不影响的,锻炼身体甚至能让我们能够更好的拼搏与奋斗。我们不能学习现在敏捷的一些错误做法,只强调快,却忽略了可持续性(敏捷强调的是可持续性的快速迭代),所以不要拼一天的工作时长,留出点时间锻炼健身,我们都加过班,长时间加班加到后面其实脑子已经不够敏锐了,效率极低,对编程这种强脑力劳动是很不利的,易出问题,还不如回去跑跑步,早点休息,明天更高效完成就好了。有时候公司或团队的氛围就是不把工程师当人看,疯狂加班,幻想着靠十个女人怀孕一个月就能把孩子生出来,我觉得实在受不了就换一家吧,没什么大不了,开心健康地活着其实就是在赚钱。(医院才是真正的销金窟,钱到那个时候就是个数字,医院里充斥着痛苦、无助、绝望、麻木,唯独没有幸福,谁经历过谁知道)

另外,在工程师的眼里,既然一切都是逻辑,为什么不把自己的身体当做程序来调试与优化呢?一样的都有输入输出,对高热量食物防御式编程等,我曾经这样试过,成功减肥30斤,当我能够穿上一条许久都穿不上的裤子时,相信我,那种成就感比写100个牛逼程序或 GitHub 上有一个10000+ Star 的仓库都要强。

版权申明:我已将本文在微信公众平台的发表权「独家代理」给 iOS 开发(iOSDevTips)微信公众号。扫下方二维码即可关注「iOS 开发」:

Xcode升级后插件失效的原理与修复办法

Xcode 的插件大大丰富了 Xcode 的功能,而且有了 Alcatraz ,插件的管理也非常容易,像我这种 Vim 党完全离不开 XVim。但是有个非常恼人的问题:一旦升级 Xcode ,插件就失效!

之前 Xcode 升级到6.2的时候遇到过插件失效的问题,Google 之后把一段很长命令复制到 Terminal 后运行一下即可,当时一看解决了,顿时觉得满足感爆棚,自己可以拯救地球了~就没有再深入,结果升级到6.3时又遇到了。“同样的招式对圣斗士是不能使用第二次的!”,同样的坑对有节操的程序员是不能掉进去第二次的!因此这一次一定要搞清楚为什么会这样,以后再次遇到了如何解决。

问题原因

Xcode 的插件放置在 ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins 目录下,为 .xcplugin 格式。通过 Show Content 可以看到 xcplugin 中存在一个 Info.plist,其中有一项为 DVTPlugInCompatibilityUUIDs,而这就是插件失效的原因。

由于 Apple 没有公开插件开发的相关资料,这里我只能通过命名跟值猜测 DVTPlugInCompatibilityUUIDs 的作用:插件通过 DVTPlugInCompatibilityUUIDs 来指定能够运行此插件的 Xcode 版本。因此,DVTPlugInCompatibilityUUIDs 中存放的是 Xcode 版本对应的 UUID,Xcode 在启动加载控件时,将当前 UUID 同插件 Info.plist 中 DVTPlugInCompatibilityUUIDs 存放的 UUID 数组进行匹配,如果没有匹配项,说明此插件无法在该版本的 Xcode 运行,插件也就失效了。

解决办法

解决办法非常简单:将当前版本的 UUID 加到 DVTPlugInCompatibilityUUIDs 中即可。但是插件比较多(1个及以上)的情况下,一个个的打开修改非常无聊跟低效,作为“懒惰”的程序员,这时候就要用上命令行,让重复劳动自动化。思路为将命令分为两部分:

  1. 通过 find 命令在插件目录下找到所有插件的 Info.plist 文件。
  2. 通过 xargs 命令对上一步的搜索结果进行“for 循环”(就这样理解吧),针对每一个 Info.plist 文件,利用 defaults write 命令将当前版本的 UUID 加到 DVTPlugInCompatibilityUUIDs 中。

此时问题来了,挖掘机技术。。。不对,是如何获取当前版本 Xcode 的 UUID 呢?首先关掉 Xcode,打开 Terminal,输入 tail -f /var/log/system.log,再次打开 Xcode,就能看到如下 log 信息:

[MT] PluginLoading: Required plug-in compatibility UUID 9F75337B-21B4-4ADC-B558-F9CADF7073A7 for plug-in at path ‘~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/Alcatraz.xcplugin’ not present in DVTPlugInCompatibilityUUIDs

可以看到,log 信息表明 Xcode 加载插件失败的原因,并且能够看到当前版本(6.3)Xcode 的 UUID 为 9F75337B-21B4-4ADC-B558-F9CADF7073A7。经过 @Kyrrr 的提醒,有一种更好的方式来获取当前版本 Xcode 的 UUID:通过 defaults read 命令从 Xcode 的 Info.plist 读取 DVTPlugInCompatibilityUUID。

最终的命令为:

1
find ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins -name Info.plist -maxdepth 3 | xargs -I{} defaults write {} DVTPlugInCompatibilityUUIDs -array-add `defaults read /Applications/Xcode.app/Contents/Info.plist DVTPlugInCompatibilityUUID`

在 Terminal 中运行上述命令就解决了插件失效的问题,在插件 Info.plist 的 DVTPlugInCompatibilityUUIDs 中也能看到新增的 UUID 了。

理解contentsScale

最近在看《iOS CoreAnimation: Advanced Techiques》时,不太理解CALayer的contentsScale属性,在后面的CATextLayer中再次遇到,于是花功夫Google了一下各类文档,下面说说自己对contentsScale的理解,可能涉及的方面有些多:

Point与Pixel

iOS中的绘图系统使用的尺寸单位为Point,而屏幕显示的单位为Pixel,为什么要这样做呢?其实就是为了隔离变化:对于绘图而言,并不关心如何在屏幕上显示,这些属于硬件细节,也不应该关心,因此框架使用了万金油方法——抽象,绘图使用与硬件无关的Point,系统根据当前屏幕的情况自动将Point转成Pixel,所以不论以后硬件屏幕如何变化,使用Point的绘图系统以不变应万变,这就是抽象的好处!

对于非Retina与Retina的屏幕,Point与Pixel的转化关系如下:

  • 非Retina:1 Point = 1 x 1 Pixel
  • Retina:1 Point = 2 x 2 Pixel

系统通过一个变量来表示这种映射关系,这就是UIScreen中的scale属性的作用。需要注意的是,Retina屏幕1个Point对应4个Pixel,但scale为2,为什么不是4呢?因为在屏幕的二维空间中,一切可显示的物体都有X轴跟Y轴性质,Retina屏幕的映射是X跟Y两个方向同时放大2倍实现的,所以scale为2(这也是为什么上面写成2 x 2而不是直接为4的原因)。

CALayer与Render

在iOS中,如果想要显示什么,可能第一时间想到的就是UIView及其子类,但是UIView本身其实并不负责显示,从UIView从UIResponder继承可以看出,UIView的主要任务是响应触摸事件(在责任链中),那UIView是如何实现显示的呢?通过组合,将责任委托给成员变量:每个UIView都有一个Backing Layer,UIView将显示的任务就交给CALayer这个小弟啦,自己作为一个Wrapper,将CALayer一些比较复杂的操作封装成简单已用的接口,供外部使用。下图就是二者的关系图(直接用笔记中的手绘,见谅):

因此对纹理封装的CALayer才是显示的核心,CALayer的contents才指定了真正的要显示的内容,理解了这一点,下面就开始介绍正主contentsScale(现在才开始真是醉了),按照惯例,先看下官方文档,我截取了一段:

This value defines the mapping between the logical coordinate space of the layer (measured in points) and the physical coordinate space (measured in pixels). Higher scale factors indicate that each point in the layer is represented by more than one pixel at render time.
Core Animation uses the value you specify as a cue to determine how to render your content.

官方文档有两个关键点:

  1. contentsScale决定了CALayer的内容如何从Point映射到Pixel。
  2. contentsScale决定了CALayer的内容如何被渲染。

为了理解这两个关键点,就要介绍下CALayer内容的来源了。CALayer内容的来源有两种:

1. 通过Core Graphics自定义绘图到CALayer的Backing Store中

CALayer会根据contentsScale创建Backing Store,并且根据contentScale设置Context的CTM(Concurrent Transform Matrix)。例如,Layer的尺寸为10 Point x 10 Point,当contentsScale为2.0时,最后生成的Backing Store大小为20 Pixel x 20 Pixel,并且在将创建的Graphics Context传入drawLayer:InContext:前,会调用CGContextScaleCTM(context, 2, 2)进行放大,这样会使生成的内容自动满足屏幕的要求。

2. 将CGImage类型的图片设置为CALayer的contents

图片的尺寸是像素相关的,iOS在加载图片时,通过图片的名称进行处理。例如对于@2x的图片,在加载为UIImage时,会将scale设置为2,size为像素大小除以scale,这样就保证了UIImage在Retina屏幕上能够正常显示。但需要注意的是:将UIImage转换成CGImage时会丢失scale属性,使用CGImageGetWidth/Height时得到的是像素尺寸。

与Core Graphics生成内容不同的是,图片作为纹理已经被上传至GPU,CALayer不需要分配Backing Store,contentsScale会在渲染时起作用:对于Retina的屏幕,如果contentsScale为2.0,与屏幕的scale匹配,则渲染系统不对内容进行处理,如果contentsScale为1.0,说明Layer的内容并不匹配屏幕scale,渲染系统会对Layer的内容进行2倍的Scale操作。由于图片本身内容没那么多,于是渲染系统会填充像素,导致模糊。

总之,contentsScale描述了CALayer内容的Scale特性,CALayer在生成内容时会根据contentsScale做处理,并且渲染系统根据contentsScale进行渲染。另,《编写可读代码的艺术》中写到的“名字才是最好的注释”在这里得到很好的诠释,理解了contentsScale之后,再回过头看看命名,清晰准确,值得学习。

需要注意scale的地方

  1. CALayer的contentsScale默认为1.0,只有在使用Core Graphics在drawRect:中自定义绘图时系统才会根据当前屏幕的情况设置,因此以下情况需要设置合适的contentsScale
    • 直接设置CALayer的contents时。
    • 创建新的CALayer时。例如,CATextLayer在Retina屏幕时如果不设置contentsScale,所显示的文字就会模糊。
  2. 在使用Image Context时需要注意:UIGraphicsBeginImageContext以1.0的比例系数创建Bitmap,所以当屏幕为Retina时,在渲染时可能会显得模糊。要创建比例系数为其它值的图片,需要使用 UIGraphicsBeginImageContextWithOptions。

参考

SCRAnimationChain的设计思路

1. 出现原因

自从块(Block)方式的animateWithDuration:delay:options:animations:completion:方法出现之后,大家就使用此方法替换原来笨重且不清晰的旧方法来实现动画。但是在实现动画序列的时候,问题来了,看下面的代码:

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
[UIView animateWithDuration:0.25 animations:^{
self.imageView.alpha = 0.0;
} completion:^(BOOL finished) {
if (finished) {
[UIView animateWithDuration:0.25 animations:^{
self.headline.alpha = 0.0;
} completion:^(BOOL finished) {
if (finished) {
[UIView animateWithDuration:0.25 animations:^{
self.content.alpha = 0.0;
} completion:^(BOOL finished) {
if (finished) {
[UIView animateWithDuration:0.25 animations:^{
self.headline.alpha = 1.0;
} completion:^(BOOL finished) {
if (finished) {
[UIView animateWithDuration:0.25 animations:^{
self.content.alpha = 1.0;
}];
}
}];
}
}];
}
}];
}
}];

为了实现动画序列,需要在前一个动画的completion块中新建下一个动画,当动画比较多的时候,嵌套层数能把人看醉了,维护起来各种蛋疼。作为一个有重度代码洁癖的程(处)序(女)员(座),怎么能够忍受这样的代码?所以就想实现一种动画的容器,将嵌套关系转换成并列关系,利用递归来封装嵌套操作,将动画序列用简单清晰的方式管理起来。

2. 设计思路

在实现之前,按照经验,先去Github上找一找有没有别人已经做好的实现,没必要重新造轮子,而且不一定造出来的有别人的好(多么痛的领悟~)。既然是开源,把别人的看懂了不就相当于自己的嘛。而且,一般你想到的自己觉得很犀利的点子说不定人家已经做烂了(多么痛的领悟x2~)。果然,找到了下面两个库,于是我读了下他们的源码,分析如下:

  • UIKitAnimationPro:将动画抽象成Action,用类似Spirit Kit的样式提供动画序列,思路很棒,但是问题在于:
    • 思路是为某一个UIView指定动画序列,这样不是特别合理,因为动画序列很有可能包含多个UIView的动画。
    • 虽然将rotate,scale等操作封装起来方便使用,但是还是不够灵活,应该提供options的接口。
    • 只能串行,没有并行。
  • CPAnimationSequence:没有上述的缺点,已经是非常全面的实现了。但是在读源码的过程中发现里面使用了组合(Composite)模式,但是并没有用好。基类不是一个接口类(Component),而是一个具体的节点类(Leaf),容器类(Composite)继承节点类。这里不是教条主义,一定要按照设计模式要求的来,而是有以下的问题:
    • 由于Composite继承了Leaf,导致Composite中从Leaf继承下来一些跟Composite无关的数据,只好使用NSAssert来确保外部不会使用,这是不合理的。
    • 并没有将组合模式的优点发挥出来,导致代码不够清晰。

基于以上原因,我决定实现一个新的动画序列容器SCRAnimationChain,来解决上面所说的问题。为什么前缀是SCR呢?因为是我名字(尚传人)拼音首字母的大写,而且根据《Effective Objective-C》的建议,由于苹果宣称保留使用所有两字母前缀的权利,为了避免以后可能跟系统库的前缀重名,所以最好使用三个字母前缀。

思路如下:

  1. 由于动画的动作被封装在块中,所以将动画抽象成Action类(封装Block),并且包含delay、duration、options等属性,用来保持最大的灵活性。
  2. 参考GCD的思路,实现Sequence跟Concurrent两种Container类,分别用来针对串行动画跟并行动画的需求。
  3. 由于Container与Action,类似文件目录与文件的关系,所以用组合模式抽象出一个接口类(Protocol)来处理他们之间的关系。这样的好处就是只要实现了接口类,不管具体是什么都可以放置到Container中,灵活性跟可扩展性都很强。

3. 实现

项目源码在这里

SCRAnimationActionProtocol

接口类,相当于组合模式中的Component,在Objective-C中用协议描述。

  • (void)runWithCompletion:(SCRAnimationCompletionBlock)completion:动画的核心就是运行,而为了能够形成链,所以增加了completion参数用来形成链。
  • (NSTimeInterval)workTime:在并行动画中需要找到最终结束时间最长的那个动画,这样才能在并行动画中将动画链传递下去。
  • (void)addAction:(id<SCRAnimationActionProtocol>)action:用来供容器添加动画Action。

SCRAnimationAction

封装具体的动画,相当于组合模式中的Leaf,里面的属性基本上是为了在调用animateWithDuration:delay:options:animations:completion:的时候用。

这里主要说明下prepare属性的用途。由于动画形成了序列,某个动画在执行时有可能前面的动画执行的一些操作会影响到当前动画,因此需要在执行此动画之前提供一个接口供其配置各种信息。

例如,在使用AutoLayout之后不能直接frame了,而是要使用contraint,在动画的块中调用contraint的superview(注意不能是contraint依附的view)的layoutIfNeeded。在动画序列开始前更新contraint的话,有可能前面的动画提前调用了layoutIfNeeded,导致动画出问题。(在Demo的ViewController中,你可以试试将那段在prepare块中的代码移到块之外,看会出现什么情况)

SCRAnimationContainer

由于SCRAnimationSequence跟SCRAnimationConcurrence内部都有一个NSMutableArray来保存Action,所以将重复的代码抽出来,形成两者的基类。总之,DRY(Don’t Repeat Yourself)!

SCRAnimationSequence

串行动画的容器。利用递归的方式实现串行执行:在runWithCompletion:中每次从内部数组中取出一个项(由于是id<SCRAnimationActionProtocol>类型,所以可以是SCRAnimationAction,SCRAnimationSequence,SCRAnimationConcurrence或者实现SCRAnimationActionProtocol协议的任意类,这就是基于接口编程跟组合模式带来的灵活性),对其调用runWithCompletion:,在completion块中再次调用本身来实现递归。

SCRAnimationConcurrence

并行动画的容器。直接用for循环,对内部数组的每一个Action调用runWithWithCompletion:,找出并行动画中workTime(delay + duration)最大的那个动画,将SCRAnimationConcurrence的completion块交给这个动画,确保completion能够得到执行。

主题更换的设计思路

现在App类似桌面软件的趋势,在功能完善之后,渐渐追求个性化,以满足不同用户的审美,主题更换就是其中一项,像Weico微博客户端,UC浏览器。所谓主题,可以看成相同功能不同展现可视资源的集合,例如,按钮无论在什么主题下都需要背景图片这个资源,只是在不同主题下是不同的背景图片而已。

如何在iOS中实现主题更换的核心思路为:

  1. 资源按主题放置:相同功能的资源名称相同,放在不同的主题路径或者前缀使用主题名。
  2. 增加中间层,隔离不同主题相同功能资源使用的变化。

1. 主题管理

主题的特性导致代码不关心资源的表现是什么,只关心资源的功能,而主题是易变化的,因此需要将易变化的部分抽离出来,整合到一个管理者中,主题的变化在管理者中完成,而不影响资源使用的地方。而且这个管理者是全局唯一的,因此使用单例。

1
2
3
4
5
6
7
8
9
+ (ThemeManager *)sharedInstance
{
static ThemeManager *sharedInstance = nil;
if (sharedInstance == nil)
{
sharedInstance = [[ThemeManager alloc] init];
}
return sharedInstance;
}

主题中的资源使用plist进行存储,颜色的RGBA值跟字体的信息可以直接存入plist,而图片则可以存入图片的位置。按主题命名plist文件,ThemeManager的初始化跟主题更换就从main bundle中按主题名字读取对应的plist文件。

1
2
3
4
5
6
7
8
9
10
11
12
- (id)init
{
if (self = [super init])
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *themeName = [defaults objectForKey:@"theme"] ?: @"default";
NSString *path = [[NSBundle mainBundle] pathForResource:themeName ofType:@"plist"];
self.theme = [NSDictionary dictionaryWithContentsOfFile:path];
}
return self;
}

代码中不再是直接使用主题相关的资源,而是通过ThemeManager得到对应主题下的资源。

1
2
3
4
5
6
7
// 直接使用资源:
UIImage *image = [UIImage imageNamed:@"xxx_btn_background"];
// 通过主题管理器使用资源:
NSDictionary *theme = [ThemeManager sharedInstance].theme;
NSString *imageName = [theme objectForKey:@"xxx_btn_background"];
UIImage *image = [UIImage imageNamed:imageName];

上面的代码在使用时还是有些复杂,代码只关心资源的功能,不关系也不应该关心取资源的细节,因此应在ThemeManager对取资源进行如下封装:

1
- (UIImage *)imageForKey:(NSString *)key;

在使用主题中的资源时,代码就变成了:

1
UIImage *image = [[ThemeManager sharedInstance] imageForKey:@"xxx_btn_background"];

2. 资源的放置

当系统将主题相关的资源文件部署到ios设备中时,在默认情况下,系统会将所有的资源plat平铺拷贝到mainBundle目录下,即使你的资源是按文件夹来组织的。(我们可以在模拟器中查看Bundle的情况,模拟器的路径是:~/Library/Application Support/iPhone Simulator

因此,在将资源文件加入到工程时,不要选默认的”Recursively create groups for any add folders”,要选择“Create Folder Reference for any add folders”,这样才能保证资源文件按照原有文件夹的组织格式被拷贝到mainBundle中。

关于上述的两个选项,就涉及到Xcode的Group(黄色)跟Folder Reference(蓝色)的概念了,参见从别处摘抄来的理解:

XCode项目中的文件夹分成两类: group 和 directory reference, 分别是虚结构和实结构. 黄颜色的 group 是默认的格式, 它的结构和磁盘上的文件夹毫无关系, 仅仅表示资源的逻辑组织结构, 这在管理源文件是非常方便. 同一段代码可以被很多项目使用, 也可能只使用一个目录的部分文件, 它不需要被拷贝到当前项目中, 但可以在当前项目中保持一个清晰的逻辑结构. 而且引用头文件时不需要指明复杂的层次结构, 因为这些文件在XCode看来是 flat 的, 即它们处在同一层文件夹里.
但是 group 带来便利的同时也导致更加棘手的麻烦, 文件重名冲突问题; 尤其当你要使用上千个资源文件时, 这种问题已经极难避免; 而且, 资源文件一般是要拷贝到目标程序中的, 虽然它们在项目中可以有结构的组织, 但是复制到程序中时将会 flat 地输出到程序的根目录中, 这将是怎样的一个灾难! 同时, 如果你在外部向文件夹中加入了上百幅图片, 你不得不把它们再向xcode中加入一遍. 归根结底, 还要求助于我们传统的蓝色的 directory reference。

3. 主题更换通知

对于没有显示的界面,更换主题是不需要通知的,因为在取资源时是根据当前主题取的,但是对于正在显示的界面,更换主题时就需要进行通知,让界面重新取资源后再重绘。由于这类通知是全局性的,因此应该使用NSNotification实现通知机制。

在ThemeManager的changeTheme中调用[NSNotificaitonCenter defaultCenter]postNotificationName:object:发出通知,而在各个涉及到主题更换的ViewController中使用addObserver:selector:name:object:监听通知事件。

4. 总结

其实主题的设计思路跟类簇很像,例如对于NSNumber,不同类型的数据其实真正返回的是NSNumber相对于此类型的子类,但是对于NSNumber的使用者而言,其并不关心NSNumber返回的具体子类是什么,只要满足NSNumber定义的接口就行。设计总是类似的,针对易变化的部分,增加一个中间层(接口)将易变化的部分封装起来,提供给使用者稳定不易变的服务。

总之,OOP跟DesignPattern在我看来主要做了两件事:

  1. 隔离变化
  2. 封装细节

参考

关于使用CALayer中mask的一些技巧

CALayer拥有mask属性,Apple的官方解释如下:

An optional layer whose alpha channel is used to mask the layer’s content. The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

mask同样也是一个CALayer。假设将CALayer本身称为ContentLayer,将mask称为MaskLayer,蒙版(Masking)的工作原理是通过MaskLayer的alpha值定义ContentLayer的显示区域:对于ContentLayer上每一个Point,计算公式为ResultLayer = ContentLayer * MaskLayer_Alpha。所以当alpha为1时Content显示,alpha为0时Content不显示,其他处于0与1之间的值导致Content半透明。

需要注意的是:

  1. MaskLayer的color不重要,主要使用opacity(CALayer中的alpha),但是注意[UIColor clearColor]其实就是alpha为0的color。
  2. ContentLayer超出MaskLayer以外的部分不会被显示出来。
  3. MaskLayer必须是个“单身汉”,不能有sublayers,否则蒙版(Masking)的结果就是未知(Undefined)。

由于mask是一个CALayer,可以通过组合产生很多非常棒的效果。例如可以将MaskLayer指定为CAGradientLayer类型实现Gradient效果,可以给MaskLayer添加动画,下面两个例子就是这种用法的经典实例:

理解anchorPoint,position,frame的关系

对于UIView的frameboundscenter都比较熟悉:

  • bounds指定UIView自身坐标系。
  • center指定UIView在superview中的位置坐标。
  • frame由互不影响(正交)的boundscenter生成。

但是我一直有这样的疑问:为什么要使用UIView中心位置的center来指定坐标,为什么不用其他位置?

后来学习了CALayer,发现UIView的主要任务其实是响应事件(这就是为什么从UIResponer继承的原因),而将显示委托给CALayer处理。一个成功的UIView背后总有一个默默贡献的CALayer,CALayer作为GPU/OpenGL纹理的一种高层封装,处理显示的方方面面,UIView则将CALayer一些功能再次封装,提供简洁好用的接口。像UIView中的framebounds就是直接使用CALayer中的framebounds

但是对于center,CALayer对应项是position,为什么不同名了呢?因为position更灵活,谁规定“指定UIView在superview中的位置坐标”一定要在UIView的中心位置呢?!而position默认在中心位置的目的我觉得是为了方便Rotation,因为一般Rotation都是绕着中心旋转,UIView为了简化使用,满足大部分情况即可,所以就将默认在中心的position封装成了center

由于CALayer的position并没有限制一定要在bounds的中心位置,所以就需要一个属性来描述positionbounds中的位置,这样才能推算出frame的origin点位置。于是anchorPoint出现了,CALayer用anchorPoint指定positionbounds中的位置,有如下特点:

  • 为了可以任意指定位置,因此anchorPoint就不使用绝对值,而是比例值。
  • 由于Rotation总是围绕position旋转,因此指定position在Layer中位置的anchorPoint就被命名为锚点(像船的锚一样固定位置)。

总之,frame是由positionboundsanchorPoint共同生成(其实还有transform,这就需要了解Current Transform Matrix的概念,为了减少复杂性,这里就不涉及了),公式如下:

  • frame.origin.x = position.x - bounds.size.width * anchorPoint.x
  • frame.origin.y = position.y - bounds.size.height * anchorPoint.y

这就解释了:

  1. 为什么anchorPoint改变会导致frame改变,而position却没变?
  2. 为什么anchorPoint要使用比例而不是具体值?

豁然开朗。

PS. 多思考,保持好奇心,多从设计者角度想想为什么这样设计,即使是简单的概念,也会有收获。Stay Hungry,Stay Foolish。

UIView中hidden、alpha、clear color与opaque的区别

关于UIView的透视度,有四个属性(下图中红框中的项)都与之相关,作为一个喜欢刨根问底的程(处)序(女)员(座),一定要搞清楚它们各自的用处跟区别是什么呢?

hidden

此属性为BOOL值,用来表示UIView是否隐藏。关于隐藏大家都知道就是让UIView不显示而已,但是需要注意的是:

  • 当前UIView的所有subview也会被隐藏,忽略subview的hidden属性。UIView中的subview就相当于UIView的死忠小弟,老大干什么我们就跟着老大,同进同退,生死与共!
  • 当前UIView也会从响应链中移除。你想你都不显示了,就不用在响应链中接受事件了。

alpha

此属性为浮点类型的值,取值范围从0到1.0,表示从完全透明到完全不透明,其特性有:

  • 当前UIView的alpha值会被其所有subview继承。因此,alpha值会影响到UIView跟其所有subview。
  • alpha具有动画效果。当alpha为0时,跟hidden为YES时效果一样,但是alpha主要用于实现隐藏的动画效果,在动画块中将hidden设置为YES没有动画效果。

backgroundColor的alpha(Clear Color)

此属性为UIColor值,而UIColor可以设置alpha的值,其特性有:

  • 设置backgroundColor的alpha值只影响当前UIView的背景,并不会影响其所有subview。这点是同alpha的区别,Clear Color就是backgroundColor的alpha为1.0。
  • alpha值会影响backgroundColor最终的alpha。假设UIView的alpha为0.5,backgroundColor的alpha为0.5,那么backgroundColor最终的alpha为0.25(0.5乘以0.5)。

opaque

此属性为BOOL值。要搞清楚这个属性的作用,就要先了解绘图系统的一些原理:屏幕上的每个像素点都是通过RGBA值(Red、Green、Blue三原色再配上Alpha透明度)表示的,当纹理(UIView在绘图系统中对应的表示项)出现重叠时,GPU会按照下面的公式计算重叠部分的像素(这就是所谓的“合成”):

Result = Source + Destination * (1 - SourceAlpha)

Result是结果RGB值,Source为处在重叠顶部纹理的RGB值,Destination为处在重叠底部纹理的RGB值。通过公式发现:当SourceAlpha为1时,绘图系统认为下面的纹理全部被遮盖住了,Result等于Source,直接省去了计算!尤其在重叠的层数比较多的时候,完全不同考虑底下有多少层,直接用当前层的数据显示即可,这样大大节省了GPU的工作量,提高了效率。(多像现在一些“美化墙”,不管后面的环境多破烂,“美化墙”直接遮盖住了,什么都看不到,不用整治改进,省心省力)。更详细的可以读下objc.io中<绘制像素到屏幕上>这篇文章。

那什么时候SourceAlpha为1呢?这时候就是opaque上场的时候啦!当opaque为YES时,SourceAlpha为1。opaque就是绘图系统向UIView开放的一个性能开关,开发者根据当前UIView的情况(这些是绘图系统不知道的,所以绘图系统也无法优化),将opaque设置为YES,绘图系统会根据此值进行优化。所以,如果在开发时某UIView是不透明的,就将opaque设置为YES,能优化显示效率。

需要注意的是:

  1. 当UIView的opaque为YES时,其alpha必须为1.0,这样才符合opaque为YES的场景。如果alpha不为1.0,最终的结果将是不可预料的(unpredictable)。
  2. opaque只对UIView及其subclass生效,对系统提供的类(像UIButton,UILabel)是没有效果的。

参考

UIImage图片拉伸

为什么进行图片拉伸

  1. 灵活:对于一些带边框的背景图而言,如果不进行图片拉伸,每当改变frame时就需要一份新size的背景图
  2. 节约资源:对于一些比较规则的图片,没必须使用frame大小,而是使用一个满足拉伸条件的较小的图片进行拉伸,这样加载图片变快,而且图片大小变小

使用方法

使用- resizeableImageWithCapInsets:(UIEdgeInsets)capInsets,iOS官方解释如下:

You use this method to add cap insets to an image or to change the existing cap insets of an image. In both cases, you get back a new image and the original image remains untouched.
During scaling or resizing of the image, areas covered by a cap are not scaled or resized. Instead, the pixel area not covered by the cap in each direction is tiled, left-to-right and top-to-bottom, to resize the image. This technique is often used to create variable-width buttons, which retain the same rounded corners but whose center region grows or shrinks as needed. For best performance, use a tiled area that is a 1x1 pixel area in size.

原理就是对UIImage指定了Insets后,在Insets中的区域不进行拉伸,只对区域外的部分进行拉伸。需要注意的有两点:

  1. UIEdgeInsetsMake的四个参数从前到后依次是top、left、bottom、right,不是成对的,别搞错了
  2. 拉伸的方式不是全方向一起拉伸!而是先左右拉伸,再上下拉伸,最后的结果是两次拉伸的叠加!不然会出现这样的问题

参考

理解锁相环PLL

目前我见到的所有芯片中都含有PLL模块,而且一直不知道如何利用PLL对晶振进行倍频的,这次利用维基百科好好的学习了下PLL的原理。

1. 时钟与振荡电路

在芯片中,最重要的就是时钟,时钟就像是心脏的脉冲,如果心脏停止了跳动,那人也就死亡了,对于芯片也一样。了解了时钟的重要性,那时钟是怎么来的呢?时钟可以看成周期性的0与1信号变化,而这种周期性的变化可以看成振荡。因此,振荡电路成为了时钟的来源

振荡电路的形成可以分两类:

  1. 石英晶体的压电效应:电导致晶片的机械变形,而晶片两侧施加机械压力又会产生电,形成振荡。它的谐振频率与晶片的切割方式、几何形状、尺寸有关,可以做得精确,因此其振荡电路可以获得很高的频率稳定度。
  2. 电容Capacity的充电放电:能够存储电能,而充放电的电流方向是反的,形成振荡。可通过电压等控制振荡电路的频率。

2. PLL与倍频

由上面可以知道,晶振由于其频率的稳定性,一般作为系统的外部时钟源。但是晶振的频率虽然稳定,但是频率无法做到很高(成本与工艺限制),因此芯片中高频时钟就需要一种叫做压控振荡器(Voltage Controlled Oscillator)的东西生成了(顾名思义,VCO就是根据电压来调整输出频率的不同)。可压控振荡器也有问题,其频率不够稳定,而且变化时很难快速稳定频率。哇偶,看到这种现象是不是很熟悉?嘿嘿,这就是标准开环系统所出现的问题,解决办法就是接入反馈,使开环系统变成闭环系统,并且加入稳定的基准信号,与反馈比较,以便生成正确的控制。

PLL

因此,为了将频率锁定在一个固定的期望值,锁相环PLL出现了!一个锁相环PLL电路通常由以下模块组成:

  • 鉴相鉴频器PFD(Phase Frequency Detector):对输入的基准信号(来自频率稳定的晶振)和反馈回路的信号进行频率的比较,输出一个代表两者差异的信号
  • 低通滤波器LPF(Low-Pass Filter):将PFD中生成的差异信号的高频成分滤除,保留直流部分
  • 压控振荡器VCO(Voltage Controlled Oscillator):根据输入电压,输出对应频率的周期信号。利用变容二极管(偏置电压的变化会改变耗尽层的厚度,从而影响电容大小)与电感构成的LC谐振电路构成,提高变容二极管的逆向偏压,二极管内耗尽层变大,电容变小,LC电路的谐振频率提高,反之,降低逆向偏压时,二极管内电容变大,频率降低
  • 反馈回路FL(Feedback Loop):通常由一个分频器实现。将VCO的输出降低到与基准信号相同级别的频率才能在PFD中比较

PLL工作的基本原理就是将压控振荡器的输出经过分频后与基准信号输入PFD,PFD通过比较这两个信号的频率差,输出一个代表两者差异的信号,再经过低通滤波器转变成一个直流脉冲电压去控制VCO使它的频率改变。这样经过一个很短的时间,VCO的输出就会稳定下来。所以:

PLL并不是直接对晶振进行倍频,而是将频率稳定的晶振作为基准信号,与PLL内部振荡电路生成的信号分频后进行比较,使PLL输出的信号频率稳定

最后,根据原理,理解一下锁相环(Phase Locked Loop)的名称

  • 为了对基准信号与反馈信号进行频率比较,二者的相位必须相同且锁住,任何时间都不能改变,这样才能方便的比较频率,所以叫锁相(Phase Locked)
  • 为了快速稳定输出系统,整个系统加入反馈成为闭环,所以叫环(Loop)

参考文档