一些动态技术的碎碎念

现在随着大前端的流行,Native小程序,RN,等看似打着原生旗号的动态化,但开发过程中都会发现非常非常的像在写前端,各种 margin padding align 等,尤其是各大框架看起来都在反复提一个词 FlexBox ,于是我们深入的聊聊在移动端,动态化与跨平台这两个词的发展。

编译动态化

每一行代码本质上他就是一个字符串,他不具备任何的可运行的能力,之所以能被运行是因为他经过了一系列的编译,把一行行人能读懂的有逻辑有语意的字符串,变成了机器可以读懂的一条条指令集

  • 输入:代码字符串
  • 编译:(简单介绍一下不展开 http://awhisper.github.io/2017/02/26/扯淡:大白话聊聊编译那点事儿/
    • 编译前端
      • 词法分析 一句话解释:识别字符串中的语言关键字
      • 语法分析 :识别循环,判断,跳转,调用等代码逻辑
      • 生成抽象语法树 AST :将代码字符串转化为机器可理解可遍历可处理的语法树
    • 编译后端
      • 生成中间码 IR :将语法树向可执行结果产物进行转化,先产生个中间产物
      • 生成结果 :将中间码根据个平台的差异,生成不同的运行结果
  • 输出:平台可运行的结果
    • iOS:可执行二进制 Mach-O 文件(汇编代码装载进入内存后即可执行)
    • 安卓:JVM的字节码(在任何有JVM的平台中都可以执行)

编译结果动态化

编译的最终结果想要执行,都得装载到运行环境里去,iOS是直接装载到内存上,而安卓是加载到JVM虚拟机里,而这个加载过程天然是支持“动态”加载的,并且这种动态化的开发模式是非常贴近原生的。

  • 安卓:插件化技术,dexloader 加载
  • iOS:动态链接库技术,dlopen 加载

安卓我不了解就不细说了,但iOS确实值得咬文嚼字一下,dylib 的全程叫动态链接库,我们有时候自己做一些通用组件库的时候都可以选择创建一个“静态链接库”又或者“动态链接库”,差异取决在是否在app启动截断就第一时间加载。但次动态指的是动态“链接(加载)” 而非 “动态更新”。

静态库中的代码都会与主程序编译到同一个可执行文件中,如果多个程序引用了同一个静态库,那么这个静态库是会在多个程序的可执行文件中存在多个副本。而动态库他的本质是希望让多个可执行文件间共用代码段,在链接装载期间,通过把独立于主程序可执行文件之外的 dylib 链接到主程序的虚拟地址上,故名:动态链接

但既然是动态链接,那么在本次加载的时候通过网络下载了最新的 dylib 文件,在下次加载最新文件,自然也能做到 “动态更新”,下面看一个在运行期间用代码,手动链接 dylib 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define Dylib_PATH  "/System/Library/PrivateFrameworks/xxxxx/xxxxx/xxxxx"

- (void)dynamicLibraryLoad
{
// dlopen 去动态加载 dylib
void *kit = dlopen(Dylib_PATH,RTLD_LAZY);   

// 在运行期动态加载的dylib应该如何调用里面的代码? OC有运行时与反射

Class pacteraClass = NSClassFromString(@"DynamicOpenMenth");
if (!pacteraClass) {
NSLog(@"Unable to get TestDylib class");
return;
}
NSObject *pacteraObject = [pacteraClass new];
[pacteraObject performSelector:@selector(startWithObject:withBundle:) withObject:self withObject:frameworkBundle];

// 只有OC的代码才能动态调用么?C代码一样有办法 dlsym 通过函数符号获取动态加载后的函数地址,进行调用

NSString *imsi = nil;
int (*CTSIMSupportCopyMobileSubscriberIdentity)() = dlsym(kit, "CTSIMSupportCopyMobileSubscriberIdentity");
imsi = (NSString*)CTSIMSupportCopyMobileSubscriberIdentity(nil);
 
// 卸载动态库
dlclose(kit);
}

只可惜这条看似最佳的动态化的路被苹果堵死了,dlopen 这个函数在 debug 包下运行以上代码,动态加载畅通无阻,但在 release 的包上,就会被苹果加入签名校验逻辑,凡事没经过签名的dylib/framework,都会动态链接失败,所以这条路在 iOS 上是被封死的,但是在安卓上,各种插件化的方案还在继续

编译结果跨平台

本身 Java 就被设计为一种一次编译处处运行的语言,只要对应平台有 JVM 这个虚拟机运行环境,换句话说跨平台的差异被 JVM 给打平了,但很可惜 iOS 平台并没有选择 JVM ,所以也就不跨到了 iOS 上。

那么剩下的跨平台的语言就是 C/C++ 了,安卓与iOS都支持用C与C++的编译产物,在iOS上 OC 与 C 和 C++ 几乎是无缝支持,而在安卓上需要 JNI 来进行桥梁与 java 互通。早年间流行的游戏引擎 cocos2dx 就是用 C++ 进行的逻辑与渲染(lua容后再表),Flutter的渲染平台就是用 C++ 写的 skia 渲染引擎做的,安卓与iOS两个平台其实都存在很多知名的库或者本地算法,底层是用 C++ 实现的。

本质上:编译后台的设计分为,先编译出中间码,再由中间码根据平台差异编译成最终结果,本身就是一个为了跨平台而做的设计“增加一个中间层”,但由于很多语言在发展过程中,不同语言的作者在做开发语言编译器的时候,使用的IR中间码,并不是统一的一套,所以设计很丰满,现实很骨干,IR中间码并没有完全解决跨平台的问题。

Web技术动态化 - “不要写死”

我们在开发的过程中即便是纯native开发,也经常会伴随着一些开发理念“不要写死”

  • 界面的文案?不要写死,服务器下发
  • 界面的文案的样式?不要写死,阴影,加粗,斜体,下划线服务器下发
  • tableView 的Cell?不要写死,七八种cell模板,服务器下发啥渲染啥
  • 程序逻辑?不要写死,根据服务器返回的type开关,看情况跳转
  • 功能模块?不要写死,看开关可以整体隐藏关闭

在编译运行的过程中,代码字符串被编译成了可执行的编译结果,在对应平台上运行。在“不要写死”这个思路中,描述配置信息等json字符串,被通过网络在程序运行的过程中,实时根据配置改变运行结果。

所有的这些“不要写死”,其实是一种可以通过网络下发的描述性值的信息,以json形式,从代码逻辑通过读取这些描述信息,去改变预先准备好的功能逻辑,从而做到功能改变。甚至有些功能页面例如“账单详情”,他的现在几乎进入了高度后端可配的状态,面对不同商家不同种类交易,完全无需前端迭代,纯靠后端强大的配置平台即可完成频繁的各业务方接入需求。

原则上讲,如果我们的描述配置语言设计的足够全面,应用程序中固化的那部分识别描述配置并且执行的固有能力也足够全面。既能考虑到任意当前已规划的需求,以及胜任未来的功能需求,那么我们的描述配置语言,无形中其实就是一种动态更新,这种描述配置语言其实也就形成了 DSL 领域专用语言,在你的应用程序配置体系中专用的描述语言。

布局引擎 - 界面动态化

在界面这一块确实就存在这种几乎能够完美涵盖所有界面需求的“DSL”设计,也存在着能够解析这种完美“DSL”设计的native固定代码“布局引擎”,从而做到一套布局引擎的程序代码,根据输入的 “DSL” 不同,呈现出风格迥异的页面。

布局引擎并不是因为动态化的诉求而产生的,本质上是为了解决屏幕多尺寸适配的问题,为了能用一种“通用”的描述方式,通过传入的窗口区域大小,实时的运算出每一个子元素在当前窗口里的 x y w h 绝对坐标,这个过程便是布局算法

类似的布局算法有很多种,但可以确定的是,浏览器内核中的布局引擎是眼下最完美的,并且还在被 W3C 以及浏览器厂商不断的完善,浏览器内核的布局引擎的输入是 HTML 这种文件来表达界面元素层级描述 CSS 这种文件来表达布局约束信息与样式信息,并且支持绝对布局,相对布局,盒子模型,弹性盒子(知名的 FlexBox),网格布局等多种布局算法相互之间嵌套使用~本质上是一种多种算法可扩展的布局引擎。(后面会略微详细介绍一下布局算法)

-w623

这种布局引擎本质上还是 native 的(浏览器内核是用c++)写的,如果不是直接使用 WebView,也有很多有着几乎一样思路的 native 布局 SDK

  • iOS 的 AutoLayout :iOS中的 xib 布局文件,本质上就是一种树型的 XML,它里面包含着界面层级信息与样式信息,经过原生代码 initWithNibName: 来读取这种 xml 最终生成界面。而如果不使用 xib,而是代码创建约束,那么原生 VFL 其实本质上也是一种DSL,而他内部的算法是通过计算约束 N 元一次方程组得出最终渲染坐标的,其算法性能出了名的差,在很多情况下即便原生 autolyout 非常的卡(在iOS 12以后得到优化)
  • 安卓的 XML 写界面:安卓的布局也很像浏览器内核,他也支持用 XML 去写,并且同样支持几种布局,从而满足丰富的界面展现需求
  • DTCoreText:基于 iOS原生 CoreText 排版库的上层封装,可以识别 HTML 从而直接渲染出native的
  • RN / Weex 的渲染框架:实际上也是 HTML/CSS 来通过 FlexBox 弹性盒子这种布局算法,构建出Native界面
  • Texture 框架:原名 AsyncDisplayKit ,FB 出品,也是基于 FlexBox 弹性盒子这种布局算法,其实还有 YogaKit , ComponentsKit 等,归根结底还是来自 FB 开源的 FlexBox 布局算法库 “Yoga”

所以本质上,HTML+CSS 这种网页的界面编写模式,和安卓原生的 xml 写界面,和 iOS 原生的 xib 渲染界面本质上是没区别的,甚至有时候还比原生快(iOS Autolayout被人喷太慢了) ,浏览器与H5慢是多方复杂原因共同导致的,但在布局渲染这块,Web的技术体系在UI的描述能力以及灵活度上确实设计得非常优秀,越来越多的原生动态化方案,在渲染这块都还离不开这一套技术体系

解释执行 - 逻辑动态化

单纯通过界面动态化,这样我们做出来的这样的一个动态App,一个界面的 DSL 中给可点击交互的元素加上一个 scheme 的路由属性,每当这个元素被点击的时候,就跳转这个路由,打开一个新界面,而新的界面又是全新的一个新下载下来的 DSL,从而实现了纯展示型页面跳转的 “动态” App。仔细分析一下,这其实就是早期最原始的浏览器的网页,每个网页由一个 url 去下载 html/css ,然后布局展现页面,每个元素的点击交互的效果是跳转一个新的 url,所以可以认为这样的一种 “动态界面” App,是最纯正的网页技术,虽然他是 Native 的。

但这种动态远不是我们所希望,我们希望可以在不同的交互与生命周期下进行:

  • 发出一个网络请求,获取最新最全的数据
  • 进行本地数据存储读取,数据持久化
  • 针对数据进行多方逻辑判断,根据逻辑结果,呈现给用户不通的表现
  • 等等

纯界面动态化是不可能满足这种需求的,俺着我们的上面探讨的思路,我们需要一种“能够描述逻辑的表达式”,以字符串的形式下发到客户端,然后通过客户端内置的一套固定的“运行环境”,来展现出不同的运行结果。

对比一下界面动态

  • “能够描述界面的表达式-字符串”:就是一种描述语言 DSL (领域特定语言)
  • “运行环境”:执行描述表达式需要一套用 native 代码编写的布局引擎,包含各种布局算法

逻辑动态会复杂的多

  • “能够描述逻辑的表达式-字符串”:单纯是 DSL 已经无法满足需求,我们需要正经代码编程的语言(脚本语言)
  • “运行环境”:编程语言的编译结果能否再任何平台下直接执行?我们需要的是一个用c编写的,脚本语言(JS/Lua)的虚拟机

脚本语言的虚拟机会严格遵照编译原理中的编译前端,即便是主体程序运行期间输入一段目标编程语言的代码逻辑,它也会先经过词法/语法分析,从而生成抽象语法树 AST,最终也会把程序转化为一条一条的运行指令,在虚拟机运行期把编译出来的指令集按顺序执行完并最终得到执行结果。(推荐一本书:《Lua设计与实现》

而脚本语言的内存控制统一被虚拟机进行整体控制,随着指令集在执行的过程中进行对象与内存空间的分配与管理,因此脚本语言的内存是跑在虚拟机的一个上下文之中,这个上下文与原生内存是隔离开的。

大家上大学的时候是否在学习计算机数据结构的时候,被老师要求做一个计算器,输入一个 “1+2*3-4/5”这种字符串,用栈这种数据结构去解析这个字符串运算出结果?这个过程就非常的像解释执行

词法分析:使用栈或者二叉树,一个字符一个字符的读取,读到数字压栈,读到符号/括号,压入符号栈

语法分析:根据你规划的 + - * / 的运算符优先级,以及括号的优先级,构建一个运算树

抽象语法树AST:这个树就可以执行出运算结果

-w260

目前最广泛使用的解释执行编程语言就是 JavaScript ~

Binding:

脚本语言能运行,只能执行逻辑,for 循环 / if 选择判断 / 函数方法 / 执行回调 等语言逻辑,但是想要命令设备去执行一些操作,还是需要调用原生平台的各类 API ,比如磁盘读写,比如网络请求,比如图形渲染。 脚本语言 JS 再怎么编写各种三方js库,也都只是在虚拟机内的上下文中进行运行,无法操作设备。想要让 js 代码所执行的虚拟机能够操作原生环境的硬件,就得构建一个桥梁叫做 Binding(对,其实就是是jsbridge,但 Binding 是更科学的叫法)

  • js 是没有发起网络请求的能力的,浏览器之所以能用 js 写 ajax 网络请求,是因为本身浏览器用 c/c++ 写了网络模块,然后把这个模块 binding 给了 js ,并且在js里封装成了 XMLHttpRequest 对象与 API
  • js 是没有能力改变界面渲染的,是的没错!浏览器内布局引擎与脚本引擎是2个独立的模块,布局引擎纯native的,js是没有能力去直接访问的,但是浏览器专门把布局引擎 binding 给了js,并且在 js里封装成了 Document 对象与 Dom 操作 API
  • js 是没有能力进行本地存储的,浏览器之所以能够用 js 写 localstorage ,就是因为浏览器把本地建值存储的模块 binding 给了 js ,并且在 js 里封装成了 localstorage API
  • 等等

-w705

我们平时给webview 写的各种 jsbridge ,其实就是浏览器里面对js 的 binding 概念的延伸,只不过浏览器里面 binding 的各种能力,都是经过 W3C 协会沉淀了十几年的标准,必须具备通用型和可扩展性,不是你想要啥功能都能给你 binding上 的。但我们做客户端的就有这个优势,在我们的自主 App 内,我们就可以利用 native 开发,自行扩展 js 的任意 native 业务能力。

这也就是所谓的前端开发受限于浏览器,浏览器不支持,前端就做不到,但客户端开发本身就在开发浏览器,前端想做而做不到的我们都能补上。

Tips:

问题:如果说所有 native 能力全都 binding 给 js,把每一个 iOS 的系统 Api(可能几万个)都 binding 给 js,那么是不是就可以直接用纯 js 直接写原生 app 的全功能了?把图形渲染能力也 binding 给 js 是不是就不需要 html/css 外加一个复杂的布局引擎,直接用 js 调用 binding 好的 UIView initWithView , addSubview ,就能写出任意的界面了?

回答:说的没错,但这样也是有代价的,每调用一次 binding 的 api 都是一次跨越上下文的接口调用,这个过程非常的耗时过程,在JS上下文的对象,需要进行序列化,然后通过一个通道与原生内存的上下文进行通信,然后在原生内存中还得反序列化才能会被正常的原生代码进行处理(js 与 lua 这个通道的底层处理都是在 c 这一层通过 data buffer 的压栈出栈处理传递数据),所以理论可行的用 js / lua 直接编写原生 App 性能还是存在问题的

  • 在JSPatch 没被封杀的时候,不仅拿 JSPatch 来 hotfix bug,直接用 JSPatch 构建全新的动态更新的功能页面,就是这个原理,只不过区别是 JSPatch 不需要把几万个 iOS 系统原生 Api 都 binding 了,因为 iOS 有 runtime,只需要把 Objc_msgSend 和 NSInvocation 给 binding 了,就可以做到用 js 代码,调用任意原生 iOS API,安卓也有反射,安卓也可以做到只用 js 代码,写出并调用任意安卓原生 API (以前我们写过在 app 黑盒运行期的调试工具,可以输入任意代码,动态调试安卓 与 iOS app,非常方便在bug现场的情况下任意调试代码,调试内存,快速追踪问题)

  • C++的游戏引擎 cocos2dx-lua 也是这个思路,c2d引擎小组的成员真的把一整个引擎的所有 C++ API 统统 binding 给了 lua,所以在游戏圈里,大量用纯 Lua 开发游戏,只有在引擎C++控件不满足需求的时候,才需要少量C++开发的开发模式。

Web技术思路下的原生动态化与跨平台

-w699

布局引擎 + 脚本引擎,构成了Web技术最重要的2大模块,同时也是浏览器内核最重要的2个核心,而后续所有的非编译结果的那种动态化方案,全都是Web技术思路下而产生的动态化方案。

并且这些方案本来就是跨平台的,底层用c去实现内核,实现脚本引擎,只要底层环境在各个平台都有,那么上层的 DSL 与 JS 就可以跨平台。

  • WebKit:如上图,输入 HTML / CSS / JS ,由浏览器内核实现
  • ReactNative:
    • 输入的是 JSX 但实际上,也是HTML CSS JS 混合在了一起
    • 布局引擎是 c 写的 FB 的开源 Yoga Flexbox 布局引擎
    • 渲染是通过 binding 模式直接调用各平台原生渲染 API ,构造 View ,Add View
    • 通过 javascriptCore 这个开源 js 解析引擎来作为虚拟机,运行构建 js 上下文
    • 通过把各种丰富的原生能力,原生界面组件,binding 给 js,从而丰富各种原生能力能够动态调用