本文适合 Kotlin/Kuikly 初学者阅读。我们不做泛泛而谈,而是以"一个页面是怎么被加载出来的"这条主线为脉络,一步一步跟踪源码,彻底弄清 Kuikly 的双线程模型是什么、为什么这么设计、以及它如何保证高性能和线程安全。
1. 为什么需要双线程?
在理解 Kuikly 的多线程模型之前,我们先想一个问题:如果把所有事情都放在主线程做,会怎样?
一个 UI 框架需要做两大类事情:
| 类别 | 具体工作 | 耗时特征 |
|---|---|---|
| 逻辑计算 | 执行业务代码、构建虚拟视图树、计算 Flexbox 布局、处理响应式更新 | CPU 密集 |
| 原生渲染 | 创建平台 View、设置属性、设置 frame、插入视图层级 | 必须在 UI 线程 |
如果全放主线程,逻辑计算会阻塞 UI 渲染,用户看到的就是"卡"。React Native 很早就采用了类似的双线程方案------JS 线程做逻辑,主线程做渲染。Kuikly 的思路一脉相承:
Context 线程 (也叫 Kuikly 线程):运行 Kotlin 业务逻辑、DSL 构建、布局计算
Main 线程(UI 线程):执行原生 View 的创建、属性设置、帧布局
这种分工带来三个核心优势:
- 不卡主线程:无论你的业务逻辑多复杂,用户的滑动、点击永远流畅
- 批量上屏:Context 线程产生的 UI 指令可以攒一批再一次性提交给主线程,减少线程切换开销
- 架构清晰: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)的串行队列。注意两个关键选择:
DISPATCH_QUEUE_SERIAL(串行):保证所有任务按顺序执行,不会并发,因此不需要锁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_VIEW、SET_VIEW_PROP、SET_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 |
| 线程间同步调用 | ConditionVariable(BlockingRunnable) |
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 等
核心要点
- 两条线程,各司其职:Context 线程负责全部 Kotlin 逻辑,主线程负责 Native View 操作
- 所有 Kuikly 的 UI 类(View、Attr、Event、Observable)只能在 Context 线程访问
- UIScheduler 是性能关键:收集 UI 指令、批量提交、减少线程切换次数
- 同步方法 vs 异步方法:需要返回值的(如计算尺寸)同步执行,纯 UI 操作异步批量
- 回到 Kuikly 线程 :使用
KuiklyContextScheduler.runOnKuiklyThread()或Dispatchers.Kuikly - 串行 = 安全:Context 线程是串行的,不存在并发竞争,不需要加锁
理解了这条链路,你就掌握了 Kuikly 框架最核心的架构设计。无论是排查性能问题、理解页面加载慢的原因、还是正确处理异步逻辑,都能做到心中有数。