LuaJavaBridge - Lua 與 Java 互操作的簡單解決方案


http://dualface.github.io/blog/2013/01/01/call-java-from-lua/

最近在游戲里要集成中國移動的 SDK,而這些 SDK 都是用 Java 編寫的。由於我們整個游戲都是使用 Lua 開發的,所以就面對 Lua 與 Java 互操作的問題。

傳統做法是先用 C/C++ 借助 JNI(Java Native Interface)編寫調用 Java 的接口函數,然后再將這些函數通過 tolua++ 導出給 Lua 使用。這種做法最大的問題就是太繁瑣,而且稍微有一點點修改,就要重新編譯,嚴重降低了開發效率。

我嘗試寫了幾個接口函數后,發現 JNI 提供了完善的接口來操作 Java,比如查找特定的 Class、Method 等等。既然有這些東西,我想完全可以實現一個很薄的轉接層。這個層會提供一些函數,讓 Lua 代碼可以直接調用到 Java 的方法。

經過一番努力,LuaJavaBridge(簡稱 luaj)誕生了。

luaj 主要特征

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

luaj 的功能很簡單,但對於集成各種 SDK 來說已經完全滿足需求了。

luaj 用法示例

下面的代碼是我們游戲中實際使用的中國移動支付 SDK 調用代碼,luaj 好不好用一目了然:

Lua 代碼:

--[[ 購買 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 方法獲取返回值

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 傳遞單個字符串參數。

GL 線程和 UI 線程的協調

cocos2d-x for Android 運行在多線程環境下,所以在 Lua 和 Java 交互時需要注意選擇適當的線程。

~

cocos2d-x 在 Android 上以兩個線程來運行,分別是負責圖像渲染的 GL 線程和負責 Android 系統用戶界面的 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 做中間層相比,配合 luja 的中間層是使用 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 功能就很簡單了:

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)
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM