前言
作為一名iOS開發(fā)工程師,App的動態(tài)化是一種趨勢,畢竟需求的增多,頻繁的提交版本、更新版本對用戶體驗上肯定會有影響。當然動態(tài)化的方案有很多種:RN,Weex,LuaView等。對于一個對H5、React 零基礎(chǔ)的小白,我準備還是從LuaView入手。最后還想說一句,沒想到在簡書寫的第一篇文章是關(guān)于LuaView的。好吧,我承認我比較懶!
什么是LuaView?
LuaView是一種運行在一個ViewController/Activity中,可以靈活加載Lua腳本,并能夠按照Native的方式運行的一種面向業(yè)務(wù)的開發(fā)技術(shù)方案。
LuaViewSDK使用lua虛擬機進行腳本解析,通過構(gòu)建lua與native之間的一系列基礎(chǔ)bridge功能,從另一個角度實現(xiàn)了動態(tài)化的native能力。
而對于為何選用Lua,其最大的優(yōu)勢就是:lua語法精煉直觀,lua虛擬機輕量高效,使用Native編程模式,Native開發(fā)人員容易上手。
以上很不要臉的取自其官方文檔的描述:?https://alibaba.github.io/LuaViewSDK/guide.html
LuaViewSDK 是阿里開源的一個實現(xiàn)動態(tài)化方案的框架。開源地址:?https://github.com/alibaba/LuaViewSDK
目前其SDK由阿里的一個團隊來維護。個人感覺推廣力沒有Weex高。官方文檔也很久沒有更新了。不過提供了一個官方技術(shù)交流群:539262083 。
LuaViewSDK的整體架構(gòu)
上圖是LuaViewSDK的架構(gòu):(由下往上)
Native? & Framework :表示了Android、iOS及其對應(yīng)的框架層。
Lua Engine:即Lua虛擬機,Android對應(yīng)LuaJ,iOS對應(yīng)LuaC。作為lua腳本和nati語言之間的橋梁,將lua腳本翻譯成native能夠識別的目標語言。
Lua-Native UI Lib:LuaView的核心組件。其實LuaView對Native的各種UI組件進行了再次封裝,并且注冊到了Lua環(huán)境中,Lua腳本可以直接創(chuàng)建和操作這些組件,來達到創(chuàng)建和控制Native組件。(其實查看SDK源碼,會發(fā)現(xiàn),不僅封裝了UI組件,還有一些方法類,如Timer,Gesture等)。
Script Manager:Lua腳本管理器,用于腳本的解壓、驗證、加解密、解壓縮等工作。
Security:Lua腳本的校驗工作(完整性和安全性的校驗)。
Lua Script & Lua UI Lib:Lua 業(yè)務(wù)腳本以及 Lua 層的 UI 庫。
LuaView的基本用法
LuaView
第一種方式,直接創(chuàng)建LuaView對象,添加到你想渲染的View上,運行腳本進行界面渲染。
//1、創(chuàng)建LuaView,LView為LuaView子類(SDK封裝的)
self.lv= [[LView alloc]initWithFrame:lvRect];
self.lv.viewController= self;
[self.view addSubview:self.lv];
//2. 加載并運行腳本
[self.lv runFile:scriptFileName];
....
//3、LuaView對象被回收之前必須清理內(nèi)存
[luaview releaseLuaView];
第二種方式,創(chuàng)建LViewController的控制器對象,其屬性 lv 就是一個LuaView 對象,故運行腳本一樣實現(xiàn)了界面渲染。其已經(jīng)做好了各種生命周期和內(nèi)存管理的處理,所以不用主動去釋放。
//1. 創(chuàng)建LuaView VC
LViewController*luaVC=[[LViewController alloc]init];
//2. 加載并運行腳本
[luaVC.lv runFile:scriptFileName];
此處遇到一坑:
由于我初次使用lua,對其語法不熟,自己創(chuàng)建demo運行腳本時,用了別人寫的一個簡單demo的腳本:繪制一個label。可是運行后,發(fā)現(xiàn)沒報錯,但是也沒繪制,界面白板,也沒用返回錯誤提示。百思不得其解!最后對比了下別人 demo 和我的 demo 的 LuaViewSDK,發(fā)現(xiàn)版本不一致,別人的是 2.5.xx.x,而我的版本是0.5.1(最新的)。而原先 LuaViewSDK 語法和 lua 標準語法有區(qū)別 :‘.’ 和 ':' 互換了。最新SDK支持的lua標準語法(冒號調(diào)用方法,點調(diào)用屬性),所以我用最新SDK 運行原來語法寫成的腳本,是有問題的,語法不一致。最新的SDK中,LuaView 的子類 LView 有一屬性 changeGrammar(默認為NO),設(shè)置為YES會進行語法轉(zhuǎn)換。若新SDK 運行老語法的lua腳本,則需要將此屬性設(shè)置為 YES 。
而由于我項目中既有自己使用lua標準語法寫的腳本,也有從別人demo拷貝過來的老語法lua腳本。故我想當然的講 changeGrammar 設(shè)置為 YES,結(jié)果發(fā)現(xiàn)老語法lua腳本正常渲染界面,標準語法腳本卻渲染失敗,沒有錯誤提示,白板。后來發(fā)現(xiàn) changeGrammar 設(shè)置為YES,并非將lua語法轉(zhuǎn)換成標準語法,而是遍歷腳本后,將 ‘.’ 和 ':' 進行互換,所以標準語法寫的lua腳本又被轉(zhuǎn)換了。
所以標準語法的lua腳本,changeGrammar 千萬別設(shè)置為 YES。
LuaViewCore
LuaViewCore其實就是Lua的虛擬機,負責(zé)實現(xiàn)了Lua腳本到Native語言的映射。查看LuaView.h/m源碼,會發(fā)現(xiàn)LuaView初始化時,會創(chuàng)建一個?LuaViewCore 的對象,即一個?LuaView?對應(yīng)一個 LuaViewCore。
當業(yè)務(wù)需要要求一個頁面有多個子View都需要lua控制渲染時,若通過創(chuàng)建多個LuaView方式來渲染,則會創(chuàng)建多個LuaViewCore,這樣或多或少會影響性能。那么如何實現(xiàn)共享一個Lua虛擬機,即共享LuaViewCore,來渲染多個界面。
//1、初始化LuaViewCore
self.lvCore = [[LuaViewCore alloc]init];
//2、運行腳本
[self.lvCore runFile:@”luaName.lua”];
//? ? [self.lvCore loadFile:@”luaName.lua”];
//3、調(diào)用腳本里的方法 topViewUI/bottomViewUI ,在指定的 self.topView/self.bottomView 進行UI渲染
//str:成功則返回nil,失敗則返回失敗原因
NSString *str0 = [self.lvCore callLua:@"topViewUI" environment:self.topView args:nil];
NSLog(@"%@",str0?str0:@"topViewUI-sucessed");
NSString *str1 = [self.lvCore callLua:@"bottomViewUI" environment:self.bottomView args:nil];
NSLog(@"%@",str1?str1:@"bottomViewUI-sucessed");
對應(yīng)的腳本 luaName.lua 如下:
function topViewUI( )
aLabel = Label();
aLabel:text("aaaa");
aLabel:frame(0, 0, 100, 30);
end
function bottomViewUI()
aLabel = Label();
aLabel:text("cccc");
aLabel:frame(0, 0, 100, 30);
end
此處遇到一坑:
正如我前面所述,其官方文檔很久沒有更新,可能維護也很少。其官方描述?LuaViewCore 用法是這樣的:LuaViewCore初始化后,load 腳本,然后就可以調(diào)用腳本的方法。但是按照這個流程,調(diào)用腳本方法,會返回錯誤信息“function is nil error”,即方法找不到。原因是,在lua中,方法的定義是放在腳本運行時的,而非編譯時。故僅僅編譯腳本,是無法調(diào)用腳本方法的。正確的流程是:LuaViewCore初始化后,run 腳本,然后就可以調(diào)用腳本的方法。(此處已和其官方團隊聯(lián)系確認,是其文檔有誤)
Native自定義功能橋接
源碼解析
在實現(xiàn)自定義功能橋接到Lua層之前,首先要從源碼入手,了解LuaView是如何封裝Native控件,并且注冊到Lua環(huán)境中,Lua腳本可以任意創(chuàng)建和操作的!
正如上面所言,LuaView 初始化時,會初始化一個 LuaViewCore,然后就沒有其他什么特別的代碼。LuaViewCore 對象作為Lua虛擬機,所以密碼就在他這里。LuaViewCore 的初始化方法如下:
myInit 方法實現(xiàn)了屬性的初值賦值等。關(guān)鍵在于 registeLibs 方法。
此方法將所有LuaViewSDK封裝的NativeUI進行了遍歷注冊到Lua環(huán)境中。
而LVClassProtocal 協(xié)議的 +(int) lvClassDefine:(lua_State *)L globalName:(NSString*) globalName 方法,即每個封裝的UI類需要實現(xiàn)的,完成類及其方法注冊到Lua環(huán)境。比如LVImage:
lua是一種嵌入式的語言,可以作為c的擴展,也可以用c來編寫模塊了擴展lua。而在進行數(shù)據(jù)交互的時候,存在這這么一個棧,這個棧的作用是存儲lua和c交互的參數(shù),返回值等。如lua調(diào)用c函數(shù)并傳入?yún)?shù),所有參數(shù)會先壓入這個棧,c函數(shù)執(zhí)行時從棧中獲取參數(shù),執(zhí)行完后,也會把返回值壓入此棧,lua從此棧中獲取返回值。(我個人理解是這樣的,如有誤,煩請指出)
所以,上面LVImage的注冊,首先是將類名和其初始化方法壓棧,通過 lua_setglobal 方法,將棧頂?shù)念惷秃瘮?shù)注冊到lua環(huán)境中,并通過globalName(此處是 "Image")進行標注。如此lua腳本中就可以通過 Image() 來創(chuàng)建LVImage對象。
而下面的 luaL_Reg 結(jié)構(gòu)體,則包含了一組 keyStr -- 方法。則是將這組函數(shù)注冊到Lua環(huán)境中,作為全局函數(shù)。Lua腳本中LVImage對象就可以調(diào)用這些方法。
以上就實現(xiàn)了一個Native控件注入到lua環(huán)境中進行使用。
現(xiàn)有LuaView控件的擴展
上面已經(jīng)解讀了LVImage是如何注冊到Lua環(huán)境中進行使用。當Lua腳本里setImage 設(shè)置圖片,傳入?yún)?shù)是url時,圖片是沒有顯示的,查看LVImage的方法會發(fā)現(xiàn),因為LVImage 的 setWebImageUrl 是沒有實現(xiàn)的。故考慮通過繼承的方式擴展LVImage的功能,讓其支持網(wǎng)絡(luò)圖片的加載。
#import "XQImage.h"
#import "LVHeads.h"
#import <SDWebImage/UIImageView+WebCache.h>
@implementation XQImage
-(void) setWebImageUrl:(NSURL*) url finished:(LVLoadFinished) finished{
[self sd_setImageWithURL:url];
}
@end
XQImage 繼承自LVImage,并且重寫了父類的方法 -(void) setWebImageUrl:(NSURL*) url finished:(LVLoadFinished) finished。(此處采用SDWebImage進行圖片下載展示)
自定義子類實現(xiàn)了,但是Lua環(huán)境注冊的還是父類LVImage,故,lua腳本初始化Image(),還是會初始化父類的實例,故無法調(diào)用到子類的圖片下載賦值方法。父類的注冊,是在LuaView初始化時,那么LuaView初始化后,需要將子類覆蓋父類注冊到lua環(huán)境中,讓 globalName(“Image”)對應(yīng)的是子類:
self.lv[@"Image"] = [XQImage class];
如此后,lua腳本 Image() 創(chuàng)建的就是native的XQImage對象。由此實現(xiàn)網(wǎng)絡(luò)圖片的加載和顯示。
完全自定義類的橋接
上節(jié)通過繼承的方式擴展 LuaView 已封裝的UI控件,并且覆蓋注冊到lua環(huán)境中。那么如何將自定義的一個類,橋接到Lua環(huán)境中使用呢?
正如前面源碼解析,了解了 LuaView封裝的NativeUI 是如何實現(xiàn)的,所以按部就班,照著這個邏輯實現(xiàn)自定義類的橋接。其中最關(guān)鍵的是實現(xiàn) LVClassProtocal 協(xié)議的方法 + (int)lvClassDefine:(lua_State *)L globalName:(NSString *)globalName; 來實現(xiàn)指定類及其初始化方法,全局函數(shù)注冊到 Lua 環(huán)境中,供 Lua 腳本直接使用。
+(int) lvClassDefine:(lua_State *)L globalName:(NSString*) globalName{
[LVUtil reg:L clas:self cfunc:lvNewItem globalName:globalName defaultName:@"XQItemLuaView"];
const struct luaL_Reg memberFunctions [] = {
{"image",? setIconImage},
{"title",? ? title},
{NULL, NULL}
};
lv_createClassMetaTable(L,META_TABLE_CustomView);
luaL_openlib(L, NULL, [LVBaseView baseMemberFunctions], 0);
luaL_openlib(L, NULL, memberFunctions, 0);
const char* keys[] = { "addView", NULL};// 移除多余API
lv_luaTableRemoveKeys(L, keys );
return 1;
}
XQItemLuaView 是我自定義的一個 UIView 的子類,遵循 LVProtocal, LVClassProtocal 協(xié)議。其上有一個 ImageView 和 Label。C函數(shù) lvNewItem 用于初始化一個 XQItemLuaView 的對象,setIconImage 用于根據(jù) Lua 腳本傳入的參數(shù),設(shè)置 iconImageView 的圖片展示。 title 為根據(jù)Lua腳本傳入的參數(shù)字符串,設(shè)置 titleLabel 的text。
LVClassProtocal 是一個靜態(tài)協(xié)議,源碼分析中可以看到,LuaView 加載其擴展類的時候,都是通過初始化 LuaViewCore 時,遍歷所有需要加載的類,調(diào)用其 + (int)lvClassDefine:(lua_State *)L globalName:(NSString *)globalName 方法,實現(xiàn)加載。
而完全自定義的類的加載,最好不要去直接更改其 LuaViewCore 源碼。所以我創(chuàng)建了一個管理自定義類注冊的操作類 XQRegisterManager 。
#import "XQRegisterManager.h"
#import "XQItemLuaView.h"
@implementation XQRegisterManager
/**
自定義類的注冊管理
@param luaState 狀態(tài)機
*/
+(void)registerClassWithLuaState:(lua_State*)luaState{
[XQItemLuaView lvClassDefine:luaState globalName:@"XQItemLuaView"];
}
@end
在 LuaView/LuaViewController 初始化后,去調(diào)用注冊自定義的類。如:
self.lv = [[LView alloc] initWithFrame:lvRect];
[XQRegisterManager registerClassWithLuaState:self.lv.luaviewCore.l];
self.lvCore = [[LuaViewCore alloc]init];
[XQRegisterManager registerClassWithLuaState:self.lvCore.l];
總結(jié)
以上是我一周時間學(xué)習(xí)LuaView的記錄。從簡單運行一個 Lua腳本開始認識這個SDK,到最后分析源碼,來實現(xiàn)自定義類的橋接。下一步的目標是,在此基礎(chǔ)上,研究資源腳本下載實現(xiàn),SDK自帶的debuger工具類的使用,以及當腳本出錯或者下載失敗的降級處理(LuaViewSDK 沒有自帶降級處理,所有運行失敗會有錯誤拋出,需要根據(jù)錯誤,自行處理降級還是顯示失敗頁面)等。
如有紕漏,歡迎指出,謝謝!