作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读

简介: GaiaX 跨端模板引擎,是在阿里文娱内广泛使用的 Native 动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX 开源解读》,带大家看看过去三年 GaiaX 的发展过程。GaiaX 开源地址:https://github.com/alibaba/GaiaX

前言

GaiaX 是由优酷应用中心技术团队研发的一款跨端高性能渲染引擎,目前该方案已经向技术社区开源,其核心目标是解决多端卡片化 UI 组件的研发效能问题。

先看一下 GaiaX 构建的总体链路:

image.png

可以看到,在 GaiaX 中从结构化的模板文件到端渲染,经过了模板解析、节点树构建、视图树构建、表达式运算、扩展交互等步骤。

本文主要将主要从表达式的方案设计、语法树构建以及表达式的跨平台实现这几个方面对表达式运算这个模块进行介绍。

表达式介绍

在 GaiaX 模板的构建过程中,表达式是较为重要的一个模块,其主要的能力是对数据进行取值或运算,并绑定到对应的视图中,它作为逻辑动态化的基础,承接着上层业务对视图数据绑定的具体描述,视图的数据变化和具体表述均由表达式作为纽带。

{
  "data":{
    "gx-expression-value":{
      "value": "$data.text"
    },
    "gx-expression-calculate":{
      "value": "1+2*3%3+$data.num"
    },
    "gx-expression-function":{
      "value": "Size($data.array)"
    }
  }
}

在表达式中,我们支持取值运算、函数计算以及表达式运算的能力,为了实现双端一致性,我们使用了 C++作为表达式的底层开发语言。

技术方案设计

表达式的解析过程

image.png

表达式的解析过程其实就是编译的过程,编译即把通过源语言编写成的源程序转化为目标程序的过程,通常编译的完整流程是把源程序通过词法分析、语法分析、语义分析、中间代码生成等步骤生成计算机可以理解的机器语言,考虑到 GaiaX 的表达式的作用范围,我们只考虑前半部分,即词法分析、语法分析以及语义分析。

我们先通过一个简单的流程图,了解一下表达式的整个解析流程:

词法分析: 词法分析是对表达式输入的字符流进行扫描,根据对应的构词规则对每个词进行划分和分类,将其组成有意义的词素序列。

语法分析: 语法分析是在词法分析的基础上,将词法分析生成的词素组成各类语法短语,并判断表达式在结构上是否正确,最终构建出符合语法规则的语法树以及对应的符号表。

语义分析: 语义分析则是结合语法分析中生成的语法树和符号表,对输入的表达式的语义规则进行分析,判断语义是否符合规则。

在 GaiaX 表达式中,我们使用 LR(1)文法进行语法分析并构建语法树,并将语法分析和语义分析结合起来,即在语法树的构建过程中,判断表达式是否符合语义规则,并在语法树构建完成后返回最终的结果。

语法树的构建 - LR(1)文法

上文说到表达式的解析过程,其实就是一个编译的过程。在 GaiaX 表达式方案中,我们采用了编译原理中经典的 LR(1)文法作为表达式语法树的构建方案。

LR(k)分析方法是 1965 年 Knuth 提出的,括号中的 k 表示向右查看输入事符号的个数。这种方法比起自顶向下的 LL(k)分析方法和自底向上的优先分析方法对文法的限制要少得多,也就是说,对于大多数用无二义性上下文无关文法描述的语有都可以用相定的 LR 分析器进行识别,而且这种方法还具有分析速度快,能准确即时地指出出错位置的特点。

LR(1)文法

LR(1)文法的意思是从左向右扫描,最右推导,往前多看一个字符。

字符 含义
L 从左到右扫描输入串
R 利用最右分析方法来识别句子
(1) 向右展望 1 个字符

在具体介绍 LR(1)分析法之前,我们需要先了解两个基本概念:

名称
含义
推导 一个字符串x通过一个规则变换成另一个字符串y,称为x推导出y,即x->y
规约 与推导相反,语法规则为x->y时,由y规约到x即为规约

GaiaX 表达式语法树构建流程

LR(1)的分析过程是规约的过程,我们以 GaiaX 表达式中的运算语法为例,深入了解 GaiaX 表达式语法树的构建流程。

在 GaiaX 表达式中,四则运算的语法如下:

S  ->  S   +  E        #加
      |S   -  E        #减
      |E
E  ->  E  *  num       #乘
      |E  /  num       #除
      |num

假设我们有表达式:a*b+c

在这个表达式中,我们可以看到,乘法的优先级是要大于加法的,那么在 LR(1)文法中,我们应该怎么去撰写我们的语法,使得乘法的优先级要大于加法呢?

在前文中,我们了解到,推导是从上往下一直推导出整个式子,而 LR(1)文法是规约的过程,规约与推导相反,是从下往上,从底部一直推到到根部节点的过程,根据这点,我们可以了解到,在 LR(1)文法中,语法的推导层级越深,那么就会越先被运算,它的优先级就越高。

所以在 GaiaX 的四则运算语法中,我们让乘除法的层级在加减法之下,从而实现乘除法的优先级大于加减法的优先级。

语法树的构建流程

在确认了具体的语法之后,我们就可以根据语法去构建具体的语法树了,我们先来看一下语法树的构建流程:

image.png

在初始化以及词法分析流程后,得到了分析表和词素序列,接下来我们通过不断把词素入栈并匹配判断是否可以规约,来构建最终的语法树。示例的语法树构建流程如下:

步骤 输入 说明
1 null num*num+num 表达式输入
2 num *num+num 移进
3 num* num+num 移进
4 E +num 移进,num*num 规约为 E
5 E+ num 移进
6 S null 移进,E+num 规约为 S,分析成功

在一步步移进和规约的过程中,当规约到最后的根结点时,便生成了一棵完整的语法树,如下图所示,可以看到,乘法的优先级更高,所以离根节点越远,加法的优先级较乘法低,离根节点越近。

image.png

跨平台实现方案设计

在前文中,我们简要介绍了 GaiaX 表达式各端调用的链路流程,我们了解到,表达式的计算能力可以被 Android、iOS 端所调用,接下来我们来了解一下 GaiaX 表达式跨平台能力的实现。

C++层调用各端取值与方法函数

在跨平台方案中,主要涉及两种情况:

第一种情况是双端调用 C++层的方法,在 iOS 端,我们可以直接通过添加头文件引用直接调用 C++函数;而在 Android 端,我们则需要使用中间层JNI来间接调用 C++层的代码。

第二种情况则是 C++层调用双端实现的方法,相较于端侧调用 C++的代码,由 C++层调用端侧实现的方法会更为复杂,我们先来看一下 C++层调用端侧代码的大致链路:

image.png由流程图我们可以看出,C++层调用 Android 以及 iOS 端的数据获取和函数调用方法,都是需要双端进行实现的,要达到双端实现 C++层能够调用的方法,我们需要先在 C++层定义GXAnalyze类并提供对应的方法虚函数。

class GXAnalyze {
  public:
    //获取数据 $
    virtual long getSourceValue(string valuePath, void* source) = 0;
    //获取方法 Function
    virtual long
    getFunctionValue(string funName, long *paramPointers, int paramsSize, void* source) = 0;
}

C++调用 Android 端方法的实现

在 Android 端中,为了实现 C++提供的虚函数,并返回给 C++层调用,我们创建了GXAnalyzeAndroid类,并在类中创建出对应的取值和方法调用接口。

class GXAnalyzeAndroid {
    // 计算逻辑的扩展
    interface IComputeExtend {
        // Computed value expression
        fun computeValueExpression(valuePath: String, source: Any?): Long
        // Computed function expression
        fun computeFunctionExpression(functionName: String, params: LongArray): Long
    }
    init {
        initNative(this) //初始化表达式并存储GXAnalyzeAndroid对象
    }
}

JNI层,我们实现了两个方法,分别是getSourceValueFromJava取值方法调用以及getFunctionValueFromJava函数方法调用,以getSourceValueFromJava为例,我们通过获取在initNative时存储的GXAnalyzeAndroid对象,通过CallLongMethod调用其实现的取值方法。

# object_in:initNative时存储的GXAnalyzeAndroid对象
static jlong getSourceValueFromJava(string valuePath, jobject source, jobject object_in) {
    JNIEnv *env = getJNIEnv(); //获取环境env变量
    if (env != nullptr) {
        jclass clazz;
        clazz = env->GetObjectClass(object_in);
        jfieldID analyze_fieldID = env->GetFieldID(clazz, "computeExtend",
                                          "Lcom/expressionDir/GXAnalyzeAndroid$IComputeExtend;");
        jobject jobject = env->GetObjectField(object_in, analyze_fieldID);
        jclass analyzeJni = env->GetObjectClass(jobject);
        jmethodID getArrId = env->GetMethodID(analyzeJni, "computeValueExpression",
                                              "(Ljava/lang/String;Ljava/lang/Object;)J");
        jlong res = env->CallLongMethod(jobject, getArrId,
                                        str2jstring(env, valuePath.c_str()),
                                        source);
        env->DeleteLocalRef(clazz);
        env->DeleteLocalRef(jobject);
        env->DeleteLocalRef(analyzeJni);
        return res;
    }
    return 0L;
}

需要注意的是,仅仅实现了JNI调用 Java\Kotlin 的方法还不足以实现在 C++层调用 Android 端的代码,我们在JNI层在实现调用解析表达式的函数的同时,通过继承的方式,实现了GXAnalyze类的虚函数。

class GXJniAnalyze : public GXAnalyze {
public:
    long getSourceValue(string valuePath, void *source) override {
        jobject dataSource = static_cast<jobject>(source);
        return getSourceValueFromJava(valuePath, dataSource, globalSelf);
    }
};
extern "C"
JNIEXPORT jlong JNICALL
Java_com_alibaba_gaiax_analyze_GXAnalyze_getResultNative(JNIEnv *env, jobject thiz, jobject self,
                                                         jstring expression, jobject data) {
    GXJniAnalyze *jAnalyze = getJniAnalyze(env, self);
    long res = jAnalyze->getExpressionResult(jstring2str(env, expression), data);
    return (jlong) (res);
}

这样,我们在调用表达式解析的方法时,实际上调用的是继承自GXAnalyze并实现虚函数后的GXJniAnalyze里的getExpressionResult方法,在 C++层,我们在调用取值或函数方法时,只需要使用this->getSourceValue(string,void*)就能调用在端侧实现了的虚函数,根据端侧的逻辑返回我们需要的结果。

C++调用 iOS 端方法的实现

在 iOS 端中,为了实现 C++提供的虚函数,并返回给 C++层调用,我们首先实现了GXAnalyzeBridge类,并在其中对取值以及函数调用方法进行了实现。

@interface GXAnalyzeBridge : NSObject
- (long)getFunctionValue:(NSString *)funName paramPointers:(long *)paramPointers paramsSize:(int)paramsSize;
- (long)getSourceValue:(NSString *)valuePath source:(id)source;
@end

紧接着,我们创建出了GXAnalyze的继承类GXAnalyzeImpl,在类中,我们重写了函数以及取值调用方法,在方法中通过调用中间层GXAnalyzeBridge的具体实现方法,达到取值以及函数能力的实现。

class GXAnalyzeImpl: public GXAnalyze {
public:
//解析取值
long getSourceValue(string valuePath, void* source);
//解析方法
long getFunctionValue(string funName, long *paramPointers, int paramsSize, string source);
};

最终,在 C++层,我们在调用表达式的取值或函数方法时,实际上调用的是在 iOS 端已经实现了的继承类GXAnalyzeImpl里对应的方法。

统一数据类型

在 GaiaX 表达式中,会有端侧调用 C++层的方法和 C++层调用端侧这两种情况,但是各端的数据类型是不一致的,而且在表达式的计算过程中,每个数据的类型也是不一致的,为了能够实现一套数据类型,我们实现了GXValue数据类,在各端可以通过调用 GXValue 的多个数据创建方法创建对应类型的数据,作为结果返回即可。

class GXValue {
public:
    int64_t tag;
    int32_t int32;  //Bool 1,0
    float float64;  //Float
    int64_t intNum;    //long
    void *ptr;      //Array,Map
    char *str;      //String
};

成果展示

image.png

性能及稳定性保障

质量保障

由于采用了跨平台技术方案,因此稳定性保障是项目的重中之重。在项目交付过程中,针对不同复杂度、取值类型进行了充分的单元测试用例设计,尽可能覆盖线上可能出现的各种边界异常情况。


image.png

性能分析

由于技术方案底层由 C++实现,因此基础性能表现是有保障的。

计算类型 表达式 耗时/次
单值 10000 0.017ms
取值 $data.xxx 0.024ms
取数据源 $$ 0.019ms
数值运算 10%5 0.029ms
三元表达式 true ? 1 : 2 0.048ms
函数计算 Size('1000') 0.029ms
复杂嵌套表达式 $data.b % 5 > 2 ? $data.b % 5 : 1 0.060ms

表 - Android 端真机测试性能结果

总结

GaiaX 表达式充分吸收了编译原理的精髓,通过跨平台技术方案从根本上保证了多端的一致性问题,为 GaiaX 在移动平台的落地提供了较高的可扩展性。目前来看,该方案在性能方面还有一定的优化空间,如规约算法、词法分析、数据结构等方面的优化,这也是我们后续重要的发力点。

跨端表达式方案目前已经在 GaiaX 开源项目(https://github.com/alibaba/GaiaX)中发布,非常欢迎广大技术爱好者一起探讨交流。

相关文章
|
6天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1636 116
|
7天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1114 5
|
13天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1954 9
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
7天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
539 112
|
19天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
2756 4
|
11天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
730 111
|
21天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2653 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
7天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
5天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)

热门文章

最新文章