金属与皮肤:Codename One 推出 Metal 渲染、皮肤设计器及多项更新
目录
- 如何思考质量
- 粘性标题半生不熟,但这是有意为之
- SIMD bug,这是我的错
- Metal来了
- 皮肤下载器的终结
- 向导如何工作
- 吃自己的狗粮
- iOS多行TextArea:将Return视为完成
- 状态栏点击滚动到顶部的诊断
- 模拟器:深色/浅色模式切换
- 提醒:周末后端维护
- 警告:Android 16将有效禁止锁定屏幕方向
- 为什么版本跳到了7.0.242
- 总结
[LOADING...]
这篇文章涵盖了很多内容。在讨论这些之前,我想先谈谈一个不太令人舒服的话题:质量。过去两周发生的两起事件需要公开解释,一个是我们正常迭代过程中出现的 bug,另一个则是我个人的严重失误。这两件事都值得给出我作为用户也会希望得到的解释。
如何思考质量
Codename One 是一家小型开源公司。我们不是拥有200名工程师的平台团队,没有专门的SRE轮岗和独立的QA部门。我们行动迅速,每周都会发布有意义的新代码,并且我们投入了大量精力确保这种速度不会以破坏你的应用为代价。
“大量精力”这句话需要具体说明,以下是实际投入的情况:
这是相当多的自动化覆盖,在代码落地之前就能捕捉到很多问题。但它无法捕捉的是全新的行为,因为新特性还没有任何可以对比的基线。一个新测试的第一张黄金截图本身也可能就是bug,直到有人真正运行该功能并告诉我们。
考虑到这一点,我们来谈谈过去两周发生的两起具体事件。
粘性标题半生不熟,但这是有意为之
上周的文章介绍了StickyHeaderContainer,其中包含节标题之间的动画过渡。几天之内,问题追踪器就收到了#4849,NONE和FADE过渡未正确表现,交换时出现可见抖动。我们在报告后数小时内就完成了修复。
这种“发布 - 听取真实用户反馈 - 修复”的循环,是我们与社区达成的共识。我们的像素对比工具非常擅长捕捉已有代码的回归问题,但它在结构上不擅长捕捉全新组件的第一个版本,因为还没有任何可比较的对象。我们本可以把StickyHeaderContainer再私下打磨两周,但那样反而会发布一个更糟糕的组件,因为没有Thomas的视角。在公开环境中迭代,配合紧密的反馈循环,才是我们这样规模的团队保持前进的方式。
SIMD bug,这是我的错
PR #4842则是一个不同的故事。iOS上的SIMD代码使用alloca将工作缓冲区放在栈上以提高速度。对于小型缓冲区这是正确的选择,但对于大型缓冲区则是错误的选择,超过一定大小后,栈请求直接失败,进程崩溃。图像API使用这些缓冲区,在处理大图像时,缓冲区超过了阈值,结果导致生产环境中的图像API崩溃。
这不是一个新特性,而是对现有成熟代码的改动。我们采取了修改长期代码时一贯的预防措施:运行现有测试,所有现有测试都通过了,然后补丁就发布了。但bug还是漏过去了,因为测试覆盖只验证了SIMD路径在小图像上的表现,从未验证过实际触发故障的大图像。测试中的这个漏洞本该由我察觉,但我没有。
问题报告后,修复在24小时内完成。补丁版本检测到过大情况,回退到堆分配。小SIMD操作保持快速的alloca路径,大操作不再崩溃。新增测试覆盖了阈值情况,因此这种特定类型的bug不会再次出现。
这本不该发生,但现实中它还会再发生——不是这个确切的bug,而是类似的。测试并不完美,我的测试当然也不完美。因此,我希望强调的是:
Codename One是bug与你的最终用户之间的第一道防线,但我们不是最后一道。 在发布前测试你的应用。如果你的应用支持,使用beta渠道(iOS的TestFlight,Android Play Console的内部或封闭测试轨道),这样像这样的bug会在影响到付费用户之前先击中你。这小小的一步是你的用户最可靠的保护。
我们还在积极构思框架内部的下一代崩溃保护。当前的崩溃保护位于EDT,捕获用户代码抛出的RuntimeException。下一代需要进一步扩展,涵盖原生崩溃、更早期的启动阶段,以及返回给开发者的更有用的诊断信息,而不仅仅是设备日志。目前还没有PR,我们仍在规划形态,但这是我们在框架层面为社区应用提供更坚实基础所做的重大投入。
质量讨论告一段落,本文的其余部分将介绍实际发布的内容。
Metal来了
PR #4799是我们数月来最大的单次变更:为iOS提供完整的Metal渲染后端。它与现有的OpenGL ES 2路径并存,通过一个构建提示启用,拥有自己的CI任务和像素对比黄金截图。
Metal是Apple的现代图形API。iOS上的OpenGL在iOS 12时已被Apple弃用,至今仍在运行,我们也一直维护着它,但Apple的“弃用”是一个缓慢的倒计时,最终平台会停止支持。现在迁移到Metal是为了提前应对这一变化,并为你的应用带来实际好处:
- 更佳的渲染性能。 更低的绘制调用开销、现代命令编码器批处理和管线状态缓存,共同带来更流畅的滚动和更快的动画过渡。
- 更省电。 Metal降低了每帧的CPU开销,意味着GPU空闲时间更少,CPU记账时间也更少。长时间运行、图形密集的应用受益最大。
- 更清晰的文字。 字形通过CoreText图集渲染,在相同大小下产生明显更锐利的渲染效果,具有合适的字距和正确的复杂脚本处理。
- 纯GPU渐变。 线性渐变和径向渐变完全在GPU上渲染,无需经过
CGContext位图的往返。 - 访问现代Apple图形特性。 新的iOS渲染特性(可变速率着色、网格着色器、Apple Silicon上的光线追踪)仅限Metal。坚持使用GL意味着我们将错失这些机会。
要在项目中启用Metal,设置构建提示:
其他一切保持不变。Java接口不变,你现有的代码继续工作。
我们计划在两周内将Metal设为默认, 前提是没有重大问题出现。ios.metal提示将保留(设置为false可回到GL),但新项目和构建服务器的默认行为将切换。如果你在发布iOS应用,请现在设置该提示,并让你的真实流程通过它。我们希望回归问题在你的真实屏幕上暴露出来,而不是在默认更改后的第二天。
Metal移植最明显的用户可见改进是文字。以下是Metal截图套件中的ShowcaseTheme截图:
[LOADING...]
[LOADING...]
以及SpanLabelTheme截图,这是正文渲染、多行、可变宽度的真正考验,正是真实应用中出现的段落:
[LOADING...]
Metal的Dialog截图也值得展示,因为半透明表面正确地与纹理背景合成:
[LOADING...]## 皮肤下载器的终结
PR #4758 将皮肤设计器(Skin Designer)作为一个 JavaScript 包,嵌入到网站的 /skindesigner/ 页面中,就像 Playground 和 Initializr 一样。你可以在浏览器中构建皮肤、保存它,并在模拟器中使用,而无需安装任何东西。
这不仅仅是网站便利性的提升——这是我们摆脱皮肤业务的方式。
在 Codename One 的整个历史中,“没有 iPhone 16 Pro Max 的皮肤”或“没有 iPad mini 7 的皮肤”一直是一个反复出现的抱怨,我们一直在尽可能快地发布皮肤。但这种模式从未能规模化。苹果发布新设备尺寸的速度远超我们任何一个人想维护并行皮肤目录的速度,而 Android 则拥有几乎无限的设备外形。今天,我们正在弃用皮肤下载器,转向一个通用的基于浏览器的创作工具。
需要明确的是,以下内容正在发生变化:
- 现有的皮肤不会消失。 今天发布的每一个皮肤都将继续工作,继续在模拟器中加载,并继续得到支持。我们不会移除它们。如果你的团队有一个基于现有皮肤的工作流程,该工作流程将继续有效。
- 我们将停止发布新的皮肤。 当下一代 iPhone 或 iPad 发布时,我们不会发布官方皮肤。任何人都可以在新设计器中几分钟内构建一个皮肤,这个“任何人”当然包括我们,也包括你。
“没有 X 设备的皮肤”问题得到了通用解决。如果你在一台不太常见的 Android 设备上运行一个小众的企业应用程序,你不再需要等待我们为它制作皮肤。构建一次,将其放入你团队的共享资产中,完成。
向导的工作原理
皮肤设计器将设备规格(分辨率、PPI、字体、安全区域内边距、挖孔)转换为一个 .skin 文件,JavaSE 模拟器可以加载该文件。它在你的浏览器中运行。无需安装。该向导故意带有主观意见。它附带一个精选的设备目录,程序化生成设备框架,并编写一个与 Codename One 附带的 iPhoneTheme.res、iOS7Theme.res 和 android_holo_light.res 主题相匹配的皮肤布局。
如果你只想要一个皮肤而不关心它是如何构建的,选择一个设备,接受默认设置,点击完成,然后下载皮肤。该文件已准备好通过模拟器皮肤菜单中的添加功能加载。
阶段 1,选择设备。 第一步显示捆绑目录中每个设备的卡片。搜索框按名称过滤(同时匹配型号和品牌),下方的芯片按外形因素缩小范围:所有 / 手机 / 平板 / 可折叠。选择一个设备会将其分辨率、PPI、屏幕尺寸、默认安全区域内边距以及来自目录的 iOS 或 Android 系统字体名称拉取进来,然后生成一个合理的起始框架:根据设备的硬件自动应用刘海、灵动岛或挖孔的预设。目录很大,网格默认限制为最新的匹配项,在搜索字段中输入内容可以查找较旧或不太常见的设备。
[LOADING...]
阶段 2,选择起始来源。 有三种方式可以生成皮肤的主体图像:
- 选择一个形状 从一个小的预设库中程序化生成设备框架(圆角矩形、刘海、灵动岛、挖孔、角落挖孔、经典 Home 键)。框架渲染为深色渐变,屏幕矩形(以及任何挖孔)被雕刻在其中。当你想要一个通用的 iPhone 或 Android 框架并且不在乎精确的硬件保真度时,这是最佳选择。
- 上传图像 打开一个图像选择器。向导将图像缩放到设备的分辨率,然后在上面雕刻屏幕矩形和挖孔。当你拥有你所针对的特定设备的营销渲染图时,使用此选项。
- 空白矩形 将边框和圆角半径缩小到几乎为零,删除所有挖孔,并关闭底部 Home 指示器。屏幕填满整个皮肤。对于桌面或 Web 模拟器,设备框架只会是视觉干扰,此选项很有用。
[LOADING...]
阶段 3,编辑器。 编辑器分为两个窗格:左侧是实时预览,绘制设备框架、屏幕色调、挖孔和 Home 指示器;右侧是带有三个标签的侧边栏。
形状 标签显示一个预设网格(圆角矩形、刘海、灵动岛、挖孔、角落挖孔、经典 Home)以及圆角半径、边框厚度的尺寸字段,以及一个用于底部 Home 指示器的开关。从 iPhone X 及之后的机型以及大多数现代 Android 设备应保持指示器开启,带有硬件 Home 键的经典设备应将其关闭。
[LOADING...]
挖孔 标签列出当前皮肤上的每个挖孔。点按一行可展开其宽度、高度和偏移量字段。底部的三个添加按钮为每种类型生成一个合理的默认值。刘海(180 x 30 视口像素)是一个物理硬件挖孔,在屏幕矩形上方的设备框架中绘制,镜像 iPhone X / 11 / 12 / 13 硬件。灵动岛(120 x 35)是一个软件预留空间,在屏幕矩形内渲染为一个不透明的药丸形状,浮动在 iOS 状态栏之上。挖孔(28 x 28)是一个 Android 挖孔摄像头,渲染方式与灵动岛类似。当向导生成 .skin 时,它会自动扩展 safePortraitTop 以覆盖任何屏幕内挖孔,以便应用程序内容出现在浮动形状下方。
[LOADING...]
信息 标签大部分是只读的,显示将要写入 skin.properties 的内容:名称、宽度、高度、PPI、每毫米像素数以及用户可编辑的安全区域内边距。该向导故意不写入 smallFontSize、mediumFontSize 或 largeFontSize,当这些缺失时,模拟器会自动从 pixelMilliRatio 推导出它们,而这正是你希望在高 PPI 屏幕上看到的。
[LOADING...]
阶段 4,完成并下载。 点击完成,将以设备的实际分辨率渲染纵向皮肤图像,带有圆角、透明屏幕、不透明挖孔以及(如果启用)Home 指示器。它通过 90 度旋转合成横向皮肤,写入标记屏幕矩形的 skin_map.png 叠加层以供模拟器的屏幕位置检测使用,将适当的原生主题捆绑到皮肤 zip 包中,并写入包含平台元数据、安全区域、PPI 和显示矩形的 skin.properties。点击下载皮肤,文件将进入浏览器的下载对话框。文件下载到磁盘后,将其放入模拟器的皮肤文件夹(或使用模拟器皮肤菜单中的添加命令),你的新设备应该会出现在选择器中。
[LOADING...]
生成的 .skin 只是一个重命名的 zip 包:
位于 Skin-Designer.asciidoc 的完整开发者指南章节,通过带注释的屏幕截图详细介绍了每个阶段,并记录了向导写入的 skin.properties 键(roundScreen、displayX/Y/Width/Height、safePortrait*、safeLandscape*、overrideNames、系统字体族、PPI 和像素比)。
吃自己的狗粮
在谈论皮肤设计器的同时,现在是一个合适的时机来指出我认为真正值得强调的一点。Initializr、Playground 和皮肤设计器 都是开源的 Codename One 应用程序。它们是用 Java 编写的,使用你用于构建 iOS 和 Android 应用程序的相同 Codename One UI 框架,并通过我们的 JavaScript 端口部署到浏览器。
你与这些工具的每一次交互——设备选择器网格、渲染设备框架和挖孔的实时预览、带有标签侧边栏的表单驱动编辑器、在浏览器标签页中生成并打包 .skin zip 的文件生成——都是你应用程序中相同的 Codename One 代码。Container、Form、BoxLayout、主题和事件处理代码与你为手机构建所编写的代码完全相同。JavaScript 端口将其转换为浏览器可以运行的内容。
这三个工具是我们能提供的关于 Codename One 能力最直接的证明:真实的、非平凡的 UI,具有状态、文件 I/O、图像生成和复杂布局,在浏览器标签页中流畅运行。如果你曾经怀疑 JavaScript 端口是否足够生产级别以用于真正的应用程序,Initializr、Playground 和皮肤设计器就是你的答案。它们也是“Codename One 能否构建超越移动设备的应用程序”这个问题的答案。相同的代码库,部署到第四个目标,无需重写。
所有这三个工具的源代码都存放在与框架本身相同的 CodenameOne 仓库中。如果你想了解一个非平凡的 Codename One 应用程序是如何构建的,这三个地方是开始阅读的好起点。
iOS 多行 TextArea:Return 作为 Done
PR #4859,由 issue #4854 驱动,为多行 TextArea 添加了一个可选标志,使 iOS 键盘的 Return 键充当 Done 键。它会关闭编辑器并触发 Done 监听器,而不是插入换行符。这是 iOS 的“提醒事项”应用的行为:一个可增长的多行任务标题字段,其中 Return 键完成输入。
之所以需要一个标志,是因为真正的 iOS 并没有将其作为内置原语公开。“提醒事项”是在 UITextView 上实现的,其代理在 shouldChangeTextInRange: 中拦截 \n。我们精确地复制了这一点,并由一个客户端属性控制,以便现有布局不受影响:
当设置该标志时,键盘的 Return 键会重新标记为 Done(UIReturnKeyDone)。默认行为不变:该标志默认为关闭,仅对多行 TextArea 生效,并且仅拦截精确的 "\n" 替换,因此粘贴的多行文本不受影响。
状态栏点击滚动到顶部的诊断信息
PR #4868,由 issue #3589 驱动,为 iOS 状态栏点击路径添加了三个互补的诊断工具。我们之前发布了一个修复(#4857),但报告者仍然在设备上看到没有滚动。为了避免再在黑暗中摸索,我们构建了使该路径可观察的工具。
- 模拟器菜单,
Simulate > iOS Status Bar Tap。 合成与scrollViewShouldScrollToTop:分发的相同的(displayWidth/2, 0)点击,弹出一个报告响应者 UIID、构建提示状态和 OK / PROBLEM 结论的对话框,然后实际触发pointerPressed和pointerReleased,以便任何已连接的滚动到顶部的功能都可观察。 - 设备端属性。
Display.getProperty("cn1.iosStatusBarTap.count")、cn1.iosStatusBarTap.lastEpochMillis、cn1.iosStatusBarTap.lastX/Y和cn1.iosStatusBarTap.proxyInstalled允许你在真实 iPhone 上检查该路径。在设备上运行你的应用程序,点击状态栏,然后读取属性。这可以区分“iOS 从未传递消息”和“iOS 传递了它,但 Codename One 组件拦截了点击”。 - 回归覆盖。
StatusBarTapDiagnosticScreenshotTest通过一个 2x3 的框架网格执行完全相同的代码路径,可见计数器上升,滚动位置交替,因此未来的回归会在 CI 中暴露出来。
模拟器:深色 / 浅色模式切换
PR #4871 在模拟器的 Simulate 菜单下添加了一个 深色 / 浅色模式 子菜单,包含三个选项:深色模式、浅色模式和不支持(默认)。
选择某个选项会翻转 Display.isDarkMode()(Boolean.TRUE / Boolean.FALSE / null),并调用 refreshSkin(...),以便分支于 @darkModeBool 的主题立即重新渲染。该选择会持久化到 cn1.simulator.darkMode 偏好设置中,以便模拟器在你离开的模式下重启。
结合我们两周前发布的 原生主题 菜单,你现在可以坐在一个皮肤上,在几秒钟内切换 iOS Modern、Material 3、iOS 7 和 Holo Light,分别对应浅色、深色和不支持模式。日常的便捷之处在于,无需重启模拟器,就能验证你自己的主题在深色模式下看起来是否正确。
注意事项:周末后端维护
本周末,我们将对我们的构建后端服务器进行一些维护。这项工作从外部看大多不可见,但它触及了足够多的基础设施,因此你可能在维护窗口期间看到间歇性的构建问题:比平时更慢的构建、偶尔需要重试,可能还有一段短暂的时间新构建被排队。
我们这样做是因为底层后端需要向前发展,而推迟这项工作的成本正在不断增加。我们将尽最大努力缩短中断时间。如果你有一个硬性的发布截止日期在本周末,请提前规划。否则影响应该很小,你可以正常构建。
警告:Android 16 将实际上禁止锁定方向
感谢 Durank 指出了 #4879。Android 16 行为变更 包括一个对 Android 如何处理方向的有意义的更改,简而言之,在大屏幕设备上,平台将忽略应用程序锁定方向的请求。如果你的应用程序调用 Display.lockOrientation(...) 或在 Android 清单中设置了固定方向,那么在手机上该锁定会得到尊重,但在平板电脑和可折叠设备上,一旦设备目标为 Android 16,该锁定将实际上被忽略。
在框架方面,我们对此能做的不多。这是一个平台级别的决定,对于一般应用程序没有公开的选择退出机制。现实的路径是设计在两种方向下都能工作的布局,并在 Android 16 到达你的用户之前,在平板上以纵向和横向两种方向测试你的应用程序。我们将继续关注 Google 发布的任何选择加入路径,但目前请相应地进行规划。
为什么版本跳到了 7.0.242
关于版本号的一点小说明:当前版本是 7.0.242,而不是你根据节奏可能预期的 7.0.238。这个差距是真实存在的,值得解释。我们对 Maven 原型(archetype)进行了一次修复,将我们在 Codename One Initializr 中添加的功能带到了从命令行创建的项目中。这个更改本身很简单,但它与我们的发布构建自动化产生了不良交互,我们不得不在中途删除几个发布版本,以使流水线恢复正常。我们在该过程中烧毁的版本号是可见的伤疤。好的一面是,命令行 mvn archetype:generate 现在产生的项目与 Initializr 生成的项目一致,这正是我们一直想要的。## 总结
过去一周,我们处理了 24 个问题,其中相当一部分直接受益于 Metal 移植。旧的仅支持 GL 的光栅化差异、视网膜屏幕下的字体缩放、多边形绘制伪影、透视变换问题——这些在 Metal 流水线中都能直接正确渲染。迁移渲染层成为了一次性清理大量微小 Bug 的最干净方式。随着新的皮肤设计器在同一周上线,两个长期存在的结构性问题从“改天再修”变成了“已修复并上线”。
如果你正在开发 iOS 应用,请在本周将 ios.metal=true 设置为 true,并用你的真实应用进行测试。我们希望现在就发现任何遗留问题,而不是在默认值切换的那一天。问题跟踪器在这里,Playground 是测试新主题最便捷的地方,皮肤设计器 已在网站上线。
本周特别感谢 Thomas(@ThomasH99) 报告了粘性标题过渡问题并跟进选择器居中问题,Francesco Galgani(@jsfan3) 提出了类似 iOS 提醒事项的返回即完成功能请求,以及报告了 #3589 问题的用户,感谢他配合我们通过多次 PR 诊断状态栏点击问题。上面“测试不能涵盖所有情况”这一节,同时也是“所以我们需要你”的一节。正是因为你们持续提交反馈,这个项目才能良好运转。