Ohhnews

分类导航

$ cd ..
foojay原文

Codename One推出免费开源Android兼容层,支持应用编译至iOS、Web和桌面端

#codename one#android兼容层#跨平台开发#开源框架#应用迁移

[LOADING...]

我们并非第一个将 Android 带到 iOS 的团队。早先的项目会把 Android 运行时连同应用一起带过去。Dalvik 是为 Android 构建的,其中包含即时编译器(JIT),而普通 iOS 应用不被允许使用 JIT。禁用 JIT 会移除 Dalvik 性能的一个重要来源。Dalvik 的设计核心是解释执行和 JIT 编译,而不是提前编译(AOT),因此拿掉 JIT 后,留下的只是一个解释器,而不是经过优化的原生应用。

什么是 Codename One? Codename One 是一个开源框架,可使用单一 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。详情请访问 codenameone.com。

我们的方法不同。我们将应用编译为面向 iOS 的原生代码,并移除它不会使用的代码。它成为一个真正的 iOS 应用,可通过 Codename One API 和原生接口访问操作系统能力。你获得的是原生执行、你可掌控的 UI,以及在初步移植后继续改进应用的路径。

你现在可以将经典 Android 应用编译为原生 Codename One 应用。 Activity、XML 布局和 AndroidX 代码可通过兼容层在 iOS、桌面端和 Web 上运行。每个导入的视图在底层都是一个 Codename One 组件,因此你可以逐个替换界面,并在迁移过程中混用两套 API。

在你的浏览器中试用导入的图库。同一图库生成了一个 8.73 MB 的压缩 iOS 应用包,其中包含它使用的兼容代码和运行时。我们在本地构建了两个版本;下表将压缩包与其解压后的大小分别列出。

该框架和兼容层是开源的,并可免费用于商业用途。你可以使用自己的工具在本地构建。可选的云构建服务提供免费和付费方案。

本周内容预告

今天我们将介绍 Android 迁移路径,包括真实的图库界面和构建流水线。本周的后续文章将涵盖全栈身份验证、数据库升级、测试,以及让这些应用具备实际构建可行性的工具:

在改造应用的同时保持其可用

迁移通常会经历一个尴尬的中间阶段。原应用仍需持续发布,而其替代品则在慢慢补齐功能。对于小团队来说,这可能意味着要同时维护两个不完整的产品。

兼容层让你可以从现有应用结构开始。在 Codename One Maven 项目中,导入一个 Android Studio 模块:

mvn cn1:import-android-project -Dcn1.android.import=/path/to/MyAndroidApp
mvn cn1:run

导入器会复制模块的 src/main,将其命名空间与清单文件协调一致,并报告兼容运行时覆盖了哪些 Gradle 依赖。在判断应用是否可移植之前,请先阅读这份报告。成功导入只是测试的开始,并不代表每个界面都已认证通过。

Android 互操作指南涵盖了手动导入、Kotlin 源码以及受支持的 API。请使用包含此更改的 Codename One 发行版,或在当前 master 分支发布期间自行构建。

你看到的是什么

[LOADING...]

这些截图来自同一个 Android 16 模拟器上、使用相同图库源码构建的应用,分辨率为 720 × 1600 像素,密度为 320 dpi。左侧应用使用 Android 框架。右侧应用使用兼容层。它们都是 Android 截图;这一比较衡量的是渲染差异,而不是 iOS 行为。

[LOADING...]

存在可见差异。Codename One 的 Android 构建使用更窄的粗体字形,在状态栏下方的绘制方式不同,并且拥有自己的 Material 阴影。自定义渲染让你能够跨平台控制这些像素;它不会让每个控件都与 Android 逐像素完全一致。

在浏览器中体验 Android 图库

打开交互式 Android 图库,或下载其源码项目。点按 Count,打开 Details,然后使用溢出菜单尝试其他界面。这是将导入的 Android 示例编译为 JavaScript 的结果,而不是视频,也不是用 HTML 重建的页面。

[LOADING...]

浏览器构建在 440 × 850 视口中的效果。计数器和 Activity 导航均通过兼容层运行。

它高效吗?

我们使用 Xcode 27.0,将图库从 master 分支修订版 ebe0e64be3 构建为未签名的 ARM64 Release 设备应用,随后启用了发布版符号剥离。它包含示例的 Activity 和资源、可达的兼容代码、Codename One 以及原生运行时。

产物大小,1 MB = 1,048,576 字节
已剥离符号的 ARM64 原生可执行文件19.19 MB
应用包,文件大小总和22.33 MB
该未签名应用包的 ZIP8.73 MB

该构建为此应用重定位了 1,316 个兼容类,并生成了 862 个兼容翻译单元。在整个应用中,其方法移除流程丢弃了 24,616 个方法,字段消除移除了 220 个从未读取的实例字段。这些计数描述的是不同阶段;它们并不是 Android 层单独节省字节数的度量。

ZIP 是压缩测量结果,不是 IPA、不是 App Store 下载大小估算,也不是设备安装后的测量值。签名和分发会改变打包结果。我们在本地构建了设备二进制文件,但没有在实体 iPhone 上运行它。测量记录包含精确的字节数、配置和可执行文件哈希。复现说明描述了构建过程。

这是该图库的体积占用,而不是最小 Hello World 示例的数字。它展示了在真实应用包中保留 Android 兼容能力所需的成本,但并未声称该应用包包含整个 Android 操作系统。

Android 代码如何到达 iPhone

运行时容器方案会在应用周围保留一个 Android 执行环境。我们的构建会把受支持的 API 解析到应用自身的代码中。这一层不捆绑 Dalvik 或 ART 运行时。

$ mermaid
flowchart TD
    Source[Android 源码和资源] --> Resources[编译 XML、R 类和工厂]
    Resources --> Compile[针对 Android 兼容 API 编译]
    Compile --> Remap[重定位 Android 包引用]
    Remap --> Link[保留可达的应用和运行时代码]
    Link --> Native[面向 iOS 的 ParparVM 原生输出]
    Link --> Web[JavaScript 浏览器应用]
    Link --> Desktop[Codename One 桌面目标]

资源编译器会为 Activity 和布局类生成构造函数。布局填充过程可以直接调用这些构造函数,而无需通过反射发现类。后续处理会将 android.*、AndroidX 和 Material 引用迁移到兼容命名空间下。即使是 Android 目标,也使用这套重定位后的实现,从而避免与操作系统类发生冲突。

随后,ParparVM 可以从原生构建中移除不可达的类和方法。库中支持某个 API,并不意味着每个应用都必须保留每一项实现。资源、可达界面和原生库仍会占用空间。这就是为什么应用的实际测量结果比兼容 JAR 的大小更重要。

最终得到的应用可以超越其 Android 出身继续发展。兼容层让现有界面运行起来;Codename One API 和原生接口则为你提供下一步的发展方向。

替换一个界面,保留下一个

以下代码摘自编译后的互操作示例,它将一个 Android 自定义视图放入 Codename One 窗体。androidContext 是已安装的 Android 运行时的上下文,MyChartView 是该示例中的 android.view.View 子类:

android.view.View chart = new MyChartView(androidContext);
form.add(BorderLayout.CENTER, chart.getPeer());

反方向同样重要。Codename One 应用可以安装一个导入的模块,并启动该模块的某个 Activity。这里的 SettingsActivity 属于该模块:

AndroidRuntime rt = AndroidRuntime.getInstance();
if (rt == null) {
    rt = AndroidRuntime.install(new com.codename1.generated.android.AndroidAppImpl());
}
Context app = rt.getApplication();
Intent intent = new Intent(app, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
app.startActivity(intent);

当最后一个 Activity 结束时,打开它的那个 Codename One 窗体会返回。你可以在 Codename One 中构建新的导航,同时保留一个导入的界面用于设置或数据录入。随着你移除对 Android 特有行为的依赖,共享的应用逻辑可以直接调用 Codename One API。

从一个完整的用户旅程开始。检查其输入、返回导航以及在各目标平台上的持久化。然后根据产品价值选择下一个界面。迁移可以停在一个有用的中间状态。

在边界找上你之前,先找到它

导入需要应用的源代码。APK 执行和 Compose UI 不在这一层的支持范围内。

这里的 Parcel 是进程内的值容器;它不会复现 Android IPC 或序列化快照。SQLite 只使用一个连接,因此并发数据库操作需要谨慎。基于反射的类加载无法移植到 iOS。MotionLayout 和 CoordinatorLayout 不在支持范围内,且部分 Material 细节会有所不同。

这些情况说明你应该阅读限制说明,而不是重写每一个受支持的界面。图库和移植测试为你提供了具体的初始覆盖范围。其余部分取决于你的依赖项和工作流。实现位于 PR #5941。

*** ** * ** ***## 本周更多内容

以 Spring 风格轻松实现全栈身份验证与登录

上周的原生后端为我们提供了应用结构。本周新增了一套安全栈,将服务器访问规则与发起请求的应用连接起来。它包括表单登录、JWT 资源服务器和 OAuth,并为有需要的应用提供多因素认证(MFA)和通行密钥(passkey)支持。

如果你用过 Spring Security,这套配置看起来会很熟悉。下面这段摘自安全示例的代码定义了一个无状态 API,并要求读取订单时需要特定作用域:

$ java
@Bean
@Order(1)
SecurityFilterChain api(HttpSecurity http) {
    http.securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/orders/**").hasAuthority("SCOPE_orders:read")
            .anyRequest().authenticated())
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

在客户端,OidcRequestAuthorizer 会把凭据附加到发往你的 API 的请求上,并协调令牌续期。如果多个请求同时需要新令牌,它们会等待一次刷新交换。若刷新被拒绝,则会清除会话,并将控制权交还给你的登录界面。

$ mermaid
sequenceDiagram
    participant App as Client requests
    participant Auth as Request authorizer
    participant API as Backend
    App->>Auth: Requests need a fresh token
    Auth->>API: One refresh exchange
    API-->>Auth: Rotated tokens
    Auth->>API: Send waiting requests
    API-->>App: Protected responses

我们会对该连接进行端到端测试。其中一个测试提供一个无效的访问令牌,检查续期能否恢复该请求,然后尝试复用已用过的刷新令牌,并预期会话结束。客户端网络代码和后端都会参与其中。浏览器凭据与重定向有各自的测试边界,周六的文章会结合原生移植覆盖范围对此进行说明。

周六的身份验证深入解析涵盖客户端设置、持久化服务器存储以及失败场景。后端安全指南和客户端身份指南提供了配置细节。

使用类似 Flyway 的语法在生产环境中更新数据库 Schema

后端的这些安全存储需要数据库表,而这些表在部署后还需要演进。你的应用也有同样的问题:如果客户跳过了三个版本,那么他们回来时仍然需要正确的本地数据库。

我们现在为客户端和服务器提供了共享的迁移引擎,使用熟悉的 Flyway 风格文件名。将原始 Schema 保留在 V1__create_note.sql 中,然后为下一次变更添加新文件:

$ query
-- V2__add_note_created.sql
ALTER TABLE note ADD COLUMN created BIGINT;
UPDATE note SET created = 0 WHERE created IS NULL;

在客户端,将这些文件放入 common/src/main/db/migration,并在打开数据库后调用 Migrations.migrate(db)。注解 ORM 在打开实体管理器时会执行这一操作。在服务器端,脚本放在后端模块的 src/main/resources/db/migration 中,并在应用开始接受请求之前运行。

$ mermaid
flowchart LR
    Fresh[New installation] --> V1[Apply V1]
    V1 --> V2[Apply V2]
    Existing[Existing database at V1] --> V2
    V2 --> Ready[Open the application]

历史记录和校验和让引擎能够检测脚本是否被更改。可重复迁移用于处理视图等对象,而如果数据库比应用更新,则会在旧代码修改其数据之前被拒绝。对于服务器,数据库锁会协调同时启动的多个实例。

周日的数据库迁移文章涵盖基线、跳过的版本以及迁移失败后的恢复。它还解释了为什么 MySQL 的 Schema 变更和事务性 PostgreSQL 迁移需要不同的恢复方案。服务器迁移参考包含在部署前检查和验证历史记录的 Maven 命令。

如何测试 Spring 兼容性?

Codename One 后端使用 Spring 风格的语法来简化移植,但它并不与 Spring 兼容。你如何知道移植后的行为是否与原始版本一致?先在 Spring 应用中编写测试,然后针对 Codename One 实现运行相同的请求和断言。

后端现在包含 @BackendTest、依赖注入和 MockMvc API,以便将这些检查迁移过来。更改框架导入和测试设置,同时保留你期望的行为。在两个应用中,请求断言都可以像这样写:

$ java
@Autowired
private MockMvc mvc;

@Test
void greetsByName() throws Exception {
    mvc.perform(get("/greet/{name}", "Ada"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.greeting").value("Hello, Ada"));
}

在移植前先在 Spring 中运行它,然后在调整测试设置后于 Codename One 中运行。状态或响应发生变化时,应该去调查,而不是重写预期值。添加被拒绝的请求和改变状态的操作,让测试套件覆盖的不只是成功响应。当传输行为属于契约的一部分时,TestRestTemplate 也可以跨越真实 HTTP 连接进行测试。

下一步是针对编译后的原生代码运行这些测试。构建会生成直接的测试调用,并在 JVM 结果旁边生成报告。严格模式会让不支持的测试用例失败,而不是让它们消失在绿色的运行结果中。Mockito 仍然是 JVM 的一个选项;显式的测试实现可以在两种环境中运行。

周一的测试文章从 Spring 测试开始,展示 Codename One 中的改动,最后给出本地原生测试命令。测试指南记录了受支持的 API 以及现有后端模块的设置。

在浏览器中运行真正的 Java,连编译器错误也不缺席

Playground 现在可以在你输入时编译 Java。我们用 Java 编译器和 ParparVM 翻译器替换了 BeanShell,这两者都在浏览器内运行。源代码变成字节码,翻译器生成 JavaScript,预览则加载新的类。你的源代码始终留在页面中。

这意味着 record 和 lambda 会经过真正的类型检查。下面是周二文章中示例的一部分:

$ java
record Item(String name, int count) {}

Item item = new Item("Orders", 3);
Label label = new Label(item.name() + ": " + item.count());

把 3 改成 "three",编译器就会指出构造函数不匹配。修正后,预览就能再次运行。下面的截图展示了公共 Playground 中这个故意犯下的错误:

[LOADING...]

这次重建还删除了 161 个生成的 BeanShell 适配器源文件,总计约 23.8 MB。这是删除的源代码,不是浏览器下载量的测量值。增量翻译会在不重建整个宿主应用的情况下生成新的用户类。

周二的 Playground 文章提供了完整的交互式示例,并解释了 Java 编译器如何在其自身翻译输出中运行。你现在就可以试用。

通过 PGO 加速应用,并采用分代/自适应 GC

基于性能分析的优化(PGO)现已在 Pro 和 Enterprise 云构建中可用。 构建服务会运行你的应用,记录哪些代码被执行,然后使用该性能分析数据再次编译。编译器可以就内联、分支布局以及将常用函数放在何处做出更好的决策。你的用户将获得优化后的原生二进制文件。

这适用于 iOS、原生 macOS 和原生 Linux。性能分析数据会应用于翻译后的 Java、Codename One 运行时以及你的原生源代码。对于 iOS 构建,在 codenameone_settings.properties 中启用它:

$ properties
codename1.arg.ios.pgo=true
codename1.arg.pgo.trainingSeconds=90

该服务会处理两次编译以及训练运行。如果你已经使用 Pro 或 Enterprise,可以在下一次受支持的云构建中启用它。如果你正在选择方案,PGO 现在又多了一个考虑 Pro的理由:构建服务可以使用来自你自己应用的执行数据来进行优化。

训练过程无人值守,因此最适合的应用会从一次干净启动开始执行具有代表性的工作。位于登录或按钮之后的代码可能永远不会被触达。周三的文章展示了训练如何工作,以及如何将结果与普通构建进行比较。

当分配行为值得这样做时,ParparVM 现在会在并发收集和分代阶段之间切换。如果几乎所有新对象都很快死亡,重置年轻代页面可以避免逐个追踪和清扫这些对象。收集器可以用更少的 CPU 来跟踪垃圾。

给该阶段更多内存可以降低收集频率,但也可能让下一次完整收集变得更糟。在开发过程中,有一次糟糕的转换保留了足够多的年轻对象,使压力测试的页数从 3,446 增加到 10,538。决定何时离开分代阶段与决定何时进入同样重要。现在,可用内存较低时会将收集器送回并发路径。

[LOADING...]

历史分配工作负载的校准比率,其中 JDK 25 等于 1。越低越好。这些是门禁记录,不是新的受控基准测试。

macOS 的记录清楚地展示了这种权衡:其时间比率从 3.696 降至 1.106,而峰值内存比率从 0.481 升至 0.937。这是 CPU 的大幅改善,同时伴随着显著的内存成本。这也解释了为什么我们的性能门禁会在五个桌面和服务器操作系统/架构组合上同时跟踪这两项指标。移动端和浏览器行为有单独的移植覆盖。

这些 GC 记录不使用 PGO。它们衡量的是同样能使普通构建受益的独立运行时工作。周三的 PGO 与自适应 GC 文章以高级构建功能开篇,然后探讨收集器的内存预算、转换失败以及回归门禁。

更小、更简单的 Maven 项目

上周我们介绍了最小化的 Gradle 项目。现在 Maven 也得到了同样的处理,更小的布局成为新应用的默认选择。你从两个 POM 开始:

my-app/
    pom.xml
    common/
        pom.xml
        codenameone_settings.properties
        src/main/java/
        src/main/resources/

模拟器和平台构建仍然可以基于该项目进行。原生实现可以放在 ios/src/main/objectivec 这样的目录中,而不需要另一个 Maven 模块。当你需要为该目标单独设置依赖或构建配置时,再添加平台 POM。

该原型可以生成选定的平台模块、全部平台模块或完全不生成。它还有 app-with-backend 和 backend-only 项目类型,因此独立服务器不需要在旁边放一个空的客户端模块。现有项目会保留其结构。

周四的 Maven 文章展示了命令、原生源代码位置以及可选布局。原型参考提供了配置细节。

结语

本周标志着 Codename One 的一个重要里程碑。多年来,我们一直运行在 Android 上。现在我们翻转了局面:Android 运行在 Codename One 上。同一个应用可以覆盖 iOS、Web 和桌面端,而它的下一个版本可以超越最初所基于的 Android API。这个领域还有更多内容即将到来。

自上周以来,后端也实现了重大飞跃。随着全栈身份验证、数据库迁移和 Spring 风格测试的到位,我们现在拥有了一个接近 beta 版本的重要后端实现。你可以将客户端和服务器一起构建,然后使用构建应用时所用的同样工具来测试它们之间的连接。## 讨论

如果你的 Android 应用能在重写完成前先登陆 iOS 或浏览器,你会先迁移哪个界面?

分享此页面

发现错误,或有内容要补充?在 GitHub 上编辑此页面

[LOADING...]

作者

Shai Almog

作家、开发者关系(DevRel)、博主、开源黑客、Java 摇滚明星、会议演讲者、讲师和企业家。

加入讨论