Skip to content

第 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 各端动态化实现差异

平台动态产物加载方式限制
Androiddex / aar类加载器(自定义 ClassLoader)几乎无限制
iOSKMP 字节码 + 解释器Kuikly 内置解释器执行Apple 审核:不能下发可执行代码,只能下发"数据驱动"
HarmonyOSso(待官方支持)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 + forEach800MB(崩溃)
List + vfor50MB
List + vfor + 图片 LRU30MB

9.6 包体优化

9.6.1 检查产物大小

Android 端构建后看 app/build/outputs/apk/release/app-release.apk

unzip -l app-release.apk | head

9.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 ProfilerAndroidCPU / 内存 / 网络监控
SystraceAndroid帧级卡顿分析
Instruments → Time ProfileriOSiOS 性能分析
Chrome DevToolsWebWeb 端调试(看 JS 调用)

9.7.1 实战:用 Profiler 定位卡顿

1. Android Studio → Run with Profiler
2. 连接设备 → 启动 App
3. 滚动卡顿的列表
4. 在 Profiler 里看:
   - CPU:哪个方法占主线程时间最多
   - Memory:堆内存有没有持续增长(泄漏迹象)
   - Network:请求是否阻塞主线程

9.8 Kuikly 跑分对比

腾讯官方公布数据(2025 年实测,QQ 浏览器内信息流):

指标RNFlutterKuikly
首屏渲染时间800ms600ms480ms
滚动帧率(千条数据)45fps58fps60fps
包体(Android)+3MB+5MB+0.4MB
内存(千条信息流)120MB150MB80MB

📌 数据仅供参考,具体场景差异较大。但 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 个标准动作:

  1. SDK 延迟初始化:非启动必需的 Module 延后到 postIdle 注册
  2. 首页骨架屏:不要等数据,先渲染占位 UI
  3. 预加载:MainActivity 启动后台拉下一页面数据
  4. 拆 body:首屏不可见的部分延后挂载
  5. 长列表必用 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 里 remove
  • TimerModule.schedule 没 cancel
  • 全局 Store 直接持有 Pager 引用

Q7. Kuikly 包体(Android)官方数据是多少?为什么这么小?

:核心 SDK 大约 300KB-1MB。小的原因:

  1. 不带渲染引擎(vs Flutter 5MB+)
  2. 复用平台原生 UI 系统,没有 Skia / 自绘资源
  3. 核心是 Kotlin commonMain 代码,被 R8 优化后压缩明显

业务代码额外占大小,但通常单个 Pager 几十 KB,整体可控。

Q8. 怎么定位"哪个 Module 拖慢了启动"?

  1. 在 Application.onCreate 里逐个 Module 注册前后打 System.currentTimeMillis()
  2. 看哪个 Module 注册时间最长
  3. 用 Android Studio 的 "Cold Start Profiler" 一键分析

最常见的"启动慢元凶":

  • 第三方 SDK 在 onCreate 里同步初始化(推送、统计、性能监控)
  • 全局 Store 加载大量数据(直接读 SharedPreferences 几十个 key)
  • 网络请求在 onCreate 里同步发

Q9. 如何让 Kuikly 项目支持"无需重启 App,立即生效"的动态更新?

:3 步:

  1. 业务模块独立编译为可加载产物(如 Android dex)
  2. 客户端有个"模块管理器",监听配置中心的版本变化事件
  3. 触发更新时:
    • 卸载旧 ClassLoader
    • 用新 ClassLoader 加载新产物
    • 重新调 KuiklyCoreEntry.triggerRegisterPages 注册新版本的 Pager

⚠️ 实操中要小心:Pager 实例可能在内存中存活,需要主动重启相关页面才能看到新代码生效。

Q10. Kuikly 跟 Flutter / RN 比,性能优势体现在哪?

:3 个维度:

  1. 包体小:Kuikly +0.4MB vs Flutter +5MB vs RN +3MB(不带任何业务代码)
  2. 内存低:原生渲染层不需要维护 Skia 渲染树
  3. 桥接快: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)
                }
            }
        }
    }
}

DynamicModuleManager.kt ↗ · PerfDemoPage.kt ↗