Sucha's Blog ~ Welcome

21年4月11日 周日 21:23

iOS 的异步刷新

技术细节大概描述一下就好,因为网上能搜到的内容蛮多的,这里主要说一下工作中涉及到的方面。

项目之前因为要 0.5 秒刷新一下部分界面,主要是数据更新,涉及到文字、以及用于标记数值程度的背景色。之前的负责人估计第一版用的是 UILabel,或者就是直接用了脸书的 Texture,就为了更新这一小块内容,居然用了 Texture,不可思议。

整个页面除了这里,应该也有其他的原因,反正有点卡,在 i7 32G 的 14.4 模拟器上,CPU 占用率大概 20%,其他部分算法的原因,在这个范围 -+4% 浮动。

我用 instrument 的 time profile 看了一下,Texture 的 layout 部分占了大头,因为需要更新标记数值程度的背景色,因此 cell node 整个 layout 更新,因为又是 RxSwift drive 了整个 table view items,所以实际上是整个 table view node 不停 layout 更新了。

即便 Texture 用的是 Yoga,但是就这两个变更点的更新,用得着 layout 整个 table view node 吗。

于是上周搜索了一下异步更新的方案,基本上就是 CPU 方案 vs GPU 方案,CPU 方案其实就是 Core Graphics,UIKit 其实就是 CPU 的,而 Core Animation 是 GPU 的,比如 CATextLayer、CAShapeLayer、CAGradientLayer,而且 CALayer 有 drawsAsynchronously 这个属性,设置为 true 后,就是异步线程刷新,文档建议自己试试看。

那么方案差不多出来了,选用 GPU 的方案,固定 frame,数据 drive 比如背景色,只更新相关 cell 背景 layer 的 frame,打开异步线程刷新,这样 layout 的负载会低一些。

另外实操后发现,CATextLayer 其实能力是比较有限的,跟 UILabel 有很大不同

因为需要配置 CTFont,以及自己计算行高后做 vertical center,于是再封装了一层普通 CALayer,将 UIFont 暴露出来,以及默认 vertical center,这个项目暂时不需要 vertical 的其他配置,就先不做这个部分的可选项了。

标记数值变化的背景色更新,用的就是普通的 CALayer,因为 frame 设置后,本来就有 tween 补间动画,只是配置颜色输入时不是 UIColor,而是 CGColor 罢了。

类似 tableview 的 UI 框架部分,用了之前介绍的 StackViewLayout,cell view 可以是任何的普通 view,不一定是需要同一种 view,只要支持 sizeToFits 就可以自动配置行高了。

配置了 rx 数据接口后,塞数据进去也很方便,上述的修改,在 i7 32G 的 14.4 模拟器上,CPU 降了大概 8%,在 6 plus 上降了有 10% 多。

是属于比较明显的改进了。

21年4月02日 周五 20:55

iOS 14 NavigationBar

工程在我进公司之前就遇到了这样的问题,导航栏左右 bar item 在 iOS 13 是好好的,但在 iOS 14 上面,从一级页面 push 进入到二级页面,进入到三级页面,或者从二级页面 pop 到一级页面,都会往中间跳动缩进一下,再回归到正常位置。

这种跳动感觉很突兀,很别扭,但是很难定位原因。新建 demo 工程自己试了一下,iOS 14 下是没问题的,我们有用一些第三方库,其中 QMUI 是涉及比较广的 UI 库,但用 QMUI 官方的 demo 试了一下,iOS 14 下也是好好的。

因为用的 QMUI 版本有点旧了,是 4.1.3 在 2020 年 5 月 release 的,于是早上更新到了 4.2.3,但还是没用,问题依旧。

模拟器断点看了 UI 层级,我们是用 custom view 放到 bar button item 里面的,custom view 的 superview 是一个 _UITAMICView 之类的,它的 superview 是 _UIButtonStackView 大概这个名字,它的 superview 是 _UINavigationBarContentView 这样的,最后才是 NavigationBar。

问题是除了 custom view,其他都是系统的 view,一开始观察,push、pop 的时候,其实是 _UIButtonStackView 和 _UITAMICView 都的 frame 有变化,一开始 frame x 是 0,稳定后 stack frame x 变为 -8,tamic frame x 变为 8,然后我们的 custom view frame x 是 0,刚好跟 navgation bar 对齐。

于是我就用 rxswift 的 observ 监听 _UITAMICView 的 frame,并做修改,发现不管用,因为总是断断续续在忙这个项目(对,其他插入的任务更紧急),第二天才发现无法直接监听的原因,是我自己对 navigation bar 了解不够所致。

navigation bar 的显示,开放给用户的接口是配置 navigation item,其实是个堆栈,最顶层的就是当前的显示配置。而 navigation item 可以配置左右 navigation bar item,还可以配置多个,其中左边是从左到右序列号增大排列,右边是从右到左序列号增大排列。

其 UI 层次规则,是每一个 navigation bar item 对应一组 _UIButtonStackView 和 _UITAMICView。因为 navigation item 左右两边 bar item 是可以动态配置的,因此这一组 view 层级也是动态显示的。

即便一开始就配置好了顶层 navigation item,但是 push 了新的 view controller 进来,顶层的 navigation item 更新后,新的 bar item custom view 会使用新建里的一组 stack view 和 tamic view。pop 后也类似,顶层 navigation item 更新,刚成为堆栈顶层 navigation item 左右 bar item 的 custom view 才建立 stack view 和 tamic view。

所以看到的缩进、跳动然后回归正常的左右 bar item custom view 动画,就是因为这种动态创建容器 view,然后 layout 造成的。

这就解释了之前为什么监听这两个 view 不成功,因为 pop 后才新建立的,之前建立的 observ 被 dispose 了。

继续观察,发现在 push、pop 的过程中,custom view frame 其实也会被设置的,一开始被设置为宽度更小的 frame,最后才设置为正常的 frame,原因我是搞不懂,但是 custom view 一直没有被释放,可以作为一个监听的基点。

于是做了改进,监听 custom view 的 frame,同时监听 .initial 和 .new,根据其在左右 bar items 的位置,配置对于 navigation bar 的偏移,这样就对了。

于是整套方案定下来了,最后结果也还说的过去,大概是继承 UINavigationBar,在建立 navigation controller 的时候,有初始化方法是可以传递 navigation bar class 进去的。

然后在 pushItem,以及 layoutSubviews 的时候,检查顶层 navigation item 的左右 bar items custom view,并将其左边前一个,或右边前一个的 view 记录下来。我是建立了一个 weak to weak 的 NSHashMap 来做记录的,然后建立对 custom view frame 的监听。

每次 frame 有更新,就检查 custom view 的 super view,其实就是 _UITAMICView,同时根据 custom view,找到排在它前面的另一个 custom view 的 _UITAMICView,根据前一个 tamic view 在 navigation bar 的位置,加上 8,作为当前 custom view _UITAMICView frame 的 x。

有点绕口了,但思路是没问题的,UI 表现上也说得过去。

说一下不好定位根本问题的原因,一方面不想做得更复杂了,系统类没有 hook,不好定位其修改点,且另外一方面,custom view frame 的修改,是在 QMUI hook 掉所有 view 的 layoutSubview 里面做的,这个到底是谁修改的,说不通了。

很无奈,不得不找一种半调子的解决办法。

21年3月25日 周四 20:33

Swift AVL Tree 和 SortedDictionary

苹果的 Collection 集合类型,我是找不到链表和树的,至于为什么,也许是底层封装好后,上层透露出来给大伙用,就不用担心了吧。

实际上没有那么美好,举个例子,一个不断更新的整数序列,新进来的整数有可能是做插入,或删除(比如元素不存在,就插入,存在就删除),且每次更新完后,都取最小的几个数字做输出,也就是一个不断求 Least(N) 的可变序列,这样的需求应该不难理解。

如果用苹果提供的数据结构,应该怎么做呢,不管是用 Array、Set 或是 Dictionary,最后的输出,总是免不了做一个 O(N * LogN) 的 sorted,其实我只是取最小的几个而已,这个全局的 sort 未免太奢侈了。

即便 Dictionary 是类似红黑树这样的结构(当然不是),对于所有元素 sorted,拿到所有元素的排序,再取 prefix,其实也是不必要的。

上面的需求,是我抄袭 skywind3000 miniavl 的原因,我将其翻译到了 Swift,然后呢,跟 Mutable Dictionary 结合,就变成了 SwiftSortedDictionary,AVL 树在 SwiftAvlTree

分析一下吧,AVL 树的插入、删除,查找都是 O(LogN),不过因为有 Dictionary 的加持,查找相关节点其实只需要 O(1),所以删除、查找变成了 O(1),当然删除比较特别,也就是做两、三个旋转而已,算 O(1) 了。

最后 Least(N) 的排序是交给 AVL 树来做的,一个闭包循环,加入一个表示序列进度的 index,一个可以控制结束的 inout 修饰的 stop 参数,就行了。

下面是测试的环境,我是在一台 i5 的老 MacOS 上测试的,基数是 256,添加了 512 个数,每次添加取 Least 16,同样的逻辑,循环了 10 次,结果对比如下:

with amount:256 addition:512 prefix:16 loop:10

Test Dicionary:
round 1: 111ms, avg: 111ms
round 2: 109ms, avg: 110ms
round 3: 109ms, avg: 110ms
round 4: 109ms, avg: 109ms
round 5: 109ms, avg: 109ms
round 6: 109ms, avg: 109ms
round 7: 109ms, avg: 109ms
round 8: 108ms, avg: 109ms
round 9: 109ms, avg: 109ms
round 10: 112ms, avg: 109ms

Test SortedDicionary:
round 1: 21ms, avg: 21ms
round 2: 18ms, avg: 19ms
round 3: 18ms, avg: 19ms
round 4: 19ms, avg: 19ms
round 5: 19ms, avg: 19ms
round 6: 19ms, avg: 19ms
round 7: 19ms, avg: 19ms
round 8: 19ms, avg: 19ms
round 9: 19ms, avg: 19ms
round 10: 19ms, avg: 19ms

当然现实中使用也是有收益的,负责的一个不具名的项目,因为要做上述类似的逻辑,之前单独使用 dictionary 结构做插入、删除,最后 sorted 函数在 time profile 排前面,使用了 sorted dictionary 后,耗时在 profile 中是完全看不到了。

不过就我负责的那个项目来说,因为还有其他的问题如频繁 UI 刷新以及 layout 的制约,肉眼可见的观感的改进不明显,但从 time profile 的结果来看,是有改进的,算达到了预期吧。

21年3月24日 周三 22:22

PageEventBus 大改动

上一周,PageEventBus 第一次用在了其他场景,让我不得不重新思考了一下使用 responder chain 隐含传递、关联 event bus 的优势、劣势,最后决定是将这个重要的特性去掉。

先说一下问题,使用了系统 UISearchViewController,然后使用了 result view controller,但是 result view controler 无法通过 responder chain 拿到 event bus,猜测是 search view controller 使用了其他的 window,或是其他的问题。

这样的话,估计如果有应用到其他的一些系统类,使用 responder chain 来拿到 event bus 这种想法,也是行不通的。

于是,做成了一个中心化的架构,默认使用带类型的 event bus 做 key,将新建立的 bus 缓存到一个中心节点中,其他同样类型的 event bus,通过查询中心节点,就能拿到 bus,然后 bus retain count +1,最后当所有持有 bus 的实例销毁后,这个类型的 bus 也会被自动销毁掉的。

这样的修改,对于单页面是没问题的,但是对于使用同样的事件类型,但具有多个实例,在不同的 UI 层次上,需要区分 bus 的情况,就需要特别处理一下了。这个时候,需要修改 agent 中默认的 event bus name,来建立多个不同的 event bus 实例。

21年3月16日 周二 21:04

PageEventBus 实践心得

在这次重构中实践了一把上个月说到的 PageEventBus,说到重构,其实只完成了一半,但是目前的心得,感觉是挺不错的。

因为页面交互很多,然后 UI 层次也比较复杂,有了 PageEventBus 后,少了很多传递数据的逻辑,一些有 UI 层次的 view,比如各种 StackView 包裹的有复杂业务功能的 view,不需要单独开业务参数接口了,整洁,同样的相关 view model 也不需要单独开业务接口了,整洁哈。

因为是在 didMovedToWindow 时候才去找 superview 持有的 bus,之前担心许多 view 下来会不会有什么影响,但实际上需要 findBus 的 view model 没有很多,所以 UI 绘制效率一点都不影响,况且只有第一次需要通过 next responder 来找,找到后就挂接上了,稳得很。

不过也有在之前没有考虑好的,比如 view controller 这个部分,因为 page model 继承于 view model,对于 controller 及 controller 上的 view 是 unowned let 的方式,意味着初始化的时候,就开始访问 controller 及其 view 了,这很不好。

因为 controller 的 view 不应该那么早被导入到内存,一般都在实际需要展示前再让它加载到内存,所以实际上 page model 没法完全独立于 controller,需要在 controller viewDidLoad 后才进行初始化。

所以这算个小遗憾吧,我现在的做法是,做一个可选闭包,在 viewDidLoad 里面执行,确保有了 viewDiDLoad 后,才初始化 page model,这样就好很多了,估计后面会改到 BlockViewController 里面做例子。

另外,还有些遗憾的点是,不复杂的业务,实际上根本用不到这个 event bus;还有就是,如果需要在不同的业务 controller 之间做数据传递,当前的 PageEventBus 又不可以拓展,比如没有相关路由的接口,可以连接两个不同的 event bus。

连接两个不同的 event bus,有时候还是很必要的,当然这个完全可以交给系统的 notification 来完成,但是接口感觉又麻烦了一点,后续感觉可以在这点上继续推进的,虽然现在的工程不一定能够用上,缺少实践心得还是感觉稍微有点不爽。

21年3月15日 周一 22:27

PinLayout 及 StackViewLayout

虽然现在 iOS 开发几乎全部都是使用 AutoLayout 了,诸如 OC 时代的 Masonry 或者 Swift 时代的 Snapit,或者 TinyConstraint,但其实基于 frame layout 还是有一些可以介绍的,就比如 PinLayout 以及 StackViewLayout

这里注意 StackViewLayout 在 dev 分支下才有内容,另外官方的 StackViewLayout 其实有循环引用,我提了一个 pull request,解决了循环引用,并加入了 distribution 的方案,估计没人看了。

PinLayout 很有意思,做为 UIView 的一个计算属性,chainable 方式不断接下来描述这个 layout 的位置、大小,最后在 deinit 的时候,才真正将传递进来的 UIView 进行设置,设置的属性是 center 及 bounds。

因为已经在一个重构的项目中使用了这两套方案,可以说一下了,相对于 AutoLayout,其实是不够方便的,但有些限制还是不错的。不够方便的是,PinLayout 描述的其实是有先后顺序的,就跟 AutoLayout 的 priority 一样,先描述的,PinLayout 会先 deinit,layout 完成后就成了其他 view layout 描述的输入。

自认为虽然不方便,但也算不错的限制是,不像 AutoLayout,只要有 superview 就可以描述,PinLayout 及 StackViewLayout,描述 subview 时,只能在 layout 的过程中,比如 layoutSubviews 或者是 sizeThatFits 里面。

另外在自动计算宽度、高度方面,PinLayout 相比 AutoLayout,要麻烦许多,需要 view 重写 sizeThatFits,限定宽度、高度后,基于这个计算另外的方向。

StackViewLayout 按照作者的说法是大量参考了 Yoga,这个组织其实也有个基于 Yoga 的 FlexLayout。大部分的功能有了,但是却没有 distribution,蛮可惜的,后面我自己加上了一个搓实现,不管怎么样,先有得用再说。

这俩个库的优势当然是计算速度极快,而且库很小,基于 frame 计算,如果没有自动宽高的话,是很安全的。

加上自动宽高的限定后,按照官网的描述,layout 逻辑被单独隔离在一个函数中,在 layoutSubivews 有使用,在 sizeThatFits 也有使用,另外 AutoLayout 用的是 intrinsicContentSize 来计算内容大小。

如果需要适配 AutoLayout,有时是得考虑重写这个函数的,这个函数默认不会刷新,需要单独刷新一下,看 UIView 有介绍。

StackViewLayout 相比 UIStackView 可以做到一些特别的方案,比如因为有 grow 和 shrink,可以玩剩余比例。而且在搭建的时候,因为 chainable 很方便,就都在一个 lazy 属性中全部描述完了,我说的是描述多层嵌套的 StackViewLayout,看起来是很赏心悦目的。

只是具体 Layout 真的得多动一下脑筋,毕竟不是 AutoLayout 是多项式计算的,这里是固定流程的,特别是一些限定了宽度高度的场合,我可是将 subview 高度自动计算完后,设置给 StackViewLayout 的,StackViewLayout 再拉伸其他的 subview。

大概就是这样了吧,还可以用下的,不晓得后面有了 SwiftUI 后会怎么样了,SwiftUI 毕竟还没有用过。

21年3月15日 周一 20:46

西行漫记读后感

也许是从某个微博用户上面读到了有关西行漫记的点点信息,于是就找来看了,读的是微信读书上面的版本,试用了几天无限卡才读完。

这本书名字实际是《红星照耀中国》,但我搜时用的就是西行漫记,到底是从哪里读来的标题忘记了,之前命名为西行漫记,是因为在解放前,国统区其实不能出版红色读物,所以命名为西行漫记是为了好低调传播。

在这之前,有在 B 站上看了沙盘推演里面李得胜的四渡赤水,徐海东等出神入化的战争艺术。

话题转回来,这本书是西方第一次了解到红色中国的窗口,当时斯诺也是费了不少劲才从白区去到红区,因为红区被完全封锁了,留有少许的时间窗口,刚好也应该是在西安事变之前的一小段窗口期,东北军不鸟常凯申,没有围剿红军,而是准备秘密合作,于是有了空隙,斯诺终于找了一个机会去了红色根据地,不过当时还有大财主的民团各地流窜,被碰到会没命,也是很危险的。

斯诺去之前是满腹孤疑,对根据地、红军的方方面面一堆的疑问,第一天就见到了周总理,被总理给安排了 90 天的行程,心里还觉得时间太长了,后面实际花了 120 天,走的时候还恋恋不舍。

里面篇幅其实挺多的,最精彩的,当然是访问李得胜的记录,以及对于红军如何得到广大农民支持的描述。

里面的不少论述在论持久战里面其实也有,当时就很客观,深入的讨论了中国人民抵抗日本的必然胜利,所需要的条件,什么是主要条件,什么是次要条件,什么是内部条件,什么是外部条件,为什么这些外部条件会成立,另外战争会分几个阶段(应该是论持久战里面的了,都混在一起说了吧),然后中国的优势和劣势,日本的优势和劣势,等等。

李得胜说这些话是很有资本的,毕竟国共内战已经 10 年了,经历了五次大围剿,还发展壮大了。

还讨论了具体会用的战略,“战略应该是一种在一条很长、流动的、不定的战线上 进行运动战的战略,快速进攻、快速退却,是一种大规模的运动战”,“我们的战略和战术应该注意避免在战争初期阶段进行大决战,而应该逐步打击敌军有生力量的志气、斗志和军事效率”。

说到得到农民拥护,当时军阀割据,为了养兵,各地都横征暴敛,征税都预征了几十年,国民党代表的是大地主、大资本家、以及国外资本的势力,广大的农民们只能是越来越穷。

为何会拥护共产党呢,是因为土地革命,革了大地主的命,将地分给广大农民,少征税,因此各地都收到广大群众的欢迎,这个国民党是做不到的。这也是红军为何是初期很穷的原因,也是为什么如燎原之火的原因,真的因为各地都是军阀,广大农民活不下去了。

对比一下,李得胜在回答斯诺对印度的看法时,说”印度不经过土地革命,是永远不会实现独立的“。当然印度在 1947 年就独立了,不过应该是类似国民党上台一样吧,代表的是大地主、大资产阶级的利益,现在种性、各地利益分割就可以看到,显然生产力远未得到解放。

估计是年龄大了吧,现在都喜欢看这样的书了。

21年2月16日 周二 18:24

Swift(3) - PageEventBus

春节在家,继续思考了一下业务上的一些痛点,并做了整理,趁春节期间的空闲,搞了一个改善页面内通讯,限定 UI 及业务逻辑绑定关系的库 PageEventBus

先说一下之前在业务上觉得不爽的点:

说一下之前的处理方法,使用 RxSwift,各种不管 UI 层级多深的 binder、controlProperty、controlEvent,都透过多层 UI,传递到业务 view controller 这一层来做绑定,带来的问题,有以下几个

基于上面的问题,我有了一些思考,希望有下面这样的东西

就想着能否用上一个页面内的总线,将参数通过总线传递,这样可以对抗业务需求上导致的参数变化,比如 view model 可以接收总线消息,也可以发送消息,有了总线后,可以不需要关心 UI 层级了,也没有参数需要透传了。

另外,UI 的输入、输出抽象到业务逻辑 view model 的输入、输出,可以认为 view 持有了 view model,view model 持有 unowned view 的不可变引用;对于 view controller,也可以相应的持有 page model。这样,UI 总是可以复用的,而 view model,才处理实际业务,如果多个业务本身可以抽象到更高的层级,那么显然 view model 也是可以做到的。

还加上改进的一点,由于上面接收、发送事件的角色都在系统展示树上,可以利用 responder chain 链,在 view didMoveToWindow 或者 controller viewDidAppear 的时机,做事件总线的绑定,意味着总线本身都不需要传递,可以自动连接上。

上面的一些思考,加上春节空余时间的实践,就是上面说到的 PageEventBus

一些代码上的具体实践:

话说这一整套限定的东西还是很多的,我准备使用 event bus 来传递 view model,view model 内部还是使用 RxSwift 来做业务绑定,view model 带 disposeBag,或者使用 view 的 disposeBag 也是可以的。

21年1月24日 周日 22:52

Swift(2)

又写了一个多月的 Swift,除了刚开始时的修修补补,慢慢扩展到页面框架部分,不过这个部分我还是不够熟悉,一部分在于我还是沿用项目之前的 ViewModel,另外一部分,很多业务细节还没能深入理解。

虽然这次的标题是 Swift(2),实际内容大部分是 RxSwift 及 ViewController 的结合,以及一些 UI 层级的实践心得。

页面框架

先说一下我们的网络层,搭建了类似 Moya 的网络层框架,基本上是够用了,但相比之前用的 Model 网络层,这里浅浅的封装,就不会有单独的 model 管理器了。简洁是简洁了很多,但是数据层面转换和粘合就得在 ViewController 处理,带来了 ViewController 的膨胀。比如因为用了 RxSwift,同一份数据,有时需要多次 transform,subscribe,这些可都在 ViewController 里面。

我曾经想过这些代码放在 ViewModel 里面会不会好一些,但我上次也许也有提到,这里的 ViewModel,更像是一个 Driver.System 的状态机,初始状态是 initial,之后是 Signal -> Driver 的状态切换,通过一个 reduce 函数检查 ViewModel 绑定的 struct 结构来判断到底应该 loading、setResult 还是其他,如果弄懂了这个,页面的状态处理就很清晰了。

只是这个 ViewModel 只负责将 app 外的数据,transform 为页面状态数据,不涉及到 UI element 数据的 transform,这是跟之前 ViewModel 的区别。ViewModel 带回来的页面状态数据,最终会在 ViewController 中 transform 并 binding、drive UI element,这个过程有时需要做很多的粘合工作,流程也会相当复杂。

另外,UI 层级方面,已有较为复杂的页面,通常 UI View 就好几层,一层一层的 UIStackView 包裹里面的自定义子 view,将需要传递的数据链路,通过 binder 传递出来,即便是 View,都跟 RxSwift 框架直接绑定了。

说一下对这个的理解,我觉得是没必要,因为这个 binder 或者 driver,其实都只算是单向的数据传递,传递出来的时候用就好,传递进去的时候,感觉其实没那么必要的了。

以上提到的,觉得最复杂的还是 ViewModel 及其 Driver.System,基本上抄了几遍,这段时间除了 K 线 ViewController 没有修改过,其他页面多少接触了一点。

目前的页面框架工作得还可以,通用性也还行,只是觉得业务复杂度还是太靠近 ViewController 了。而且当前的框架,ViewController 绑定 TableView 被封装得很厚重,仅支持单种 cell 的 TableView,不知道当时这个模版构建的时候是怎么想的。

还有 TableView 的 DataSource,依赖了 RxDataSource,在 TableView 上,其实不支持 HeaderView 的数据流绑定,还得用回系统提供的 delegate 方式,不伦不类的,还不如全部都用系统的方式呢,因为这个模版框架,将 createCell 的部分,dataSOurce 的数据创建部分,放在了 TableViewCell 里面,这个我是非常不认同的。

我总觉得 Cell 有复用的可能,两者结合不应该那么紧密,即便有 protocol 作为粘合,有时候还是觉得抽象过了,没必要。

这段时间还将 Carthage 的库切换到了静态库上,模拟器运行节省了不少时间,但这个改进在真机上几乎觉察不出来。

做了这部分的心得是,我们依赖的第三方库实在是太多了,这也造成了我们启动慢,因为打包好的 App 就有 150M+。比如我们的数据库,为何要用 Realm 呢,是因为 ORM 方便吗,还是因为它更快,总觉得还不如用系统提供的 SQLite3 好了,或者浅浅地封装一层一层,比如 GRDB 就好。

因为这个库太大了,打包的时候都可以看到打的慢。

UI 层级

学习使用 UIStackView,其实这个我在几年前就有做了一个基于 frame 计算的 StackView 框架,可惜是公司财产,不能开源出来(显然我也没有源码)。

UIStackView 使用有些前提,一方面需要 addArrangedView 添加进来,但是如果只是删除 ArrangedView,其实还在 subviews 里面。

另一方面,每个 view 需要有自己的 size,以及 hugging、compress 优先级,要不感觉最后计算宽度大小的时候,总是不准确。

还有学会了在 tableview 的 header view 上面放置复杂的 UI 层级,这个 tableView 的 header view,不支持 AutoLayout,只能是指定 frame 高度。

但是呢,又能通过将这个 view systemLayoutSizeFittingSize 拿到高度,设置给 frame 来确定这个 tableview header view 的高度。

还有 table view 的 cell 是可以不指定高度的,通过 AutoLayout 计算出来,但是呢,collection view 不可以。

这段时间因为对这些 UI 规则不熟悉,浪费了很多时间,有时想着,还不如我来设置 frame 呢,工作早就做完了。

另外,复杂一点的首页等等 ViewController,各种 NSLayout warning,见怪不怪,设置符合 AutoLayout 的规则,太难了。

哦,对了,我们有两套 layout 的库,一个是 SnapKit,一个是 TinyConstraint,常常是混用的状态,真的有点混乱。

--

大概先这样吧。

20年12月13日 周四 22:38

Swift(1)

ObjC 我挺熟悉的,但切换到 Swift 的路并不顺利,虽然我之前写过一个结合 Swift + ObjC 运行时的换肤库 LWTheme,现在看来,确实是相当 ObjC 呀。

写了一点点商业 Swift 代码后,我现在还不能说我熟悉 Swift 了,Swift 内容太多了,特别是 Enum 以及带泛型的 Protocol,抽象能力太强了。

先开始吧。

函数式书写静态类型推断

纯 Swift 相比 ObjC,类型推断能力大大加强,算是一个静态语言,没有什么动态运行时,但 Target-Action 部分,在 Switch 里面,其实还是依赖 ObjC 运行时来跑的。

带 Optional 的静态推断,最大的好处,是类型安全,带 nil 的指针不会到处飞了。一眼开过去,Swift 的语法很像脚本语言,实际使用起来,也很函数式,很多时候,确实是用更少的代码粘合了更多的功能,带来的问题是能量压缩是想当大的。

反面的例子如下:

let stringArray = ["a", "bb", "cc"]
let countsArray = stringArray.filter { $0.count >= 2 }.map { $0.count }
print(countsArray)

输出是 [2,2]

假若 stringArray 带的是其他的类型,而在 countsArray 里面的 filter、map 操作有很多,那里面一长串的 $0 看的真的让人头大,因为实际的代码可不会像上面这样只有 2 行,也不会仅仅只有一种 transform,而是各种转换带下来,表达密度真的太大了。

闭包及简写

闭包相比 ObjC 容易写多了,但也有一些变种,比如上面 filter、map 的参数就是闭包,闭包只有一行的时候是可以省略 return 的,还有尾随闭包,比如上面的 filter 及 map,下面这个也是尾随闭包

let seq = ["A", "B", "C"].sorted { $0 > $1 }
print(seq)

输出是 ["C", "B", "A"]

不得不说 Swift 的语法有点过于自由,这些隐含的缺省用法,看代码的时候得留意了。

Enum 的相关值

Swift 的 Enum 语法不仅仅是只有枚举,以及 raw value,还可以带相关值,提升为了一个带标记的容器,使得即便作为一种输入,其输入内容其实也是千变万化的,只是标记了一种路径输入而已,输入内容是带进来的,比如

enum Goods {
    case name(String)
    case price(Int,Int) // 任性的将小数做为单独的 Int 带入
}

除了上面,常见的例子还有定义网络请求,将部分请求参数做为相关值带进来。

结构体

struct 相比 class,不能被继承,以及都是值传递而不是引用传递,这点让人不是那么满意,虽然写代码的时候往往不会注意到这点。

因为现实世界中的结构体其实可以很大的,有时候不是刚开始设计的问题,是后续的需求催大的。

泛型

有了严格的类型推导,泛型到处都可以存在,这种抽象容器能力,是 ObjC 所达不到的了。

protocol 及 extension

Swift 的 protocol 结合泛型,然后 extension 使用 protocol 将 class 能力大大扩大了,感觉就像是一种带入的计算属性一样,强。

现实中的 protocol 加上泛型,结合类实现,将类抽象为一个巨大的逻辑容器,代码真是让人看得头大,但另外一方面,不得不说如果是类似数据构建的页面,数据逻辑的抽象层让人放心了。

当然 UI 操作逻辑的抽象层也是可以做的。

访问控制

相比 ObjC 运行时的无所不能,Swift 带来了严格的访问控制,open、public、internal、fileprivate、private 应该有这五种吧,让 ObjC 转行过来的同学头都大了。

--

潦草的先这样吧。

20年11月9日 周一 16:02

JavaScript(1)

学习了一下 javascript,使用的是 JavaScript 教程,基础的 ES5,包含的内容可太丰富了。

跟其他语言的差别,主要在语法、面向对象,基础库和运行环境这几个部分。

语法

语法部分,'==' 可真够让人头疼的,为了比较会自动转换类型,所以教程建议直接使用 ‘===’ 进行严格比较。

相对高级的语言,都有 << 和 >>,甚至 >>> 操作符,可惜了,Lua 5.1 比如 LuaJIT 并没有这么好用的操作符,只能通过 bit 库来使用,或者塞给 C 代码处理吧。

JS 里面的 null 和 undefined 也得注意一下,它们几乎是一样的,除了 null 在 Number(null) 时为 0,而 Number(undefined) 为 NaN。

面向对象

这里一开始让我挺头疼的,比如原型对象及继承关系。

对比 C++/Java 的面向对象实现都隐藏在语言核心运行时里面,通过语法‘类’来实现继承,在类里面写构造函数,描述继承关系。且 C++ 因为是零成本抽象,极端强调运行时性能,继承、面向对象的逻辑在编译期几乎都解决了,只留下了动态派发给运行时处理。Java 有反射,任何时候都能拿到其继承关系,其所有的类都继承自 Object。两者作为静态语言,在运行时无法动态修改继承关系。

JS 虽然跟 Java 一样所有类都继承自 Object,但其继承的语言特性,是通过原型对象来实现的,运行时可以访问。原型对象就是类的 prototype 属性,构造函数则是 prototype.constructor,因为有 new 关键字的存在,定义一个新类,只需要定义构造函数就好了,比如

function Car(color) {
    this.color = color;
    console.log("color:" + color);
}

var car = new Car("white");

会输出 'color:white"。如果定义 Bus 继承 Car 是这么实现的:

function Bus(color) {
    Car.call(this, color);
}
Bus.prototype = Object.create(Car.prototype)
Bus.prototype.constructor = Bus

var bus = new Bus("red")

将输出 'color:red',如果需要在运行时修改类的继承关系,propotype 和 prototype.constructor 必须同时修改。

感觉原型对象这个词,在继承关系上,跟 Objective-C 的类对象有一点像,不过实际表现及使用,千差万别。

对比 Lua 实现面向对象,感觉还是有一点像的,不过 Lua 里面实现面向对象,继承关系,需要了解的可太多了,需要了解 Lua 的元表(metatable)才好。

基础库和运行环境

作为一个动态脚本语言,JS 的基础库和运行环境挺丰富的,作为 Web 端统治性的语言,运行环境包含对 HTML DOM 的操作,以及浏览器环境,这两个部分我还没怎么看呢。

作为后端语言,基于 Node.js 事件驱动、非阻塞式 I/O 模型,极大降低了后端开发的难度,不过 Node 下的运行环境,我还没怎么接触。

虽然 JS 是单线程的 VM,但是 HTML 5 及 Node 都实践了其多线程交互的方式,也有了不同领域下的解决方案,其成熟程度,丰富程度,对比 Lua 可要好太多了。

引擎部分,标准桌面、后端用的是 Chrome V8,嵌入式应该用的是其他方案。现在已经有了前后兼容的 ES5、ES6 标准,貌似 ES7 也快要出来了。

--

总的来说,JS 是一门比较完善,还在不断渐进式发展,且考虑兼容性的编程语言,慢慢的将变成桌面应用编程语言,部分后端编程语言,可不再是脚本语言了。

相比 Go,JS 是动态类型,也不是静态编译的;相比 Lua,语言成熟度,库的丰富程度,应用范围都好好很多,没有 Lua 目的是宿主语言的累赘,虽然 Lua 还有速度上的优势。

而且,学会 JS 的好处可是,前端通吃,后端独立。