前言
本文基于 Lua 5.3.
Lua 是一個輕量級腳本語言,常用于嵌入其他語言作為補充。關(guān)于更多Lua本身的問題不在本文討論范圍之內(nèi)。
在 Android 中嵌入 Lua 優(yōu)點很多,借助 Lua 腳本語言的優(yōu)勢,可以輕松實現(xiàn)動態(tài)邏輯控制,應(yīng)用可以隨時從服務(wù)器讀取最新 Lua 腳本文件,在不更新應(yīng)用的情況下修改程序邏輯。
可惜 Lua 官方只提供了 C API ,而 Android 主要使用 JAVA 作為開發(fā)語言。我們可以借助 JNI 來間接實現(xiàn)在 Android 中嵌入 Lua 。
準備
自己實現(xiàn) JNI 是一件很費力的事情,還好有人已經(jīng)造好了輪子叫做 Luajava ,并且又有人基于 Luajava 做了 Android 專用庫。網(wǎng)上流傳最廣的是 Androlua ,不過作者已經(jīng)多年不維護了,對 Lua 的支持依然停留在5.1并且有一些bug,有人Fork了這個項目,并將其更新至 Lua 5.3 :New Androlua ,不過這個項目也存在一些問題,我修復(fù)了一下但是作者并沒有處理我的 pull request ,各位可以直接使用我修復(fù)優(yōu)化后的:Android-Lua.
在修復(fù)bug的同時,我也添加了一些中文注釋,減少第一次接觸 Lua C API 朋友們的學習記憶成本。
由于最終需要調(diào)用 Lua C API,所以請先配置 NDK 開發(fā)環(huán)境。在 Android Studio 中打開 SDK Manager,切換到 SDK Tools 標簽頁,勾選CMake、LLDB、NDK下載安裝之。

導入工程
僅僅想實現(xiàn) Android Lua 互相調(diào)用,不關(guān)心具體過程的,可以直接添加依賴:
implementation 'cc.chenhe:android-lua:1.0.2' 然后后邊的導入部分可以跳過了。
Clone github 項目到本地并用 Android Studio 打開。大致可以看到下圖目錄結(jié)構(gòu)。(由于后期更新,結(jié)構(gòu)不一定完全相同)

其中
androidlua是庫,app是demo工程。庫中,lua下是 Lua 解釋引擎,luajava是 JNI 的有關(guān)代碼。*.mk 文件是NDK配置文件,詳情請參考Google NDK 文檔。
你可以將 androidluaModule 導入自己工程作為依賴庫使用。
Lua API 知識普及
此時我們已經(jīng)可以在 Android 與 Lua 直接互相調(diào)用了。但是在開始之前,還要學習下 Lua C API 的有關(guān)東西,因為這是與 Lua 交互的基礎(chǔ)。
Lua 與 C 依靠一個虛擬的棧來完成數(shù)據(jù)交換。包括變量、函數(shù)、參數(shù)、返回值等在內(nèi)的一切數(shù)據(jù),都要放入棧中來共享。為了調(diào)用方便,這個棧并不是嚴格遵循棧的規(guī)則。從索引來看,棧底索引為1,往上依次遞增。而棧頂索引是-1,往下依次遞減。因此,正負索引都是合法的,但是0不可以。下面是棧的示意圖:

常用 Lua C API 介紹
Lua 提供了大量的 C API,與其他語言的交互完全依賴這些 API,下面的基礎(chǔ)教程中本文會介紹幾個基礎(chǔ)的,具體可以查看Lua 官方手冊
這些函數(shù)均由 Lua 提供,在 Luajava 中被封裝在 LuaState 類下。
luaL_openlibs
加載 Lua 標準庫,一般需要調(diào)用一下。
luaL_dostring
執(zhí)行一段 Lua 腳本。
luaL_dofile
執(zhí)行給定文件中的 Lua 腳本。
lua_dump
獲取當前棧的內(nèi)容。
java 中對應(yīng)函數(shù)是dumpStack(),返回String,可以直接輸出,便于調(diào)試。
lua_pushXXX
將各種類型的數(shù)據(jù)壓入棧,以便未來使用。
lua_toXXX
將棧中指定索引處的值以xxx類型取出。
lua_getglobal
獲取 Lua 中的全局變量(包括函數(shù)),并壓入棧頂,以便未來使用。
參數(shù)就是要獲取的變量的名字。
lua_getfield
獲取 Lua 中 某一 table 的元素,并壓入棧頂。
第一個參數(shù)是 table 在棧中的索引,第二個參數(shù)是要獲取元素的 key.
lua_pcall
執(zhí)行 Lua 函數(shù)。
第一個參數(shù)是此函數(shù)的參數(shù)個數(shù),第二個是返回值個數(shù),第三個是錯誤處理函數(shù)在棧中的索引。
下面的內(nèi)容已過時,具體教程與 Demo 請參閱 GitHub: Android-Lua。
Enjoy coding
終于要開始調(diào)用了,想想還有點小激動呢~
為了方便說明,我們先定義一個腳本文件叫 test.lua,然后將其放在assets目錄下,因為這下面的文件不會被編譯。
可以使用下面的函數(shù)來讀取 assets 中文件的內(nèi)容:
public static String readAssetsTxt( Context context, String fileName ){
try {
InputStream is = context.getAssets().open( fileName );
int size = is.available();
/* Read the entire asset into a local byte buffer. */
byte[] buffer = new byte[size];
is.read( buffer );
is.close();
/* Convert the buffer into a string. */
String text = new String( buffer, "utf-8" );
/* Finally stick the string into the text view. */
return(text);
} catch ( IOException e ) {
e.printStackTrace();
}
return("err");
}
創(chuàng)建 Lua 棧
之前說了,依靠一個虛擬的棧來完成數(shù)據(jù)交換。那么首先我們當然要創(chuàng)建這個棧。
LuaState lua = LuaStateFactory.newLuaState(); //創(chuàng)建棧
lua.openLibs(); //加載標準庫
lua.close(); //養(yǎng)成良好習慣,在執(zhí)行完畢后銷毀Lua棧。
執(zhí)行 Lua 腳本
我們可以使用LdoString()來執(zhí)行一段簡單的腳本。
String l = "local a = 1";
lua.LdoString(l);
這樣就執(zhí)行了一個很簡單的腳本,他聲明了一個全局變量l,值為1.
當然,也可以使用LdoFile()來加載腳本文件,不過這需要你先把腳本復(fù)制到 SD 卡才行。因為在 APK 中是沒有“路徑”可言的。
讀取 Lua 變量與 table
首頁要說明的是,只有全局變量(非 local)才可以讀取。
test.lua:
a = 111;
t = {
["name"] = "Chenhe",
[2] = 2222,
}
android:
lua.getGlobal("a"); //獲取變量a并將值壓入棧
Log.i("a", lua.toInteger(-1) + ""); //以int類型取出棧頂?shù)闹担ㄒ簿褪莂)
lua.getGlobal("t"); //獲取變量t并壓入棧頂,此時table位于棧頂。
lua.getField(-1, "name"); //取出棧頂?shù)膖able的name元素,壓入棧頂。
Log.i("t.name", lua.toString(-1)); //以string類型取出棧頂?shù)闹担ㄒ簿褪莟.name)
Log.i("dump",lua.dumpStack()); //輸出當前棧
運行后可以看到log:
I/a: 111
I/t.name: Chenhe
I/dump: 1: number = 111.0
2: table
3: string = 'Chenhe'
我們已經(jīng)成功讀取了 Lua 中的變量。
執(zhí)行 Lua 函數(shù)
調(diào)用 Lua 函數(shù)的一般流程為:
- 獲取函數(shù)并入棧。
- 壓入各個參數(shù)(如果有)
- 調(diào)用函數(shù),指明參數(shù)個數(shù)、返回值個數(shù)、錯誤處理函數(shù)。
- 獲取返回值(如果有)
test.lua:
function test(a, b)
return a + b, a - b;
end
android:
lua.getGlobal( "test" ); //獲取函數(shù)并入棧
lua.pushInteger( 5 ); //壓入第一個參數(shù)a
lua.pushInteger( 3 ); //壓入第二個參數(shù)b
lua.pcall( 2, 2, 0 ); //執(zhí)行函數(shù),有2個參數(shù),2個返回值,不執(zhí)行錯誤處理。
Log.i( "r1", lua.toInteger( -2 ) + "" ); //輸出第一個返回值
Log.i( "r2", lua.toInteger( -1 ) + "" ); //輸出第二個返回值
Log.i( "dump", lua.dumpStack() );
運行后可以看到log:
I/r1: 8
I/r2: 2
I/dump: 1: number = 8.0
2: number = 2.0
這就成功地執(zhí)行了 Lua 函數(shù),并取得2個返回值。而之前入棧的函數(shù)以及參數(shù),在執(zhí)行的時候 Lua 已經(jīng)彈出了,所以最后棧里只剩下2個返回值。
傳入 Java 對象
得益于 Luajava 以及 Androlua 的封裝,我們可以直接將對象作為參數(shù)傳入,并在 Lua 中直接執(zhí)行對象的成員函數(shù)。
test.lua:
function setText(tv,s)
tv:setText("set by Lua."..s);
tv:setTextSize(50);
end
android:
lua.getGlobal("setText"); //獲取函數(shù)
lua.pushJavaObject(textView); //把TextView傳入
lua.pushString("Demo"); //傳入一個字符串
lua.pcall(2,0,0); //執(zhí)行函數(shù),有2個參數(shù)。

注入 Lua 變量
有時我們需要在 android 中創(chuàng)建 Lua 變量,這樣就可以在 Lua 中直接使用了。注意,這里創(chuàng)建的變量都是全局的(非 local)。
test.lua:
function setText(tv)
tv:setText("set by Lua."..s); --這里的s變量由java注入
tv:setTextSize(50);
end
android:
lua.pushString( "from java" ); //壓入欲注入變量的值
lua.setGlobal( "s" ); //壓入變量名
lua.getGlobal( "setText" ); //獲取Lua函數(shù)
lua.pushJavaObject( textView ); //壓入?yún)?shù)
lua.pcall( 1, 0, 0 ); //執(zhí)行函數(shù)

可以看到 Lua 成功調(diào)用了 Java 注入的變量s.
Lua 調(diào)用 java
Lua 調(diào)用 java 函數(shù)相對復(fù)雜,畢竟 java 不是腳本語言。我們需要將 java 函數(shù)包裝成一個 JavaFunction 類,實例化之后注冊到 lua,這樣才可以從 lua 調(diào)用。下面看一個例子:
public class MyJavaFunction extends JavaFunction {
public MyJavaFunction(LuaState luaState) {
super(luaState);
}
@Override
public int execute() {
// 獲取Lua傳入的參數(shù),注意第一個參數(shù)固定為上下文環(huán)境。
String str = L.toString(2);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(System.currentTimeMillis());
L.pushString(simpleDateFormat.format(date) + str);
return 1; // 返回值的個數(shù)
}
public void register() {
try {
// 注冊為 Lua 全局函數(shù)
register("testJava");
} catch (LuaException e) {
e.printStackTrace();
}
}
}
我們創(chuàng)建一個類繼承 JavaFunction 并實現(xiàn) execute 方法,它將在被 lua 調(diào)用時執(zhí)行。前面說過,lua 與 c 靠棧來交換數(shù)據(jù),故調(diào)用函數(shù)所傳的參數(shù)也會入棧。需要注意的是第一個參數(shù)恒為 lua 的上下文環(huán)境,實際傳入的參數(shù)是從2開始。在這個簡單的例子中,我們?nèi)〉昧?lua 傳遞的字符串,并將其拼接在由 java 獲取的時間字符串后邊一起返回給 lua。
與傳參類似,返回值也是直接入棧即可。最后我們需要返回返回值的個數(shù),這樣 lua 就知道從棧里取出幾個元素作為返回值了。這些元素會被自動出棧。這里我們利用 L.pushString() 將拼接后的字符串返回給 lua,并返回 1 表示有1個返回值。
最后,調(diào)用 register 方法注冊到 lua,傳入的字符串參數(shù)就是在 lua 中的函數(shù)名。
new MyJavaFunction(lua).register();
像這樣實例化剛才包裝的類就可以成功注冊了?,F(xiàn)在我們可以在 lua 中調(diào)用 testJava(String),它將返回一個字符串,內(nèi)容是當前的時間加上我們傳入的字符串。