因为动态化的东西我第一次看实现方案的源码,而且目前还是大三的学生,缺少很多实践经验说错的地方还请原谅,也希望能指出,被告知。想了很久还是决定写出来,求大神勿喷。
并且我的一个朋友bestswifter写了一篇关于ReactNative源码分析的一品文章,React Native 从入门到原理,感兴趣也可以阅读下。
最近看到很多场对动态化提出了很多技术方案,原因就是客户端的业务需求越来越复杂,尤其是一些业务快速发展的互联网产品,肯定会造成版本的更新迭代跟不上业务的变化,尤其是App Store不确定性的审核,这个时候动态化的想法就自然的产生了。我不知道其他人是如何理解动态化的,但是我觉得,动态化指的就是我们不发布新的版本就可以实现大量的应用内容更新,这里的内容不应该仅仅是一些基本信息,应该涉及到应用的主题框架,甚至是布局,排版等。
因为我自己主要专注iOS,所以本次的源码分析和实现主要围绕iOS进行。
App的设计方案
现在移动端有三种主流的设计方案,分别是Web App、Hybrid App、 Native App。简单的叙述下,这三种
- Web App:指的就是利用H5打造的应用,不需要下载,存活于浏览器中,类似轻应用。图像渲染由HTML,CSS完成,性能比较慢,个人感觉体验不是很好,模仿原生界面,大部分依赖于网络。
- Native App:指的就是原生程序,存活在操作系统中(iOS,Android)一个完整的App,但是需要客户下载安装使用。图像的渲染由本地API完成,采用原生组件,支持离线网络。
- Hybrid App:部分H5和部分Native的混合架构,这种方案以H5的动态性为基础,通过定义Native的扩展(Bridge)来实现动态化,大部分依赖于网络;
- Native View方案:使用Native进行渲染的Native View方案,通过修改预定结构中的数据,实现动态化
- ReactNative:通过JavaScript脚本引擎支持页面DOM转换和逻辑控制来实现动态化
动态对比
Hybrid App具备一定的动态能力,但是Hybrid的H5部分体验较差。Web App的体验跟网络有很大的关系,网络环境不好,体验会很差,而且H5的渲染能力比较差。Native View方案不支持逻辑代码的替换。ReactNative的JS引擎不够轻量,不适合大数量的ListView处理。甚至还有更多的动态划方案,尽管ReactNative很火,就像我一个朋友提到过的,到目前位置并没有一种方案统一了动态化方案。
发现LuaView
同样为了更加深入的了解动态化的实现,我尝试去分析一种方案的源码更加深入的去了解。这里我选择了阿里聚划算开源的LuaView,这里我并不了解聚划算的动态化方案是如何构建的,但是原因肯定是因为聚划算的业务不断的扩展,由于聚划算的业务变化需求,因此LuaView的实践性肯定是经过考验的,从实践的角度出发,我选择尝试分析它。
学习Lua的体会
我玩过愤怒的小鸟,用过Photoshop,但是我现在才知道Lua在它们两个中就有应用,接触后,发现Lua是一种轻量级的语言,它的官方版本只包括一个精简的核心和最基本的库,这就让它非常非常的小,编译后也仅仅就是百于k而已,这根Lua的设计目标有关系,它的目标就是成为一个很容易嵌入其它语言中使用的语言,而且Lua可以用于嵌入式硬件,不仅可以嵌入其他编程语言,而且可以嵌入微处理器中。
很多人会发现Lua很轻量,并不具备网络请求,图形UI等能力,但是很多应用使用Lua作为自己的嵌入式语言,因为他本身的接口易于扩展使得它可以通过宿主语言完成能力扩展
以上的Lua的这些特性就让我们发现,使用Lua构建动态化方案的核心就在于将Android,iOS原生的UI、网络、存储、硬件控制等能力桥接到Lua层。如果做到,这种方案就可以支持UI动态搭建、脚本、资源、逻辑动态下发。借助Lua语言的可扩展性,我们可以很方便地在Native跟Lua之间搭建起桥梁,将Native的各种能力迁移到Lua层。
分析LuaView
通过上面繁琐无聊的介绍,我们就可以来分析一波LuaView是如何将Android,iOS原生的UI、网络、存储、硬件控制等能力桥接到Lua层的。
LuaView的意图就是利用Lua去构建Native UI。LuaView没有去自己构建一个UI库,而是借用Android,iOS原生UI,Android支持的Lua引擎为LuaJ,iOS支持的Lua引擎为LuaC。
根据聚划算团队的说明,
LuaView的一条重要设计原则就是同一份逻辑只写一份代码,这需要在设计SDK的时候尽可能得考虑到两个端的共性跟特性,将API构建在两个端的共性领域中,对于两端的特性领域则交由各自的Native部分来实现。
为了实现这种能力,肯定需要构建一个桥接平台,并且设计好统一的API。
源码分析
源码看了很久,然后总算能总结出一些东西,因为还是学生,可能有些地方的实践跟我想的有差异,还希望大家提出。
在分析源码前不得不具体说说Lua,上面也提到过,这个Lua很轻量,很小。因此lua是一个嵌入式语言,就是说它不是一个单独的程序,而是一套可以在其它语言中使用的库,lua可以作为c语言的扩展,反过来也可以用c语言编写模块来扩展lua,这两种情况都使用同样的api进行交互。lua与c主要是通过一个虚拟的“栈”来交换数据。
这个虚拟“栈”是很关键的一个点,Lua利用一个虚拟的堆栈来给C传递值或从C获取值。每当Lua调用C函数,都会获得一个新的堆栈,该堆栈初始包含所有的调用C函数所需要的参数值(Lua传给C函数的调用实参),并且C函数执行完毕后,会把返回值压入这个栈(Lua从中拿到C函数调用结果)。
我自己就是理解Lua引擎在App中其实起到一个内置系统的能力,我们把Lua脚本注入应用程序,Lua引擎自己解析,运行,然后去调用原生UI,这就需要为我们的操作系统进行扩展,利用的就是lua可以作为c语言的扩展,反过来也可以用c语言编写模块来扩展lua
这些理论可能说起来很繁琐,也可能是我自己总结的不够清晰,我们现在来引入实践代码进行分析,最后我们在尝试自己去手动实现一些简单的动态化能力,这样会有更清晰的认知。
看一下LuaView的结构
lv514可以理解为Lua的源码,为什么说可以理解为?因为作者对Lua的源码进行了部分的更改,例如类名,还有一个函数名,举个典型的例子:
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 27 28 29 30 31 |
struct lv_State { CommonHeader; lu_byte status; StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ global_State *l_G; CallInfo *ci; /* call info for current function */ const Instruction *savedpc; /* `savedpc' of current function */ StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ CallInfo *end_ci; /* points after end of ci array*/ CallInfo *base_ci; /* array of CallInfo's */ int stacksize; int size_ci; /* size of array `base_ci' */ unsigned short nCcalls; /* number of nested C calls */ unsigned short baseCcalls; /* nested C calls when resuming coroutine */ lu_byte hookmask; lu_byte allowhook; int basehookcount; int hookcount; lv_Hook hook; TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ GCObject *openupval; /* list of open upvalues in this stack */ GCObject *gclist; struct lv_longjmp *errorJmp; /* current error recover point */ ptrdiff_t errfunc; /* current error handling function (stack index) */ // void* lView; }; |
这个状态机被进行了更改,并且加入的新元素
1 |
void* lView; |
对比下原来的
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 27 28 |
struct lua_State { CommonHeader; lu_byte status; StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ global_State *l_G; CallInfo *ci; /* call info for current function */ const Instruction *savedpc; /* `savedpc' of current function */ StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ CallInfo *end_ci; /* points after end of ci array*/ CallInfo *base_ci; /* array of CallInfo's */ int stacksize; int size_ci; /* size of array `base_ci' */ unsigned short nCcalls; /* number of nested C calls */ unsigned short baseCcalls; /* nested C calls when resuming coroutine */ lu_byte hookmask; lu_byte allowhook; int basehookcount; int hookcount; lua_Hook hook; TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ GCObject *openupval; /* list of open upvalues in this stack */ GCObject *gclist; struct lua_longjmp *errorJmp; /* current error recover point */ ptrdiff_t errfunc; /* current error handling function (stack index) */ }; |
lvsdk中存在就是很多扩展后的控件,通过编写Lua脚本可以直接调用的原生UI
具体为什么要更改我也不知道,如果你知道了,希望能私信告诉我,如果你想查看源码:看这里Lua源码下载
我刚刚编写了一个简单Lua脚本,并且进行下测试
1 2 3 4 5 6 7 8 9 10 |
button3 = Button(); button3.frame(150,250,100,100); button3.image("button0.png","button1.png"); button3.callback( function() Alert("测试"); end ); |
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 27 28 29 30 31 32 33 34 35 |
// // ViewController.m // luaTest // // Created by LastDays on 16/6/7. // Copyright © 2016年 LastDays. All rights reserved. // #import "ViewController.h" #import <LView.h> @interface ViewController () @property(nonatomic,strong) LView *lview; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. CGRect cg = self.view.bounds; cg.origin = CGPointZero; self.lview = [[LView alloc] initWithFrame:cg]; self.lview.viewController = self; [self.view addSubview:self.lview]; [self.lview runFile:@"lastdays.lua"]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end |
效果图:
可以看到调用的原生UI。
先来分析
1 |
self.lview = [[LView alloc] initWithFrame:cg]; |
在初始化中主要是执行两个方法,我主要挑这其中的主要代码说,就不全贴上来了,如果感兴趣可以下载源码看,其中一个是初始化用于加密解密的rsa以及对脚本资源文件进行管理的bundle
1 2 3 4 |
-(void) myInit{ self.rsa = [[LVRSA alloc] init]; self.bundle = [[LVBundle alloc] init]; } |
另一个就是:
1 2 3 |