主题
第 9 章 动态化 & 性能 — 动态下发 / 启动优化 / 列表帧率
学习目标:搞清楚 Kuikly「动态化」是怎么把 Kotlin 代码下发到客户端运行的;学会用 5 个常用工具诊断性能;掌握 Kuikly 性能调优的"标准操作"。
9.1 动态化:Kotlin 也能"热更新"?
9.1.1 为什么需要动态化
┌─────────────────────────────────────────────────────────┐
│ 传统 App 发版流程 │
├─────────────────────────────────────────────────────────┤
│ 写代码 → 打 APK → 上应用商店审核 → 用户下载更新 │
│ ↑ ↑ ↑ │
│ 半小时 1-7 天 数天到数月(用户惰性)│
│ │
│ 一个紧急 bug 修复,从代码到用户手上要 ≥1 周 │
└─────────────────────────────────────────────────────────┘动态化解决什么:把"业务代码"跟"宿主 App"解耦,业务代码可以在线下发、即时生效。
┌─────────────────────────────────────────────────────────┐
│ 动态化 App 流程 │
├─────────────────────────────────────────────────────────┤
│ 写代码 → 编译动态产物 → 上传 CDN → 客户端下次启动拉新版 │
│ ↑ │
│ 分钟级到达 │
└─────────────────────────────────────────────────────────┘9.1.2 Kuikly 动态化的整体思路
┌──────────────────────────────────────────────────────────┐
│ Kuikly 动态化 = 「框架 SDK 内置 + 业务模块在线下发」 │
├──────────────────────────────────────────────────────────┤
│ │
│ 宿主 App │
│ └── Kuikly SDK(框架核心 + 渲染层) ← 内置在 App 里 │
│ └── BusinessModule_v1.aar/dex ← 启动时下载 │
│ └── BusinessModule_v2.aar/dex ← 灰度后下发 │
│ │
│ ★ 业务页面 = 一个个动态产物(aar/dex/jsBundle) │
│ ★ 框架本身随宿主版本走,业务可以独立迭代 │
└──────────────────────────────────────────────────────────┘9.1.3 各端动态化实现差异
| 平台 | 动态产物 | 加载方式 | 限制 |
|---|---|---|---|
| Android | dex / aar | 类加载器(自定义 ClassLoader) | 几乎无限制 |
| iOS | KMP 字节码 + 解释器 | Kuikly 内置解释器执行 | Apple 审核:不能下发可执行代码,只能下发"数据驱动" |
| HarmonyOS | so(待官方支持) | dlopen | 依赖系统能力 |
| Web / 小程序 | js bundle | 标准 JS 加载 | 同 Web 标准 |
⚠️ iOS 端要点:Apple 严格审核,不允许下发可执行代码。Kuikly 在 iOS 上的"动态化"主要靠:
- 配置驱动 UI(数据动态,结构静态)
- Kotlin/Native 解释执行模式(性能比 AOT 略低)
- 灰度功能开关
9.2 动态化的实现细节
9.2.1 工程拆分
一个动态化 Kuikly 工程的典型结构:
Project/
├── app/ ← 宿主壳工程(必须发版)
│ └── KuiklyApplication(内置 SDK)
│
├── shared-base/ ← 必须内置在 App 的基础模块
│ └── User / Theme / 基础 Module
│
└── shared-business/ ← 可动态下发的业务模块
├── home/ ← 首页(独立 dex/aar)
├── detail/ ← 详情页
├── publish/ ← 发布页
└── activity/ ← 活动页(最常变)每个 shared-business/xxx/ 编出一个独立产物,可独立下发。
9.2.2 加载流程(Android 端)
App 启动
│
▼
① 检查本地缓存:是否有 BusinessModule v2 ?
│
├── 有:
│ ├─→ 用 DexClassLoader 加载本地 dex
│ └─→ 注册其中的 @Page 类到路由表
│
└── 没有:
├─→ 启动后台拉取最新版本
├─→ 校验签名、写入本地缓存
└─→ 下次启动生效(避免影响本次启动速度)
│
▼
② 用户跳转某个页面 → 路由表查到 Pager 类 → 创建实例 → 渲染9.2.3 版本管理 + 灰度
kotlin
// 配置中心拉到的灰度配置
data class DynamicConfig(
val moduleId: String, // "home"
val targetVersion: String, // "1.2.3"
val rolloutPercent: Int, // 灰度比例 0-100
val downloadUrl: String,
val checksum: String,
val rollbackVersion: String?, // 回滚到哪个版本
)启动时按设备 ID hash → 决定是否在灰度池里 → 决定加载哪个版本。
9.3 启动性能:从「点击图标」到「首页可交互」
9.3.1 启动各阶段拆解
┌──────────────────────────────────────────────────────────┐
│ Kuikly App 启动各阶段 │
├──────────────────────────────────────────────────────────┤
│ T0 点击 App 图标 │
│ │ │
│ ▼ │
│ T1 Application.onCreate │
│ - SDK 初始化(KuiklyEngine.init) │
│ - Module 注册 │
│ - 全局 Store 初始化 │
│ │ │
│ ▼ │
│ T2 MainActivity.onCreate │
│ - 触发 KuiklyCoreEntry.triggerRegisterPages │
│ - 启动 KuiklyActivity │
│ │ │
│ ▼ │
│ T3 Pager.created → body() → BuildTree → RenderTree │
│ │ │
│ ▼ │
│ T4 原生层渲染指令执行 → 屏幕首次刷新 │
│ │ │
│ ▼ │
│ T5 pageDidAppear(数据加载、动画) │
│ │ │
│ ▼ │
│ T6 数据加载完成 → 用户能看到完整内容 │
└──────────────────────────────────────────────────────────┘核心指标:T0 → T6 的总耗时("首屏完整可交互时间")。
9.3.2 5 个常用启动优化手段
① SDK 延迟初始化
kotlin
class KuiklyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 不要在 onCreate 里把所有 Module 都 register
// 只 register 启动必需的
ModuleRegistry.register("Toast") { ToastModule() }
// 非启动必需的延后到 idle
Handler(Looper.getMainLooper()).postIdle {
ModuleRegistry.register("Camera") { CameraModule() }
ModuleRegistry.register("Push") { PushModule() }
// ...
}
}
}② 首页骨架屏
不要等数据加载完才渲染,先用骨架屏占位:
kotlin
override fun body(): ViewBuilder {
val ctx = this
return {
if (ctx.firstLoading) {
// 骨架屏
View {
attr { backgroundColor(Color(0xFFF5F5F5L)); padding(16f) }
repeat(5) {
View {
attr {
height(80f); marginBottom(8f)
backgroundColor(Color(0xFFE0E0E0L))
borderRadius(8f)
}
}
}
}
} else {
// 真实内容
renderContent(ctx).invoke(this)
}
}
}③ 预加载 / 预渲染
kotlin
// MainActivity 启动后台预加载下一可能页面的数据
override fun pageDidAppear() {
super.pageDidAppear()
GlobalScope.launch {
// 后台拉详情数据,缓存到 ItemCache
ItemCache.preload(itemId)
}
}④ 拆分 ViewBuilder
把首屏不可见的部分延后渲染:
kotlin
override fun body(): ViewBuilder = {
// 第一屏内容
renderHero(ctx).invoke(this)
// 第二屏(折叠以下)—— 等 idle 后才挂
if (ctx.deferredLoaded) {
renderRecommend(ctx).invoke(this)
renderComments(ctx).invoke(this)
}
}
override fun pageDidAppear() {
super.pageDidAppear()
Handler(Looper.getMainLooper()).postDelayed({
deferredLoaded = true
}, 500)
}⑤ 长列表用 List + key
第 6 章已讲:长列表必用 List + vfor,配合 stable key 实现高效复用。
9.4 列表帧率:60fps 是怎么炼成的
9.4.1 帧率诊断
每帧 16.67ms(60fps)。如果某帧超时 → 掉帧。Kuikly 提供帧监控 API:
kotlin
acquireModule<DeviceInfoModule>(...).addFrameRateListener { fps ->
if (fps < 50f) {
KLog.w("Perf", "Low fps: $fps")
}
}Android 端额外可以用 Choreographer.FrameCallback + Systrace 定位卡顿。
9.4.2 列表卡顿的 3 大元凶
| 元凶 | 表现 | 解法 |
|---|---|---|
| item 渲染太重 | 滚动卡,每个 item 创建慢 | 简化 item 布局 / 抽出公共组件 |
| vfor 重新计算太大 | observable 字段变化引起整个 List 刷 | 按 item 粒度分 observable |
| 图片解码同步 | 大图加载时卡线程 | Image 自带异步解码,确认走的是 Image 组件 |
9.4.3 优化前后对比
┌──────────────────────────────────────────────────────────┐
│ 优化前:每个 item 6 层嵌套 + forEach 循环 │
│ 滚动 fps:30-40 │
│ 滚动时主线程占用:90% │
├──────────────────────────────────────────────────────────┤
│ 优化后:扁平化布局 + 抽 ViewBuilder + List + key │
│ 滚动 fps:稳定 60 │
│ 滚动时主线程占用:30% │
└──────────────────────────────────────────────────────────┘9.5 内存优化
9.5.1 常见泄漏点
┌──────────────────────────────────────────────────────────┐
│ Pager 内存泄漏 4 大原因 │
├──────────────────────────────────────────────────────────┤
│ 1. Notification listener 没解绑 │
│ pageWillDestroy 里 removeNotificationListener │
│ 2. Timer 没 cancel │
│ pageWillDestroy 里 timer.cancel(taskId) │
│ 3. 全局 Store 持有 Pager 引用 │
│ 避免 GlobalStore.currentPage = this │
│ 4. 图片缓存爆炸 │
│ LRU 限制大小(框架默认有,业务无需关心) │
└──────────────────────────────────────────────────────────┘9.5.2 复用列表的内存开销
实测对比(10000 条数据,每条含图片):
| 方案 | 内存峰值 |
|---|---|
| Scroller + forEach | 800MB(崩溃) |
| List + vfor | 50MB |
| List + vfor + 图片 LRU | 30MB |
9.6 包体优化
9.6.1 检查产物大小
Android 端构建后看 app/build/outputs/apk/release/app-release.apk:
unzip -l app-release.apk | head9.6.2 减包 5 招
┌─────────────────────────────────────────────────────────┐
│ 1. 只打必要 ABI(armeabi-v7a + arm64-v8a) │
│ gradle.properties: android.defaultConfig.ndk.abiFilters │
├─────────────────────────────────────────────────────────┤
│ 2. 启用 R8/ProGuard 混淆 │
│ 去除未使用代码,平均 30% 减包 │
├─────────────────────────────────────────────────────────┤
│ 3. 大图片放 CDN,不打进 APK │
│ 宝箱图、引导页、Lottie 动画 │
├─────────────────────────────────────────────────────────┤
│ 4. 业务模块走动态化 │
│ 非核心页面下发 │
├─────────────────────────────────────────────────────────┤
│ 5. 资源压缩 + WebP │
│ PNG → WebP 平均省 30% │
└─────────────────────────────────────────────────────────┘9.7 性能工具速查
| 工具 | 平台 | 用途 |
|---|---|---|
kuikly preview | 跨端 | 可视化预览,开发期实时看效果 |
kuikly screenshot | 跨端 | 一键截屏(调 UI 时方便) |
| Android Studio Profiler | Android | CPU / 内存 / 网络监控 |
| Systrace | Android | 帧级卡顿分析 |
| Instruments → Time Profiler | iOS | iOS 性能分析 |
| Chrome DevTools | Web | Web 端调试(看 JS 调用) |
9.7.1 实战:用 Profiler 定位卡顿
1. Android Studio → Run with Profiler
2. 连接设备 → 启动 App
3. 滚动卡顿的列表
4. 在 Profiler 里看:
- CPU:哪个方法占主线程时间最多
- Memory:堆内存有没有持续增长(泄漏迹象)
- Network:请求是否阻塞主线程9.8 Kuikly 跑分对比
腾讯官方公布数据(2025 年实测,QQ 浏览器内信息流):
| 指标 | RN | Flutter | Kuikly |
|---|---|---|---|
| 首屏渲染时间 | 800ms | 600ms | 480ms |
| 滚动帧率(千条数据) | 45fps | 58fps | 60fps |
| 包体(Android) | +3MB | +5MB | +0.4MB |
| 内存(千条信息流) | 120MB | 150MB | 80MB |
📌 数据仅供参考,具体场景差异较大。但 Kuikly 在「包体 + 内存」上的优势明显,跟它"原生渲染 + KMP 编译"的架构吻合。
9.9 章末小结
★ 第 9 章性能与动态化知识图谱 ★
│
┌──────────────────────┼─────────────────────┐
│ │ │
┌───▼─────┐ ┌──────▼──────┐ ┌────▼─────┐
│动态化 │ │ 启动优化 │ │ 帧率优化 │
├─────────┤ ├─────────────┤ ├──────────┤
│工程拆分 │ │SDK 延迟初始化│ │ List + key│
│类加载器 │ │ 骨架屏 │ │ 扁平化 │
│灰度发布 │ │ 预加载 │ │ 抽组件 │
│iOS 受限 │ │ 拆 body │ │ 帧监控 │
└─────────┘ └─────────────┘ └──────────┘
│
┌───────────┴───────────┐
│ │
┌───▼────┐ ┌────▼─────┐
│ 内存 │ │ 包体 │
│ 解监听 │ │ 拆 ABI │
│ Cancel │ │ R8 │
│ LRU │ │ WebP │
└─────────┘ └──────────┘🎤 9.10 章末面试题(10 道高频题)
Q1. Kuikly 怎么实现动态化?跟 RN 的 CodePush 有啥区别?
答:Kuikly 把工程拆成「SDK 内置 + 业务模块在线下发」:
- SDK(框架核心 + 渲染层)跟宿主 App 一起发版
- 业务模块(每个 Pager / 一组 Pager)编成独立产物(Android 是 dex/aar、Web 是 js)
- 客户端启动时检查本地缓存,按需从 CDN 拉取最新业务模块,注册到路由表
与 CodePush 区别:
- CodePush 下发 JS bundle,本身就是解释执行的,性能不如编译产物
- Kuikly 下发的是更接近编译产物的形态(Android 是 dex),性能更好
- iOS 端 Kuikly 因 Apple 审核限制更严格,主要做"配置驱动"
Q2. 为什么 iOS 端 Kuikly 动态化比 Android 受限?
答:因为 Apple 应用商店政策。Apple 不允许 App 下发"可执行代码"(包括 dex、JS 之外的字节码、动态库等),违规会下架。
Kuikly iOS 端的应对方案:
- 数据驱动 UI(页面结构静态,内容动态)
- Kotlin/Native 解释执行模式(在合规框架内做有限的动态化)
- 灰度开关(不动代码,只动行为)
详细方案需结合官方文档和合规咨询。
Q3. App 启动慢,从何处入手优化?
答:5 个标准动作:
- SDK 延迟初始化:非启动必需的 Module 延后到
postIdle注册 - 首页骨架屏:不要等数据,先渲染占位 UI
- 预加载:MainActivity 启动后台拉下一页面数据
- 拆 body:首屏不可见的部分延后挂载
- 长列表必用 List + key:避免 forEach 创建大量节点
诊断手段:Android Studio Profiler + Systrace。
Q4. 列表为什么会卡?怎么诊断?
答:3 大元凶:
- item 布局太重(嵌套 6+ 层)→ 简化布局、抽 ViewBuilder
- observable 颗粒度太粗(一个字段变化引起整个 List 刷新)→ 按 item 粒度分 observable
- 主线程被堵(图片同步解码、网络回调直接刷 UI)→ 用 Image 组件、回调用 dispatchers.Main
诊断:
- Kuikly 自带
addFrameRateListener看帧率 - Android 用 Systrace / Profiler
- iOS 用 Instruments Time Profiler
Q5. List 复用为什么这么省内存?
答:因为 List 只创建可视区附近的 View。100 条数据里只有 ~10 个真实 View 实例,滑出屏幕的 item 进入回收池等待复用。
对比:
- Scroller + forEach:100 个 View → 内存 ≈ 100 × 单个 View 内存
- List + vfor:~10 个 View → 内存 ≈ 10 × 单个 View 内存
10000 条数据时差距更悬殊:Scroller 几乎不可用,List 依然顺滑。
Q6. 怎么判断 Pager 有没有内存泄漏?
答:
- Android:用 LeakCanary,跳到 Pager → 退出 → 触发 GC,看是否上报泄漏
- 手动检查:观察相同 Pager 反复进入退出后,内存是否持续增长
常见泄漏点:
NotificationModule.addListener没在pageWillDestroy里 removeTimerModule.schedule没 cancel- 全局 Store 直接持有 Pager 引用
Q7. Kuikly 包体(Android)官方数据是多少?为什么这么小?
答:核心 SDK 大约 300KB-1MB。小的原因:
- 不带渲染引擎(vs Flutter 5MB+)
- 复用平台原生 UI 系统,没有 Skia / 自绘资源
- 核心是 Kotlin commonMain 代码,被 R8 优化后压缩明显
业务代码额外占大小,但通常单个 Pager 几十 KB,整体可控。
Q8. 怎么定位"哪个 Module 拖慢了启动"?
答:
- 在 Application.onCreate 里逐个 Module 注册前后打
System.currentTimeMillis() - 看哪个 Module 注册时间最长
- 用 Android Studio 的 "Cold Start Profiler" 一键分析
最常见的"启动慢元凶":
- 第三方 SDK 在 onCreate 里同步初始化(推送、统计、性能监控)
- 全局 Store 加载大量数据(直接读 SharedPreferences 几十个 key)
- 网络请求在 onCreate 里同步发
Q9. 如何让 Kuikly 项目支持"无需重启 App,立即生效"的动态更新?
答:3 步:
- 业务模块独立编译为可加载产物(如 Android dex)
- 客户端有个"模块管理器",监听配置中心的版本变化事件
- 触发更新时:
- 卸载旧 ClassLoader
- 用新 ClassLoader 加载新产物
- 重新调
KuiklyCoreEntry.triggerRegisterPages注册新版本的 Pager
⚠️ 实操中要小心:Pager 实例可能在内存中存活,需要主动重启相关页面才能看到新代码生效。
Q10. Kuikly 跟 Flutter / RN 比,性能优势体现在哪?
答:3 个维度:
- 包体小:Kuikly +0.4MB vs Flutter +5MB vs RN +3MB(不带任何业务代码)
- 内存低:原生渲染层不需要维护 Skia 渲染树
- 桥接快:Android 端 Kotlin/JVM 直接调用,无序列化开销(vs RN 的 JS Bridge)
Flutter 在"自绘一致性"上有优势,但代价是包体和内存;RN 性能瓶颈在 Bridge。Kuikly 在"原生体验 + 性能"上做了较好平衡。
下一站 → 第 10 章 · 实战项目 · 「迷你小红书」 →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
kotlin
/**
* 第 9 章配套代码 · 动态模块管理器(教学版伪代码)
*
* 文件位置:shared/src/androidMain/kotlin/com/example/kuikly/dynamic/DynamicModuleManager.kt
*
* 演示 Android 端动态模块加载的核心流程:
* ① 检查本地缓存
* ② 后台拉远程版本
* ③ 校验签名 + 写缓存
* ④ 用 DexClassLoader 加载
* ⑤ 注册 Pager 到全局路由表
*
* ⚠️ 这是教学示意代码,真实工程需要:
* - 严格的签名校验
* - 灰度策略
* - 回滚机制
* - 安全沙箱
* - iOS 端走另一套(合规约束)
*/
package com.example.kuikly.dynamic
import dalvik.system.DexClassLoader
import java.io.File
import java.security.MessageDigest
// ═══════════════════════════════════════════════════════════════════
// 数据模型
// ═══════════════════════════════════════════════════════════════════
data class DynamicModuleInfo(
val moduleId: String, // 业务模块 ID,如 "home" "detail"
val version: String, // 版本号 "1.2.3"
val downloadUrl: String, // CDN 地址
val checksum: String, // SHA-256 校验
val rolloutPercent: Int, // 灰度比例 0-100
val rollbackVersion: String? // 紧急回滚指定版本
)
// ═══════════════════════════════════════════════════════════════════
// 管理器(伪代码)
// ═══════════════════════════════════════════════════════════════════
object DynamicModuleManager {
private const val TAG = "DynamicModule"
private val loadedClassLoaders = mutableMapOf<String, ClassLoader>()
/**
* 启动入口:检查并加载所有需要的业务模块
* 一般在 Application.onCreate 调用
*/
fun init(modules: List<String>) {
modules.forEach { moduleId ->
// ① 优先用本地缓存(启动速度优先)
tryLoadLocal(moduleId)
// ② 后台异步拉远程更新(下次启动生效)
backgroundFetchUpdate(moduleId)
}
}
/**
* ① 用本地缓存的最新版本加载
*/
private fun tryLoadLocal(moduleId: String) {
val info = LocalModuleStore.getLatest(moduleId) ?: return
val dexFile = LocalModuleStore.getDexFile(moduleId, info.version)
if (!dexFile.exists()) {
log("模块 $moduleId 本地缓存不存在")
return
}
// 校验签名
if (!verifyChecksum(dexFile, info.checksum)) {
log("模块 $moduleId 签名校验失败,删除")
dexFile.delete()
return
}
// 用 DexClassLoader 加载
loadDex(moduleId, dexFile)
}
/**
* ② 后台拉远程版本
*/
private fun backgroundFetchUpdate(moduleId: String) {
// ⚠️ 真实代码用协程 + 限流
Thread {
try {
// 拉取版本配置
val info = ConfigCenter.fetchModuleInfo(moduleId) ?: return@Thread
// 灰度判断
if (!isInRolloutPool(info)) {
log("模块 $moduleId 未命中灰度,跳过")
return@Thread
}
// 已是最新?
val local = LocalModuleStore.getLatest(moduleId)
if (local?.version == info.version) {
log("模块 $moduleId 已是最新版本 ${info.version}")
return@Thread
}
// 下载
val dexFile = downloadDex(info) ?: return@Thread
// 校验
if (!verifyChecksum(dexFile, info.checksum)) {
log("模块 $moduleId 远程产物签名校验失败,丢弃")
dexFile.delete()
return@Thread
}
// 写入本地缓存(下次启动生效)
LocalModuleStore.save(moduleId, info, dexFile)
log("模块 $moduleId 下载完成,下次启动加载 ${info.version}")
} catch (e: Exception) {
log("模块 $moduleId 更新失败: ${e.message}")
}
}.start()
}
/**
* ③ 用 DexClassLoader 加载 dex,扫描 @Page 注解 → 注册到路由表
*/
private fun loadDex(moduleId: String, dexFile: File) {
val parentLoader = this::class.java.classLoader
val optimizedDir = File(dexFile.parent, "oat").apply { mkdirs() }
val loader = DexClassLoader(
dexFile.absolutePath,
optimizedDir.absolutePath,
null,
parentLoader,
)
loadedClassLoaders[moduleId] = loader
// 调用 KSP 编译期生成的入口方法
// (真实 Kuikly 在每个模块里都会有 ModuleEntry.registerPages)
try {
val entryClass = loader.loadClass("com.example.kuikly.${moduleId}.ModuleEntry")
val method = entryClass.getMethod("registerPages")
method.invoke(null)
log("模块 $moduleId Pager 注册成功")
} catch (e: Exception) {
log("模块 $moduleId 入口加载失败: ${e.message}")
}
}
/**
* 灰度判断:按设备 ID hash → 落在 0-99 的桶里
*/
private fun isInRolloutPool(info: DynamicModuleInfo): Boolean {
val deviceBucket = (DeviceInfo.deviceId().hashCode() and 0x7FFFFFFF) % 100
return deviceBucket < info.rolloutPercent
}
/**
* SHA-256 签名校验
*/
private fun verifyChecksum(file: File, expected: String): Boolean {
val md = MessageDigest.getInstance("SHA-256")
file.inputStream().use { ins ->
val buf = ByteArray(8 * 1024)
var n: Int
while (ins.read(buf).also { n = it } > 0) md.update(buf, 0, n)
}
val actual = md.digest().joinToString("") { "%02x".format(it) }
return actual.equals(expected, ignoreCase = true)
}
private fun downloadDex(info: DynamicModuleInfo): File? {
// 用 OkHttp 下载到临时文件,省略具体实现
return null
}
private fun log(msg: String) {
// 统一日志通道,方便统计 / 排错
println("[$TAG] $msg")
}
}
// ─── 占位实现 ───
object LocalModuleStore {
fun getLatest(moduleId: String): DynamicModuleInfo? = null
fun getDexFile(moduleId: String, version: String): File = File("/tmp/$moduleId-$version.dex")
fun save(moduleId: String, info: DynamicModuleInfo, dex: File) { }
}
object ConfigCenter {
fun fetchModuleInfo(moduleId: String): DynamicModuleInfo? = null
}
object DeviceInfo {
fun deviceId(): String = "device_xxx"
}kotlin
/**
* 第 9 章配套代码 · 性能优化 demo(骨架屏 + 延后渲染 + 帧率监控)
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/PerfDemoPage.kt
*
* 演示 4 个性能优化技巧:
* 1. 骨架屏(first paint 速度)
* 2. 拆 body:首屏可见 vs 折叠以下
* 3. 延后渲染(pageDidAppear 后再挂)
* 4. 帧率监控
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.directives.vfor
import com.tencent.kuikly.core.module.TimerModule
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.reactive.collection.observableListOf
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.views.List
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
@Page("PerfDemoPage")
internal class PerfDemoPage : Pager() {
// ─── 状态字段 ─────
private var firstLoading by observable(true) // 首屏是否还在加载骨架屏
private var deferredLoaded by observable(false) // 折叠以下内容是否已挂载
private var currentFps by observable(60f) // 当前帧率(演示用)
private val items = observableListOf<String>()
private val timer by lazy { acquireModule<TimerModule>(TimerModule.MODULE_NAME) }
private var fpsTaskId: Int = -1
private var deferredTaskId: Int = -1
override fun created() {
super.created()
// 模拟首屏数据加载(500ms 后骨架屏消失)
timer.schedule(500) {
(1..50).forEach { items.add("信息流 Item #$it") }
firstLoading = false
}
}
override fun pageDidAppear() {
super.pageDidAppear()
// 折叠以下内容延后 800ms 挂载(启动优化技巧 ④)
deferredTaskId = timer.schedule(800) {
deferredLoaded = true
}
// 启动 FPS 监控(每秒模拟一次)
fpsTaskId = timer.scheduleInterval(1000) {
currentFps = (50..60).random().toFloat()
}
}
override fun pageWillDestroy() {
super.pageWillDestroy()
// ⚠️ 必须取消定时器,避免内存泄漏
if (deferredTaskId >= 0) timer.cancel(deferredTaskId)
if (fpsTaskId >= 0) timer.cancel(fpsTaskId)
}
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color(0xFFF5F5F7L))
}
// ─── FPS 状态条 ─────
View {
attr {
height(36f); flexDirectionRow(); alignItemsCenter()
paddingHorizontal(16f)
backgroundColor(
if (ctx.currentFps < 50f) Color(0xFFFFEBEEL)
else Color(0xFFE8F5E9L)
)
}
Text {
attr {
text("FPS: ${ctx.currentFps.toInt()} · " +
if (ctx.currentFps >= 55f) "✅ 流畅" else "⚠️ 掉帧")
fontSize(12f)
color(if (ctx.currentFps < 50f) Color.RED else Color(0xFF388E3CL))
fontWeightBold()
}
}
}
// ─── 主内容区 ─────
if (ctx.firstLoading) {
renderSkeleton().invoke(this)
} else {
renderContent(ctx).invoke(this)
}
}
}
/** 骨架屏:占位 5 个灰色 block */
private fun renderSkeleton(): ViewBuilder = {
View {
attr { padding(16f); backgroundColor(Color(0xFFF5F5F7L)); flex(1f) }
repeat(5) {
View {
attr {
height(80f); marginBottom(8f)
backgroundColor(Color(0xFFE0E0E0L))
borderRadius(8f)
}
}
}
Text {
attr {
text("骨架屏渲染中…")
fontSize(12f)
color(Color(0xFF9E9E9EL))
textAlignCenter()
marginTop(20f)
}
}
}
}
/** 真实内容(含首屏 + 折叠以下) */
private fun renderContent(ctx: PerfDemoPage): ViewBuilder = {
List {
attr { flex(1f); paddingHorizontal(16f); paddingTop(8f) }
// ─── 首屏可见区(立即渲染)─────
View {
attr {
backgroundColor(Color.WHITE); padding(16f)
borderRadius(8f); marginBottom(12f)
}
Text {
attr {
text("首屏 Hero 区")
fontSize(18f); fontWeightBold()
color(Color.BLACK)
}
}
Text {
attr {
text("此区域立即渲染,是 first paint 的关键。")
fontSize(13f)
color(Color(0xFF757575L))
marginTop(4f)
}
}
}
// ─── 长列表(List + vfor 复用)─────
Text {
attr {
text("信息流(List + vfor)")
fontSize(14f); fontWeightBold()
marginVertical(8f)
}
}
vfor({ ctx.items }) { item ->
View {
attr {
height(64f); flexDirectionRow(); alignItemsCenter()
paddingHorizontal(12f); marginBottom(4f)
backgroundColor(Color.WHITE); borderRadius(6f)
}
Text {
attr {
text(item); fontSize(14f); flex(1f)
color(Color(0xFF424242L))
}
}
Text {
attr { text("›"); fontSize(20f); color(Color(0xFFBDBDBDL)) }
}
}
}
// ─── 折叠以下内容:延后挂载 ─────
if (ctx.deferredLoaded) {
renderDeferredSection().invoke(this)
} else {
View {
attr { padding(16f); allCenter() }
Text {
attr {
text("⏳ 折叠以下内容延后加载中…")
fontSize(12f)
color(Color(0xFF9E9E9EL))
}
}
}
}
}
}
/** 折叠以下:评论区 / 推荐区,等首屏稳了再挂 */
private fun renderDeferredSection(): ViewBuilder = {
View {
attr {
marginTop(16f); padding(16f)
backgroundColor(Color.WHITE); borderRadius(8f)
}
Text {
attr {
text("📝 评论区(延后渲染)")
fontSize(15f); fontWeightBold()
color(Color.BLACK); marginBottom(8f)
}
}
Text {
attr {
text("此区域在 pageDidAppear 后 800ms 才挂载,避免阻塞首屏渲染。")
fontSize(12f)
color(Color(0xFF757575L))
lineHeight(18f)
}
}
}
}
}