<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kotlin Multiplatform on Trevor's Blog</title><link>http://mings.in/tags/kotlin-multiplatform/</link><description>Recent content in Kotlin Multiplatform on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Fri, 30 Oct 2026 08:10:11 +0800</lastBuildDate><atom:link href="http://mings.in/tags/kotlin-multiplatform/index.xml" rel="self" type="application/rss+xml"/><item><title>同样写 Compose，腾讯这条路多养了十五万行原生代码</title><link>http://mings.in/posts/kuikly/</link><pubDate>Fri, 30 Oct 2026 08:10:11 +0800</pubDate><guid>http://mings.in/posts/kuikly/</guid><description>同样能写 Compose，腾讯选了原生渲染而不是 Skia 自绘：六端背后是四套渲染器、十五万行平台代码。</description><content:encoded><![CDATA[<p>腾讯 TDS 的 KuiklyUI 今天冲上 GitHub 趋势榜，三千六百星。它是个 KMP 的 UI 框架，一码六端。</p>
<p>六端这个说法得拆开看。Android、iOS、鸿蒙是正式支持，Web 和微信小程序标着 Beta，macOS 是 Alpha。文档里写得清楚，别把趋势榜当成熟度。</p>
<p><img alt="六端支持情况与四套渲染器" loading="lazy" src="/images/kuikly/fig1.png"></p>
<p>它和 Compose Multiplatform 最容易混，因为两边都能写 Compose。但底下是两条完全不同的路，我把仓库扒了一遍，账能算出来。</p>
<h2 id="一条自己画一条租别人的">一条自己画，一条租别人的</h2>
<p>Compose Multiplatform 用 Skia 自绘。你写一个 Box，它自己拿画笔把像素涂出来，六个平台涂出来一模一样，因为画笔是同一支。</p>
<p>Kuikly 不画。它把 Box 翻译成各平台自己的 View，Android 上是 View，iOS 上是 UIView，鸿蒙上是 ArkUI 组件。渲染这活外包给系统。</p>
<p>官方那篇 Compose DSL 发布文写得很直白，原文是「CMP 采用 Skia 自绘，而 Kuikly Compose DSL 可以直接复用现有成熟的原生渲染体系，拥有与原生系统完全一致的使用体验，例如滑动手感、输入框、动画、无障碍等」。</p>
<p>收益和代价都在这一句里。滑动手感、输入框光标、无障碍朗读这些，系统给了，你不用自己模拟一遍，而那正是自绘框架最费劲的地方。代价是每个平台都得有一套翻译层，系统长什么样你都得跟。</p>
<p><img alt="两条路的分岔：Skia 自绘与原生渲染的对照" loading="lazy" src="/images/kuikly/fig4.png"></p>
<h2 id="账算出来是这样">账算出来是这样</h2>
<p>我把仓库 clone 下来，按目录数了一遍行数。</p>
<p>共享的 Kotlin 二十万九千行，其中 core 三万三千行，剩下十七万五千行在 compose 目录。</p>
<p>各平台的渲染器加起来十五万五千行。Android 三万一千行 Kotlin，iOS 三万七千行 Objective-C，鸿蒙五万一千行 ArkTS 加 C++，Web 三万五千行 Kotlin。</p>
<p>一份共享代码，配四套平台渲染器。这个比例是这条路线的固定成本，不是腾讯写得糙。CMP 那边这笔账为零，因为它只有一支画笔要维护。</p>
<p>鸿蒙那五万一千行值得单看一眼，其中 ArkTS 只有五千九百行，剩下的四万两千行是 C++ 和 C。ArkUI 的调用要走原生层，光靠 ArkTS 接不住这个量。这也说明原生渲染这条路，越往深走越靠近系统那一端。</p>
<p><img alt="共享代码与平台渲染器的行数对比" loading="lazy" src="/images/kuikly/fig2.png"></p>
<h2 id="那十七万五千行不是它写的">那十七万五千行不是它写的</h2>
<p>compose 目录是从 JetBrains 的 compose-multiplatform-core 分叉出来的，基于 Compose 1.7.3。</p>
<p>README 里自己交代了，把包名从 androidx.compose 改成 com.tencent.kuikly.compose，用不上的功能注释掉，不一路升级到底，方便以后跟上游。</p>
<p>所以它跑 Compose 不是兼容，是把 Compose 搬过来再改。官方敢说「兼容百分之九十五以上的 Jetpack Compose API」，因为物理上就是同一份代码，改的是对接层。</p>
<h2 id="它先有自己的-dslcompose-是后接的">它先有自己的 DSL，Compose 是后接的</h2>
<p>这点很容易被忽略。Kuikly 一开始根本没有 Compose，它有一套自研 DSL，用 ViewBuilder 搭界面，页面用 @Page 注解标出来，状态靠 observable 装饰器。</p>
<p>demo 目录里两种写法并存，自研 DSL 的页面有三百个，Compose 写的一百二十三个。数量对比说明主次，Compose 是后来接上去的一层，不是地基。</p>
<p>我翻了一个自研 DSL 的页面，写法是这样，跟 Compose 完全两个路子。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Page</span><span class="p">(</span><span class="s2">&#34;NetworkExamplePage&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">internal</span> <span class="k">class</span> <span class="nc">NetworkExamplePage</span> <span class="p">:</span> <span class="n">BasePager</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">var</span> <span class="py">output</span> <span class="k">by</span> <span class="n">observable</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">override</span> <span class="k">fun</span> <span class="nf">body</span><span class="p">():</span> <span class="n">ViewBuilder</span> <span class="p">=</span> <span class="p">{</span> <span class="n">NavBar</span> <span class="p">{</span> <span class="n">attr</span> <span class="p">{</span> <span class="n">title</span> <span class="p">=</span> <span class="s2">&#34;网络请求示例&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>同一件事，Compose 那边写的是 <code>@Composable</code> 加重组，自研这边是声明式的一棵树。两套并存意味着这个框架要维护两条前端的口子。</p>
<h2 id="真正的活干在中间那层">真正的活干在中间那层</h2>
<p>Compose 重组时不停增删改节点，这些改动要通过 Applier 交给渲染后端。Android 和 Skia 都有官方实现，Kuikly 得自己写一个。</p>
<p>它的 KuiklyApplier 继承 AbstractApplier，每个 KNode 一边挂 LayoutNode 管布局，一边挂 DeclarativeBaseView 管渲染，一对一。</p>
<p>麻烦在坐标系，官方举的例子一行代码就露馅。</p>
<pre tabindex="0"><code>Box(Modifier.padding(10.dp).size(50.dp).background(Color.Blue))
</code></pre><p>background 绑在里层坐标系，画出来是 50×50。加上 padding 之后外层边界已经是 70×70。而 Kuikly 的 View 只有一个 frame，你把哪个数填进去都不对。</p>
<p>填外层，背景跟着铺满 70×70，画大了。填里层，尺寸对了，padding 偏移丢了，子节点全错位。给每个 Modifier 建一个 View 更不行，三层 Modifier 就三层嵌套，页面一复杂层级就炸。</p>
<p><img alt="一个 View 只有一个 frame" loading="lazy" src="/images/kuikly/fig3.png"></p>
<p>定下来的规矩是「绘制坐标系决定 Size，相对位置决定 Position」。一句话背后是两套渲染语义的对齐，这类框架真正的时间都花在这儿。</p>
<h2 id="十一个构建脚本">十一个构建脚本</h2>
<p>repo 根目录摆着十一份 Gradle 脚本，从 Kotlin 1.3.10 一直排到 2.1.21，每份都配一个同名 settings 文件。</p>
<p>不是因为喜欢整齐。Kotlin Multiplatform 的 Gradle 插件 API 这些年一直在变，一套脚本吃不下所有版本，只能每个大版本备一份，用的时候切过去。</p>
<p>这个细节比任何性能宣传都有说服力。国内大厂做长期框架，最大的成本是三年后还有人在用老版本 Kotlin，你得让他也能跑起来。</p>
<h2 id="许可证里有一条要看清">许可证里有一条要看清</h2>
<p>LICENSE 主体是 MIT 那套，但多了一句限制。原文的关键词是 <code>may not modify, extend, or create derivative works</code>，目的限定在 <code>adding or enabling dynamic code loading / live updating capability</code>，而且要先拿到腾讯的书面同意。</p>
<p>翻过来就是，动态下发和热更新这套能力，没有腾讯书面同意，你不能自己改出来。</p>
<p>而动态下发恰恰是它最大的卖点。Compose Multiplatform 编译期出包，改一行要重新发版；Kuikly 能把页面当资源下发，改完直接生效。这个差距对国内业务的吸引力，比任何性能数字都大。</p>
<p>GitHub 上这仓库的 license 字段显示 NOASSERTION，原因就在这，它不是一份标准协议。</p>
<h2 id="谁适合用">谁适合用</h2>
<p>官方给的验证数据是内部三十多个 App、五亿日活场景，外部二十多个团队接了，包括快手、方正证券、MiniMax。迁移成本他们写着五十多个标准 Compose 页面一次性搬完，还有用 AI 生成了七十三个功能 demo，零修改编译运行。</p>
<p>要鸿蒙或者微信小程序，这是现在最省事的一条路，CMP 还没覆盖。只做 Android、iOS 和桌面，自己养四套渲染器这件事不太划算，那十五万行是要你替它维护的。</p>
<p>我这边只验证到工程结构和代码量这一层，外加编译器边界。gradle 配置能过，十三个模块，两分四十九秒；core 模块编译成 macOS arm64 的原生库也跑通了，一分零六秒，JDK17 加 Kotlin/Native 就够，不用 Android SDK。</p>
<p>六个端的完整 App 没跑，那要 Xcode 配 CocoaPods 和 DevEco Studio 各自一套。</p>
<p>你要是选跨端框架，会先问它覆盖几个端，还是先问它改一版要发几次包？</p>
]]></content:encoded></item></channel></rss>