LuaJavaBridge - Lua 與 Java 交互

在遊戲開發中對接android平臺時,我們不得不用到java,但是,遊戲中的邏輯全是通過lua編寫的,最初的方法就是通過中間層 C++,c++通過Jni實現跟java的交互,然後再聽過toolua++,把接口暴露給lua,但是這樣的就很繁瑣,現在有了LuaJ,我們就能夠使用lua通過luaj直接調用java了。

luaj 主要特徵

  • 可以從 Lua 調用 Java Class Static Method
  • 調用 Java 方法時,支持 int/float/boolean/String/Lua function 五種參數類型
  • 可以將 Lua function 作爲參數傳遞給 Java,並讓 Java 保存 Lua function 的引用
  • 可以從 Java 調用 Lua 的全局函數,或者調用引用指向的 Lua function

這裏注意下:java中的方法必須是靜態的,lua中的函數必須是全局的。

luaJ用例:

--[[

購買 1000 金幣
Java 方法原型:
public static void GameInterface_doBilling(final String billingIndex,
        final boolean useSms,
        final boolean isRepeated,
        final int luaFunctionId)
]]

-- 用於處理支付結果的函數
local function callback(result)
    if result == "success" then
        game.state:increaseCoins(1000)
        game.state:save()
    end
end

-- 調用 Java 方法需要的參數
local args = {
    "001",    -- billingIndex
    true,     -- useSms
    true,     -- isRepeated
    callback  -- luaFunctionId
}
-- Java 類的名稱
local className = "com/qeeplay/frameworks/ChinaMobile_SDK"
-- 調用 Java 方法
luaj.callStaticMethod(className, "GameInterface_doBilling", args)

luaj 實現原理

luaj 的核心目標有兩個:從 Lua 調用 Java, 從 Java 調用 Lua。整理出來就是如下幾點:

  • 查找並調用指定的 Java 方法
  • 檢查調用結果,並從 Java 方法獲取返回值
  • 將 Lua function 作爲參數傳遞給 Java 方法
  • 在 Java 方法中調用 Lua function

查找並調用指定的 Java 方法

JNI 提供了 FindClass() 方法用於查找指定的 Class,所以 luaj.callStaticMethod() 的第一個參數就是要調用的 Java Class 的完整類名稱(類名稱中的“.”要替換爲“/”)。

找到指定 Class 後,利用 JNI 的 GetStaticMethodID() 方法就可以找到這個類的指定靜態方法,前提是要提供靜態方法的名稱和簽名。

所謂簽名,就是指 Java 方法的參數類型和返回類型定義。例如前面示例代碼中 GameInterface_doBilling() 方法的簽名是 (Ljava/lang/String;ZZI)V 。關於 Java 方法簽名的具體定義,可以參考:JNI Type Signatures

由於簽名寫起來有點囉嗦,所以 luaj 可以根據調用參數自動猜測方法簽名。示例代碼中,luaj.callStaticMethod() 的第二個參數指定了要查找的方法名稱,但並沒有提供方法的簽名,這就是利用了 luaj 的自動猜測簽名功能。

示例代碼一共指定了 4 個參數,分別是:字符串、布爾值、布爾值、Lua function。

-- 調用 Java 方法需要的參數
local args = {
    "001",          -- billingIndex
    true,           -- useSms
    true,           -- isRepeated
    callback        -- luaFunctionId
}

luaj 根據這 4 個參數,會構造出正確的 GameInterface_doBilling() 方法簽名。注意 Lua function 是以整數的形式傳入 Java 方法,所以 Java 方法的第四個參數是 int 類型)。

不幸的是 Lua 裏沒有辦法準確判斷一個數值是整數還是浮點數,所以 luaj 在猜測方法簽名時,假定所有的數值都是浮點數。因此下面的代碼第二個調用就會失敗:

local args = {1} -- 生成的方法簽名是 (F)V

 

--[[

Java 方法原型:

public static void TestMethod1(final float integerValue)

]]

-- 調用成功

luaj.callStaticMethod(className, "TestMethod1", args)

--[[

Java 方法原型:

public static void TestMethod2(final int integerValue)

]]

-- 調用失敗,正確的方法簽名應該是 (I)V

luaj.callStaticMethod(className, "TestMethod2", args)

爲此,luaj 允許開發者指定完整的方法簽名。而且除了整數和浮點數的情況,在需要從 Java 方法獲得返回值時,也需要開發者指定完整的方法簽名。示例代碼如下:

local args ={"StringValue", 1, 3.14}
--[[
Java 方法原型:
public static int TestMethod3(final String stringValue,

        final int integerValue,

        final float floatValue)
]]

-- 定義簽名
-- 參數: [S]tring, [I]nteger, [F]loat
-- 返回值: [I]nt
local sig = "(Ljava/lang/String;IF)I"
-- 調用方法並獲得返回值

local ok, ret = luaj.callStaticMethod(className, "TestMethod3", args, sig)

簽名使用“(依次排列的參數類型)返回值類型”的格式,幾個例子如下:

這裏列出不同類型對應的 Java 簽名字符串:

Java 方法裏接收 Lua function 的參數必須定義爲 int 類型,具體原因詳見“將 Lua function 作爲參數傳遞給 Java 方法”小節。

檢查調用結果,並從 Java 方法獲取返回值

luaj 調用 Java 方法時,可能會出現各種錯誤,因此 luaj 提供了一種機制讓 Lua 調用代碼可以確定 Java 方法是否成功調用。

luaj.callStaticMethod() 會返回兩個值:

  • 當成功時,第一個值爲 true,第二個值是 Java 方法的返回值(如果有)。
  • 當失敗時,第一個值爲 false,第二個值是錯誤代碼。

下面的代碼展示瞭如何檢查返回結果和獲得返回值:

Java 代碼

public static int AddTwoNumbers(final int number1,final int number2) {
    return number1 + number2;
}

lua 代碼

local args = {2, 3}
local sig = "(II)I"
local ok, ret = luaj.callStaticMethod(className, "AddTwoNumbers", args, sig)

if not ok then
    print("luaj error:", ret)
else
    print("ret:", ret) -- 輸出 ret: 5
end

錯誤代碼定義如下:

將 Lua function 作爲參數傳遞給 Java 方法

很多時候,我們需要一種方法讓 Java 代碼可以向 Lua 代碼傳遞一些消息。例如在大部分遊戲平臺的 SDK 中,涉及支付的部分都是異步操作的。在支付操作結束後,Java 代碼需要通知 Lua 支付成功與否。

Lua 虛擬機中,Lua function 以值的形式保存。但這個值無法直接給 Java 用,所以 luaj 做了一個 Lua function 引用表。當一個 Lua function 傳遞給 Java 時,這個 function 對應的值會被存在引用表中,並獲得一個唯一的引用 ID (整數)。Java 代碼拿到這個引用 ID 後,就可以很方便的調用該 Lua function 了。

回顧最開始的示例代碼,GameInterface_doBilling() 函數用於接收 Lua function 的參數就是 int 類型。因爲實際傳入 Java 函數的值是 Lua function 的引用 Id。

在 Java 方法中調用 Lua function

在 Java 代碼中拿到 Lua function 的引用 ID 後,就可以很方便的調用該 Lua function 了:

LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "hello");

這裏出現的 LuaJavaBridge 是 luaj 的 Java 部分定義的工具 class。 callLuaFunctionWithString() 方法可以將一個字符串參數傳遞給指定的 Lua function。

LuaJavaBridge 還提供了 callLuaGlobalFunctionWithString() 方法,可以直接調用 Lua 中指定名字的全局函數。這樣可以在沒有 Lua function 引用 ID 的情況下和 Lua 代碼交互。

由於自己的項目暫時沒更多需求,所以目前 luaj 只支持向 Lua function 傳遞單個字符串參數。還有就是將方法定義爲native函數,也能夠實現需求。

到此,lua跟java的相互調用就完成了。

但是遊戲其中可能會涉及到線程的切換問題,畢竟我們的遊戲是運行在GL線程,而平臺的系統界面是在UI線程上的,要是涉及到平臺系統的UI更新,肯定要做線程切換的,總而言之

  • 在 cocos2d-x 啓動後,Lua 代碼將由 GL 線程調用,因此從 Lua 中調用的 Java 方法如果涉及到系統用戶界面的顯示、更新操作,那麼就必須讓這部分代碼切換到 UI 線程上去運行。
  • 反之亦然,從 Java 調用 Lua 代碼時,需要讓這個調用在 GL 線程上執行,否則 Lua 代碼雖然執行了,但會無法更新 cocos2d-x 內部狀態。

下面是 GameInterface_doBilling() 方法的主要代碼:

public static void GameInterface_doBilling(final String billingIndex,
    final boolean useSms,
    final boolean isRepeated,
    final int luaFunctionId) {
  context.runOnUiThread(new Runnable() {
    @Override
    public void run() {
      GameInterface.doBilling(useSms, isRepeated, billingIndex, new BillingCallback() {
        ...
        @Override
        public void onBillingSuccess() {
          context.runOnGLThread(new Runnable() {
            @Override
            public void run() {
              LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "success");
              LuaJavaBridge.releaseLuaFunction(luaFunctionId);
            }
          });
        }
        ...
      });
    }
  });
}

方法中,構造了一個 Runnable 對象,用來包裝需要執行的 Java 代碼。這個 Runnable 對象被指定運行在 UI 線程上。這樣當調用 GameInterface.doBilling() 方法時就可以正確顯示出支付界面。

當用戶支付成功後,GameInterface.doBilling() 會調用 BillingCallback.onBillingSuccess() 方法。這個方法裏構造了另一個 Runnable 對象,包裝了調用 Lua function 的代碼。

看上去代碼不少,實際上就是在兩個線程間互相切換。確保 Lua function 跑在 GL 線程,Java 代碼跑在 UI 線程。

Lua function 的引用計數器

Lua 虛擬機具有自動垃圾回收機制。Lua function 既然是值,那麼在沒有被使用時自然會被回收掉。所以 luaj 提供了 retainLuaFunction() 和 releaseLuaFunction() 兩個函數用於增減 Lua function 的引用計數。

將一個 Lua function 以引用 ID 的形式傳入 Java 時,luaj 會自動增加引用 ID 的計數器,所以在 Java 方法裏可以放心的異步調用 Lua function。但在不需要使用該 Lua function 後,一定要調用 releaseLuaFunction() 減少該引用 ID 的計數器。當計數器爲 0 時,會自動釋放該 Lua function。

如果瞭解 cocos2d-x 中 CCObject 的 autorelease 機制,那麼對引用計數應該很熟悉,兩者是完全相同的實現機制。

連接第三方 SDK 和 cocos2d-x 的中間層

雖然 luaj 可以讓開發者從 Lua 中直接調用 Java 代碼。但大部分第三方 SDK 在初始化時都需要指定當前應用程序的 Activity 對象,並且還要切換不同線程,所以對於大多數第三方 SDK,我們仍然要寫一箇中間層用於 Lua 和 Java 的交互。

與使用 JNI 做中間層相比,配合 luaj 的中間層是使用 Java 來編寫的,不但更簡單明瞭,而且處理線程切換也非常簡單。

要實現一箇中間層,只有兩個步驟:

  • 實現供 luaj 調用的 Java 接口
  • 修改遊戲的 Java 入口文件,將應用程序的 Activity 對象傳入 SDK

第一步請參考:“中國移動遊戲基地和短信支付 SDK”中間層源代碼

第二步也相當簡單,只需要在遊戲的 onCreate() 中調用 中間層 class 的 setContext() 方法:

public class mygame extends Cocos2dxActivity {
  protected void onCreate(Bundle savedInstanceState) {
    ChinaMobile_SDK.setContext(this); // init sdk
    super.onCreate(savedInstanceState);
  }
  ...
}

做好一切準備工作後,在遊戲的 Lua 代碼裏訪問 SDK 功能就很簡單了:

安裝 luaj

luaj 分爲三個部分:

  • LuaJavaBridge.java, com_qeeplay_frameworks_LuaJavaBridge.h/.cpp - 供 Java 端使用的工具類,包含 Java 接口定義文件和 JNI 實現。
  • LuaJavaBridge.h/.cpp - 供 Lua 端使用的工具類。
  • luaj.lua - LuaJavaBridge 的 Lua 包裝,提供更簡單和靈活的接口。

最後,還有ios平臺,與android類似,也有直接用lua調用oc的中間層luac,因此在遊戲開發過程中,最好通過平臺區分實現一個通用接口,最終在指定平臺實現相對應的功能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章