Sucha's Blog ~ Archive for April, 2021

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 里面做的,这个到底是谁修改的,说不通了。

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