Sucha's Blog ~ Welcome

20年10月02日 周一 22:48

独立 SideBar

将 site 和 blog 的边栏 sidebar 独立到各自不同的 js 文件上去了,比如

修改后的好处,是需要更新 archive links 时,也只需要更新对应的 sidebar js,而不需要更新每个月份输出的 HTML 了。而之前作为 static site generator,是独立输出到每个月份 HTML 上面去的。

内容跟 sidebar 分离后,完全可以没有什么顾忌的更新 sidebar 了,当然其实我也几乎不会更新 sidebar,感觉没啥必要,🤗

21年9月30日 周四 23:59

9 月份干嘛了

其实 9 月份做了蛮多的事情,公司那边慢慢忙起来了,在公司里面做了一个数据库抽象模块,其实就是一个抽象的 DAO(Data Access Object),使用的是 GRDB,起因是使用的 Realm 版本太旧了,启动后未知原因的 crash 飙升,堆栈上看是启动时候 Realm 在初始化 ObjC 这一层某个 sub class 时候就挂了,都还没有任何 DB 的操作逻辑呢,其实就是 DidFinishLaunch 都没走完。

因为工程早就都是 Swift 了,不想呆在 ObjC 里面,太多的 swizzle 黑魔法,对工程代码质量要求太高,小公司很难保证。因为 DB 操作大部分都仅仅是序列化逻辑,上没有复杂的 join、foreign key 操作,所以只是简单封装了一下 GRDB,抽象独立为 Pod 库,不污染工程代码就好。基于这个前提,抽象了 fetch、push、watch 接口,Pod 内部建立内存模型跟 DB 模型的对应,其实就是表操作。可以通过上述的几个接口,接入不同的带 param 的 enum 参数,就可以完成功能。有了这一层抽象,后续即便 GRDB 因为各种原因不能使用,我们不需要修改业务代码,只需要在 Pod 这一层做内存到外存的转换就好。

另外还抽象了一个指定 key 的 JSONObject 模型,里面可以放入各种深度、层次的 JSON 数据,insert、update、delete、watch 时候,都关联到 key 上就好,对于一些需要序列化的数据,又不想用 UserDefault 来做,就可以使用这个 JSONObject 来存入 GRDB,实际上是作为 blob 数据存储进去的,目前只支持一个 key,一般都是什么什么 id 之类的,这也是目前能够支持绝大多数业务的序列化功能。

另外学写了一下 SPA(Single Page Application),Web 发展迭代速度要比移动 App 快多了,可选的很多,React、Vue、Preact 都有,我稍微摸索了一下 React,觉得有点拎不动,然后 Vue、Preact 也一样,再说这些东西依赖很重,nodejs 几乎是必须的,build 过程少不了,最后我选了 Mithril。之前有听说过,然后在阮一峰那边又被推了一下。教程相对简单,而且是只支持 browser,不需要 build 过程,我学习了两三天,做了一个 Wiki 管理工具,用的 Markdown IDE 是 SimpleMDE Markdown Editor

选择 Mithril 的原因除了它自己吹嘘的之外,还有其他的考量,比如可以利用上之前的 HTTP Server Cincau。这个新作的 Wiki 管理工具,最后就作为了 Cincau 支持的一个 SPA Demo 页面。其实 SPA 对后端没什么依赖,后端主要是模版 SSR(Server Side Render)。当然有了这个 Wiki 工具,我就可以将之前另外一个小站点的 Wiki 移过来,直接在搭建在 Cincau 上面,话说之前的 Wiki 是 doku + php + nginx 来搭建的,改成 Luajit + Cincau 后,资源依赖少了很多,不过相对功能也少了很多,这些坑只好自己趟了,对于我这个职业 App 开发者来说,也是一个学习的过程。

上面说到 SSR,因为不大喜欢之前搭配的 etlua,于是自己做了 lua-html-tags,这个之前 8 月份的时候提过了。

话说之前 doku 除了基本的 Wiki 功能(用得比较早,当时还不知道 CommonMark 或者 GFM 的语法),还有 revision,以及 show diff 的功能。我觉得这个功能挺好的,对于刚用 Mithril 做的 Wiki 工具来说,可以有效避免某些错误操作导致的 clear 逻辑(当然这从来就没发生过),对心理造成的压力,毕竟有 revision,我可以随时恢复到任何一个版本。

增量保存用的工具是 bsdiff,其实用系统里面自带的 GNU 的 diffutils 或者 patch 也是可以的,只是在 server 这一层,需要调用太多次 shell 脚本完成相应的功能,觉得不大好。于是将这两个工具做成了 Lua 库 lua-bsdiff,并 apply 了两个觉得挺不错的 patch。

不过上面提到的 Wiki 支持 revision 功能,在 cincau 的 demo 上面是没有的,目前没打算开源,🙂

9 月份忘记写 blog 了,10 月初初回顾了一下,后面有时间再详细铺开,先这样吧,🤗

21年8月31日 周二 22:37

lua-html-tags

上周才知道 StackBlitz 的在线代码编辑工具,是一个 React JS 编辑器,包括了自动导入的包管理等功能,React 这边随便编辑修改页面添加变量,右侧的页面瞬间刷新,看着这样的前端开发实在太香了。相比之下,虽然 Swift 开发还有类型信息,但是为了上真机跑起来,还得编译链接,老费劲了。

其中 React 将页面和逻辑都放到一起的做法,感觉挺好的。

因为之前做了一个 Cincau web 框架,用了 etlua 做模版渲染引擎,发现其实还是要写很多的 HTML,而且之前的做法是将模版和实际的逻辑页面分开、model 分开,为了搭建一个页面,心智经常要顾及太多文件,再加上我搭建的多页面跳转的 demo,router 加上各个页面、model、template,真的让人头大。

但为此将模版文件放到业务逻辑里面,又感觉太啰嗦了。

就想找一个将 HTML tag 和 Lua 结合起来的描述语言,其实看过一些短小的,后来看到较大的是这个 lua-resty-tags,但是这个使用的时候需要建立 tags 描述的,非开箱即用感觉不够专业呀。

我描述一下自己的需求吧

最终是自己摸索着建立了一个 lua-html-tags,这些 tags 实际上都是 Lua function,因为 Lua 语法的关系,function 可以不加括号接受一个 string 和 table 作为参数,让之前相对简洁的 HTML 描述得以实现。

简单描述一下实现逻辑:

可以看到,从最外层的 table 开始遍历后,处理方式都是一致的递归描述,举个简单的例子

local Tags = require("html-tags")

local function pageSpec()
    return {
        html {
            head {
                meta { name="generator", content="MarkdownProjectCompositor.lua" },
                title "Example"
            },
            body {
                div {
                    { id="body" },
                    p {
                        "content 1, ",
                        "content 2"
                    }
                }
            }
        }
    }
end

print(Tags.render(pageSpec, {}))

最终会生成这样的 HTML(经过了部分换行编辑)

<html>
<head>
    <meta name="generator" content="MarkdownProjectCompositor.lua" />
    <title>Example</title>
</head>
<body>
    <div id="body">
        <p>content 1, content 2</p>
    </div>
</body>
</html>

由于这个页面描述就是 Lua,所以加入相关的变量、函数计算是很简单的事情,而且因为限定了 _ENV 和 fenv,所以 HTML tags 和自定义的 tags 对相关函数外的 Lua 的运行环境都没有影响。

感觉还可以说一下 _ENV 和 setfenv 等相关的事情。实践下来,是觉得 getfenv、setfenv 的灵活性很高,_ENV 比较受限,但也许从语言设计者的角度来说,_ENV 更安全一些吧。

先说一下这个 include tag 的作用,就是引入一个 Lua 文件描述的子页面,最终是输出一串字符串到这个 tag 的位置。使用场景时,比如我做了很多页面,但是想用同样的 HTML head 描述,当我改变 head 的描述是时,希望所有页面都能同样做更改,那么我将这个 head 文件拎出来单独描述就好。

比如将上面的例子命名为 head_tpl.lua,那么引入的时候可以是这样:

local function pageSpec()
    return {
        html {
            include "/path/to/head_tpl.lua",
            body {
                ...
            }
        }
    }
end

当这个文件被 loadfile 进入 Lua,就成了一个 function,之前我说过给 pageSpec 设定了 fenv,而 include 是早已经建立好的函数,有自己的 fenv,这时候 include 进来的 head_tpl 函数,如果我不设置 fenv,直接调用获取结果的话,用的是 include 的 fenv。

在 5.2 时,如果我不事先记录 pageSpec 的 fenv,从 pageSpec 调用了 include 函数,在 include 函数里,我无法获取到 pageSpec 函数的 fenv,即便你知道堆栈上的前一个函数有我需要的 fenv,但就是拿不到。

在 5.1 的时候就很简单了,getfenv(2) 可以取到堆栈上前一个函数的 fenv,然后 setfenv 就行,方便极了。

[特例]:HTML tags function 在实现时,如果紧接着的 table 参数里面的第一项还是 table,是特别作为属性 key / value 用 pairs 函数遍历的,比如之前例子里面的 div id="class"

21年7月17日 周六 21:10

定制 UINavigationBar 的转场动画

最近 app UI 改版,交互设计师给出了 UINavigationController 下 Push/Pop 的转场动画,Push 的时候,fromView 会被缓慢往左推,toView 往左慢慢全部盖住 fromView,盖住之前,fromView 大概往左侧移动了 1/3 个页面;Pop 的时候则相反。

单单这样的转场动画是容易实现的,网上的教程一大堆,但难点在 UINavigationBar 也需要实现同样的交互。问题是 UINavigationBar 的动画是系统提供的,UIViewController 仅仅提供左、右、中的 CustomView,而且就我们的 App 来说,首页是隐藏的,在二级页面才会显示出来。由于 UINavigationBar 是系统管理的,转场动画的框架是没有对 UINavigationBar 提供支持的,得自己做。

网上搜索了一下,美团的技术博客提供了思路,具体我用的方案,在 iOS 14 模拟器上验证了蛮久,加上一些调试,如下:

Push 时,对 fromView 的 UINavigationBar 截图;如果是 hidden 状态则不用截图。toView 的 NavigationBar 是假的,但却是用真的 UINavigationBarItem 来构建的,真的 UINavigationBar,系统还未开始绘制呢。假的 NavigationBar 使用了 UINavigationBarItem 的 leftBarItem、rightBarItem 和 titleView 或者 title string。

这里需要注意将 fromViewController 上面对真的 UINavigationBarItem 的链接进行切断,比如 titleView 给他设置一个空的 UIView,leftBarItems、rightBarItems 给设置空的数组来填充。

fromView 截图的 UINavigationBar 按照转场动画的时长 Push 到左侧,toView 带的假的 NavigationBar 则从右侧 Push 进来,等动画结束,将 UINavigationBarItem 上的数据再拿给 fromViewController 使用,将假的 NavigationBar removeFromSuperview。

Pop 的时候需要区分是按返回按钮的 Pop,还是全局右划可中断的 interactive Pop。

先说简单的按返回按钮的 Pop,fromView 在对 UINavigationBar 截图后往右,toView 带着截图的 NavigationBar 出来,等动画结束,截图 NavigationBar removeFromSuperview 就好,这里比较简单。

可中断的 interactive Pop 复杂一些,举例我们用的是 FDFullscreenPopGesture,它其实是截获系统提供的全局右划返回功能。但系统的全局右划返回,当 UINavigationBar 的 backItem 被设置为 nil 等时候,是不起作用的。这个库是拿到系统手势检测 view,添加手势,检测到后调用系统的全局右划返回接口,发起是可中断的右划返回动画。

这个库暴露出来的检测手势 gesture 实例,我们后面会用到。

可中断的全局右划返回 Pop 跟直接调用 UINavigationController 的 popViewController 很不一样,由于动画可以中断,因此需要在 fromView 上建立假的 NavigationBar,截图就好。

手势的开始,来自于监听上面说到的 gesture 的 target action,可以监听 begin、change、end 事件,并计算右滑的百分比,用于调整截图 NavigationBar 跟随 fromView。难点来了,手势结束的时候,实际上右划动画也许结束,也许会中断返回,具体会如何,其实是系统判定的,我们并不知道后面动画要怎么样的,包括时长多久也都不知道,因此这里,需要建立一个跟踪机制,我用的是 DisplayLink 来做的,模拟器上看,比较跟手,但是太极端的情况,也会偶尔拉跨。

另外上面说到的假的 NavigationBar 将 UINavigationBarItem 给真的 UINavigationBar 时,其实会闪一下;以及截图 NavigationBar 的时机,实际上比转场动画要早,也就是截图代码不能放在转场动画开始的地方,那个地方才截图的话,太晚了。

因此需要在 UINavigationController 的 push、pop 接口地方做截图,另外全局右划的可中断 pop,截图是在手势开始的 begin,因为流程是 begin 开始后,才会调用到 UINavigationController 的 pop。

太多细节细节,不现场调试根本说不清楚,也不晓得会不会在后面的系统更新中变化,这里大体说了思路,以及需要注意的地方。

当然上面也提到过最后的效果,其实蛮不错的,

说一下前提,如果 UINavigationBar 设置了 translant 且真的用上了半透的话,效果应该是不行的,因为系统真实的 UINavigationBar 动画在底下会动,得实际遮住才行,因此这套东西,我是觉得只能用在非 translant 的场景下,当然,如果只是设置了 translant,但实际上又带了 opaque 的背景,仅仅是为了带一丢丢高斯模糊背景的话,是没问题的。

21年7月11日 周日 12:08

PinStackView

目前工程主要使用 AutoLayout,但一些复杂页面上面,先不说各种约束优先级的问题,单单就因为约束不对造成的约束失败,在 debug console 里面打印一大堆,都不好定位在哪里,别说去修复了。

总觉得大部分的场景,其实使用固定 frame 的 layout 就足够了,不需要那么复杂,于是我在工程里面引进了 PinLayout

因为使用 PinLayout 构建复杂页面确实太辛苦,其只用于搭建不复杂的元素,之前使用的是 StackViewLayout,但测试反馈问题比较多,我自己也发现,不大稳定,偶尔还有 crash,毕竟这个库是未完成的状态,然后还自带内存泄漏。

于是就想着自己写一个,毕竟 StackViewLayout 也用了这么久,接口已经蛮熟悉的了,而在功能上面,需要的又不多。

于是就有了 PinStackView,这个库相比 PinStackLayout 来说,简单很多,其实只有一个文件,另外,其处理流程上也是很简单的。

配置

最后增加了一个 autoSizeChangedCallback 的回调函数,如果 style 设置为了 auto,PinStackView 在 axis 上的长度由其 item 动态长度计算得到,每次对 bounds 有更改,都会 callback 通知。

另外上面的设置其实隐含了一些前提,在 fixed style 时,PinStackView 的大小是外部设置的,此时才有 start、end、equal 这些 distribution;而在 auto style 下,只有 start distribution,其实很好理解。

管理接口

下面是管理 view 的接口,添加、插入都会返回 PinStackItemInfo

item 配置

对于每个管理 view,在添加、插入的时候,都绑定了一个 PinStackItemInfo 用来描述其 layout 信息,下面的接口都可以进行链式配置

上面第二点,如果设置了 alignment 或者 alignSelf 为 stretch,会覆盖 item 在 cross axis 的长度

计算流程

建议看代码吧,其实没啥好说的,fxied style 下面,equal 就是平均 axis 的长度,start、end 的计算是最复杂的,因为可以带 grow(),需要先计算固定长度的,后面再分配动态长度的。

auto style 其实相对好计算,计算完所有管理 view 沿着 axis 的长度后,对比 PinStackView 在 axis 的长度,有变化的话就修改 PinStackView 的长度,然后 callback。说一下 callback,这个通知,可以回调到外部依赖 PinStackView frame 的地方,重新设定相关的 layout。

这个当然有可能造成 layout 的循环,之前的 StackViewLayout 就有这样的场景,我在这里将这个依赖交给了用户,用户自己来控制。

PinStackView 里面对于管理 view 的宽高计算是一个焦点,代码在 calcViewSize() 里面

将 StackViewLayout 换成 PinStackView 后,循环 layout 的场景不再出现,layout 也挺正常的,但是公司人员变动太大,貌似这个需求测完了,但上线遥遥无期,再说吧。

21年6月15日 周一 21:15

MoonCake Programming Language

从 4 月底开始就很少玩游戏,因为迷上了另外一件事情,基于 Lua,创建一个语法上类似 Swift 的编程语言,一方面学习 LPeg, 另外一方面,按照自己的想法,对 Lua 的语法修修改改。

基本上,Lua 用了很多的词来分割语意单元,比如 C/C++ 里面的语法块,是放在 '{' 和 '}' 包裹中的,在 Lua 中是 do 和 end 中,或者 then 和 end 中, 当内容一多的时候,都是词;还有就是 function,连 Swift 都是 func 就可以了,为何要一个完整的单词呢。

还有少不了的 local,避免全局变量污染,就只好都是 local 了,我觉得默认就行,总是 local 有点多余。

其实最开始就是上面这 3 点,当然也参考了一下有名的 MoonScript,因为它是 MIT 协议开源的嘛,还想抄来着,结果实际上,从头到尾几乎都是重新构建的, 只有 global name,真的是抄的,加上了 LuaJIT 的 jit 等不多的几个而已。

LPeg 的 P、S、Cg、Ct、Cmt、Cp、Cb、Cf,刚开始的一两天,真的头大,后面就变成肌肉记忆了。

LPeg 的匹配规则有两种,一种是全文完全匹配,得到的是完整的 AST,另外一种是寻找匹配,拿到部分匹配的内容。对于一门编程语言,只能选择第一种。

语言中的 class / struct 当然是基于 metamethod 的,其中,class 被设计为跟普通的 table 一样,可以随机创建 key / value,当然少不了继承;而 struct,想学 Swift,没有继承, 且 key 不能为 nil,为了效率考虑,子类以及实例,都会读取自己的类或者父类,如果不为 nil,就 rawset 到本地,下次访问就快了,但是对于父类不断变动的信息,实际上是获取不到的, 这种情况只有通过类方法来解决。

实际上还加了 extension 的逻辑,使得 struct 不能继承这点其实是可以被突破的,而且 class 和 struct 两者就单单使用上来说,区分没有那么大。

我还加了 guard、switch 关键字,都是 if 的语法糖。

还有 defer 这个关键字,处理比较有意思,实际上我是能做到在任何 scope 都是起作用的,但是我嫌麻烦,而且还读到某 Swift 大 V 直觉认为 Swift 的 defer 只支持 function scope 而导致某开源软件的 bug,当然我之前也是这么认为的,所以最后在这个语言中,我将 defer 限定为了 function scope,毕竟能用就行了,直觉理解不好的我觉得也不大好。

说一下 continue,我在这个语言中也实现了,基于 goto,我的理解 continue 其实就是 goto 的语法糖,为何 Lua 不选择支持不理解。

语言鼓励偏向 Swift 一样基于 class / struct 的数据和方法来编程,也有相关 VSCode 的 extension 做简单的支持,比如高亮、outline,至于 LSP 嘛,那是没有的。

最后爆地址,在 MoonCake.

21年5月24日 周一 22:10

编写 VSCode Extension

如果只是搭一个框架,VSCode Extension 的编写没有想象中那么无从入手,照着文档 VSCode插件开发全攻略 一步一步往下学就是了。

其实我的要求有 2 个

至于 language server 的支持,自然是不敢想的。

语法高亮

先说语法高亮吧,用不到 typescript 编程,只需要会正则就好,但是这个正则的规则,是跟 TextMate 一样的,文档在这里

这个正则默认是行匹配的,不会跨行,如果需要跨行,就需要设置 begin、end capture,另外,这个正则匹配,在设置的时候,就需要指定高亮颜色,偏偏 VSCode 提供的几种高亮颜色其实不怎么够用,我是想抄 EmmyLua 的一些高亮,比如全局变量的高亮,没学会人家是怎么搞的。

一些实践来的经验:

"strings_long": {
	"name": "string.quoted.single.name",
	"begin": "\\[(=*)\\[",
	"end": "\\]\\1\\]",
	"patterns": [
		{
			"name": "constant.character.escape.name",
			"match": "\\\\."
		}
	]
}

侧边栏大纲

这个需要 typescript 编程了,我的 typescript 小学生水平都可以搞出来 outline,想必是不难的。

先吐槽一下,typescript 是要编译成 javascript 才能用的,另外有引入库的话,记得 npm install,否则会像我一样,浪费了大量的时间在这些基础上面。

outline 其实已经是 language server 的范畴了,只不过我们简单处理,将这个抽象借口部署在本地,就是简单的文本处理就好。

不贴代码了,反正可以在网上找到大量的的例子,比如一个行处理,split 空格拿到 token 关键字的 LS,其实不堪用,但是我是没办法,暂时先这么用着了,毕竟海没学会更好的处理方法。

然后呢,最外层的循环用的是变量 i,我是刚开始不小心,里面一层小循环也用变量 i 了,然后这个 typescript 停不下来了,也是无语,没有语法 scope 的吗,难过。

分析 token 语法的结构,是提供给 VSCode 一个带层次的 node 列表,首层是 outline 第一级,如果需要二级的画,比如 class node 里面再塞入 funciton、property 之类的,就是在这个一级 node 的 children 列表里面塞入 node,VSCode 就会帮你将这个 outline 画出来。

对了,这些 node 需要指定类型,比如是 class,还是 function,还是 field,其实还有很多,property,namescope 之类的,但是实话说,只有前面这 3 个是比较好看的,其他的应该是我比较少见,不明白这些个 icon 是想干嘛。

有了上面这些基础,我也终于理解了好些 Lua extension 为何只用上面这 3 个 outline 类型了,另外,我还知道了为何这些 Lua extension 列出了一大堆 local 变量,明明更重要的应该是全局变量呀。

有个坑必须要说一下。

不晓得是不是 outline 默认了一个 node 就是一行的原因,同一行塞入多个 node,会造成接下来的行拿不到内容,我猜测是我小学生水平的 typescript 哪里搞错了,但具体到底是哪里,目前无从知晓。

先这样吧。

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 实例。