从页面加载过程看 Kuikly 的多线程架构

本文适合 Kotlin/Kuikly 初学者阅读。我们不做泛泛而谈,而是以"一个页面是怎么被加载出来的"这条主线为脉络,一步一步跟踪源码,彻底弄清 Kuikly 的双线程模型是什么、为什么这么设计、以及它如何保证高性能和线程安全。

1. 为什么需要双线程?

在理解 Kuikly 的多线程模型之前,我们先想一个问题:如果把所有事情都放在主线程做,会怎样?

一个 UI 框架需要做两大类事情:

类别 具体工作 耗时特征
逻辑计算 执行业务代码、构建虚拟视图树、计算 Flexbox 布局、处理响应式更新 CPU 密集
原生渲染 创建平台 View、设置属性、设置 frame、插入视图层级 必须在 UI 线程

如果全放主线程,逻辑计算会阻塞 UI 渲染,用户看到的就是"卡"。React Native 很早就采用了类似的双线程方案------JS 线程做逻辑,主线程做渲染。Kuikly 的思路一脉相承:

Context 线程 (也叫 Kuikly 线程):运行 Kotlin 业务逻辑、DSL 构建、布局计算
Main 线程(UI 线程):执行原生 View 的创建、属性设置、帧布局

这种分工带来三个核心优势:

  1. 不卡主线程:无论你的业务逻辑多复杂,用户的滑动、点击永远流畅
  2. 批量上屏:Context 线程产生的 UI 指令可以攒一批再一次性提交给主线程,减少线程切换开销
  3. 架构清晰:Kotlin 侧完全不需要关心线程问题,因为它永远运行在同一条线程上

2. 两条线程各自负责什么

让我们用一张表格来明确两条线程的职责边界:

sql 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Context 线程 (Kuikly 线程)                 │
│                                                              │
│  ✦ Kotlin 业务代码执行                                         │
│  ✦ Pager 生命周期(willInit → didInit → body → createBody)    │
│  ✦ DSL 构建(attr{} / event{} 块的执行)                       │
│  ✦ Flexbox 布局计算(flexNode.calculateLayout)                │
│  ✦ 响应式系统(observable 变化 → 重新执行绑定块)                  │
│  ✦ 协程调度(setTimeout、LifecycleScope)                      │
│  ✦ Module 调用的发起端                                         │
│                                                              │
│  产出:一系列"UI 指令"(创建 View、设属性、设 Frame......)           │
│       通过 BridgeManager.callNativeMethod() 发送给 Native 侧  │
└──────────────────────┬──────────────────────────────────────┘
                       │ UI 指令
                       ▼
┌─────────────────────────────────────────────────────────────┐
│                    Main 线程 (UI 线程)                        │
│                                                              │
│  ✦ 接收 UI 指令并批量执行                                       │
│  ✦ 创建原生 View(TextView、ImageView......)                      │
│  ✦ 设置 View 属性(背景色、字体、圆角......)                        │
│  ✦ 设置 View Frame(位置和大小)                                │
│  ✦ 插入 View 到父视图                                          │
│  ✦ 事件触发后通知 Context 线程                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

3. 完整的页面加载时序

在深入每一步之前,先看完整的时序图,对全局有个概念:

scss 复制代码
时间轴 ──────────────────────────────────────────────────────────→

Main 线程    Context 线程    说明
    │              │
    │ init()       │         ① 用户调用 KuiklyRenderView.init()
    │──schedule──→│         ② 切换到 Context 线程
    │              │
    │         initContext()  ③ 加载 Kotlin 引擎、创建 NativeBridge
    │              │
    │        callKotlinMethod  ④ 调用 CREATE_INSTANCE
    │          (CREATE_INSTANCE)
    │              │
    │         PagerManager     ⑤ 通过页面注册表找到 creator
    │          .createPager()
    │              │
    │         pager.onCreatePager()  ⑥ Pager 生命周期开始
    │           ├─ willInit()
    │           ├─ initModule()
    │           ├─ didInit() → body()  ← 构建虚拟视图树
    │           └─ createBody()
    │               ├─ createFlexNode()
    │               ├─ createRenderView()  → callNative(CREATE_RENDER_VIEW)
    │               ├─ setViewProp()       → callNative(SET_VIEW_PROP)  ×N
    │               └─ layoutIfNeed()      → callNative(SET_RENDER_VIEW_FRAME) ×N
    │              │
    │              │  ── 上面这些 callNative 是异步的,被收集到 UIScheduler ──
    │              │
    │←─batch UI──│         ⑦ UIScheduler 批量提交到主线程
    │              │
    │ 执行 UI 指令   │         ⑧ 主线程依次执行:创建 View、设属性、设 Frame
    │ createView()  │
    │ setProp()     │
    │ setFrame()    │
    │              │
    │ 首屏渲染完成    │         ⑨ viewDidLoad,触发 FirstFramePaint
    │──sendEvent─→│
    │         onReceivePagerEvent   ⑩ Kotlin 收到首屏事件
    │          ("pageFirstFramePaint")

现在,让我们逐步深入每一个阶段的源码实现。


4. 第一阶段:主线程上的准备工作

4.1 Android 端入口

一切从 KuiklyRenderView.init() 开始。这个方法必须在主线程调用

kotlin 复制代码
// KuiklyRenderView.kt
override fun init(contextCode: String, pageName: String, 
                  params: Map<String, Any>, size: Size?, assetsPath: String?) {
    assert(isMainThread())  // ← 断言:必须在主线程
    initKuiklyClassLoaderIfNeed(contextCode)
    val initRenderCoreTask = { sz: SizeF ->
        initRenderCore(contextCode, pageName, params, sz, assetsPath)
    }
    // 如果已知 size,立即初始化;否则等 onSizeChanged 回调
}

为什么必须在主线程? 因为 KuiklyRenderView 本身是一个 Android View,View 的初始化、添加子 View 等操作都必须在 UI 线程。

4.2 iOS 端入口

iOS 端同理,KuiklyRenderView 是一个 UIView

objc 复制代码
// KuiklyRenderView.m
- (instancetype)initWithSize:(CGSize)size contextCode:(NSString *)contextCode
    contextParam:(KuiklyContextParam *)contextParam params:(NSDictionary *)params
    delegate:(id<KuiklyRenderViewDelegate>)delegate {
    if (self = [super init]) {
        _renderCore = [[KuiklyRenderCore alloc] initWithRootView:self
                                                     contextCode:contextCode
                                                    contextParam:contextParam
                                                          params:coreParams
                                                        delegate:self];
    }
    return self;
}

两端的模式完全一致:主线程创建宿主 View → 初始化 RenderCore → RenderCore 内部切换到 Context 线程。


5. 第二阶段:切换到 Context 线程创建页面

5.1 KuiklyRenderCore.init() ------ 关键的线程切换点

这是整个页面加载过程中最重要的线程切换。我们以 Android 端为例:

kotlin 复制代码
// KuiklyRenderCore.kt
override fun init(...) {
    // 还在主线程:创建调度器
    uiScheduler = KuiklyRenderCoreUIScheduler { ... }
    renderLayerHandler = KuiklyRenderLayerHandler().apply { init(renderView) }
    initNativeMethodRegisters()  // 注册 Native 方法回调表

    // ★★★ 关键:切换到 Context 线程 ★★★
    performOnContextQueue {
        initContextHandler(contextCode, url, params, contextInitCallback)
    }
}

performOnContextQueue 是什么?让我们看看 Context 线程是怎么创建的。

5.2 Android 的 Context 线程:HandlerThread

kotlin 复制代码
// KuiklyRenderCoreContextScheduler.kt
object KuiklyRenderCoreContextScheduler : IKuiklyRenderCoreScheduler {

    const val THREAD_NAME = "HRContextQueueHandlerThread"

    private val handler by lazy {
        Handler(object : HandlerThread(THREAD_NAME, Process.THREAD_PRIORITY_FOREGROUND) {
            override fun onLooperPrepared() { 
                NativeBridge.isContextThread = true  // ← 标记当前线程为 Context 线程
            }
        }.apply { start() }.looper)
    }

    override fun scheduleTask(delayMs: Long, task: KuiklyRenderCoreTask) {
        handler.postDelayed(task, delayMs)  // 把任务 post 到 Context 线程的消息队列
    }
}

Kotlin 语法讲解------这段代码有几个值得深入理解的语法:

object 单例声明

kotlin 复制代码
object KuiklyRenderCoreContextScheduler : IKuiklyRenderCoreScheduler { ... }

Kotlin 的 object 声明创建了一个单例 。整个 App 进程中只有一个 KuiklyRenderCoreContextScheduler 实例,这意味着只有一条 Context 线程。所有 Kuikly 页面共享这一条线程。

by lazy 懒初始化

kotlin 复制代码
private val handler by lazy { ... }

by lazy 是属性委托的一种,它的含义是:第一次访问 handler 时才执行花括号里的代码。这保证了 Context 线程不会在 App 启动时就被创建,而是在第一个 Kuikly 页面加载时才创建。

HandlerThread

HandlerThread 是 Android 提供的一个带消息循环的线程。它内部有一个 Looper(消息泵),配合 Handler 使用时,可以不断地从消息队列取任务执行。这就是 Context 线程的本质:一个串行的任务队列,所有 Kotlin 逻辑都按顺序在这里执行。

ThreadLocal 标记

kotlin 复制代码
// NativeBridge.kt (Android)
companion object {
    private val _isContextThread = ThreadLocal<Boolean>()
    var isContextThread: Boolean
        get() = _isContextThread.get() ?: false
        set(value) { _isContextThread.set(value) }
}

ThreadLocal 是 Java/Kotlin 的线程局部变量。每个线程都有自己独立的 isContextThread 值。当 Context 线程启动时,onLooperPrepared 被调用,此时在 Context 线程内将 isContextThread 设为 true。之后任何地方调用 NativeBridge.isContextThread 都能判断当前是否在 Context 线程上。

5.3 iOS 的 Context 线程:GCD 串行队列

objc 复制代码
// KuiklyRenderThreadManager.m
static dispatch_queue_t gContextQueue = NULL;

+ (dispatch_queue_t)contextQueue {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_attr_t queue_attr = dispatch_queue_attr_make_with_qos_class(
            DISPATCH_QUEUE_SERIAL,          // 串行队列
            QOS_CLASS_USER_INTERACTIVE, 0); // 最高 QoS 优先级
        gContextQueue = dispatch_queue_create("com.tencent.kuikly.context", queue_attr);
        dispatch_queue_set_specific(gContextQueue, &gContextQueue, ...);
    });
    return gContextQueue;
}

iOS 使用 GCD(Grand Central Dispatch)的串行队列。注意两个关键选择:

  1. DISPATCH_QUEUE_SERIAL(串行):保证所有任务按顺序执行,不会并发,因此不需要锁
  2. QOS_CLASS_USER_INTERACTIVE(用户交互级):这是 iOS 最高优先级的 QoS,告诉系统"这个队列的任务很紧急,直接影响用户体验"

判断是否在 Context 队列上:

objc 复制代码
+ (BOOL)isContextQueue {
    if (dispatch_get_specific(&gContextQueue)) {  // ← 用 queue-specific data 判断
        return YES;
    }
    return NO;
}

dispatch_get_specific 是 GCD 提供的一个机制:你可以给队列绑定一个 key-value,然后在任意地方查询当前队列是否绑定了这个 key。如果返回非 NULL,说明当前代码正在这个队列上执行。

5.4 在 Context 线程上初始化 Kotlin 引擎

切换到 Context 线程后,initContextHandler 做了三件事:

kotlin 复制代码
// KuiklyRenderCore.kt
private fun initContextHandler(contextCode: String, url: String, params: Map<String, Any>, ...) {
    // 1. 创建 ContextHandler(通过反射加载 KuiklyCoreEntry)
    contextHandler = KuiklyRenderJvmContextHandler()
    
    contextHandler?.apply {
        // 2. 注册 Kotlin→Native 的回调通道
        registerCallNative { method, args ->
            performNativeMethodWithMethod(method, args)
        }
        
        // 3. 初始化 Kotlin 引擎
        init(contextCode)
        
        // 4. ★ 调用 CREATE_INSTANCE,触发 Kotlin 侧创建页面 ★
        call(KuiklyRenderContextMethodCreateInstance, listOf(instanceId, url, params))
    }
}

其中第 4 步的 call(CREATE_INSTANCE, ...) 最终会进入 Kotlin 侧的 BridgeManager.callKotlinMethod()

kotlin 复制代码
// BridgeManager.kt
fun callKotlinMethod(methodId: Int, arg0: Any?, arg1: Any?, ...) {
    currentPageId = arg0 as String
    when (methodId) {
        KotlinMethod.CREATE_INSTANCE -> {
            PagerManager.createPager(arg0 as String, arg1 as String, arg2 as String)
        }
        // ...
    }
}

然后进入 PagerManager.createPager(),通过之前 @Page 注解注册的 creator 创建页面实例:

kotlin 复制代码
// PagerManager.kt
fun createPager(pagerId: String, url: String, pagerData: String) {
    val pagerName = pageNameFromUrl(url)
    reactiveObserverMap[pagerId] = ReactiveObserver()
    val pager: IPager? = pagerCreator(pagerName)?.invoke()  // ← 调用注册的 creator 创建 Pager 实例
    pagerMap[pagerId] = pager
    pager.onCreatePager(pagerId, JSONObject(pagerData))      // ← 进入页面生命周期
}

以上所有代码都在 Context 线程上执行 。从 performOnContextQueue 开始,后面的一切逻辑------页面创建、body 构建、布局计算------全部在 Context 线程上串行运行。


6. 第三阶段:Kotlin→Native 通信------同步 vs 异步

页面在 Context 线程上执行 onCreatePager() 时,会调用 body() 方法构建虚拟视图树,然后 createBody() 进行布局计算。在此过程中,Kotlin 需要告诉 Native 端"帮我创建一个 TextView"、"把它的背景色设为红色"、"把它放到 (10, 20) 位置"等。

这些指令通过 BridgeManager 发出:

kotlin 复制代码
// BridgeManager.kt 中定义的 Native 方法
object NativeMethod {
    const val CREATE_RENDER_VIEW = 1      // 创建原生 View
    const val REMOVE_RENDER_VIEW = 2      // 移除原生 View
    const val INSERT_SUB_RENDER_VIEW = 3  // 插入子 View
    const val SET_VIEW_PROP = 4           // 设置 View 属性
    const val SET_RENDER_VIEW_FRAME = 5   // 设置 View 的位置和大小
    const val CALCULATE_RENDER_VIEW_SIZE = 6  // 计算 View 尺寸(同步!)
    const val CREATE_SHADOW = 9           // 创建 Shadow 节点(同步!)
    // ...
}

6.1 关键设计:哪些是同步的?哪些是异步的?

这是 Kuikly 多线程模型中最精妙的设计之一。让我们看 Native 端收到 Kotlin 调用时的处理:

kotlin 复制代码
// KuiklyRenderCore.kt (Android)
private fun performNativeMethodWithMethod(method: KuiklyRenderNativeMethod, args: List<Any?>): Any? {
    val cb = nativeMethodRegistry[method.value]
    cb?.also {
        assert(!isMainThread())  // ← 断言:当前在 Context 线程

        if (isSyncMethodCall(method, args)) {
            // ★ 同步方法:直接在 Context 线程执行,立即返回结果
            return it(method, args)
        } else {
            // ★ 异步方法:加入 UIScheduler 的任务队列,稍后批量在主线程执行
            uiScheduler?.scheduleTask {
                it(method, args)
            }
        }
    }
    return null
}

iOS 端的逻辑完全一致:

objc 复制代码
// KuiklyRenderCore.m
- (id)p_performNativeMethodWithMethod:(KuiklyRenderNativeMethod)method args:(NSArray *)args {
    KuiklyRenderNativeMethodCallback methodCallback = _nativeMethodRegistry[@(method)];
    if (methodCallback) {
        [KuiklyRenderThreadManager assertContextQueue];
        if ([self p_shouldSyncCallWithWithMethod:method args:args]) {
            return methodCallback(method, args);  // 同步执行
        } else {
            [self.uiScheduler addTaskToMainQueueWithTask:^{  // 异步批量
                methodCallback(method, args);
            }];
        }
    }
    return nil;
}

哪些方法是同步的?

objc 复制代码
// iOS 端的判断逻辑
- (BOOL)p_shouldSyncCallWithWithMethod:(KuiklyRenderNativeMethod)method args:(NSArray *)args {
    return method == KuiklyRenderNativeMethodCalculateRenderViewSize ||  // 计算 View 尺寸
           method == KuiklyRenderNativeMethodCreateShadow ||             // 创建 Shadow
           method == KuiklyRenderNativeMethodRemoveShadow ||             // 移除 Shadow
           method == KuiklyRenderNativeMethodSetShadowForView ||         // 设置 Shadow
           method == KuiklyRenderNativeMethodSetShadowProp ||            // Shadow 属性
           method == KuiklyRenderNativeMethodSetTimeout ||               // 定时器
           method == KuiklyRenderNativeMethodCallShadowMethod ||         // Shadow 方法
           method == KuiklyRenderNativeMethodSyncFlushUI ||              // 同步刷新
           // ...
}

为什么要区分同步和异步? 因为有些操作,Kotlin 侧必须立即拿到结果才能继续。比如:

  • CALCULATE_RENDER_VIEW_SIZE:计算 Text 的实际尺寸。Kotlin 的 Flexbox 布局引擎需要知道一段文本在给定宽度下会占多高,这个值必须同步返回,布局计算才能继续
  • CREATE_SHADOW:创建 Shadow 节点(如文本的阴影计算)需要 Native 端立即返回

而像 CREATE_RENDER_VIEWSET_VIEW_PROPSET_RENDER_VIEW_FRAME 这些纯 UI 操作,Kotlin 不需要返回值,可以攒起来批量执行。

6.2 NativeBridge 的 expect/actual 机制

BridgeManager 通过 NativeBridge 与 Native 通信。这里用到了 Kotlin Multiplatform 的核心机制------expect/actual

kotlin 复制代码
// commonMain(公共层)------只声明接口,不提供实现
expect open class NativeBridge() {
    fun toNative(methodId: Int, arg0: Any?, arg1: Any?, ...): Any?
    fun destroy()
}

expect 关键字的含义是:"我声明了这个东西存在,但具体实现由各平台提供。"

Android 实现:

kotlin 复制代码
// androidMain
actual open class NativeBridge actual constructor() {
    var delegate: NativeBridgeDelegate? = null
    
    actual fun toNative(methodId: Int, ...): Any? {
        return delegate?.callNative(methodId, ...)  // 调用 Java/Kotlin 层的回调
    }
}

iOS 实现:

kotlin 复制代码
// appleMain
actual open class NativeBridge actual constructor() {
    var iosNativeBridgeDelegate: IOSNativeBridgeDelegate? = null
    
    actual fun toNative(methodId: Int, ...): Any? {
        return iosNativeBridgeDelegate?.callNative(methodId, ...)  // 调用 ObjC 层的回调
    }
}

这样,公共层的 BridgeManager 调用 nativeBridge.toNative() 时,编译器会自动根据目标平台选择正确的实现。这就是 Kotlin Multiplatform 的"同一份代码,多平台运行"的基础机制。


7. 第四阶段:UIScheduler------批量上屏的艺术

UIScheduler 是 Kuikly 多线程模型中性能优化的核心。它的设计思想是:

不要来一条 UI 指令就切一次线程,而是攒一批指令,一次性切到主线程全部执行完。

7.1 Android 端实现

kotlin 复制代码
// KuiklyRenderCoreUIScheduler.kt
class KuiklyRenderCoreUIScheduler(...) : IKuiklyRenderCoreScheduler {
    
    // Context 线程上暂存的任务列表
    private var mainThreadTasksOnContextQueue: MutableList<KuiklyRenderCoreTaskExecutor>? = null
    // 主线程上待执行的任务列表
    private var mainThreadTasks = mutableListOf<KuiklyRenderCoreTaskExecutor>()
    
    // 在 Context 线程调用------收集任务
    private fun addTaskToMainQueue(task: KuiklyRenderCoreTaskExecutor) {
        assert(!isMainThread())  // ← 必须在 Context 线程
        val tasks = mainThreadTasksOnContextQueue ?: mutableListOf<KuiklyRenderCoreTaskExecutor>().apply {
            mainThreadTasksOnContextQueue = this
        }
        tasks.add(task)
        setNeedSyncMainQueueTasks()  // ← 标记"有任务需要同步到主线程"
    }
}

setNeedSyncMainQueueTasks 的实现非常精巧:

kotlin 复制代码
private fun setNeedSyncMainQueueTasks() {
    assert(!isMainThread())
    if (needSyncMainQueueTasksBlock != null) {
        return  // ← 已经标记过了,不重复标记!这就是"批量"的秘诀
    }
    
    // 创建"同步闭包"------包含了把 Context 线程任务搬到主线程的逻辑
    needSyncMainQueueTasksBlock = { sync ->
        val performTasks = mainThreadTasksOnContextQueue
        mainThreadTasksOnContextQueue = null
        synchronized(this) {
            mainThreadTasks.addAll(performTasks?.toList() ?: listOf())
        }
        performOnMainQueueWithTask(sync = sync) {
            // 在主线程执行所有任务
            var tasks: List<KuiklyRenderCoreTaskExecutor>?
            synchronized(this) {
                tasks = mainThreadTasks.toList()
                mainThreadTasks.clear()
            }
            runMainQueueTasks(tasks)
        }
    }
    
    // ★ 关键:把"执行同步闭包"这件事,再 post 到 Context 线程消息队列的尾部
    KuiklyRenderCoreContextScheduler.scheduleTask {
        performSyncMainQueueTasksBlockIfNeed(false)
    }
}

为什么要 post 到 Context 线程队列尾部? 因为当前 Context 线程正在执行 Kotlin 逻辑(比如 body 方法还没执行完),会不断产生新的 UI 指令。把"同步到主线程"这个动作 post 到队列尾部,就能保证当前一轮逻辑全部执行完后,再一次性把所有指令提交给主线程

让我们画出这个时序:

css 复制代码
Context 线程的消息队列
┌─────────────────────────────────────────────┐
│ [当前任务: 执行 body、布局计算、产生 UI 指令]      │
│ [排队: performSyncMainQueueTasksBlockIfNeed] │  ← 在当前任务结束后执行
└─────────────────────────────────────────────┘

当前任务执行过程中,产生的 UI 指令被收集到 mainThreadTasksOnContextQueue
当前任务执行完,轮到 performSync... → 把收集的指令批量 post 到主线程

7.2 iOS 端实现

iOS 的逻辑完全一致,只是用 ObjC + GCD 实现:

objc 复制代码
// KuiklyRenderUIScheduler.m
- (void)addTaskToMainQueueWithTask:(dispatch_block_t)taskBlock {
    KR_ASSERT_CONTEXT_HTREAD;  // 断言在 Context 线程
    [_mainThreadTasksOnContextQueue addObject:taskBlock];
    [self p_setNeedSyncMainQuequeTasks];
}

- (void)p_setNeedSyncMainQuequeTasks {
    if (!_needSyncMainQueueTasksBlock) {
        KR_WEAK_SELF
        self.needSyncMainQueueTasksBlock = ^{
            // 同步 UI 任务前,先通知 Kotlin 做 layoutIfNeed
            [strongSelf p_dispatchWillPerformUITasksDelegator];
            NSArray *tasks = weakSelf.mainThreadTasksOnContextQueue;
            weakSelf.mainThreadTasksOnContextQueue = nil;
            // 加锁搬运任务
            [weakSelf.threadLock threadSafeInBlock:^{
                [weakSelf.mainThreadTasks addObjectsFromArray:tasks];
            }];
            // 切到主线程批量执行
            [KuiklyRenderThreadManager performOnMainQueueWithTask:^{
                for (dispatch_block_t task in mainThreadTasks) {
                    task();
                }
            } sync:[NSThread isMainThread]];
        };
        // 再调度一次到 Context 线程尾部
        [KuiklyRenderThreadManager performOnContextQueueWithBlock:^{
            [weakSelf performSyncMainQueueTasksBlockIfNeed];
        }];
    }
}

7.3 一个容易被忽略的细节:layoutIfNeed 的时机

注意 iOS 端有一行 [strongSelf p_dispatchWillPerformUITasksDelegator],Android 端对应的是 UIScheduler 构造时传入的闭包:

kotlin 复制代码
// KuiklyRenderCore.kt
uiScheduler = KuiklyRenderCoreUIScheduler {
    // 同步主线程任务前,告诉 Kotlin 侧去 layoutIfNeed
    contextHandler?.call(KuiklyRenderContextMethodLayoutView, listOf(instanceId))
}

这个回调在"把 UI 指令提交给主线程之前"被调用,它的作用是确保 Kotlin 侧已经完成布局计算,所有 View 的 Frame 都已经确定。否则可能出现"View 已经创建了,但还不知道放在哪里"的情况。


8. 第五阶段:首屏完成与后续事件闭环

8.1 首屏渲染完成

UIScheduler 在主线程执行完第一批 UI 指令后,会标记 viewDidLoad = true

kotlin 复制代码
// KuiklyRenderCoreUIScheduler.kt
private fun runMainQueueTasks(tasks: List<KuiklyRenderCoreTaskExecutor>?) {
    assert(isMainThread())
    val uiTasks = tasks ?: return
    isPerformingMainQueueTask = true
    for (task in uiTasks) {
        task.execute()
    }
    isPerformingMainQueueTask = false
    
    if (!viewDidLoad) {
        viewDidLoad = true           // ← 首屏标记
        performViewDidLoadTasksIfNeed()  // ← 执行等待首屏完成的任务
    }
}

然后 Native 端会通过 sendEvent 通知 Kotlin 侧"首屏完成了":

scss 复制代码
sendEvent("pageFirstFramePaint") → Context 线程 → Pager.onReceivePagerEvent()

8.2 后续事件------从主线程回到 Context 线程

用户的触摸、滑动等事件发生在主线程,需要通知到 Context 线程的 Kotlin 逻辑。以 sendEvent 为例:

Android 端:

kotlin 复制代码
// KuiklyRenderCore.kt
override fun sendEvent(event: String, data: Map<String, Any>, shouldSync: Boolean) {
    performOnContextQueue(sync = shouldSync) {  // ← 切到 Context 线程
        contextHandler?.call(KuiklyRenderContextMethodUpdateInstance,
            listOf(instanceId, event, data))
    }
}

iOS 端:

objc 复制代码
// KuiklyRenderCore.m
- (void)sendWithEvent:(NSString *)event data:(NSDictionary *)data {
    [KuiklyRenderThreadManager performOnContextQueueWithBlock:^{
        [self.contextHandler callWithMethod:KuiklyRenderContextMethodUpdateInstance
                                      args:@[self.instanceId, event, data]];
    } sync:![NSThread isMainThread]];  // ← 非主线程时同步调用
}

注意 iOS 端的 sync:![NSThread isMainThread]:如果当前已经在 Context 线程上(非主线程),就用 dispatch_sync 直接同步执行;如果在主线程上,就用 dispatch_async 异步投递(避免主线程阻塞等待 Context 线程)。

这样就形成了完整的事件循环:

scss 复制代码
用户操作(主线程) → sendEvent → Context 线程(Kotlin 处理逻辑)
                                    ↓
                              产生新的 UI 指令
                                    ↓
                              UIScheduler 批量
                                    ↓
                              主线程执行(UI 更新)

9. iOS 端 vs Android 端的线程实现对比

维度 Android iOS
Context 线程 HandlerThread(基于 Java 的 Looper/Handler 机制) GCD dispatch_queue_create(串行队列)
线程优先级 Process.THREAD_PRIORITY_FOREGROUND QOS_CLASS_USER_INTERACTIVE
判断是否在 Context 线程 ThreadLocal<Boolean>NativeBridge.isContextThread dispatch_get_specific(queue-specific data)
切到 Context 线程 handler.post { task } dispatch_async(contextQueue, task)
切到主线程 Handler(Looper.getMainLooper()).post { task } dispatch_async(dispatch_get_main_queue(), task)
UIScheduler 基于 MutableList + synchronized 基于 NSMutableArray + KuiklyRenderThreadLock
线程间同步调用 ConditionVariableBlockingRunnable dispatch_sync
Kotlin→Native 桥接 反射加载 KuiklyCoreEntry 类(JVM) ObjC 反射加载 Kotlin/Native Framework
Native→Kotlin 调用 IKuiklyCoreEntry.callKotlinMethod() [contextHandler callWithMethod:args:]

虽然实现方式不同,但架构完全一致:单一 Context 线程 + UIScheduler 批量提交 + 主线程执行。

9.1 iOS 特有的 C 桥接

iOS 端还额外提供了一组 C 函数,供 Kotlin/Native 编译出的代码调用:

c 复制代码
// KuiklyRenderThreadBridge.m
void com_tencent_kuikly_ScheduleContextTask(const char* pagerId, 
                                             void (*onSchedule)(const char*)) {
    NSString *pId = [NSString stringWithUTF8String:pagerId];
    [KuiklyRenderThreadManager performOnContextQueueWithBlock:^{
        onSchedule([pId UTF8String]);
    } sync:NO];
}

bool com_tencent_kuikly_IsCurrentOnContextThread(const char* pagerId) {
    return [KuiklyRenderThreadManager isContextQueue];
}

为什么需要 C 函数?因为 Kotlin/Native 编译为 iOS 的 Framework 时,不能直接调用 ObjC 的类方法(除非通过 cinterop),但可以调用 C 函数。这些 C 函数充当了"桥梁"角色。


10. 从其他线程回到 Kuikly 线程

当你在 Kotlin 中使用了多线程(比如 KMP 的 expect/actual 机制、或者 kotlinx 协程切换到 IO 线程),完成异步操作后需要回到 Kuikly 线程更新 UI。Kuikly 提供了 KuiklyContextScheduler 来实现这一点。

10.1 公共层 API

kotlin 复制代码
// KuiklyContextScheduler.kt (commonMain)
internal object KuiklyContextScheduler : SynchronizedObject() {
    fun runOnKuiklyThread(pagerId: String, block: (cancel: Boolean) -> Unit) {
        // 如果已经在 Kuikly 线程上,直接执行
        if (platformIsOnKuiklyThread(pagerId)) {
            block(false)
            return
        }
        // 否则,调度到 Kuikly 线程
        addTask(pagerId, block)
        platformScheduleOnKuiklyThread(pagerId)
    }
}

注意 block: (cancel: Boolean) -> Unit 的设计------参数 cancel 告诉你页面是否已经销毁。如果页面已经销毁(BridgeManager.containNativeBridge(pagerId) 返回 false),cancel 为 true,你应该放弃操作。这是一个很好的安全机制

10.2 Android 实现

kotlin 复制代码
// KuiklyContextScheduler.android.kt
internal actual inline fun platformIsOnKuiklyThread(pagerId: String): Boolean {
    return NativeBridge.isContextThread  // ← 读 ThreadLocal
}

internal actual inline fun platformScheduleOnKuiklyThread(pagerId: String) {
    KuiklyRenderCoreContextScheduler.scheduleTask {
        KuiklyContextScheduler.runTask(pagerId)  // ← post 到 Context 线程
    }
}

10.3 iOS 实现

kotlin 复制代码
// KuiklyContextScheduler.ios.kt
internal actual inline fun platformIsOnKuiklyThread(pagerId: String): Boolean {
    return com_tencent_kuikly_IsCurrentOnContextThread(pagerId)  // ← 调用 C 函数
}

internal actual inline fun platformScheduleOnKuiklyThread(pagerId: String) {
    com_tencent_kuikly_ScheduleContextTask(pagerId, staticCFunction { pIdPtr ->
        // 这个 staticCFunction 是 Kotlin/Native 提供的机制
        // 它将 Kotlin 函数转为 C 函数指针,以便 ObjC/C 代码回调
        val pId = pIdPtr!!.toKString()
        KuiklyContextScheduler.runTask(pId)
    })
}

10.4 使用 Dispatchers.Kuikly

如果你使用 kotlinx 协程,可以通过自定义 Dispatcher 切回 Kuikly 线程:

kotlin 复制代码
override fun created() {
    super.created()
    val ctx = this
    // 在 Kuikly 线程启动协程
    GlobalScope.launch(Dispatchers.Kuikly[ctx]) {
        // 切到 IO 线程做耗时操作
        val data = withContext(Dispatchers.IO) {
            fetchDataFromNetwork()
        }
        // 自动回到 Kuikly 线程,安全更新 UI
        ctx.dataObservable = data
    }
}

Dispatchers.Kuikly[ctx] 的实现原理就是把协程的恢复操作调度到 KuiklyContextScheduler.runOnKuiklyThread() 上。


11. 线程安全验证机制

Kuikly 提供了开发阶段的线程安全验证:

kotlin 复制代码
// Pager.kt
companion object {
    var VERIFY_THREAD
        get() = com.tencent.kuikly.core.utils.VERIFY_THREAD
        set(value) { com.tencent.kuikly.core.utils.VERIFY_THREAD = value }
    
    var VERIFY_REACTIVE_OBSERVER
        get() = com.tencent.kuikly.core.utils.VERIFY_REACTIVE_OBSERVER
        set(value) { com.tencent.kuikly.core.utils.VERIFY_REACTIVE_OBSERVER = value }
}

开启方式:

kotlin 复制代码
@Page("DebugPage")
class DebugPage : BasePager() {
    override fun willInit() {
        super.willInit()
        Pager.VERIFY_THREAD = true             // 开启线程校验
        Pager.VERIFY_REACTIVE_OBSERVER = true   // 开启响应式观察者校验
        
        Pager.verifyFailed { exception ->
            // 自定义验证失败的处理
            println("线程安全验证失败: ${exception.message}")
            throw exception  // 开发阶段直接崩溃,方便定位
        }
    }
}

VERIFY_THREAD = true 时,如果你在非 Context 线程访问了 observable 属性或调用了 UI 相关 API,框架会立即抛出异常。强烈建议在开发阶段开启这两个选项。


12. 三种异步编程方式的选择

理解了多线程模型后,让我们看看 Kuikly 提供的三种异步编程方案以及它们的适用场景:

方式一:Module 机制 + Kuikly 内建协程

kotlin 复制代码
override fun created() {
    super.created()
    val ctx = this
    lifecycleScope.launch {
        val type = fetchLocal()       // 挂起函数(底层通过 Module 调 Native)
        val data = fetchRemote(type)  // 挂起函数
        ctx.dataObservable = data     // 安全更新 UI
    }
}
  • 线程模型:始终在 Kuikly 线程。异步操作通过 Module 下沉到 Native 层执行(Native 可以自由使用多线程),结果通过回调返回到 Kuikly 线程
  • 优势:无线程安全问题,支持动态化,无额外依赖
  • 适用场景:大多数业务需求(网络请求、本地存储、系统 API 调用)

方式二:KMP 多线程 + kuiklyx 回调

kotlin 复制代码
override fun created() {
    super.created()
    val ctx = this
    asyncKmpFetchData { data ->
        KuiklyContextScheduler.runOnKuiklyThread(ctx.pagerId) { cancel ->
            if (cancel) return@runOnKuiklyThread
            ctx.dataObservable = data  // 回到 Kuikly 线程更新 UI
        }
    }
}
  • 线程模型:KMP 代码可以在任意线程执行,完成后显式切回 Kuikly 线程
  • 优势:无通信开销(直接跨平台调用,不经过 Bridge)
  • 劣势:不支持动态化,需要手动管理线程切换
  • 适用场景:对性能要求极高、不需要动态化的场景

方式三:kotlinx 协程 + Dispatchers.Kuikly

kotlin 复制代码
override fun created() {
    super.created()
    val ctx = this
    GlobalScope.launch(Dispatchers.Kuikly[ctx]) {
        val data = withContext(Dispatchers.IO) {
            kmpFetchData()  // 在 IO 线程执行
        }
        // 自动回到 Kuikly 线程
        ctx.dataObservable = data
    }
}
  • 线程模型:利用 kotlinx 协程的 Dispatcher 机制自由切换线程
  • 优势:代码可读性最好,与 Kotlin 社区标准对齐
  • 劣势:不支持动态化,需要引入 kotlinx 协程库 + kuiklyx 协程库
  • 适用场景:复杂的异步逻辑,需要协程语法提升可读性

选择决策树

markdown 复制代码
你需要动态化吗?
├─ 是 → 方式一(Module + Kuikly 内建协程)
└─ 否 → 你需要多线程执行耗时任务吗?
         ├─ 否 → 方式一
         └─ 是 → 你需要协程语法吗?
                  ├─ 否 → 方式二(KMP + kuiklyx 回调)
                  └─ 是 → 方式三(kotlinx + Dispatchers.Kuikly)

13. 总结:一张图看清全貌

scss 复制代码
                          ┌──────────────────────────────────────┐
                          │           主线程 (UI Thread)           │
                          │                                      │
                          │   KuiklyRenderView                   │
                          │     ├─ init()  ──schedule──→ ①       │
                          │     │                                │
                          │   UIScheduler ←─── 批量UI指令 ←─ ④  │
                          │     ├─ createView()                  │
                          │     ├─ setProp()                     │
                          │     ├─ setFrame()                    │
                          │     └─ viewDidLoad ──sendEvent──→ ⑤ │
                          │                                      │
                          └──────────────┬───────────────────────┘
                                         │ ①②④⑤ 线程间通信
                          ┌──────────────┴───────────────────────┐
                          │       Context 线程 (Kuikly Thread)     │
                          │                                      │
                          │ ② initContextHandler                 │
                          │     ├─ 加载 KuiklyCoreEntry           │
                          │     ├─ 注册 callNative 回调           │
                          │     └─ call(CREATE_INSTANCE)         │
                          │                                      │
                          │ ③ PagerManager.createPager()         │
                          │     └─ pager.onCreatePager()         │
                          │         ├─ willInit()                │
                          │         ├─ initModule()              │
                          │         ├─ didInit() → body()        │
                          │         └─ createBody()              │
                          │             ├─ createFlexNode()      │
                          │             ├─ layoutIfNeed()        │
                          │             └─ callNative ×N ──→ ④  │
                          │                                      │
                          │ ⑤ onReceivePagerEvent                │
                          │     ("pageFirstFramePaint")          │
                          │                                      │
                          │ ⑥ 后续:响应式更新、事件处理、协程...    │
                          │     └─ 新的 callNative → UIScheduler │
                          │                                      │
                          └──────────────────────────────────────┘

线程间通信方式:
  ① 主线程 → Context: handler.post() / dispatch_async(contextQueue)
  ④ Context → 主线程: UIScheduler 批量 → uiHandler.post() / dispatch_async(mainQueue)
  ⑤ 主线程 → Context: sendEvent → performOnContextQueue
  
同步调用(不经 UIScheduler,Context 线程直接执行并返回):
  CalculateRenderViewSize、CreateShadow、SetTimeout 等

核心要点

  1. 两条线程,各司其职:Context 线程负责全部 Kotlin 逻辑,主线程负责 Native View 操作
  2. 所有 Kuikly 的 UI 类(View、Attr、Event、Observable)只能在 Context 线程访问
  3. UIScheduler 是性能关键:收集 UI 指令、批量提交、减少线程切换次数
  4. 同步方法 vs 异步方法:需要返回值的(如计算尺寸)同步执行,纯 UI 操作异步批量
  5. 回到 Kuikly 线程 :使用 KuiklyContextScheduler.runOnKuiklyThread()Dispatchers.Kuikly
  6. 串行 = 安全:Context 线程是串行的,不存在并发竞争,不需要加锁

理解了这条链路,你就掌握了 Kuikly 框架最核心的架构设计。无论是排查性能问题、理解页面加载慢的原因、还是正确处理异步逻辑,都能做到心中有数。

相关推荐
JMchen1236 小时前
Android UDP编程:实现高效实时通信的全面指南
android·经验分享·网络协议·udp·kotlin
JMchen1237 小时前
Android网络安全实战:从HTTPS到双向认证
android·经验分享·网络协议·安全·web安全·https·kotlin
JMchen12320 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
儿歌八万首1 天前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19431 天前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
有位神秘人1 天前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
Jinkxs1 天前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&1 天前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
golang学习记1 天前
IntelliJ IDEA 2025.3 重磅发布:K2 模式全面接管 Kotlin —— 告别 K1,性能飙升 40%!
java·kotlin·intellij-idea