From 701a97501c70c005a4a9887ee6a9391da37bb5ca Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:46:24 +0800 Subject: [PATCH] [Feature][Android] Add typed Lynx thread strategy Summary of change: - Add Sparkling-owned thread strategy values for the supported Lynx modes. - Support a global default with a per-container override. - Apply page configuration before view construction without exposing the builder. - Preserve the Lynx default when neither configuration is present. TEST: ./gradlew :sparkling:testDebugUnitTest --tests com.tiktok.sparkling.SparklingThreadStrategyTest --tests com.tiktok.sparkling.hybridkit.lynx.HybridLynxKitThreadStrategyTest --tests com.tiktok.sparkling.hybridkit.config.HybridConfigTest --tests com.tiktok.sparkling.hybridkit.HybridKitTest --no-daemon TEST: PATH=/tmp/sparkling-ktlint-bin:$PATH scripts/lint.sh kotlin Co-authored-by: TRAE CLI --- docs/en/apis/sparkling-sdk-android.md | 12 +++++ docs/zh/apis/sparkling-sdk-android.md | 11 ++++ .../com/tiktok/sparkling/SparklingContext.kt | 1 + .../sparkling/SparklingThreadStrategy.kt | 25 +++++++++ .../hybridkit/config/SparklingLynxConfig.kt | 8 +++ .../sparkling/hybridkit/lynx/HybridLynxKit.kt | 16 +++++- .../sparkling/SparklingThreadStrategyTest.kt | 26 ++++++++++ .../sparkling/hybridkit/HybridKitTest.kt | 40 +++++++++++++++ .../hybridkit/config/HybridConfigTest.kt | 4 ++ .../lynx/HybridLynxKitThreadStrategyTest.kt | 51 +++++++++++++++++++ 10 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingThreadStrategy.kt create mode 100644 packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingThreadStrategyTest.kt create mode 100644 packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKitThreadStrategyTest.kt diff --git a/docs/en/apis/sparkling-sdk-android.md b/docs/en/apis/sparkling-sdk-android.md index 189e5a17..f4e4382b 100644 --- a/docs/en/apis/sparkling-sdk-android.md +++ b/docs/en/apis/sparkling-sdk-android.md @@ -21,6 +21,7 @@ val baseInfoConfig = BaseInfoConfig(isDebug = BuildConfig.DEBUG) val lynxConfig = SparklingLynxConfig.build(this) { // optional: add global Lynx behaviors/modules, template provider, etc. // setSharedProcessDensityOverride(2.0f) + setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS) } val hybridConfig = SparklingHybridConfig.build(baseInfoConfig) { setLynxConfig(lynxConfig) @@ -78,6 +79,7 @@ Configuration object passed to both container types. |----------|-------------| | `scheme` | The `hybrid://...` URL to load. | | `sparklingUIProvider` | Implements `SparklingUIProvider` for custom loading/error/toolbar views. | +| `threadStrategy` | Optional per-container `SparklingThreadStrategy`. Overrides the global default. | | `hybridSchemeParam` | Parsed scheme parameters (auto-populated from `scheme`). | | `lynxViewport` | Optional `SparklingLynxViewport(widthPx, heightPx)` fixed viewport in physical pixels. Programmatic configuration overrides parsed scheme dimensions. | | `containerId` | Unique container identifier (auto-generated). | @@ -86,6 +88,16 @@ For advanced hosts that already provide `LynxKitInitParams`, set its `lynxViewpo params take precedence over `SparklingContext.lynxViewport`, which takes precedence over canonical scheme `width` and `height`. All three paths require a complete positive width/height pair. +## Thread strategy + +`SparklingThreadStrategy` provides typed mappings for the Lynx rendering +strategies `ALL_ON_UI`, `MOST_ON_TASM`, `PART_ON_LAYOUT`, and `MULTI_THREADS`. +Set an optional global default with +`SparklingLynxConfig.Builder.setDefaultThreadStrategy(...)`, or override it for +one container with `SparklingContext.threadStrategy`. The per-container value +takes precedence. If neither value is set, Sparkling leaves the Lynx SDK +default unchanged. + ## SparklingUIProvider Interface for customizing container UI. Applies to both full-page and embedded containers. diff --git a/docs/zh/apis/sparkling-sdk-android.md b/docs/zh/apis/sparkling-sdk-android.md index 9b38edcc..351981d2 100644 --- a/docs/zh/apis/sparkling-sdk-android.md +++ b/docs/zh/apis/sparkling-sdk-android.md @@ -21,6 +21,7 @@ val baseInfoConfig = BaseInfoConfig(isDebug = BuildConfig.DEBUG) val lynxConfig = SparklingLynxConfig.build(this) { // 可选:添加全局 Lynx 行为/模块、模板提供者等 // setSharedProcessDensityOverride(2.0f) + setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS) } val hybridConfig = SparklingHybridConfig.build(baseInfoConfig) { setLynxConfig(lynxConfig) @@ -78,6 +79,7 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造** |------|------| | `scheme` | 要加载的 `hybrid://...` URL。 | | `sparklingUIProvider` | 实现 `SparklingUIProvider` 以自定义加载/错误/工具栏视图。 | +| `threadStrategy` | 可选的容器级 `SparklingThreadStrategy`,优先于全局默认值。 | | `hybridSchemeParam` | 解析后的 scheme 参数(从 `scheme` 自动填充)。 | | `lynxViewport` | 可选的 `SparklingLynxViewport(widthPx, heightPx)`,以物理像素指定固定 viewport。程序化配置会覆盖 scheme 中解析的尺寸。 | | `containerId` | 唯一的容器标识符(自动生成)。 | @@ -86,6 +88,15 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造** init params、`SparklingContext.lynxViewport`、canonical scheme 的 `width` 和 `height`。 三种入口都只接受完整的正数宽高组合。 +## 线程策略 + +`SparklingThreadStrategy` 为 Lynx 的 `ALL_ON_UI`、`MOST_ON_TASM`、 +`PART_ON_LAYOUT` 和 `MULTI_THREADS` 渲染线程策略提供类型安全的映射。 +可以通过 `SparklingLynxConfig.Builder.setDefaultThreadStrategy(...)` +设置可选的全局默认值,也可以通过 `SparklingContext.threadStrategy` +为单个容器覆盖。容器级配置优先;两者都未设置时,Sparkling 不改变 +Lynx SDK 的默认策略。 + ## SparklingUIProvider 自定义容器 UI 的接口。适用于全页和嵌入式容器。 diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt index 5e061979..27787d9a 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt @@ -104,4 +104,5 @@ class SparklingContext : HybridContext() { var lifecycleDelegate: SparklingLifecycleDelegate? = null var lynxViewCreatedListener: SparklingLynxViewCreatedListener? = null var lynxViewport: SparklingLynxViewport? = null + var threadStrategy: SparklingThreadStrategy? = null } diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingThreadStrategy.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingThreadStrategy.kt new file mode 100644 index 00000000..08b53c94 --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingThreadStrategy.kt @@ -0,0 +1,25 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling + +import com.lynx.tasm.ThreadStrategyForRendering + +/** + * Controls how Lynx rendering work is distributed across threads. + */ +enum class SparklingThreadStrategy { + ALL_ON_UI, + MOST_ON_TASM, + PART_ON_LAYOUT, + MULTI_THREADS, + ; + + internal fun toLynxThreadStrategy(): ThreadStrategyForRendering = + when (this) { + ALL_ON_UI -> ThreadStrategyForRendering.ALL_ON_UI + MOST_ON_TASM -> ThreadStrategyForRendering.MOST_ON_TASM + PART_ON_LAYOUT -> ThreadStrategyForRendering.PART_ON_LAYOUT + MULTI_THREADS -> ThreadStrategyForRendering.MULTI_THREADS + } +} diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingLynxConfig.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingLynxConfig.kt index abb36b9f..4ab7a64d 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingLynxConfig.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingLynxConfig.kt @@ -9,6 +9,7 @@ import com.lynx.tasm.LynxEnv import com.lynx.tasm.base.LLog import com.lynx.tasm.behavior.Behavior import com.lynx.tasm.provider.AbsTemplateProvider +import com.tiktok.sparkling.SparklingThreadStrategy import com.tiktok.sparkling.hybridkit.lynx.SparklingLynxModuleWrapper class SparklingLynxConfig private constructor( @@ -20,6 +21,7 @@ class SparklingLynxConfig private constructor( val globalModules: MutableMap, val additionInit: LynxEnv.() -> Unit, val sharedProcessDensityOverride: Float?, + val defaultThreadStrategy: SparklingThreadStrategy?, val logLevel: Int = LLog.INFO, ) : ILynxConfig { companion object { @@ -39,6 +41,7 @@ class SparklingLynxConfig private constructor( private val globalModules = mutableMapOf() private var additionInit: LynxEnv.() -> Unit = {} private var sharedProcessDensityOverride: Float? = null + private var defaultThreadStrategy: SparklingThreadStrategy? = null fun setCheckPropsSetter(checkPropsSetter: Boolean) { isCheckPropsSetter = checkPropsSetter @@ -77,6 +80,10 @@ class SparklingLynxConfig private constructor( sharedProcessDensityOverride = density } + fun setDefaultThreadStrategy(threadStrategy: SparklingThreadStrategy?) { + defaultThreadStrategy = threadStrategy + } + fun build() = SparklingLynxConfig( context, @@ -87,6 +94,7 @@ class SparklingLynxConfig private constructor( globalModules, additionInit, sharedProcessDensityOverride, + defaultThreadStrategy, ) } } diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKit.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKit.kt index 51d4f962..35df3ccd 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKit.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKit.kt @@ -15,6 +15,7 @@ import com.lynx.tasm.behavior.Behavior import com.lynx.tasm.behavior.BehaviorBundle import com.lynx.tasm.service.LynxServiceCenter import com.tiktok.sparkling.SparklingContext +import com.tiktok.sparkling.SparklingThreadStrategy import com.tiktok.sparkling.applyLynxViewport import com.tiktok.sparkling.resolveLynxViewport import com.tiktok.sparkling.hybridkit.HybridCommon @@ -99,9 +100,11 @@ object HybridLynxKit { } val viewBuilder = createLynxViewBuilder(lynxConfig) - (hybridContext as? SparklingContext)?.resolveLynxViewport()?.let { viewport -> + val sparklingContext = hybridContext as? SparklingContext + sparklingContext?.resolveLynxViewport()?.let { viewport -> viewBuilder.applyLynxViewport(viewport) } + applyThreadStrategy(viewBuilder, sparklingContext?.threadStrategy, lynxConfig?.defaultThreadStrategy) var lynxViewRef: SimpleLynxKitView? = null (lynxConfig?.templateProvider ?: LynxEnv.inst().templateProvider)?.let { templateProvider -> viewBuilder.setTemplateProvider( @@ -112,7 +115,6 @@ object HybridLynxKit { } val bridge = SparklingBridge() bridge.registerLynxModule(viewBuilder, hybridContext.containerId) - val sparklingContext = hybridContext as? SparklingContext val lynxView = SimpleLynxKitView(context, hybridContext, viewBuilder, kitInitParams, lifeCycle) lynxViewRef = lynxView @@ -132,4 +134,14 @@ object HybridLynxKit { LynxViewBuilder().apply { lynxConfig?.sharedProcessDensityOverride?.let(::setDensity) } + + internal fun applyThreadStrategy( + viewBuilder: LynxViewBuilder, + pageThreadStrategy: SparklingThreadStrategy?, + defaultThreadStrategy: SparklingThreadStrategy?, + ) { + (pageThreadStrategy ?: defaultThreadStrategy)?.let { + viewBuilder.setThreadStrategyForRendering(it.toLynxThreadStrategy()) + } + } } diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingThreadStrategyTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingThreadStrategyTest.kt new file mode 100644 index 00000000..901d1828 --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingThreadStrategyTest.kt @@ -0,0 +1,26 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling + +import com.lynx.tasm.ThreadStrategyForRendering +import org.junit.Assert.assertEquals +import org.junit.Test + +class SparklingThreadStrategyTest { + @Test + fun allStrategiesMapToTheirLynxCounterparts() { + val expected = + mapOf( + SparklingThreadStrategy.ALL_ON_UI to ThreadStrategyForRendering.ALL_ON_UI, + SparklingThreadStrategy.MOST_ON_TASM to ThreadStrategyForRendering.MOST_ON_TASM, + SparklingThreadStrategy.PART_ON_LAYOUT to ThreadStrategyForRendering.PART_ON_LAYOUT, + SparklingThreadStrategy.MULTI_THREADS to ThreadStrategyForRendering.MULTI_THREADS, + ) + + assertEquals(expected.keys, SparklingThreadStrategy.values().toSet()) + expected.forEach { (strategy, lynxStrategy) -> + assertEquals(lynxStrategy, strategy.toLynxThreadStrategy()) + } + } +} diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/HybridKitTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/HybridKitTest.kt index d087a1a0..992e9b9c 100644 --- a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/HybridKitTest.kt +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/HybridKitTest.kt @@ -11,8 +11,10 @@ import android.view.WindowManager import android.view.Display import android.view.accessibility.AccessibilityManager import com.lynx.tasm.LynxView +import com.lynx.tasm.ThreadStrategyForRendering import com.tiktok.sparkling.SparklingContext import com.tiktok.sparkling.SparklingLynxViewCreatedListener +import com.tiktok.sparkling.SparklingThreadStrategy import com.tiktok.sparkling.hybridkit.HybridContext import com.tiktok.sparkling.hybridkit.HybridKit import com.tiktok.sparkling.hybridkit.HybridCommon @@ -24,6 +26,7 @@ import com.tiktok.sparkling.hybridkit.base.HybridLoadSession import com.tiktok.sparkling.hybridkit.base.Theme import com.tiktok.sparkling.hybridkit.config.BaseInfoConfig import com.tiktok.sparkling.hybridkit.config.SparklingHybridConfig +import com.tiktok.sparkling.hybridkit.config.SparklingLynxConfig import com.tiktok.sparkling.hybridkit.scheme.HybridSchemeParam import io.mockk.* import org.json.JSONObject @@ -178,6 +181,43 @@ class HybridKitTest { assertNull(sparklingContext.lynxViewCreatedListener) assertNotNull(HybridKit.createKitView(scheme, sparklingContext, mockContext)) } + + @Test + fun testPageThreadStrategyIsAppliedDuringLynxViewCreation() { + val lynxConfig = + SparklingLynxConfig.build(mockApplication) { + setDefaultThreadStrategy(SparklingThreadStrategy.PART_ON_LAYOUT) + } + HybridCommon.setHybridConfig( + SparklingHybridConfig.build(BaseInfoConfig(isDebug = false)) { + setLynxConfig(lynxConfig) + }, + mockApplication, + ) + val scheme = + HybridSchemeParam( + engineType = HybridKitType.LYNX, + bundle = "https://example.com/main.lynx.bundle", + ) + var strategyAtCreation: ThreadStrategyForRendering? = null + val sparklingContext = + SparklingContext().apply { + hybridSchemeParam = scheme + threadStrategy = SparklingThreadStrategy.MULTI_THREADS + lynxViewCreatedListener = + SparklingLynxViewCreatedListener { lynxView -> + strategyAtCreation = lynxView.threadStrategyForRendering + } + } + + val result = HybridKit.createKitView(scheme, sparklingContext, mockContext) + + assertEquals(ThreadStrategyForRendering.MULTI_THREADS, strategyAtCreation) + assertEquals( + ThreadStrategyForRendering.MULTI_THREADS, + (result as LynxView).threadStrategyForRendering, + ) + } } @RunWith(RobolectricTestRunner::class) diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt index 93b5c312..0759e7fb 100644 --- a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt @@ -2,6 +2,7 @@ package com.tiktok.sparkling.hybridkit.config import android.app.Application import com.lynx.tasm.LynxEnv +import com.tiktok.sparkling.SparklingThreadStrategy import com.tiktok.sparkling.hybridkit.lynx.SparklingLynxModuleWrapper import io.mockk.mockk import org.junit.Assert.assertEquals @@ -108,6 +109,7 @@ class HybridConfigTest { assertTrue(empty.globalModules.isEmpty()) assertNotNull(empty.additionInit) assertNull(empty.sharedProcessDensityOverride) + assertNull(empty.defaultThreadStrategy) val moduleWrapper = mockk(relaxed = true) var initCalled = false @@ -118,11 +120,13 @@ class HybridConfigTest { setTemplateProvider(null) addLynxModules(mapOf("foo" to moduleWrapper)) setAdditionInit { initCalled = true } + setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS) } assertFalse(cfg.isCheckPropsSetter) assertEquals(1, cfg.globalModules.size) assertSame(moduleWrapper, cfg.globalModules["foo"]) + assertEquals(SparklingThreadStrategy.MULTI_THREADS, cfg.defaultThreadStrategy) // exercise additionInit lambda cfg.additionInit.invoke(mockk(relaxed = true)) assertTrue(initCalled) diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKitThreadStrategyTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKitThreadStrategyTest.kt new file mode 100644 index 00000000..669e8d48 --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/lynx/HybridLynxKitThreadStrategyTest.kt @@ -0,0 +1,51 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling.hybridkit.lynx + +import com.lynx.tasm.LynxViewBuilder +import com.lynx.tasm.ThreadStrategyForRendering +import com.tiktok.sparkling.SparklingThreadStrategy +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Test + +class HybridLynxKitThreadStrategyTest { + @Test + fun pageStrategyOverridesGlobalDefault() { + val builder = LynxViewBuilder() + + HybridLynxKit.applyThreadStrategy( + builder, + SparklingThreadStrategy.MULTI_THREADS, + SparklingThreadStrategy.PART_ON_LAYOUT, + ) + + assertEquals(ThreadStrategyForRendering.MULTI_THREADS, builder.threadStrategy) + } + + @Test + fun globalDefaultAppliesWhenPageStrategyIsUnset() { + val builder = LynxViewBuilder() + + HybridLynxKit.applyThreadStrategy( + builder, + null, + SparklingThreadStrategy.MOST_ON_TASM, + ) + + assertEquals(ThreadStrategyForRendering.MOST_ON_TASM, builder.threadStrategy) + } + + @Test + fun lynxDefaultRemainsUntouchedWhenBothStrategiesAreUnset() { + val builder = mockk(relaxed = true) + + HybridLynxKit.applyThreadStrategy(builder, null, null) + + verify(exactly = 0) { + builder.setThreadStrategyForRendering(any()) + } + } +}