1 of 63

Compose 是如何实现跨平台的?

霍丙乾 bennyhuo

2 of 63

讲者简介

  • 霍丙乾 bennyhuo,腾讯视频客户端高级工程师
  • Google 开发者专家(GDE,Kotlin 方向)
  • Bilibili/公众号/YouTube/抖音/小红书霍丙乾 bennyhuo
  • 深入理解 Kotlin 协程》作者(机械工业出版社,2020.6)
  • 深入实践 Kotlin 元编》作者(机械工业出版社,2023.9)

3 of 63

分享经历

2017.11

Android 技术大会

2018.11

JetBrains 北京开发者大会

2020.5

GDG Android 11 Meetup

2020.10 / 11

GDG DevFest / 全球移动开发者峰会

2021.7

GDG 社区说

2021.11 / 12

GDG DevFest / Kotlin 中文开发者大会

2022.9

GDG 社区说

2022.10

GDG DevFest

2023.4

GDG 社区说

2023.5

KUG 北京 KotlinConf Global

2023.6

Java 核心技术大会

2023.10

JetBrains 码上道

2023.11

GDG DevFest

2023.12

机械工业出版社

2024.2

GDG 社区说

4 of 63

Compose 的发展历程

5 of 63

2019.5 Google IO:首次公开预览

2020.8 Alpha

2021.2 Beta

2021.7 正式发布

2021.8 1.0 alpha

Jetpack Compose

Compose Multiplatform

2023.4 1.4.0 iOS alpha | Web Experimental

2024.5 1.6.10 iOS beta | Web alpha

2020.11 M1

6 of 63

Compose 的整体架构

7 of 63

Compose 的整体架构

Compiler

Runtime

8 of 63

Compose 的整体架构

Compiler

Runtime

语法检查

函数变换

符号导出

组件逻辑

组件渲染

原生混合排版

9 of 63

Compose 的整体架构

Compiler

Runtime

语法检查

函数变换

符号导出

组件逻辑

组件渲染

原生混合排版

10 of 63

Kotlin 跨平台的基本思路

11 of 63

Kotlin 的平台支持

Windows

Linux

macOS

Kotlin Native

WASM

iOS

...

Android

Kotlin JVM

Kotlin JS

Browser

Node.js

Java vm

Android

Browser

Kotlin Compiler

12 of 63

Kotlin 的编译产物

Kotlin Native

WASM

Kotlin JVM

Kotlin JS

JavaScript

WebAssembly

JVM Bytecode

Native Binary

C/C++/...

Java

JavaScript/Rust/...

TypeScript

13 of 63

Kotlin 的源码结构

Kotlin Common

Kotlin-Jvm

Kotlin-Native

Java

C/Objective-C

expect

expect

actual

actual

14 of 63

Kotlin 的源码结构 示例

expect fun getCurrentTimeMilliseconds(): Long

actual fun getCurrentTimeMilliseconds(): Long {

return System.currentTimeMillis()

}

actual fun getCurrentTimeMilliseconds(): Long {

return (NSDate().timeIntervalSince1970 * 1000).toLong()

}

15 of 63

Compose 的跨平台实现

16 of 63

Compose 编译时的跨平台处理

17 of 63

Composable 函数

@JvmInline�value class User(val name: String)��@Composable�fun Greeting(user: User) {� Text(user.name)�}

18 of 63

Composable 函数

19 of 63

参数可变

class KlibAssignableParamTransformer(...)

: AbstractComposeLowering(...), ModuleLoweringPass {� ...�� override fun visitFunction(declaration: IrFunction): IrStatement {� val assignableParams = declaration.valueParameters.filter { it.isAssignable }� val variables = assignableParams.map {� val variable = IrVariableImpl(

...� name = it.name,� type = it.type,� isVar = true,

...� )� variable.parent = declaration� variable.initializer = IrGetValueImpl(..., it.symbol)� variable� }� ...� return super.visitFunction(declaration)� }�}�

20 of 63

参数可变

21 of 63

内联类型

if (type.isInlineClassType()) {� if (context.platform.isJvm()) {� return coerceInlineClasses(this, type, type.unboxInlineClass()).unboxValueIfInline()� } else {� val primaryValueParameter = klass.primaryConstructor?.valueParameters?.get(0)� ...� val fieldGetter = klass.getPropertyGetter(primaryValueParameter!!.name.identifier)� ?: error("Expected a getter")� return irCall(symbol = fieldGetter, dispatchReceiver = this).unboxValueIfInline()� }�}

22 of 63

内联类型

23 of 63

$dirty 变量

changedParam.irCopyToTemporary(� // LLVM validation doesn't allow us to have val here.� isVar = !context.platform.isJvm() && !context.platform.isJs(),� nameHint = "\$dirty",� exactName = true�)

val $dirty = $changed�if ($changed and 0b1110 == 0) {� $dirty = $dirty or if ($composer.changed(name)) 0b0100 else 0b0010�}

24 of 63

$dirty 变量

25 of 63

stability 字段

class Greeting {� private val platform = getPlatform()�� fun greet(): String {� return "Hello, ${platform.name}!"� }�}

26 of 63

stability 字段

27 of 63

stability 字段

private fun IrClass.makeStabilityFieldNonJvm(): IrField {� val stabilityFieldName = this.uniqueStabilityFieldName()� val fieldParent = this.getPackageFragment()�� return context.irFactory.buildField {� ...}.also { stabilityField ->� stabilityField.parent = fieldParent� makeStabilityProp(stabilityField, fieldParent)� }�}

private fun IrClass.makeStabilityFieldJvm(): IrField {� return context.irFactory.buildField {� ...}.also { stabilityField ->� stabilityField.parent = this@makeStabilityFieldJvm� }�}

28 of 63

klib 写入稳定性推导注解

class ClassStabilityFieldSerializationPlugin(...) : DescriptorSerializerPlugin {� ...� override fun afterClass(...) {� ...�� if (proto.flags and hasAnnotationFlag == 0) {� proto.flags = proto.flags or hasAnnotationFlag� }�� val parametersValue = classStabilityInferredCollection?.getParametersValue(descriptor)� if (parametersValue != null) {� proto.addExtension(� KlibMetadataSerializerProtocol.classAnnotation,� createAnnotationProto(extension, parametersValue)� )� }� }�}

29 of 63

Compose 运行时的跨平台处理

30 of 63

组件渲染

Hardware

Canvas

OwnedLayer

Compose Node

@Composable fun

31 of 63

组件渲染 - Android

Canvas

OwnedLayer

AndroidCanvas

GraphicsLayerOwnerLayer

RenderNodeLayer

ViewLayer

AndroidComposeView

32 of 63

组件渲染 - iOS

Canvas

OwnedLayer

SkiaBackedCanvas

SkiaLayer

Skia

CAMetalLayer / Metal

33 of 63

混合排版

Native Canvas

Compose Canvas

34 of 63

混合排版 - Android

Native Canvas

Canvas

35 of 63

混合排版 – Android – 示例

Native 控件

Compose 控件

36 of 63

混合排版 – Android – 示例

Native 控件

Compose 控件

37 of 63

混合排版 – Android – 示例

Native Canvas

Native 控件

Compose 控件

38 of 63

混合排版 – Android

val layoutNode: LayoutNode = run {� // Prepare layout node that proxies measure and layout passes to the View.� val layoutNode = LayoutNode()�val coreModifier = Modifier� .nestedScroll(NoOpScrollConnection, dispatcher)� .semantics(true) {}� .pointerInteropFilter(this)� .drawBehind {� drawIntoCanvas { canvas ->� (layoutNode.owner as? AndroidComposeView)?.drawAndroidView(...)� }� }� .onGloballyPositioned {� layoutAccordingTo(layoutNode)� }

39 of 63

混合排版 – Android

val layoutNode: LayoutNode = run {� // Prepare layout node that proxies measure and layout passes to the View.� val layoutNode = LayoutNode()�val coreModifier = Modifier� .nestedScroll(NoOpScrollConnection, dispatcher)� .semantics(true) {}� .pointerInteropFilter(this)� .drawBehind {� drawIntoCanvas { canvas ->� (layoutNode.owner as? AndroidComposeView)?.drawAndroidView(...)� }� }� .onGloballyPositioned {� layoutAccordingTo(layoutNode)� }

事件

绘制

布局

40 of 63

混合排版 - iOS

Native Canvas

(UIView)

Compose Canvas

(UIView/CAMetalLayer)

41 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

42 of 63

混合排版 – iOS – 示例

Compose Canvas

(UIView/CAMetalLayer)

Native Canvas

(UIView)

Native 控件

Compose 控件

43 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

Compose Canvas

(UIView/CAMetalLayer)

UIView

44 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

45 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

46 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

47 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

48 of 63

混合排版 – iOS – 示例

Compose Canvas

(UIView/CAMetalLayer)

Native 控件

Compose 控件

49 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

50 of 63

混合排版 – iOS – 示例

Compose Canvas

(UIView/CAMetalLayer)

Native 控件

Compose 控件

51 of 63

混合排版 – iOS – 示例

Compose Canvas

(UIView/CAMetalLayer)

Native 控件

Compose 控件

52 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

53 of 63

混合排版 – iOS – 示例

Native 控件

Compose 控件

54 of 63

混合排版 – iOS

EmptyLayout(� modifier.onGloballyPositioned { coordinates ->� ...� interopContext.deferAction {� embeddedInteropComponent.wrappingView.setFrame(...)� }� ...� interopContext.deferAction { onResize(...) }� }.drawBehind {� drawRect(Color.Transparent, blendMode = BlendMode.Clear)� }.trackUIKitInterop(embeddedInteropComponent.wrappingView).let {� if (interactive) it.then(InteropViewCatchPointerModifier()) else it� }

...�)

事件响应区域

绘制透明区域

布局

55 of 63

混合排版 – iOS – 缺陷

Native 控件

Compose 控件

56 of 63

混合排版 – iOS – 缺陷

Native 控件

Compose 控件

57 of 63

混合排版 – iOS – 缺陷

Native 控件

Compose 控件

不支持 Native 控件有透明区域!

58 of 63

其他问题

  • 事件分发:注意原生控件的层级以及与 Compose 事件的分发机制
  • 嵌套滑动:要将 Compose 与原生滑动事件的处理联动起来
  • Z Index:原生控件的 Z Index 要按照 Compose 的布局设置
  • 控件裁剪:原生控件需要按照 Compose 父组件进行裁剪
  • 同步绘制:确保 Compose 内容绘制与原生控件的更新在同一帧内

59 of 63

参考资料

60 of 63

敬请期待

61 of 63

关注我

62 of 63

😃 🧐

63 of 63

谢谢大家