Android Lua 相互調(diào)用

GitHub: Android-Lua

前言

本文基于 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 標簽頁,勾選CMakeLLDB、NDK下載安裝之。

NDK環(huán)境

導入工程

僅僅想實現(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)不一定完全相同)

目錄結(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 棧

常用 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ù)的一般流程為:

  1. 獲取函數(shù)并入棧。
  2. 壓入各個參數(shù)(如果有)
  3. 調(diào)用函數(shù),指明參數(shù)個數(shù)、返回值個數(shù)、錯誤處理函數(shù)。
  4. 獲取返回值(如果有)

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ù)。
執(zhí)行結(jié)果

注入 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ù)
執(zhí)行結(jié)果

可以看到 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)容是當前的時間加上我們傳入的字符串。

相關(guān)鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,983評論 25 709
  • 1. 寫在前面 很多時候我們都需要借助一些腳本語言來為我們實現(xiàn)一些動態(tài)的配置,那么就會涉及到如何讓腳本語言跟原生語...
    杰嗒嗒的阿杰閱讀 3,500評論 9 31
  • 第一篇 語言 第0章 序言 Lua僅讓你用少量的代碼解決關(guān)鍵問題。 Lua所提供的機制是C不擅長的:高級語言,動態(tài)...
    testfor閱讀 2,936評論 1 7
  • MVVM 是Model-View-ViewModel 的縮寫,它是一種基于前端開發(fā)的架構(gòu)模式,其核心是提供對Vie...
    Www劉閱讀 718評論 1 7
  • 匯算清繳靠自己的幾十塊
    楊孟杰閱讀 107評論 0 0

友情鏈接更多精彩內(nèi)容