因爲動態化的東西我第一次看實現方案的源碼,而且目前還是大三的學生,缺少很多實踐經驗說錯的地方還請原諒,也希望能指出,被告知。想了很久還是決定寫出來,求大神勿噴。
並且我的一個朋友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
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腳本,並且進行下測試
|
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。
先來分析
|
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
看源碼:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
// 註冊函數
+
(
void
)
registryApi
:
(
lv_State
*
)
L
lView
:
(
LView
*
)
lView
{
//清理棧
lv_settop
(
L
,
0
)
;
lv_checkstack
(
L
,
128
)
;
// 註冊靜態全局方法和常量
[
LVRegisterManager
registryStaticMethod
:L
lView
:lView
]
;
// 註冊System對象
[
LVSystem
classDefine
:L
]
;
// 基礎數據結構data
[
LVData
classDefine
:L
]
;
[
LVStruct
classDefine
:L
]
;
// 註冊UI類
lv_settop
(
L
,
0
)
;
[
LVBaseView
classDefine
:L
]
;
[
LVButton
classDefine
:L
]
;
[
LVImage
classDefine
:L
]
;
[
LVLabel
classDefine
:L
]
;
[
LVScrollView
classDefine
:L
]
;
[
LVTableView
classDefine
:L
]
;
[
LVCollectionView
classDefine
:L
]
;
[
LVPagerView
classDefine
:L
]
;
[
LVTimer
classDefine
:L
]
;
[
LVPagerIndicator
classDefine
:L
]
;
[
LVCustomPanel
classDefine
:L
]
;
[
LVTransform3D
classDefine
:L
]
;
[
LVAnimator
classDefine
:L
]
;
[
LVTextField
classDefine
:L
]
;
[
LVAnimate
classDefine
:L
]
;
[
LVDate
classDefine
:L
]
;
[
LVAlert
classDefine
:L
]
;
// 註冊DB
[
LVDB
classDefine
:L
]
;
//清理棧
lv_settop
(
L
,
0
)
;
// 註冊手勢
[
LVGestureRecognizer
classDefine
:L
]
;
[
LVTapGestureRecognizer
classDefine
:L
]
;
[
LVPinchGestureRecognizer
classDefine
:L
]
;
[
LVRotationGestureRecognizer
classDefine
:L
]
;
[
LVSwipeGestureRecognizer
classDefine
:L
]
;
[
LVLongPressGestureRecognizer
classDefine
:L
]
;
[
LVPanGestureRecognizer
classDefine
:L
]
;
//清理棧
lv_settop
(
L
,
0
)
;
[
LVLoadingIndicator
classDefine
:L
]
;
// http
[
LVHttp
classDefine
:L
]
;
// 文件下載
[
LVDownloader
classDefine
:L
]
;
// 文件
[
LVFile
classDefine
:L
]
;
// 聲音播放
[
LVAudioPlayer
classDefine
:L
]
;
// 調試
[
LVDebuger
classDefine
:L
]
;
// attributedString
[
LVStyledString
classDefine
:L
]
;
// 註冊 系統對象window
[
LVRegisterManager
registryWindow
:L
lView
:lView
]
;
// 導航欄按鈕
[
LVNavigation
classDefine
:L
]
;
//清理棧
lv_settop
(
L
,
0
)
;
//外鏈註冊器
[
LVExternalLinker
classDefine
:L
]
;
//清理棧
lv_settop
(
L
,
0
)
;
return
;
}
|
簡單介紹下這兩個函數的作用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:方法
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
|
+
(
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函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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函數
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static
int
image
(
lv_State
*L
)
{
LVUserDataInfo
*
user
=
(
LVUserDataInfo
*
)
lv_touserdata
(
L
,
1
)
;
if
(
user
)
{
NSString
*
normalImage
=
lv_paramString
(
L
,
2
)
;
// 2
NSString
*
hightLightImage
=
lv_paramString
(
L
,
3
)
;
// 2
//NSString* disableImage = lv_paramString(L, 4);// 2
//NSString* selectedImage = lv_paramString(L, 5);// 2
LVButton
*
button
=
(
__bridge
LVButton
*
)
(
user
->
object
)
;
if
(
[
button
isKindOfClass
:
[
LVButton
class
]
]
)
{
[
button
setImageUrl
:normalImage
placeholder
:nil
state
:UIControlStateNormal
]
;
[
button
setImageUrl
:hightLightImage
placeholder
:nil
state
:UIControlStateHighlighted
]
;
//[button setImageUrl:disableImage placeholder:nil state:UIControlStateDisabled];
//[button setImageUrl:selectedImage placeholder:nil state:UIControlStateSelected];
lv_pushvalue
(
L
,
1
)
;
return
1
;
}
}
return
0
;
}
|
現在可以重新看一下我們原來寫的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"
]
;
}
|
lview.viewController = self;