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

news2024/10/1 9:16:30

GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。

前言

GaiaX【https://github.com/alibaba/GaiaX】是由优酷应用中心技术团队研发的一款跨端高性能渲染引擎,目前该方案已经向技术社区开源,其核心目标是解决多端卡片化UI组件的研发效能问题。
先看一下GaiaX构建的总体链路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vZwRQ7gE-1676431808555)(/img/bVc6nzq)]

图 - 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的四则运算语法中,我们让乘除法的层级在加减法之下,从而实现乘除法的优先级大于加减法的优先级。

语法树的构建流程

在确认了具体的语法之后,我们就可以根据语法去构建具体的语法树了,我们先来看一下语法树的构建流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mLZeGZcM-1676431808557)(/img/bVc6nAj)]
图 - 语法树构建流程
在初始化以及词法分析流程后,得到了分析表和词素序列,接下来我们通过不断把词素入栈并匹配判断是否可以规约,来构建最终的语法树。
示例的语法树构建流程如下:

步骤输入说明
1nullnum*num+num表达式输入
2num*num+num移进
3num*num+num移进
4E+num移进,num*num规约为E
5E+num移进
6Snull移进,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 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
};

成果展示

在这里插入图片描述

图 - GaiaStudio表达式编辑&运算

性能及稳定性保障

质量保障

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

图 - 单元测试用例

性能分析

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

计算类型表达式耗时/次
单值100000.017ms
取值$data.xxx0.024ms
取数据源$$0.019ms
数值运算10%50.029ms
三元表达式true ? 1 : 20.048ms
函数计算Size(‘1000’)0.029ms
复杂嵌套表达式$data.b % 5 > 2 ? $data.b % 5 : 10.060ms

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

总结

GaiaX表达式充分吸收了编译原理的精髓,通过跨平台技术方案从根本上保证了多端的一致性问题,为GaiaX在移动平台的落地提供了较高的可扩展性。目前来看,该方案在性能方面还有一定的优化空间,如规约算法、词法分析、数据结构等方面的优化,这也是我们后续重要的发力点。
跨端表达式方案目前已经在GaiaX开源项目(https://github.com/alibaba/GaiaX)中发布,非常欢迎广大技术爱好者一起探讨交流。

GaiaX开源解读系列文章

系列文章持续更新中,请各位拭目以待
• 《GaiaX开源解读 | 基于优酷业务特色的跨平台技术》
• 《GaiaX开源解读 | 跨端动态化模板引擎详解,看完你也能写一个》
• 《GaiaX开源解读 | 给Stretch(Rust编写的Flexbox布局引擎)新增特性,我掉了好多头发》
• 《GaiaX开源解读 | 表达式作为逻辑动态化的基础,我们是如何设计的》
• 《GaiaX开源解读 | 向经典致敬 ReactNaitve与GaiaX渲染核心技术分析》
• 《GaiaX开源解读 | 为了保障双端一致性,我们做了哪些努力》
• 《GaiaX开源解读 | 一条龙的模板研发体系,你不来看看么》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/348214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

详解ArrayList

目录 1.数据结构 2.初始化 2.1.默认构造 2.2.带参构造 3.扩容 3.1.判断需要多少容量 3.2.判断是否需要扩容 3.3.扩容 4.遍历 5.拷贝 6.序列化 JDK版本&#xff1a;JDK8 1.数据结构 底层使用Object类型的数组实现&#xff0c;线程不安全&#xff0c;添加元素时如果内存…

【遇见青山】基于Redis的Feed流实现案例

【遇见青山】基于Redis的Feed流实现案例1.关注推送2.具体代码实现1.关注推送 关注推送也叫做Feed流&#xff0c;直译为投喂。为用户持续的提供"沉浸式”的体验&#xff0c;通过无限下拉刷新获取新的信息。 Feed流产品有两种常见模式&#xff1a; 这里我们实现基本的TimeL…

Python 爬虫工程师面试经验分享,金三银四

&#x1f643; 作为一个 Python 爬虫工程师&#xff0c;我可以分享一些我在面试中的经验和建议。 首先一点是在面试中要表现自信、友好、乐于合作&#xff0c;同时对公司的业务和文化也要有一定的了解和兴趣&#xff0c;这些也是公司在招聘中看重的因素。 文章目录&#x1f55b…

第06章_MySQL多表查询

第06章_多表查询 讲师&#xff1a;尚硅谷-宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a;http://www.atguigu.com 多表查询&#xff0c;也称为关联查询&#xff0c;指两个或更多个表一起完成查询操作。 前提条件&#xff1a;这些一起查询的表之…

node.js基于Vue的英语在线学习网站 vscode+mysql

该系统的基本功能包括管理员、学生、教师三个角色功能模块。 对于管理员可以使用的功能模块主要有首页、个人中心&#xff0c;学生管理、教师管理、班级管理、课程管理&#xff0c;在线学习管理、作业管理、试卷管理、试题管理、 在线论坛、系统管理、考试管理等功能。 对于学生…

STM32F765ZIT6中文规格STM32F765ZGT6引脚图 微控制器MCU

说明STM32F7 32 位 MCUFPU 基于高性能的 ARMCortex-M7 32 位 RISC 内核&#xff0c;工作频率高达 216MHz。Cortex-M7 内核具有单浮点单元(SFPU)精度&#xff0c;支持所有 ARM 单精度数据处理指令与数据类型。同时执行全套 DSP 指令和存储保护单元&#xff08;MPU&#xff09;&a…

各CCF期刊点评网站/学术论坛的信息汇总及个人评价

CCF中文期刊投稿选择之篇章一:各CCF期刊点评网站/学术论坛的信息汇总及个人评价中文科技期刊A类&#xff08;EI检索&#xff09;中文期刊投稿点评网站整理1.小木虫学术论坛2. Letpub3. Justscience4. 发表记5. 会伴&#xff08;Conference Partner)6. ijouranl7. 掌桥科研这是以…

Win11的两个实用技巧系列之如何关闭登录密码?

Win11如何关闭登录密码?Win11关闭登录密码的两种解决方法win11是电脑更新后的全新系统&#xff0c;每次开启需要输入密码。有的用户嫌麻烦想要关闭&#xff0c;下面小编就为大家带来了关闭的方法&#xff0c;一起来看看吧有不少用户在升级或者第一次使用Win11系统的时候&#…

uni-app做微信小程序的分包处理

我们的都知道微信小程序有随即随用&#xff0c;用完即走的优点&#xff0c;并且它开发门槛低&#xff0c;但是它也有一个致命的缺点&#xff0c;就是代码包体积的限制&#xff0c;这一缺点让小程序的开发有了一定的限制&#xff0c;现在有一方法可以减少代码包的体积&#xff0…

界面组件Telerik ThemeBuilder R1 2023开创应用主题研发新方式!

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供最完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序&#xff0c;目前提供UI for ASP.NET包含一个完…

汉诺塔递归算法精讲

文章目录前言一、汉诺塔是个啥&#xff1f;二、手动解法三、解法抽象四、递归解法五、总结前言 递归算法是计算机算法中的基础算法&#xff0c;也是非常重要的算法&#xff0c;从某种程度上讲&#xff0c;它有一点儿AI的影子。人脑是可以完成递归思路的&#xff0c;但是对不起…

《爆肝整理》保姆级系列教程python接口自动化(十五)--参数关联接口(详解)

简介 我们用自动化新建任务之后&#xff0c;要想接着对这个新建任务操作&#xff0c;那就需要用参数关联了&#xff0c;新建任务之后会有一个任务的Jenkins-Crumb&#xff0c;获取到这个Jenkins-Crumb&#xff0c;就可以通过传这个任务Jenkins-Crumb继续操作这个新建的任务。 …

自适应布局之淘宝无限适配+rem+微信rpx自适应

一、自适应布局 所谓前端适配&#xff0c;就是为了让移动设计稿在大部分的移动设备上看起来有一致的展示效果&#xff0c;目前比较流行的方法有两种。一种是强制meta viewport宽度为设计稿宽度&#xff0c;一种是使用rem自适应布局的flexible.js。 二、当前流行的移动端自适应…

【刷题笔记】--盛最多水的容器--双指针

题目&#xff1a; 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不…

Python 高级编程之正则表达式(八)

文章目录一、概述二、正则表达式语法1&#xff09;字符匹配2&#xff09;字符集合3&#xff09;定位符4&#xff09;分组1、定义分组2、引用分组3、命名分组三、Python 的 re 模块1&#xff09;re.match() 方法2&#xff09;re.search() 方法3&#xff09;re.match() 与 re.sea…

2023最牛教程,手把手教你成为年薪30W的测试开发

随着互联网行业的高速发展&#xff0c;快速高质量的产品版本迭代成为企业始终立于不败之地的迫切需求&#xff0c;而在短期迭代的快节奏中&#xff0c;传统测试工作面对更大压力&#xff0c;无法持续提供高效率高质量的人力支撑&#xff0c;所以越来越多的企业需要技术更为全面…

Leetcode-每日一题1250. 检查「好数组」(裴蜀定理)

题目链接&#xff1a;https://leetcode.cn/problems/check-if-it-is-a-good-array/description/ 思路 方法&#xff1a;数论 题目意思很简单&#xff0c;让你在数组 nums中选取一些子集&#xff0c;可以不连续&#xff0c;子集中的每个数再乘以任意的数的和是否为1&#xff…

netty

Netty的介绍Netty是异步的&#xff08;指定回调处理&#xff09;、基于事件驱动的网络应用框架&#xff0c;用于快速开发高性能、高可靠性的网络IO程序。Netty本质是一个NIO框架&#xff0c;适用于服务器通讯相关的多种应用场景&#xff0c;分布式节点远程调用中Netty往往作为R…

RTT 消息邮箱

1.邮箱概念 邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子&#xff0c;有两个线程&#xff0c;线程 1 检测按键状态并发送&#xff0c;线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信&#xff0c;线程 …

SpringCloud保姆级搭建教程五---Redis

首先&#xff0c;这个和微服务没有直接的关系&#xff0c;只是在代码开发当中要使用的一个工具而已&#xff0c;为了提高这个系统的性能&#xff0c;加快查询效率等方面而使用它1、首先&#xff0c;要先安装redis到电脑上&#xff0c;这里依然是在windows上演示&#xff0c;之后…