http://dualface.github.io/blog/2013/01/01/call-java-from-lua/html
最近在遊戲裏要集成中國移動的 SDK,而這些 SDK 都是用 Java 編寫的。因爲咱們整個遊戲都是使用 Lua 開發的,因此就面對 Lua 與 Java 互操做的問題。java
傳統作法是先用 C/C++ 藉助 JNI(Java Native Interface)編寫調用 Java 的接口函數,而後再將這些函數經過 tolua++ 導出給 Lua 使用。這種作法最大的問題就是太繁瑣,並且稍微有一點點修改,就要從新編譯,嚴重下降了開發效率。git
我嘗試寫了幾個接口函數後,發現 JNI 提供了完善的接口來操做 Java,好比查找特定的 Class、Method 等等。既然有這些東西,我想徹底能夠實現一個很薄的轉接層。這個層會提供一些函數,讓 Lua 代碼能夠直接調用到 Java 的方法。github
通過一番努力,LuaJavaBridge(簡稱 luaj)誕生了。多線程
luaj 的功能很簡單,但對於集成各類 SDK 來講已經徹底知足需求了。oracle
下面的代碼是咱們遊戲中實際使用的中國移動支付 SDK 調用代碼,luaj 好很差用一目瞭然:異步
Lua 代碼:ide
--[[ 購買 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 的核心目標有兩個:從 Lua 調用 Java, 從 Java 調用 Lua。整理出來就是以下幾點:函數
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 在猜想方法簽名時,假定全部的數值都是浮點數。所以下面的代碼第二個調用就會失敗:
爲此,luaj 容許開發者指定完整的方法簽名。並且除了整數和浮點數的狀況,在須要從 Java 方法得到返回值時,也須要開發者指定完整的方法簽名。示例代碼以下: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)
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)
luaj 調用 Java 方法時,可能會出現各類錯誤,所以 luaj 提供了一種機制讓 Lua 調用代碼能夠肯定 Java 方法是否成功調用。
luaj.callStaticMethod() 會返回兩個值:
下面的代碼展現瞭如何檢查返回結果和得到返回值:
Java 代碼
Lua 代碼public static int AddTwoNumbers(final int number1, final int number2) { return number1 + number2; }
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
不少時候,咱們須要一種方法讓 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 的引用 ID 後,就能夠很方便的調用該 Lua function 了:
LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "hello");
這裏出現的 LuaJavaBridge 是 luaj 的 Java 部分定義的工具 class。 callLuaFunctionWithString() 方法能夠將一個字符串參數傳遞給指定的 Lua function。
LuaJavaBridge 還提供了 callLuaGlobalFunctionWithString() 方法,能夠直接調用 Lua 中指定名字的全局函數。這樣能夠在沒有 Lua function 引用 ID 的狀況下和 Lua 代碼交互。
因爲本身的項目暫時沒更多需求,因此目前 luaj 只支持向 Lua function 傳遞單個字符串參數。
cocos2d-x for Android 運行在多線程環境下,因此在 Lua 和 Java 交互時須要注意選擇適當的線程。
~
cocos2d-x 在 Android 上以兩個線程來運行,分別是負責圖像渲染的 GL 線程和負責 Android 系統用戶界面的 UI 線程。
下面是 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 虛擬機具備自動垃圾回收機制。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 機制,那麼對引用計數應該很熟悉,二者是徹底相同的實現機制。
~
雖然 luaj 可讓開發者從 Lua 中直接調用 Java 代碼。但大部分第三方 SDK 在初始化時都須要指定當前應用程序的 Activity 對象,而且還要切換不一樣線程,因此對於大多數第三方 SDK,咱們仍然要寫一箇中間層用於 Lua 和 Java 的交互。
與使用 JNI 作中間層相比,配合 luja 的中間層是使用 Java 來編寫的,不但更簡單明瞭,並且處理線程切換也很是簡單。
~
要實現一箇中間層,只有兩個步驟:
第一步請參考:「中國移動遊戲基地和短信支付 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 功能就很簡單了:
local luaj = require("luaj") local className = "com/qeeplay/frameworks/ChinaMobile_SDK" -- 初始化 SDK local args = { CHINA_MOBILE_SP_APP_NAME, CHINA_MOBILE_SP_CP_NAME, CHINA_MOBILE_SP_TEL } luaj.callStaticMethod(className, "GameInterface_initializeApp", args) -- 支付 local function callback(result) if result == "success" then -- 支付成功 end end local args = { billingIndex, true, true, callback } luaj.callStaticMethod(className, "GameInterface_doBilling", args) -- 顯示遊戲基地界面 luaj.callStaticMethod(className, "GameCommunity_launchGameCommunity") -- 提交玩家的遊戲成績 local args = { "0", -- 排行榜Id newBestScores, -- 新的最佳成績 } local sig = "(Ljava/lang/String;I)V" luaj.callStaticMethod(className, "GameCommunity_commitScoreWithRank", args, sig)