跟着官网学 Lynx 之 把 Lynx 在 Android 跑起来

4,820 阅读6分钟

前言

早上了解到字节跳动开源了一套新的跨端渲染框架 - Lynx,对 Lynx 开源框架贡献的各位大佬们感觉到满满的敬佩,开始对 Lynx 开源框架进行了阅读,计划从 0 到 1,使用 Lynx 搭建一个完整的 App,来体验一下开源的力量!

Lynx 是什么?

官网介绍:lynxjs.org/zh/blog/lyn…

这是一套帮助 Web 开发者复用现有经验,通过一份代码同时构建移动端原生界面与 Web 端界面的技术方案。Lynx 专为多样化、富交互的场景打造,得以在 TikTok 这样量级的应用中支撑那些生动且吸引人的界面。它有着高性能、多功能的渲染引擎、性能优先的双线程 UI 编程范式等诸多特性。

总结如下:

  1. 高性能:Lynx 通过首屏直出、轻量化的 JS 逻辑设计以及多线程渲染等技术,显著提升了页面加载速度和交互流畅性。
  2. 跨平台:支持 iOS 和 Android,开发者可以使用一份代码同时构建移动端和 Web 端界面,简化开发流程。
  3. 轻量化:相比其他框架,Lynx 的包体积小,接入成本低,适合多种场景(如卡片、嵌入、全页等)。
  4. 高效通信:通过优化的 JS Binding 技术,Lynx 实现了高效的 JavaScript 与 Native 通信,减少了性能开销。
  5. 易用性:基于前端 DSL,开发者可以快速上手,降低了学习成本。
  6. 场景通用:适用于多种业务场景,包括卡片模式、半屏页、独立 App 等,已在字节跳动多个产品中落地。

跟着官网学 Lynx 之 把 Lynx 在 Android 跑起来

接下来,我们将会跟着官网学习,把 Lynx 完整在 Android 环境中运行起来,并且针对 Lynx 的 API,做一些尝试,最终完成一个完全由 Lynx 框架驱动的 todo-list APP。

第一步:导入依赖

1. 核心模块依赖

implementation("org.lynxsdk.lynx:lynx:3.2.0-rc.0")
implementation("org.lynxsdk.lynx:lynx-jssdk:3.2.0-rc.0")
implementation("org.lynxsdk.lynx:lynx-trace:3.2.0-rc.0")
implementation("org.lynxsdk.lynx:primjs:2.11.1-rc.0")

这些依赖就是 Lynx Engine 核心能力,其中包含了解析 Bundle、样式解析、排版以及渲染视图等基础能力,以及 Lynx 页面依赖的 JavaScript 运行时基础代码。

2. Lynx Service

Lynx Service 主要是做一些跟端能力相关的支持,例如 LynxImageService 默认是使用 Fresco 图片库实现,在没有集成 Fresco 组件的应用上则可以依赖其他图片库,比如 Glide 来实现。Lynx 提供了标准的原生 Image、Log、Http 服务的能力,接入方可以快速接入并使用;

这意味着,用户可以直接使用自己的网络库或者框架来实现这些Service的能力,达到复用基建的效果。

需要新增以下依赖:


// integrating image-service
implementation("org.lynxsdk.lynx:lynx-service-image:3.2.0-rc.0")

// image-service dependencies, if not added, images cannot be loaded; if the host APP needs to use other image libraries, you can customize the image-service and remove this dependency
implementation("com.facebook.fresco:fresco:2.3.0")
implementation("com.facebook.fresco:animated-gif:2.3.0")
implementation("com.facebook.fresco:animated-webp:2.3.0")
implementation("com.facebook.fresco:webpsupport:2.3.0")
implementation("com.facebook.fresco:animated-base:2.3.0")

// integrating log-service
implementation("org.lynxsdk.lynx:lynx-service-log:3.2.0-rc.0")

// integrating http-service
implementation("org.lynxsdk.lynx:lynx-service-http:3.2.0-rc.0")

implementation("com.squareup.okhttp3:okhttp:4.9.0")
  1. 配置混淆规则

Lynx 要求配置以下混淆规则,这证明 Lynx 在运行环境中,有对以下类有反射或者一些 JNI 的依赖。

# LYNX START
# use @Keep to annotate retained classes.
-dontwarn android.support.annotation.Keep
-keep @android.support.annotation.Keep class **
-keep @android.support.annotation.Keep class ** {
    @android.support.annotation.Keep <fields>;
    @android.support.annotation.Keep <methods>;
}
-dontwarn androidx.annotation.Keep
-keep @androidx.annotation.Keep class **
-keep @androidx.annotation.Keep class ** {
    @androidx.annotation.Keep <fields>;
    @androidx.annotation.Keep <methods>;
}

# native method call
-keepclasseswithmembers,includedescriptorclasses class * {
    native <methods>;
}
-keepclasseswithmembers class * {
    @com.lynx.tasm.base.CalledByNative <methods>;
}

# to customize a module, you need to keep the class name and the method annotated as LynxMethod.
-keepclasseswithmembers class * {
    @com.lynx.jsbridge.LynxMethod <methods>;
}

-keepclassmembers class *  {
    @com.lynx.tasm.behavior.LynxProp <methods>;
    @com.lynx.tasm.behavior.LynxPropGroup <methods>;
    @com.lynx.tasm.behavior.LynxUIMethod <methods>;
}

-keepclassmembers class com.lynx.tasm.behavior.ui.UIGroup {
    public boolean needCustomLayout();
}

# in case R8 compiler may remove mLoader in bytecode.
# as mLoader is not used in java and passed as a WeakRef in JNI.
-keepclassmembers class com.lynx.tasm.LynxTemplateRender {
    private com.lynx.tasm.core.LynxResourceLoader mLoader;
}

# the automatically generated setter classes use the class names of LynxBaseUI and ShadowNode and their subclasses.
-keep class com.lynx.tasm.behavior.ui.LynxBaseUI
-keep class com.lynx.tasm.behavior.shadow.ShadowNode
-keep class com.lynx.jsbridge.LynxModule { *; }
-keep class * extends com.lynx.tasm.behavior.ui.LynxBaseUI
-keep class * extends com.lynx.tasm.behavior.shadow.ShadowNode
-keep class * extends com.lynx.jsbridge.LynxModule { *; }
-keep class * extends com.lynx.jsbridge.LynxContextModule
-keep class * implements com.lynx.tasm.behavior.utils.Settable
-keep class * implements com.lynx.tasm.behavior.utils.LynxUISetter
-keep class * implements com.lynx.tasm.behavior.utils.LynxUIMethodInvoker
-keep class com.lynx.tasm.rendernode.compat.**{
    *;
}
-keep class com.lynx.tasm.rendernode.compat.RenderNodeFactory{
    *;
}
# LYNX END

完成以上配置之后,我们接下来就开始正式编写代码部分了!

第二步:初始化 Lynx 环境

4. 初始化 LynxEngine、LynxService

如果 APP 的首屏就需要 Lynx 的页面支持的话,Lynx 团队推荐是在 App 启动的过程中,就把 Lynx 的环境初始化出来,所以我们需要在自定义 Application 中,将 LynxEngine、LynxService 进行初始化。

Lynx 官网提醒我们,需要主动将 LynxService 进行注入。

代码如下:

import android.app.Application
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.memory.PoolConfig
import com.facebook.imagepipeline.memory.PoolFactory
import com.lynx.service.http.LynxHttpService
import com.lynx.service.image.LynxImageService
import com.lynx.service.log.LynxLogService
import com.lynx.tasm.service.LynxServiceCenter

class YourApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        initLynxService()
        initLynxEnv()
    }

    private fun initLynxService() {
        // init Fresco which is needed by LynxImageService
        val factory = PoolFactory(PoolConfig.newBuilder().build())
        val builder = ImagePipelineConfig.newBuilder(applicationContext).setPoolFactory(factory)
        Fresco.initialize(applicationContext, builder.build())

        LynxServiceCenter.inst().registerService(LynxImageService.getInstance())
        LynxServiceCenter.inst().registerService(LynxLogService)
        LynxServiceCenter.inst().registerService(LynxHttpService)
    }
    
    private fun initLynxEnv() {
        LynxEnv.inst().init(
            this,//应用生命周期上下文对象,即 ApplicationContext;
            null,//本地 so 文件加载器,默认可传 null,即使用系统默认加载器;
           null,//全局的 AppBundle 加载器,默认可传 null;
            null//自定义组件列表,默认可传 null,如有定制化需求可参考自定义组件;
        )
    }
}

为什么要 初始化 Service 呢?

这里可能会有大家会有疑问:为什么需要先 初始化 Service?

根据上文,我们可以看到 Service 主要是将一些动作交给了 APP 端侧进行托管,例如 Log 等能力,我们查看源码,可以发现,在 LynxEnv 初始化的过程中,会使用到日志的能力,最后会分发到端侧注册的 LogService,所以,如果没有提前初始化的话,就会出现 init 过程 Log 丢失的问题,具体源码可见:service.logByPlatform(level, tag, msg);。所以,Lynx 官网虽然把这两部分分为两步走,但是从我的角度,则可以把这个依赖关系暴露出来,这样子可以做到更好的了解 Lynx 环境的初始化过程。

第三步:使用 LynxView

5. 创建 Lynx Bundle 资源加载器

Lynx 编译产物称为 Bundle 资源。Lynx Engine 自身并没有集成下载资源的能力,因此需要宿主应用来提供 AbsTemplateProvider 的具体实现,并在构造 LynxView 时注入,Lynx 会采用注入的资源加载器来获取真实的 Bundle 内容。

接口如下:

public abstract class AbsTemplateProvider {
  public interface Callback {
    void onSuccess(byte[] template);

    void onFailed(String msg);
  }

  public abstract void loadTemplate(@NonNull String url, Callback callback);

  public void loadTemplate(@NonNull String url, Callback callback, LynxContext context) {
    loadTemplate(url, callback);
  }
}

我们需要实现的是抽象方法 loadTemplate(@NonNull String url, Callback callback); 这里 Lynx 设计者提供了很宽松的设计,只需要接入者将正确的 Lynx 资源数据转换成字节流返回给 Lynx Engine,就可以帮助我们渲染出来Lynx页面,所以我们可以通过接入网络库加载线上数据或者文件读取本地数据等方案来实现这个接口。

官网提供的是本地 assets 文件读取,那我们就直接 ctrl + c\ ctrl + v 即可!

首先,下载 Lynx Demo 产物:unpkg.com/@lynx-examp…,放入assets文件夹中:

目录结构如下:

app
└── src
    └── main
        ├── java
        ├── res
        └── assets
            └── main.lynx.bundle

实现加载器:

import android.content.Context
import com.lynx.tasm.provider.AbsTemplateProvider
import java.io.ByteArrayOutputStream
import java.io.IOException

class DemoTemplateProvider(context: Context) : AbsTemplateProvider() {

    private var mContext: Context = context.applicationContext

    override fun loadTemplate(uri: String, callback: Callback) {
        Thread {
            try {
                mContext.assets.open(uri).use { inputStream ->
                    ByteArrayOutputStream().use { byteArrayOutputStream ->
                        val buffer = ByteArray(1024)
                        var length: Int
                        while ((inputStream.read(buffer).also { length = it }) != -1) {
                            byteArrayOutputStream.write(buffer, 0, length)
                        }
                        callback.onSuccess(byteArrayOutputStream.toByteArray())
                    }
                }
            } catch (e: IOException) {
                callback.onFailed(e.message)
            }
        }.start()
    }
}

6. 引入 LynxView 并加载

我们只需要通过 LynxViewBuilder 创建一个 LynxView,并且将 LynxView 挂载在视图上即可。

通过源码可以看到 LynxView 底层是基于 View,源码地址:LynxView extends UIBodyViewUIBodyView extends FrameLayout,FrameLayout 则继承于 ViewGroup,继而继承于 View。

所以我们可以直接创建 LynxView,然后完成挂载,并且将 bundle 资源注入 Lynx 中:

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val lynxView = buildLynxView()
        setContentView(lynxView)
        val uri = "main.lynx.bundle"
        lynxView.renderTemplateUrl(uri, "")
    }
    private fun buildLynxView(): LynxView {
        val viewBuilder: LynxViewBuilder = LynxViewBuilder()
        viewBuilder.setTemplateProvider(DemoTemplateProvider(this))
        return viewBuilder.build(this)
    }
}

第四步:运行 APP

至此,我们完成了将 Lynx 产出在 Android 中运行起来的全过程了。

20250306111052_rec_.gif

下一步

下一步我们将会继续了解 Lynx 的前端构建的内容,并且完成 Lynx 页面的编写,实现我们整个 todo-list APP的构建!

项目代码

Android Demo:github.com/shuhaoLIN/l…

Lynx todo-list:github.com/shuhaoLIN/l…