作为逻辑动态化的基础,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)中发布,非常欢迎广大技术爱好者一起探讨交流。

相关文章
|
数据采集 移动开发 文字识别
服务阿里 9 个APP|揭秘新奥创升级的质量演变
新奥创技术体系,是手机淘宝端搭载着星环中台的整个商业化研发体系,孵化出的面对无线电商领域的技术体系。过去一年在手淘完成了下单、详情、购物车三大业务域的改造,接下来还会在订单、手淘导购等领域进行技术升级。目前新奥创已经接入阿里内的9个 App,逐步成为阿里集团无线领域电商系的技术解决方案。 本文主要围绕新奥创技术体系的升级,剖析架构升级对测试保障带来的新的转变,也是新的机遇。
5543 0
服务阿里 9 个APP|揭秘新奥创升级的质量演变
|
Web App开发 Android开发 iOS开发
iOS 调试:通过 Safari/Chrome 调试 WebView
iOS 调试:通过 Safari/Chrome 调试 WebView
8313 0
iOS 调试:通过 Safari/Chrome 调试 WebView
|
Android开发 数据安全/隐私保护
Android TextView 使用以及属性(方法)大全(下)
TextViewXML属性和相关方法说明(2)
1321 0
|
4月前
|
消息中间件 JavaScript 安全
HarmonyOSNext性能核弹:用Node-API引爆ArkTS/C++跨语言
本文介绍HarmonyOS Next中通过Node-API实现ArkTS与C++的跨语言交互,适合教育科普学习。内容涵盖三步核心流程:C++模块注册(开店准备)、接口映射(设计菜单)及ArkTS调用(点外卖)。重点包括SO库命名规则、线程安全规范及实战示例(如两数相加)。附带血泪经验总结,助你掌握丝滑跨语言调用技巧。
156 7
vuecli3打包时开启了productionSourceMap为true却没有生成sourcemap文件?
vuecli3打包时开启了productionSourceMap为true却没有生成sourcemap文件?
588 0
|
开发者 iOS开发
iOS App上架新规解析:如何进行App备案
iOS App上架新规解析:如何进行App备案
2234 0
|
存储 缓存 JSON
跨端动态化模板引擎详解,看完你也能写一个 | GaiaX 开源解读
GaiaX 跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的 Native 动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX 开源解读》,将带大家看看过去三年GaiaX的发展过程。 GaiaX 开源地址:https://github.com/alibaba/GaiaX
1725 0
跨端动态化模板引擎详解,看完你也能写一个 | GaiaX 开源解读
|
存储 缓存 网络协议
淘宝 APP 网络架构演进与弱网破障实践
淘宝 APP 网络架构演进与弱网破障实践
446 1
淘宝 APP 网络架构演进与弱网破障实践
|
移动开发 监控 前端开发
2023 年大淘宝 Web 端技术概览
2022 年,大淘宝前端团队进行了调整:重新组织生产关系,按业务线拆分整合进对应的业务技术团队,同时保留了大前端虚线组织,确保研发基建的一致性、技术的持续投入以及推进人员的成长。 整个变化涉及超过三百人的前端团队,经过了半年多的运转,整个团队在技术上也进行了对应的聚焦和收敛。 新的组织协作形态下,大淘宝 Web 领域的工程师们正在做哪些技术工作、有什么技术产品,特在 2023 年开年之际向行业前端同学进行分享。
4222 1
2023 年大淘宝 Web 端技术概览
|
存储 自然语言处理 算法
GaiaX开源解读 | 表达式作为逻辑动态化的基础,我们是如何设计的
GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。
563 0