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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。

前言

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

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

图 - GaiaX构建总体链路

可以看到,在 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++作为表达式的底层开发语言。



技术方案设计

表达式的解析过程

图 - 表达式各端解析流程

表达式的解析过程其实就是编译的过程,编译即把通过源语言编写成的源程序转化为目标程序的过程,通常编译的完整流程是把源程序通过词法分析、语法分析、语义分析、中间代码生成等步骤生成计算机可以理解的机器语言,考虑到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的四则运算语法中,我们让乘除法的层级在加减法之下,从而实现乘除法的优先级大于加减法的优先级。

语法树的构建流程

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


图 - 语法树构建流程

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

示例的语法树构建流程如下:

步骤
输入
说明
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,分析成功


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

图 - 示例语法树


跨平台实现方案设计

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

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

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

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

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

图 - 双端取值和函数方法的调用与实现

由流程图我们可以看出,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 JNICALLJava_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};


成果展示

图 - GaiaStudio表达式编辑&运算


性能及稳定性保障

质量保障

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

图 - 单元测试用例

性能分析

由于技术方案底层由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在移动平台的落地提供了较高的可扩展性。目前来看,该方案在性能方面还有一定的优化空间,如规约算法、词法分析、数据结构等方面的优化,这也是我们后续重要的发力点。

相关文章
|
4月前
|
机器学习/深度学习 分布式计算 前端开发
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
|
6月前
|
SQL 存储 缓存
第四章 逻辑架构(1)
第四章 逻辑架构
42 1
|
6月前
|
SQL 存储 缓存
第四章 逻辑架构(2)
第四章 逻辑架构
38 1
|
7月前
【突破常规:让函数规范成为注目的亮点】(下)
【突破常规:让函数规范成为注目的亮点】
|
7月前
【突破常规:让函数规范成为注目的亮点】(上)
【突破常规:让函数规范成为注目的亮点】
|
JSON 数据可视化 JavaScript
OneCode 基于“真实代码”代码的建模设计,无缝整合二次开发
在很多优秀的低代码平台中都支持了本地代码导出的设计,方便开发者二次集成,但能够导出的前提是已经通过低代码平台进行了初步的数据建模,界面绘制等基础性的操作。这些导出的代码虽然很大程度上减轻了开发者的代码量,但在项目的迭代过程中,遇到数据或需求变更。这些代码就又会成为开发者巨大的负担,重新由低代码平台建模会产生代码上的冲突无法解决,而重新用code编写这一步代码则又面临手工代码与“机器代码”的整合问题。而更为致命的问题是项目上线后,当直接用户希望通过低代码工具进行维护系统时更是“闪崩”。 本文将结合OneCode的底层编译原理来讲解 OneCode基于真实代码的建模解决方案。
LiteFlow学习三之业务编排处理之外
LiteFlow学习三之业务编排处理之外
336 0
|
人工智能 大数据 程序员
一文看懂开源图化框架中的循环设计逻辑!
相信大家在日常工作中,已经精通各种循环逻辑的实现。就拿我来说吧,多年的工作经验,已经让我可以熟练的使用 C++,Python,英语等多种语言,循环多次输出“hello word”。不过大家有没有想过一个这样的问题:如何在一个有向无环图(Directed Acyclic Graph,简称dag)中实现循环呢?
766 0
一文看懂开源图化框架中的循环设计逻辑!
|
人工智能 数据可视化 前端开发
如何用smardaten无代码平台进行复杂逻辑编排?
如何用smardaten无代码平台进行复杂逻辑编排?
|
存储 自然语言处理 算法
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读
GaiaX 跨端模板引擎,是在阿里文娱内广泛使用的 Native 动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX 开源解读》,带大家看看过去三年 GaiaX 的发展过程。 GaiaX 开源地址:https://github.com/alibaba/GaiaX
433 0
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读

热门文章

最新文章