Ohhnews

分类导航

$ cd ..
foojay原文

NFC、加密、生物识别及新的Build Cloud预览

#nfc#加密#生物识别#build cloud#蓝牙

目录

全新的构建云 UI —— 预览设备 API 成为一等公民

上周是关于默认值的。这周是关于设备 API 迁移到框架核心、一个彻底改变蓝牙开发的小型模拟器变更,以及我们期望获得反馈的全新构建云 UI 预览。这里还包含一些其他内容——而我在 上周 提到的 Metal 默认切换状态与我的预期有所不同,这点值得在最后提一下。

什么是 Codename One? Codename One 是一个开源框架,用于从单个 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。了解更多信息请访问 codenameone.com

全新的构建云 UI —— 预览

本周最显著的变化隐藏在构建云登录背后。我们已经使用了多年的控制台正在被替换。新 UI 现已上线 此处,同时您仍然可以在 此处 找到当前控制台。在我们将它设为默认之前,我们希望获得您的关注和反馈。 [LOADING...]

整个控制台使用 Java 17 针对 Codename One UI 框架编写,然后通过我们的 JavaScript 端口编译为 JavaScript,并作为静态资源从构建云内部提供。这些代码与您为手机构建时编写的 FormContainerBoxLayoutToolbartheme.css 完全相同。

这与 Initializr、Playground 和 Skin Designer 已经遵循的流程相同。四个非平凡的 Codename One 应用作为生产工具部署到浏览器。如果您曾怀疑 JavaScript 端口能否承载复杂的应用程序 UI,这就是我们能给出的最直接的答案。

设备 API 成为一等公民

本周更大的结构性变化是,三个原本存在于 cn1libs 中或根本不可用的 API 现已内置到框架核心中:生物识别、密码学和 NFC。统一的想法是,你不应该为了完成这些基础工作而添加一个 cn1lib。cn1lib 模型对于真正第三方的功能以及那些在核心中意义不大的特性仍然有用。我们正在吸收的现有 cn1libs 在已依赖它们的项目上将继续保持不变——但进入核心的标准已经提高。

生物识别 —— PR #4987

Touch ID、Face ID 和 Android BiometricPrompt 现在位于 com.codename1.security.Biometrics 中。与原始的指纹 API(该 API 在面部扫描出现之前就已存在,但未重命名)相比,该 API 使用更简单的语义。您可以使用 canAuthenticate() 进行访问控制,然后通过一个返回 AsyncResourceauthenticate(...) 调用,在失败路径上返回类型为 BiometricError 的错误码。

$ java
Biometrics b = Biometrics.getInstance();
if (!b.canAuthenticate()) {
  // 无硬件或未登记生物特征
  return;
}
b.authenticate("解锁您的账户").onResult((success, err) -> {
  if (err != null) {
    BiometricError code = ((BiometricException) err).getError();
    switch (code) {
      case USER_CANCELED: return;
      case LOCKED_OUT: fallToPassword(); return;
      case NOT_ENROLLED: askToEnroll(); return;
      default: fallToPassword();
    }
  } else {
    unlock();
  }
});

在 iOS 上,它封装了 LocalAuthentication.framework;在 Android API 29+ 上,它使用 BiometricPrompt,而在 API 23-28 上,它通过反射适配器保留旧的 FingerprintManager 路径。构建服务器和本地构建会无缝处理权限和框架链接,因此您无需执行任何操作,也无需添加构建提示。它开箱即用

Java SE 模拟器有一个新的 Simulate -> Biometric Simulation 子菜单,包含一个 Available 开关、每种模态的注册选项,以及下一次 authenticate(...) 调用的可配置结果。这样您就可以在不离开模拟器的情况下,演练每个代码分支——成功、用户取消、锁定、无硬件。

如果您一直依赖老牌的 FingerprintScanner cn1lib,它将继续正常工作。新代码应使用 com.codename1.security.Biometrics

密码学 —— PR #4994

常规密码学功能(哈希、MAC、对称和非对称加密、签名、JWT、OTP)现在位于 com.codename1.security 中,并随框架一起发布。纯 Java 算法(Hash、Hmac、Base32、JWT 和 OTP 机制)在每个支持的平台上产生相同的输出。需要真实密钥的部分——AES、RSA、ECDSA、SecureRandom——通过每个端口的原生加密提供程序路由,因此您可以获得设备提供的硬件支持的原语。

典型的 AES-GCM 往返过程:

$ java
SecretKey key = KeyGenerator.aes(256);
byte[] nonce = SecureRandom.bytes(12);
byte[] enc = Cipher.aesEncrypt(Cipher.AES_GCM, key, nonce, null,
"secret".getBytes("UTF-8"));
byte[] dec = Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, null, enc);

SHA-256 哈希:

$ java
byte[] digest = Hash.sha256("hello".getBytes("UTF-8"));
String hex = Hash.toHex(digest);

签名 JWT:

$ java
byte[] hsKey = KeyGenerator.hmac(256);
String token = Jwt.signHs256(hsKey)
.claim("sub", "user-42")
.claim("exp", System.currentTimeMillis() / 1000 + 3600)
.compact();

Jwt parsed = Jwt.verifyHs256(token, hsKey); // 签名错误时抛出异常
String sub = parsed.getClaim("sub").asString();

以及与 Google Authenticator / Authy 兼容的 TOTP:

$ java
byte[] sharedSecret = Base32.decode("JBSWY3DPEHPK3PXP");
String code = Otp.totp(sharedSecret); // 当前 30 秒窗口
boolean ok = Otp.verifyTotp(code, sharedSecret, /* 允许误差 */ 1);

该 PR 还附带了一个匹配的 UI 小部件——com.codename1.components.OtpField——一个分段式、自动前进的 OTP 输入框,支持粘贴分发和完成监听器,因此“输入您的 6 位代码”屏幕现在只需几行粘合代码:

$ java
OtpField otp = new OtpField(6);
otp.setCompleteListener(code -> {
  if (Otp.verifyTotp(code, sharedSecret, 1)) {
    proceed();
  } else {
    otp.setError("错误代码");
  }
});
form.add(otp);

我们特意选择了保守的默认值:新的认证 AES 使用 AES/GCM/NoPadding,新的 RSA 使用 RSA/ECB/OAEPWithSHA-256AndMGF1Padding,常量时间 HMAC 比较,SecureRandom 上无偏的 intBelow(n)。MD5 / SHA-1 / PKCS#1 / ECB 变换仍然存在,因为真实应用仍然需要与遗留系统互操作,但文档将其标记为仅用于互操作。

NFC —— PR #4996

com.codename1.nfc 是第三个新增项。一个单一的 Nfc 入口点,一个 NdefMessage / NdefRecord 对(带有类型工厂:createUricreateTextcreateMimecreateExternalcreateApplicationRecord),每个技术的 Tag 子类(IsoDepMifareClassicMifareUltralightNfcANfcBNfcFNfcV),以及用于模拟非接触式卡的 HostCardEmulationService 基类。

读取 NDEF URI 标签——即“点击海报”模式:

$ java
Nfc nfc = Nfc.getInstance();
if (!nfc.canRead()) return; // 无 NFC 硬件 / NFC 已禁用

nfc.readTag(new NfcReadOptions()
  .setNdefOnly(true)
  .setAlertMessage("靠近海报"))
  .onResult((tag, err) -> {
    if (err != null) return;
    tag.readNdef().onResult((msg, e) -> {
      if (e == null) {
        String url = msg.getFirstRecord().getUriPayload();
        Display.getInstance().execute(url);
      }
    });
  });

与 EMV / 交通卡交换 APDU:

$ java
nfc.readTag(new NfcReadOptions()
  .setTechFilter(TagType.ISO_DEP)
  .setIsoSelectAids(myAid))
  .onResult((tag, err) -> {
    if (err != null) return;
    IsoDep iso = tag.getIsoDep();
    if (iso == null) return;
    iso.transceive(myCommandApdu).onResult((resp, e) -> {
      if (ApduResponse.isSuccess(resp)) {
        /* 解析响应 */
      }
    });
  });

通过主机卡模拟充当非接触式卡:

$ java
class LoyaltyCard extends HostCardEmulationService {
  public String[] getAids() { return new String[] { "F0010203040506" }; }
  public byte[] processCommand(byte[] apdu) {
    return ApduResponse.withStatus(loyaltyId.getBytes("UTF-8"),
      ApduResponse.swSuccess());
  }
}
Nfc.getInstance().registerHostCardEmulationService(new LoyaltyCard());

Android 使用 NfcAdapter 前台调度 / 读取器模式和 HostApduService;当引用此类时,Maven 插件和构建守护程序会自动注入两个清单条目。iOS 使用 Core NFCNFCNDEFReaderSessionNFCTagReaderSession)进行读取,并使用 CardSession(iOS 17.4+,仅限欧盟)进行 HCE;NFCReaderUsageDescription plist 条目和授权由构建服务器和本地构建自动注入(同样,无缝是关键)。Java SE 模拟器有一个 Simulate -> NFC 菜单,让您可以点击虚拟标签、编辑其 NDEF 有效载荷,并向任何已注册的 HostCardEmulationService 发送 APDU,因此您可以坐在办公桌前,无需卡片或读取器即可驱动每条代码路径。

在没有 NFC 的平台上(桌面部署、JavaScript 端口),返回基类并报告设备不受支持,因此应用程序代码不需要平台 if 语句——始终通过 canRead() 进行门控,您就安全了。

cn1libs 现在可以拥有模拟器菜单——这改变了蓝牙

PR #4988 是一个看似微小的改动,却打开了一整类用户体验。Java SE 模拟器现在扫描其类路径上的每个 jar,查找 META-INF/codenameone/simulator-hooks.properties,并允许任何 cn1lib 贡献自己的菜单项。cn1lib 不引用任何 Swing 类型——数据文件仅命名一个用于菜单组的 name=...,以及一系列指向公共静态无参数方法的 itemN 条目,每个条目带有一个可选的 labelN。模拟器负责其余部分。

一个骨架钩子文件:

name=Bluetooth
namespace=bluetooth

item1=com.example.bt.sim.Hooks#toggleAdapter
label1=切换适配器开关

item2=com.example.bt.sim.Hooks#addDemoPeripheral
label2=添加演示外围设备

将该文件放入 cn1lib 的 javase/ 模块中,下次启动模拟器时,您将看到一个 Bluetooth 菜单,其中包含两个项目,每个项目在 CN1 EDT 上运行,切换适配器开关添加演示外围设备 执行其名称所指示的操作。每个条目也可以通过 CN.execute("bluetooth:item1") 跨平台调用,这使得相同的钩子可用于屏幕截图测试或脚本化演示。没有 labelN 的项目是仅 API 的——向执行器注册但隐藏于菜单——这正是测试套件用来准备脚本化状态的方式。

我们有意选择了数据驱动的方式。我们将在未来一年重写模拟器 UX,我们不希望 cn1lib 直接依赖 JMenu / JMenuItem,也不希望在模拟器 UI shell 更改时重新编译它们。中立的 SimulatorHook 记录(menuNamelabelRunnable)是契约;其上的 UI 是可替换的。

真正可以调试的蓝牙

模拟器钩子之所以在本周落地,是因为我们同时正在并行开发 bluetoothle-codenameone cn1lib,而该 cn1lib 需要这个钩子才能真正好用。结果是一个蓝牙调试体验,在原生平台上开箱即用得要好得多。

该库现在有两个后端:一个真实的 BLE 后端,与真实硬件通信(iOS 上的 CoreBluetooth,Android 上的 BluetoothLeScanner / BluetoothGatt,Java SE 上的新原生桌面桥接),以及一个完全内存中的模拟器。

需要说明的是,模拟器现在连接到您设备上的硬件蓝牙并开始扫描真实设备。我从 Mac 上使用 IntelliJ/IDEA 调试蓝牙设备,能够看到真实设备!!!

cn1lib 的 simulator-hooks.properties 提供了七个钩子,将模拟器放入模拟器的菜单栏中:

Bluetooth
├── 切换适配器开关
├── 添加演示外围设备
├── 断开所有外围设备
├── 推送演示通知
├── 清除外围设备
├── 切换后端 → 原生 BLE(真实硬件)
└── 切换后端 → 模拟器

因此,典型的蓝牙迭代循环如下所示:

  1. 在 Java SE 模拟器中打开您的应用。模拟器后端默认开启。
  2. 打开 Bluetooth -> 添加演示外围设备。您的扫描会检测到一个假的外围设备。逐步执行您的发现代码。
  3. 打开 Bluetooth -> 推送演示通知。您的特征监听器被触发。逐步执行您的处理程序。
  4. 打开 Bluetooth -> 切换适配器开关。您的“适配器关闭”分支运行。逐步执行它。
  5. 当您对模拟器中的行为满意时,打开 Bluetooth -> 切换后端 -> 原生 BLE(真实硬件),然后您笔记本电脑的实际蓝牙无线电接管。相同的应用,相同的代码,真实的外围设备。

相比之下,iOS 或 Android 上的传统蓝牙迭代循环需要真实设备、真实外围设备。iOS 上的模拟器根本没有 BLE 栈,而 Android 模拟器有一个不匹配真实硬件的部分栈。您最终只能在真实设备上进行每次更改,连接线缆,而当出现问题时,您必须弄清楚错误是在您的代码中、外围设备固件中、操作系统 BLE 栈中,还是三者的某种交互中。

使用 cn1-bluetooth 模拟器后端,前四个变量缩减为一个变量:您的代码。当它在模拟器中工作但在设备上不工作时,您已经将问题缩小到平台 BLE 栈或外围设备,这是一个可处理的问题。当它在模拟器中也不工作时,您正在调试自己的代码,就在自己的笔记本电脑上。

如果您有自己的 cn1lib,可能受益于“Simulate -> Whatever”菜单——假 GPS 坐标、脚本化推送通知、确定性相机帧——钩子文件是最简单的交付方式。两行属性、一个公共静态无参数方法,然后模拟器就内置了该功能。## 应用内购买一致性——PR #4990 论坛报告称 submitReceipt 被重复调用,由此引出了 Purchase.synchronizeReceipts 中的三项紧密相关的修复。三者根源相同:代码在 App Store / Play Store 填充所有字段时正常运作,但在某个字段为 null 时静默出错。

  1. removePendingPurchase 仅匹配 transactionId。当收据的 transactionId 为 null(某些已恢复的 Android 购买的真实场景)时,该调用静默无操作,收据仍留在待处理队列中,synchronizeReceipts 末端的递归不断拉取同一张收据,导致该收据被无限次重新提交。修复方案改为匹配收据本身,并在任一侧 transactionId 为 null 时使用后备元组 (sku, storeCode, purchaseDate, orderData)
  2. 递归调用 synchronizeReceipts(0, callback) 在每次迭代中重新注册调用方的 SuccessCallback,因此若待处理队列中有 N 张收据,用户的回调会被触发 N 次。递归调用现在传递 null,因为原始回调已存在于 synchronizeReceiptsCallbacks 中。
  3. 回调刷新本身即使在队列尚未实际清空时也会触发,这掩盖了重复提交的表面问题,让人误以为是回调本身存在 bug。

单独来看,这些修复都不算重大,但其症状——订阅每隔几秒就被重新向服务器验证——看起来与服务器 bug 完全一致,已经浪费了实际开发者的大量时间。此修复已发布,回归测试覆盖了 null-transactionId 路径,确保此类问题不再重现。

UTF-8:兼容 JDK 的替换语义 + NEON ASCII 快速路径——PR #4989

在 iOS 上,String.getBytes("UTF-8")new String(bytes, "UTF-8") 在两个方面落后于标准 JDK。解码器在遇到格式错误的输入时会抛出 RuntimeException("Decoding Error")——而 Java 世界的其余部分会按最大子部分发出 U+FFFD 并继续处理。编码器在非 Apple 构建中会退化为每个字符 1 字节的存根,并且存在一个静默的 ISO-8859-2 → NSISOLatin1 别名,当 NSString 拒绝输入时会隐藏编码错误。

新的解码器采用 Hoehrmann DFA 算法,并实现了与 JDK 兼容的 REPLACE 语义:每个最大子部分违规发出一个 U+FFFD,截断的尾随序列也会发出一个 U+FFFD。编码器是一个可移植的 UTF-16 → UTF-8 编码器,支持代理对合并;Apple 路径现在直接使用它,因此在常见情况下不再涉及 NSString。编码器还为 POSIX / 测试回退提供了真正的实现,取代了旧的 TODO 存根。

有趣的部分是 SIMD 工作。ASCII 前缀扫描(vmaxvq_u8)和 u8 → u16 扩展(vmovl_u8)在 __ARM_NEON 条件下启用,且仅在输入 ≥ 64 字节时启动。独立微基准测试显示,在 ASCII 密集型负载上,速度比纯标量 DFA 提升约 53 倍。集成级基准测试无法达到这个数字,因为在 ParparVM 上,每次调用都分配新的 char[] 是主要开销,但这些辅助函数在 SIMD 所针对的解析器风格热路径(JSON 解析、日志扫描,以及那种大部分为 ASCII 偶尔有非 ASCII 码点的文本)上仍然发挥了作用。

如果你的应用解析大量 UTF-8(大多数应用都如此,因为大多数网络 API 都是 JSON over HTTP),那么这次改进会带来一个安静但可衡量的速度提升,并且 iOS 上的行为与模拟器不再有细微差异。

两项长期存在的 JVM 修复

PR #4980 —— 迭代式 GC 标记,修复 iOS 上深度图的栈溢出

Issue #3136 已存在很长时间。ParparVM 垃圾回收器的标记阶段是递归的:每跟踪一个可达引用,它就会压入一个栈帧,因此长链表链或任何深度对象图都可能耗尽 GC 自身的栈并导致应用崩溃。重现方法很简单——构建一个包含 50000 个节点的 LinkedList,强制触发 GC——但在真实应用中的症状是模糊的:在最大客户数据集上出现莫名其妙的仅限 iOS 的崩溃,通常是在数据结构引入数周后才会显现。

修复方案将递归标记替换为使用显式工作栈的迭代标记。该栈位于堆上并按需增长,因此唯一的上限是实际内存。长链表、深树、解析为 POJO 的深层嵌套 JSON——所有这些以前在 iOS 上都是潜在的崩溃点,现在不再是了。

PR #4985 —— 在 PUTFIELD / MULTIANEWARRAY 中不依赖 C 参数求值顺序

Issue #3108 是另一个问题。几个 PUTFIELDMULTIANEWARRAY 翻译路径发出的 C 代码依赖于参数求值顺序。C 并未规定函数参数的求值顺序。不同的编译器、不同的优化级别,有时甚至同一编译器在不同 -O 级别下会产生不同的顺序,可见的结果是偶尔出现“误编译”、“字段被赋错值”、“数组维度变为负数”等 bug,且无法可靠重现。

修复方案并不惊艳:将操作数求值提升到调用存储函数之前的命名局部变量中,这样求值顺序由 C 抽象机固定,而不是留给编译器决定。此类修复的代码改动很小,测试却很困难,其效果体现为“平台更稳健”,而非某个特定功能。

我之所以将这些修复与其余部分分开提出来,是因为这两个问题你可能遇到过但并未意识到,而且它们都属于那种不会出现在功能清单中,但会悄然提升所有 iOS 应用基准质量的底层基础设施工作。

iOS 和 Android 上的硬件键盘和鼠标——PR #4982

Issue #3498 自 iPadOS 开始提供完善的触控板支持、且 Android 转向将自己定位为 Google 希望在 Chromebook 上使用的操作系统以来,就一直位于愿望清单上。框架已经暴露了 pointerHover* 和完整的键盘事件接口,但端口并未传递悬停事件,并丢失了大量硬件键盘按键——F 键、Esc、Tab、Home/End、PgUp/PgDn、Insert 在 Android 上全部以 keyPressed(0) 形式到达,而 Enter 键除非设置 sendEnterKey=true,否则会被静默忽略。

此 PR 将 Android 上的 ACTION_HOVER_ENTER/MOVE/EXIT 转发到框架的悬停接口,用连接设备的实际键映射替换内置键盘映射查找,将 CTRL/FN/CAPS 纳入元状态,并点亮了 iOS 上的等效路径。结果:蓝牙鼠标、蓝牙键盘、触控笔悬停、Chromebook 触控板、iPad Magic Keyboard——所有这些现在都按最终用户的预期工作。按钮在悬停时高亮。Tab 移动焦点。F 键产生 F 键代码。Cmd-C 复制。Esc 关闭对话框。

这在结构上是重要的,原因有二。Android 希望用笔记本电脑形态取代 ChromeOS,这意味着我们的 Android 应用将更频繁地落在带有连接键盘和触控板的笔记本电脑设备上,并且它们需要像真正的桌面应用那样运行。而搭配 Magic Keyboard 的 iPad 应用在用户期望上越来越与桌面应用难以区分。Codename One 的核心理念是“一次编写,在所有屏幕上运行”——屏幕现在有了键盘,而我们已支持它。

扩展的 CSS 渐变和模糊——PR #4957

CSS 编译器此前会拒绝超过两色线性渐变(四个基本角度)、两色径向渐变(中心)之外的任何内容,并回退到 CEF 光栅化位图来处理其他所有情况。filterbackdrop-filter 则完全被忽略。位图回退虽然可行,但会失去 GPU 路径,并且无法随组件缩放。

此 PR 将完整的 CSS 渐变范围和 filter: blur(...) 全程移至原生基元。你现在可以使用多色线性与径向渐变、锥形渐变、重复线性与重复径向渐变、完整的形状与范围语法,以及针对 filterbackdrop-filter 的高斯模糊。在 GPU 上绘制。可与其他所有内容组合。

.HeroCard {
  background: conic-gradient(from 30deg, #ff7a00, #ff2d95, #6750a4, #ff7a00);
  border-radius: 24px;
  filter: blur(0.5px);
}

.GlassDialog {
  background: rgba(255, 255, 255, 0.18);
  backdrop-filter: blur(18px);
  border-radius: 28px;
}

以上正是你如今在现代 Web 栈上可以编写的内容。Codename One 现在将其编译为你目标平台上的 Metal/GL/Android Canvas/Swing 路径,中间没有离屏位图。结合我们三周前发布的 iOS Modern 和 Material 3 原生主题,以及上周发布的强调色板覆盖,你现在可以用纯 CSS 构建真正现代的 UI。

关于 Metal:社区先行一步

我之前说过希望在本周将 ios.metal=true 设为默认值。但这一切换并未发生——我想明确解释原因,因为这是最能体现我们目标的最好版本。

社区先行一步。来自真实应用的 bug 报告、截图以及人们自行发现问题的 pull request 共同完成了付费 QA 环节的工作。剩余的回归列表比我一周前预期的要短得多。剩下的项目大多很细微(特定混合模式与特定背景的组合、PR #4924 中的诊断测试已定位的旋转下裁剪边缘情况、设备区域设置在会话中改变时的一个字体回退边界情况)。没有一个是阻塞性的。

因此,我们不会强行在截止日期前切换,而是会在回归列表读为零时再切换。这不会太久——按照我们当前关闭问题的速度,大约一到三周——而首先切换的应用将基于一个经过比以往任何渲染迁移都更多真实屏幕测试的 Metal 默认值。

如果你是过去两周切换了提示、截取屏幕截图并提交问题的开发者之一:感谢你们。请继续这样做。Metal 管道最终作为默认值发布时,状态将比没有你们时好得多。如果你尚未切换,构建提示仍为 ios.metal=true。我们仍然欢迎你用它跑一遍你的屏幕截图。

总结

这一周的重点是抬高基础水平线。NFC、生物识别和密码学不再是可选的附加组件。模拟器钩子框架开启了 cn1lib UX 的一个新类别——蓝牙是第一个也是最大的受益者——这在任何原生平台上开箱即用地组装都非常困难。JVM 中两个在 iOS 上长期存在的 bug 终于被淘汰。UTF-8 的表现与标准 JDK 一致,且在关键处更快。硬件键盘和触控板的行为与真正的桌面应用一致。CSS 覆盖了现代 Web 栈所覆盖的一切。

而 Build Cloud 预览版现在就在服务器上,等着你来打破它。请务必尝试。

特别感谢 Metal 管道社区测试者中的一长串名字(你们知道你们是谁;我们正在跟踪问题,将在下一篇文章中致谢),以及 Dave,他提交了 #3136 并附带了 50,000 节点的 LinkedList 重现示例,这使得 GC 标记的修复从一个月的研究变成了一天的修复。

问题追踪器在这里PlaygroundInitializrSkin Designer 仍然是了解 JavaScript 端口能力的最佳场所。Build Cloud 预览版在你登录后位于 cloud.codenameone.com 的 /console/ 路径下。

文章 NFC、加密、生物识别以及全新的 Build Cloud 首次出现在 foojay 上。