Android Lua 相互調用

前言

本文基於 Lua 5.3.html

Lua 是一個輕量級腳本語言,經常使用於嵌入其餘語言做爲補充。關於更多Lua自己的問題不在本文討論範圍以內。
在 Android 中嵌入 Lua 優勢不少,藉助 Lua 腳本語言的優點,能夠輕鬆實現動態邏輯控制,應用能夠隨時從服務器讀取最新 Lua 腳本文件,在不更新應用的狀況下修改程序邏輯。
惋惜 Lua 官方只提供了 C API ,而 Android 主要使用 JAVA 做爲開發語言。咱們能夠藉助 JNI 來間接實如今 Android 中嵌入 Lua 。java

準備

本身實現 JNI 是一件很費力的事情,還好有人已經造好了輪子叫作 Luajava ,而且又有人基於 Luajava 作了 Android 專用庫。網上流傳最廣的是 Androlua ,不過做者已經多年不維護了,對 Lua 的支持依然停留在5.1而且有一些bug,有人Fork了這個項目,並將其更新至 Lua 5.3 :New Androlua ,不過這個項目也存在一些問題,我修復了一下可是做者並無處理個人 pull request ,各位能夠直接使用我修復優化後的:Android-Lua. android

在修復bug的同時,我也添加了一些中文註釋,減小第一次接觸 Lua C API 朋友們的學習記憶成本。git

因爲最終須要調用 Lua C API,因此請先配置 NDK 開發環境。在 Android Studio 中打開 SDK Manager,切換到 SDK Tools 標籤頁,勾選CMakeLLDBNDK下載安裝之。
NDK環境github

導入工程

僅僅想實現 Android Lua 互相調用,不關心具體過程的,能夠直接添加依賴:
implementation 'cc.chenhe:android-lua:1.0.2' 而後後邊的導入部分能夠跳過了。服務器

Clone github 項目到本地並用 Android Studio 打開。大體能夠看到下圖目錄結構。(因爲後期更新,結構不必定徹底相同)
目錄結構
其中 androidlua是庫,app是demo工程。庫中,lua下是 Lua 解釋引擎,luajava是 JNI 的有關代碼。*.mk 文件是NDK配置文件,詳情請參考Google NDK 文檔app

你能夠將 androidluaModule 導入本身工程做爲依賴庫使用。ide

Lua API 知識普及

此時咱們已經能夠在 Android 與 Lua 直接互相調用了。可是在開始以前,還要學習下 Lua C API 的有關東西,由於這是與 Lua 交互的基礎。函數

Lua 與 C 依靠一個虛擬的棧來完成數據交換。包括變量、函數、參數、返回值等在內的一切數據,都要放入棧中來共享。爲了調用方便,這個棧並非嚴格遵循棧的規則。從索引來看,棧底索引爲1,往上依次遞增。而棧頂索引是-1,往下依次遞減。所以,正負索引都是合法的,可是0不能夠。下面是棧的示意圖:
Lua 棧學習

經常使用 Lua C API 介紹

Lua 提供了大量的 C API,與其餘語言的交互徹底依賴這些 API,下面的基礎教程中本文會介紹幾個基礎的,具體能夠查看Lua 官方手冊

這些函數均由 Lua 提供,在 Luajava 中被封裝在 LuaState 類下。

luaL_openlibs

加載 Lua 標準庫,通常須要調用一下。

luaL_dostring

執行一段 Lua 腳本。

luaL_dofile

執行給定文件中的 Lua 腳本。

lua_dump

獲取當前棧的內容。
java 中對應函數是dumpStack(),返回String,能夠直接輸出,便於調試。

lua_pushXXX

將各類類型的數據壓入棧,以便將來使用。

lua_toXXX

將棧中指定索引處的值以xxx類型取出。

lua_getglobal

獲取 Lua 中的全局變量(包括函數),並壓入棧頂,以便將來使用。
參數就是要獲取的變量的名字。

lua_getfield

獲取 Lua 中 某一 table 的元素,並壓入棧頂。
第一個參數是 table 在棧中的索引,第二個參數是要獲取元素的 key.

lua_pcall

執行 Lua 函數。
第一個參數是此函數的參數個數,第二個是返回值個數,第三個是錯誤處理函數在棧中的索引。

Enjoy coding

終於要開始調用了,想一想還有點小激動呢~

爲了方便說明,咱們先定義一個腳本文件叫 test.lua,而後將其放在assets目錄下,由於這下面的文件不會被編譯。
可使用下面的函數來讀取 assets 中文件的內容:

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");
}

建立 Lua 棧

以前說了,依靠一個虛擬的棧來完成數據交換。那麼首先咱們固然要建立這個棧。

LuaState lua = LuaStateFactory.newLuaState(); //建立棧
lua.openLibs(); //加載標準庫

lua.close(); //養成良好習慣,在執行完畢後銷燬Lua棧。

執行 Lua 腳本

咱們可使用LdoString()來執行一段簡單的腳本。

String l = "local a = 1";
lua.LdoString(l);

這樣就執行了一個很簡單的腳本,他聲明瞭一個全局變量l,值爲1.

固然,也可使用LdoFile()來加載腳本文件,不過這須要你先把腳本複製到 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類型取出棧頂的值(也就是a)

lua.getGlobal("t"); //獲取變量t並壓入棧頂,此時table位於棧頂。
lua.getField(-1, "name"); //取出棧頂的table的name元素,壓入棧頂。
Log.i("t.name", lua.toString(-1)); //以string類型取出棧頂的值(也就是t.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'

咱們已經成功讀取了 Lua 中的變量。

執行 Lua 函數

調用 Lua 函數的通常流程爲:

  1. 獲取函數併入棧。
  2. 壓入各個參數(若是有)
  3. 調用函數,指明參數個數、返回值個數、錯誤處理函數。
  4. 獲取返回值(若是有)

test.lua:

function test(a, b) 
    return a + b, a - b;
end

android:

lua.getGlobal( "test" ); //獲取函數併入棧
lua.pushInteger( 5 ); //壓入第一個參數a
lua.pushInteger( 3 ); //壓入第二個參數b
lua.pcall( 2, 2, 0 ); //執行函數,有2個參數,2個返回值,不執行錯誤處理。
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

這就成功地執行了 Lua 函數,並取得2個返回值。而以前入棧的函數以及參數,在執行的時候 Lua 已經彈出了,因此最後棧裏只剩下2個返回值。

傳入 Java 對象

得益於 Luajava 以及 Androlua 的封裝,咱們能夠直接將對象做爲參數傳入,並在 Lua 中直接執行對象的成員函數。

test.lua:

function setText(tv,s)
    tv:setText("set by Lua."..s);
    tv:setTextSize(50);
end

android:

lua.getGlobal("setText"); //獲取函數
lua.pushJavaObject(textView); //把TextView傳入
lua.pushString("Demo"); //傳入一個字符串
lua.pcall(2,0,0); //執行函數,有2個參數。

執行結果

注入 Lua 變量

有時咱們須要在 android 中建立 Lua 變量,這樣就能夠在 Lua 中直接使用了。注意,這裏建立的變量都是全局的(非 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函數
lua.pushJavaObject( textView ); //壓入參數
lua.pcall( 1, 0, 0 ); //執行函數

執行結果

能夠看到 Lua 成功調用了 Java 注入的變量s.

相關連接

相關文章
相關標籤/搜索