因為動態化的東西我第一次看實現方案的源碼,而且目前還是大三的學生,缺少很多實踐經驗說錯的地方還請原諒,也希望能指出,被告知。想了很久還是決定寫出來,求大神勿噴。
並且我的一個朋友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的源碼進行了部分的更改,例如類名,還有一個函數名,舉個典型的例子:
這個狀態機被進行了更改,並且加入的新元素
void* lView;
對比下原來的
lvsdk中存在就是很多擴展后的控件,通過編寫Lua腳本可以直接調用的原生UI
具體為什么要更改我也不知道,如果你知道了,希望能私信告訴我,如果你想查看源碼:看這里Lua源碼下載 http://www.lua.org/ftp/
我剛剛編寫了一個簡單Lua腳本,並且進行下測試
button3 = Button();
button3.frame(150,250,100,100);
button3.image("button0.png","button1.png");
button3.callback(
function()
Alert("測試");
end
);
效果圖:
動圖
可以看到調用的原生UI。
先來分析
self.lview = [[LView alloc] initWithFrame:cg];
在初始化中主要是執行兩個方法,我主要挑這其中的主要代碼說,就不全貼上來了,如果感興趣可以下載源碼看,其中一個是初始化用於加密解密的rsa以及對腳本資源文件進行管理的bundle
-(void) myInit{
self.rsa = [[LVRSA alloc] init];
self.bundle = [[LVBundle alloc] init];
}
另一個就是:
-(void) registeLibs{
if( !self.stateInited ) {
self.stateInited = YES;
self.l = lvL_newstate();//lv_open(); /* opens */
lvL_openlibs(self.l);
[LVRegisterManager registryApi:self.l lView:self];
self.l->lView = (__bridge void *)(self);
}
}
這里我們使用lvL_newstate()函數創建一個新的lua執行環境,但是這個函數中環境里什么都沒有,因此需要使用lvL_openlibs(self.l);加載所有的標准庫,之后可以使用。所有lua相關的東西都保存在lv_State這個結構中,通過lvL_newstate()創建一個新的 Lua 虛擬機時,第一塊申請的內存將用來保存主線程和這個全局狀態機。其實我個人感覺這就是一個內置在App中的運行環境,專門運行Lua腳本。
[LVRegisterManager registryApi:self.l lView:self];
這行代碼,我就把它理解為擴展,對Lua API的一個擴展。上面我們提到過,使用Lua構建動態化方案的核心就在於將Android,iOS原生的UI、網絡、存儲、硬件控制等能力橋接到Lua層。他主要就是為了完成這里,在這里注冊大量的API
看源碼:
簡單介紹下這兩個函數的作用lv_settop(L, 0),lv_checkstack(L, 128),lv_settop(L, 0)設置棧頂索引,即設置棧中元素的個數,如果index<0,則從棧頂往下數,lv_checkstack(L, 128)確保堆棧上至少有 extra 個空位.按照上面注釋的解釋就是為了做清理棧的工作。
因為這里注冊了太多的API,主要是為了弄清原理,那么我們就選擇我們腳本中使用的Button來分析。也就是這行代碼
[LVButton classDefine:L ];
LVButton繼承自UIButton並且遵循LVProtocal協議。看一下classDefine:方法
+(int) classDefine:(lv_State *)L {
{
lv_pushcfunction(L, lvNewButton);
lv_setglobal(L, "Button");
}
const struct lvL_reg memberFunctions [] = {
{"image", image},
{"font", font},
{"fontSize", fontSize},
{"textSize", fontSize},
{"titleColor", titleColor},
{"title", title},
{"textColor", titleColor},
{"text", title},
{"selected", selected},
{"enabled", enabled},
//{"showsTouchWhenHighlighted", showsTouchWhenHighlighted},
{NULL, NULL}
};
lv_createClassMetaTable(L,META_TABLE_UIButton);
lvL_openlib(L, NULL, [LVBaseView baseMemberFunctions], 0);
lvL_openlib(L, NULL, memberFunctions, 0);
const char* keys[] = { "addView", NULL};// 移除多余API
lv_luaTableRemoveKeys(L, keys );
return 1;
}
其中這段代碼:
lv_pushcfunction(L, lvNewButton);
lv_setglobal(L, "Button");
lvNewButton是一個函數,我們上面說過,我們跟Lua環境的交互主要是通過一個虛擬的棧,lv_pushcfunction(L, lvNewButton)的作用就是將lvNewButton函數壓入棧頂,然后使用lv_setglobal(L, “Button”)將棧頂的lvNewButton函數傳入Lua環境中作為全局函數。這樣就是擴展我們的Lua環境,現在我們就可以編寫Lua腳本,通過Button()關鍵字來調用lvNewButton函數。
const struct lvL_reg memberFunctions [] = {
{"image", image},
{"font", font},
{"fontSize", fontSize},
{"textSize", fontSize},
{"titleColor", titleColor},
{"title", title},
{"textColor", titleColor},
{"text", title},
{"selected", selected},
{"enabled", enabled},
//{"showsTouchWhenHighlighted", showsTouchWhenHighlighted},
{NULL, NULL}
};
來看下lvL_Reg的結構體
typedef struct lvL_Reg {
const char *name;
lv_CFunction func;
} lvL_Reg;
看到該結構體也可以看出,包含name,和func。name就是為了,在注冊時用於通知Lua該函數的名字。結構體數組中的最后一個元素的兩個字段均為NULL,用於提示Lua注冊函數已經到達數組的末尾。
這里我們其實可以理解為為Button添加庫,像image,font這些在源碼中可以看到,是一些靜態的c函數
例如:
現在可以重新看一下我們原來寫的Lua腳本了
button = Button();
button.frame(150,250,100,100);
button.image("button0.png","button1.png");
button.callback(
function()
Alert("LatDays");
end
);
可以看到我么你的Button,還有image就是我們上面添加的標識。感興趣可以下載demo做一些更改。就會發現兩者是對應的。
我個人理解原因就是像Lua源碼解析中說的,global_State 里面有對主線程的引用,有注冊表管理所有全局數據,有全局字符串表,有內存管理函數, 有 GC 需要的把所有對象串聯起來的相關信息,以及一切 Lua 在工作時需要的工作內存。
UI的擴展我們看完了,現在來分析下Lua腳本文件是如何運行的。
- (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"];
}
這段代碼中我們可以看到[self.lview runFile:@”lastdays.lua”]做的Lua腳本文件的加載。接着放下看就可以發現我們利用前面介紹的bundle來讀取腳本文件中的代碼,並且存儲為NSData類型。
NSData* code = [self.bundle scriptWithName:fileName];
接着往下查看,就會看到這個函數:
-(NSString*) runData:(NSData *)data fileName:(NSString*)fileName{
if( self.l==NULL ){
LVError( @"Lua State is released !!!");
return @"Lua State is released !!!";
}
if( fileName==nil ){
static int i = 0;
fileName = [NSString stringWithFormat:@"%d.lua",i];
}
if( data.length<=0 ){
LVError(@"running chars == NULL, file: %@",fileName);
return [NSString stringWithFormat:@"running chars == NULL, file: %@",fileName];
}
#ifdef DEBUG
[self checkDeuggerIsRunningToLoadDebugModel];
[self checkDebugOrNot:data.bytes length:data.length fileName:fileName];
#endif
int error = -1;
error = lvL_loadbuffer(self.l, data.bytes , data.length, fileName.UTF8String) ;
if ( error ) {
const char* s = lv_tostring(self.l, -1);
LVError( @"%s", s );
#ifdef DEBUG
NSString* string = [NSString stringWithFormat:@"[LuaView][error] %s\n",s];
lv_printToServer(self.l, string.UTF8String, 0);
#endif
return [NSString stringWithFormat:@"%s",s];
} else {
return lv_runFunction(self.l);
}
}
這個函數中
lvL_loadbuffer(self.l, data.bytes , data.length, fileName.UTF8String) ;
lua相關的東西都保存在lv_State這個結構中,lvL_loadbuffer加載lua代碼,加載之后就是運行,又是誰完成的運行呢?
我猜測是lv_runFunction(self.l),然后就順着找下去,看到以下源碼:
NSString* lv_runFunctionWithArgs(lv_State* l, int nargs, int nret){
if( l && lv_type(l, -1) == LV_TFUNCTION ) {
if( nargs>0 ){
lv_insert(l, -nargs-1);
}
int errorCode = lv_pcall( l, nargs, nret, 0);
if ( errorCode != 0 ) {
const char* s = lv_tostring(l, -1);
LVError( @"%s", s );
#ifdef DEBUG
NSString* string = [NSString stringWithFormat:@"[LuaView][error] %s",s];
lv_printToServer(l, string.UTF8String, 0);
#endif
return [NSString stringWithFormat:@"%s",s];
}
return nil;
}
return @"function is nil error";
}
這段代碼中我找到了這個函數:
int errorCode = lv_pcall( l, nargs, nret, 0);
它的意思就是在lua保護模式下運行語句。如果運行出錯,函數會把錯誤信息壓到“棧”里。我們可以通過
char* s = lv_tostring(l, -1);
來獲取錯誤信息。在調用函數前,用戶應該先把語句所需的參數都壓到棧里,參數nargs說明有多少個參數,nresults表示返回值有多少個。如果msgh值為0表示“棧”里返回的是原始的錯誤信息。在函數執行完后,lua會把函數以及其使用的參數從棧里刪掉,並且把結果壓入棧里。lua函數並不會打印任何信息,它只會返回一個錯誤代碼,然后由調用者對錯誤進行適當的處理。
可以看到
lv_printToServer(l, string.UTF8String, 0);
這段代碼就是將輸出信息在控制台輸出,具體的實現就不描述了。
源碼分析總結
LuaViewSDK 可以通過 Lua 腳本橋接所有 Native 的功能,所以具有與 Native 一樣豐富的性能。以上這些就是我自己總結的LuaView調用原生UI的過程,我們如何擴展Lua,並且我們又是如何加載Lua腳本,並且調用Native UI。
當然,LuaViewSDK當中還包含着包的管理和下載,還有更新的一些方案,我還沒有仔細看,看了以后還會總結分享。
這里看在聚划算團隊分享的LuaView SDK整體架構
如圖,以下為聚划算團隊說明LuaView SDK的整體架構可以表示為五層。
自下而上第一、二層依次是OS層和Framework層,分別表示了Android、iOS以及對應的框架層。
緊接着是Lua虛擬機,在Android、iOS平台分別是LuaJ和LuaC,兩個虛擬機都是目前兩個語言中用的最廣泛,最穩定的虛擬機。
處在第三層的還有腳本管理模塊以及安全控制模塊,它們分別負責Lua本地腳本管理(包括腳本的解包、驗證、加解密、解壓縮等工作)和Lua腳本的安全校驗工作(腳本完整性校驗以及腳本安全校驗等)。
處在第四層的是LuaView的核心Lib庫,包括兩部分,一部分是Lua UI Lib,主要是所有的UI組件(如Button、Label、Image、TableView等);一部分是Lua Non-UI Lib,主要是所有非UI組件(如Http、Json、Audio等)。
處在最上層的是Lua業務腳本代碼以及Lua層的Lib庫(方便第三方使用的Lua寫的Lib庫)。
還有很多細節的地方,和經典的設計,因為能力和實踐經驗有限,沒有發現,如果有更多的認知,希望告訴我,接下來就是自己嘗試做橋接。
使用Lua實現動態化
首先還是引入我們LuaC的引擎,就引用LuaViewSDK的LuaC吧
我們就自己實現一個Lable吧,其實實現也是在模仿LuaViewSDK進行實現,主要還是為了驗證上面的分析
首先創建我們的LDSView來承載我們的Lable
//生成庫
-(void)generateLibs{
self.lvState = lvL_newstate();
lvL_openlibs(self.lvState);
[RegisterManager registerApiWithlvState:self.lvState];
self.lvState->lView = (__bridge void *)(self);
}
首先就是創建我們的Lua腳本運行環境,並且以上語句的具體作用上面都有過講解,self.lvState->lView這里主要是因為我們所需的運行東西都在lv_State結構體中,我們就在這個結構體中添加了一指針,來指向我們的當前LDSView。這樣就在狀態機中存在着lView了。
-(NSString *)runScriptWithScriptName:(NSString *)scriptName{
NSData *code = [self.bundle scriptWithName:scriptName];
NSString *result = [[NSString alloc] initWithData:code encoding:NSUTF8StringEncoding];
NSLog(@"%@",result);
return[self runData:code fileName:scriptName];
}
這里就是copy了LuaViewSDK資源文件的管理代碼,主要就是為了進行代碼的讀取,並且將本地腳本代碼讀取出來的代碼傳入下面的函數來運行
-(NSString*) runData:(NSData *)data fileName:(NSString*)fileName{
lvL_loadbuffer(self.lvState, data.bytes , data.length, fileName.UTF8String) ;
return lds_runFunctionWithArgs(self.lvState, 0, 0);
}
可以看到運行的話就是利用lvL_loadbuffer來加載代碼,然后就是調用lds_runFunctionWithArgs這個c函數來運行腳本代碼。
NSString* lds_runFunctionWithArgs(lv_State* l, int nargs, int nret){
lv_pcall( l, nargs, nret, 0);
return nil;
}
這里的實現的關鍵就是lv_pcall,調用它,讓Lua腳本在Lua運行環境中以安全的方式進行運行。
上面這些就是代碼的加載和運行。
然后就是將UILabel橋接到Lua層,實現擴展,來讓我們能夠使用Lua腳本去調用原生的UI。
#import "RegisterManager.h"
@implementation RegisterManager
+(void)registerApiWithlvState:(lv_State*)lvState{
[LDSLable classDefine:lvState];
}
@end
也就是這里,我們在這里進行注冊,具體看一下classDefine:的實現
+(void)classDefine:(lv_State *)lvState{
lv_pushcfunction(lvState, ldsNewLabel);
lv_setglobal(lvState, "Label");
}
我們上面提到過,Lua跟C的交互主要是通過一個虛擬的棧來實現的,lv_pushcfunction將函數ldsNewLabel壓入棧中,然后利用lv_setglobal將棧頂的lvNewButton函數傳入Lua環境中作為全局函數,並且實現讓我們能夠在Lua腳本中利用Label關鍵字調用ldsNewLabel函數。
我們來看下ldsNewLabel函數
static int ldsNewLabel(lv_State *lvState){
NSString* text = @"成功調用";
LDSLable* label = [[LDSLable alloc] init:text lv_State:lvState];
LDSView* view = (__bridge LDSView *)(lvState->lView);
[view addSubview:label];
NSLog(@"進入label的創建");
return 1;
}
這就是一個簡單的c函數。其中
LDSLable* label = [[LDSLable alloc] init:text lv_State:lvState];
就是來生成UILabel控件的。我們的LDSLable其實是繼承自UILabel的,看到以下代碼,大家就能夠明白了。
-(id) init:(NSString*)text lv_State:(lv_State*) lvState{
self = [super init];
if( self ){
self.text = text;
self.backgroundColor = [UIColor blueColor];
self.textAlignment = NSTextAlignmentLeft;
self.clipsToBounds = YES;
self.font = [UIFont systemFontOfSize:14];
self.frame = CGRectMake(30, 30, 200, 200);
}
return self;
}
在這里我們完成了很多基本屬性的設置。
另一段代碼
LDSView* view = (__bridge LDSView *)(lvState->lView);
[view addSubview:label];
還記得我們在lv_State狀態機中添加了什么嗎?對,就是在這里用到了,讓我們狀態機中的LDSView添加了label控件。
現在看一下Lua的腳本文件lastdays.lua
label = Label();
就這么一句話,就可以生成一個原生UILabel。
看一下我們的調用
- (void)viewDidLoad {
[super viewDidLoad];
[self LDSView];
}
-(void)LDSView{
CGRect cg = self.view.bounds;
cg.origin = CGPointZero;
self.ldsView = [[LDSView alloc] initWithFrame:cg];
[self.view addSubview:self.ldsView];
[self.ldsView runScriptWithScriptName:@"lastdays.lua"];
}
來看下效果:
動圖
我沒有編寫其他控制屬性的函數,直接就在init全部搞定了,就是簡單的實現下證明下上面的思路,結果看來還是正確。如果你也感興趣歡迎去文章的上面查看兩個代碼的源碼。
總結
我個人感覺LuaView在聚划算中得到大量的使用,並且這么長時間,運行狀態也很穩定說明LuaView還是有很多我值得學習的地方,上面的分析感覺也就是LuaView的一些皮毛,還有很多關鍵的點,和設計模式沒有分析出來,並且Lua和C的交互擴展主要是依靠那個虛擬的”棧”,這里的原理我還沒有看的太明白,后面還會大量的學習。並且還希望多交流,因為現在才大三,缺少很多實踐經驗,有很多地方理解的可能是錯誤的,希望能被指出,勿噴勿噴。