原文地址:http://www.cnblogs.com/mokey/p/4443561.html
最近在游戲里要集成中國移動的 SDK,而這些 SDK 都是用 java 編寫的。由于我們整個游戲都是使用 Lua 開發(fā)的,所以就面對 Lua 與 Java 互操作的問題。
傳統(tǒng)做法是先用 C/C++ 借助 JNI(Java Native Interface)編寫調(diào)用 Java 的接口函數(shù),然后再將這些函數(shù)通過 tolua++ 導(dǎo)出給 Lua 使用。這種做法最大的問題就是太繁瑣,而且稍微有一點點修改,就要重新編譯,嚴(yán)重降低了開發(fā)效率。
我嘗試寫了幾個接口函數(shù)后,發(fā)現(xiàn) JNI 提供了完善的接口來操作 Java,比如查找特定的 Class、Method 等等。既然有這些東西,我想完全可以實現(xiàn)一個很薄的轉(zhuǎn)接層。這個層會提供一些函數(shù),讓 Lua 代碼可以直接調(diào)用到 Java 的方法。
經(jīng)過一番努力,LuaJavaBridge(簡稱 luaj)誕生了。
luaj 的功能很簡單,但對于集成各種 SDK 來說已經(jīng)完全滿足需求了。
下面的代碼是我們游戲中實際使用的中國移動支付 SDK 調(diào)用代碼,luaj 好不好用一目了然:
Lua 代碼:
| --[[購買 1000 金幣Java 方法原型:public static void GameInterface_doBilling(final String billingIndex, final boolean useSms, final boolean isRepeated, final int luaFunctionId)]]-- 用于處理支付結(jié)果的函數(shù)local function callback(result) if result == "success" then game.state:increaseCoins(1000) game.state:save() endend-- 調(diào)用 Java 方法需要的參數(shù)local args = { "001", -- billingIndex true, -- useSms true, -- isRepeated callback -- luaFunctionId}-- Java 類的名稱local className = "com/qeeplay/frameworks/ChinaMobile_SDK"-- 調(diào)用 Java 方法luaj.callStaticMethod(className, "GameInterface_doBilling", args) |
上面的代碼就不解釋了,注釋已經(jīng)寫得非常明白。
luaj 的核心目標(biāo)有兩個:從 Lua 調(diào)用 Java, 從 Java 調(diào)用 Lua。整理出來就是如下幾點:
查找并調(diào)用指定的 Java 方法檢查調(diào)用結(jié)果,并從 Java 方法獲取返回值將 Lua function 作為參數(shù)傳遞給 Java 方法在 Java 方法中調(diào)用 Lua functionJNI 提供了 FindClass() 方法用于查找指定的 Class,所以 luaj.callStaticMethod() 的第一個參數(shù)就是要調(diào)用的 Java Class 的完整類名稱(類名稱中的“.”要替換為“/”)。
找到指定 Class 后,利用 JNI 的 GetStaticMethodID() 方法就可以找到這個類的指定靜態(tài)方法,前提是要提供靜態(tài)方法的名稱和簽名。
所謂簽名,就是指 Java 方法的參數(shù)類型和返回類型定義。例如前面示例代碼中 GameInterface_doBilling() 方法的簽名是 (Ljava/lang/String;ZZI)V 。關(guān)于 Java 方法簽名的具體定義,可以參考:JNI Type Signatures。
由于簽名寫起來有點啰嗦,所以 luaj 可以根據(jù)調(diào)用參數(shù)自動猜測方法簽名。示例代碼中,luaj.callStaticMethod() 的第二個參數(shù)指定了要查找的方法名稱,但并沒有提供方法的簽名,這就是利用了 luaj 的自動猜測簽名功能。
示例代碼一共指定了 4 個參數(shù),分別是:字符串、布爾值、布爾值、Lua function。
| -- 調(diào)用 Java 方法需要的參數(shù)local args = { "001", -- billingIndex true, -- useSms true, -- isRepeated callback -- luaFunctionId} |
luaj 根據(jù)這 4 個參數(shù),會構(gòu)造出正確的 GameInterface_doBilling() 方法簽名。注意 Lua function 是以整數(shù)的形式傳入 Java 方法,所以 Java 方法的第四個參數(shù)是 int 類型)。
不幸的是 Lua 里沒有辦法準(zhǔn)確判斷一個數(shù)值是整數(shù)還是浮點數(shù),所以 luaj 在猜測方法簽名時,假定所有的數(shù)值都是浮點數(shù)。因此下面的代碼第二個調(diào)用就會失敗:
| local args = {1} -- 生成的方法簽名是 (F)V--[[Java 方法原型:public static void TestMethod1(final float integerValue)]]-- 調(diào)用成功luaj.callStaticMethod(className, "TestMethod1", args)--[[Java 方法原型:public static void TestMethod2(final int integerValue)]]-- 調(diào)用失敗,正確的方法簽名應(yīng)該是 (I)Vluaj.callStaticMethod(className, "TestMethod2", args) |
為此,luaj 允許開發(fā)者指定完整的方法簽名。而且除了整數(shù)和浮點數(shù)的情況,在需要從 Java 方法獲得返回值時,也需要開發(fā)者指定完整的方法簽名。示例代碼如下:
| local args ={"StringValue", 1, 3.14}--[[Java 方法原型:public static int TestMethod3(final String stringValue, final int integerValue, final float floatValue)]]-- 定義簽名-- 參數(shù): [S]tring, [I]nteger, [F]loat-- 返回值: [I]ntlocal sig = "(Ljava/lang/String;IF)I"-- 調(diào)用方法并獲得返回值local ok, ret = luaj.callStaticMethod(className, "TestMethod3", args, sig) |
~
簽名使用“(依次排列的參數(shù)類型)返回值類型”的格式,幾個例子如下:
| 簽名 | 解釋 |
|---|---|
| ()V | 參數(shù):無,返回值:無 |
| (I)V | 參數(shù):int,返回值:無 |
| (Ljava/lang/String;)Z | 參數(shù):字符串,返回值:布爾值 |
| (IF)Ljava/lang/String; | 參數(shù):整數(shù)、浮點數(shù),返回值:字符串 |
這里列出不同類型對應(yīng)的 Java 簽名字符串:
| 類型名 | 類型 |
|---|---|
| I | 整數(shù),或者 Lua function |
| F | 浮點數(shù) |
| Z | 布爾值 |
| Ljava/lang/String; | 字符串 |
| V | Void 空,僅用于指定一個 Java 方法不返回任何值 |
Java 方法里接收 Lua function 的參數(shù)必須定義為 int 類型,具體原因詳見“將 Lua function 作為參數(shù)傳遞給 Java 方法”小節(jié)。
~
luaj 調(diào)用 Java 方法時,可能會出現(xiàn)各種錯誤,因此 luaj 提供了一種機制讓 Lua 調(diào)用代碼可以確定 Java 方法是否成功調(diào)用。
luaj.callStaticMethod() 會返回兩個值:
當(dāng)成功時,第一個值為 true,第二個值是 Java 方法的返回值(如果有)。當(dāng)失敗時,第一個值為 false,第二個值是錯誤代碼。下面的代碼展示了如何檢查返回結(jié)果和獲得返回值:
Java 代碼| 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: 5end |
~
錯誤代碼定義如下:
| 錯誤代碼 | 描述 |
|---|---|
| -1 | 不支持的參數(shù)類型或返回值類型 |
| -2 | 無效的簽名 |
| -3 | 沒有找到指定的方法 |
| -4 | Java 方法執(zhí)行時拋出了異常 |
| -5 | Java 虛擬機出錯 |
| -6 | Java 虛擬機出錯 |
~
很多時候,我們需要一種方法讓 Java 代碼可以向 Lua 代碼傳遞一些消息。例如在大部分游戲平臺的 SDK 中,涉及支付的部分都是異步操作的。在支付操作結(jié)束后,Java 代碼需要通知 Lua 支付成功與否。
Lua 虛擬機中,Lua function 以值的形式保存。但這個值無法直接給 Java 用,所以 luaj 做了一個 Lua function 引用表。當(dāng)一個 Lua function 傳遞給 Java 時,這個 function 對應(yīng)的值會被存在引用表中,并獲得一個唯一的引用 ID (整數(shù))。Java 代碼拿到這個引用 ID 后,就可以很方便的調(diào)用該 Lua function 了。
回顧最開始的示例代碼,GameInterface_doBilling() 函數(shù)用于接收 Lua function 的參數(shù)就是 int 類型。因為實際傳入 Java 函數(shù)的值是 Lua function 的引用 Id。
~
在 Java 代碼中拿到 Lua function 的引用 ID 后,就可以很方便的調(diào)用該 Lua function 了:
| LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "hello"); |
這里出現(xiàn)的 LuaJavaBridge 是 luaj 的 Java 部分定義的工具 class。 callLuaFunctionWithString() 方法可以將一個字符串參數(shù)傳遞給指定的 Lua function。
LuaJavaBridge 還提供了 callLuaGlobalFunctionWithString() 方法,可以直接調(diào)用 Lua 中指定名字的全局函數(shù)。這樣可以在沒有 Lua function 引用 ID 的情況下和 Lua 代碼交互。
由于自己的項目暫時沒更多需求,所以目前 luaj 只支持向 Lua function 傳遞單個字符串參數(shù)。
~
cocos2d-x for Android 運行在多線程環(huán)境下,所以在 Lua 和 Java 交互時需要注意選擇適當(dāng)?shù)木€程。
~
cocos2d-x 在 Android 上以兩個線程來運行,分別是負(fù)責(zé)圖像渲染的 GL 線程和負(fù)責(zé) Android 系統(tǒng)用戶界面的 UI 線程。
在 cocos2d-x 啟動后,Lua 代碼將由 GL 線程調(diào)用,因此從 Lua 中調(diào)用的 Java 方法如果涉及到系統(tǒng)用戶界面的顯示、更新操作,那么就必須讓這部分代碼切換到 UI 線程上去運行。反之亦然,從 Java 調(diào)用 Lua 代碼時,需要讓這個調(diào)用在 GL 線程上執(zhí)行,否則 Lua 代碼雖然執(zhí)行了,但會無法更新 cocos2d-x 內(nèi)部狀態(tài)。下面是 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); } }); } ... }); } });} |
~
方法中,構(gòu)造了一個 Runnable 對象,用來包裝需要執(zhí)行的 Java 代碼。這個 Runnable 對象被指定運行在 UI 線程上。這樣當(dāng)調(diào)用 GameInterface.doBilling() 方法時就可以正確顯示出支付界面。
當(dāng)用戶支付成功后,GameInterface.doBilling() 會調(diào)用 BillingCallback.onBillingSuccess() 方法。這個方法里構(gòu)造了另一個 Runnable 對象,包裝了調(diào)用 Lua function 的代碼。
看上去代碼不少,實際上就是在兩個線程間互相切換。確保 Lua function 跑在 GL 線程,Java 代碼跑在 UI 線程。
~
Lua 虛擬機具有自動垃圾回收機制。Lua function 既然是值,那么在沒有被使用時自然會被回收掉。所以 luaj 提供了 retainLuaFunction() 和 releaseLuaFunction() 兩個函數(shù)用于增減 Lua function 的引用計數(shù)。
將一個 Lua function 以引用 ID 的形式傳入 Java 時,luaj 會自動增加引用 ID 的計數(shù)器,所以在 Java 方法里可以放心的異步調(diào)用 Lua function。但在不需要使用該 Lua function 后,一定要調(diào)用 releaseLuaFunction() 減少該引用 ID 的計數(shù)器。當(dāng)計數(shù)器為 0 時,會自動釋放該 Lua function。
如果了解 cocos2d-x 中 CCObject 的 autorelease 機制,那么對引用計數(shù)應(yīng)該很熟悉,兩者是完全相同的實現(xiàn)機制。
~
雖然 luaj 可以讓開發(fā)者從 Lua 中直接調(diào)用 Java 代碼。但大部分第三方 SDK 在初始化時都需要指定當(dāng)前應(yīng)用程序的 Activity 對象,并且還要切換不同線程,所以對于大多數(shù)第三方 SDK,我們?nèi)匀灰獙懸粋€中間層用于 Lua 和 Java 的交互。
與使用 JNI 做中間層相比,配合 luja 的中間層是使用 Java 來編寫的,不但更簡單明了,而且處理線程切換也非常簡單。
~
要實現(xiàn)一個中間層,只有兩個步驟:
實現(xiàn)供 luaj 調(diào)用的 Java 接口修改游戲的 Java 入口文件,將應(yīng)用程序的 Activity 對象傳入 SDK第一步請參考:“中國移動游戲基地和短信支付 SDK”中間層源代碼
第二步也相當(dāng)簡單,只需要在游戲的 onCreate() 中調(diào)用 中間層 class 的 setContext() 方法:
| public class mygame extends Cocos2dxActivity { protected void onCreate(Bundle savedInstanceState) { ChinaMobile_SDK.setContext(this); // init sdk super.onCreate(savedInstanceState); } ...} |
~
做好一切準(zhǔn)備工作后,在游戲的 Lua 代碼里訪問 SDK 功能就很簡單了:
| local luaj = require("luaj")local className = "com/qeeplay/frameworks/ChinaMobile_SDK"-- 初始化 SDKlocal 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 -- 支付成功 endendlocal 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) |
~
luaj 分為三個部分:
LuaJavaBridge.java, com_qeeplay_frameworks_LuaJavaBridge.h/.cpp - 供 Java 端使用的工具類,包含 Java 接口定義文件和 JNI 實現(xiàn)。LuaJavaBridge.h/.cpp - 供 Lua 端使用的工具類。luaj.lua - LuaJavaBridge 的 Lua 包裝,提供更簡單和靈活的接口。下載地址:
Java/C++ 部分源代碼Lua 部分源代碼~
步驟:
將 LuaJavaBridge.java 添加到 Android 項目中;修改 proj.android/jni/Android.mk:| LOCAL_SRC_FILES := ... / luaj/jni/com_qeeplay_frameworks_LuaJavaBridge.cpp / luaj/luabinding/LuaJavaBridge.cppLOCAL_C_INCLUDES := ... / luaj |
| #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)#include "LuaJavaBridge.h"#endifbool AppDelegate::applicationDidFinishLaunching(){ ... CCLuaEngine* pEngine = CCLuaEngine::defaultEngine(); CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) LuaJavaBridge_luabinding_open(pEngine->getLuaState());#endif ...} |
| jint JNI_OnLoad(JavaVM *vm, void *reserved){ ... LuaJavaBridge_setJavaVM(vm); return JNI_VERSION_1_4;} |
~
[Lua] luaj.callStaticMethod(className, methodName, args, methodSig)
調(diào)用指定的 Java class static method,允許傳入 int/float/boolean/string/function 五種類型的參數(shù)。
[Java] LuaJavaBridge.callLuaFunctionWithString(int luaFunctionId, String value)
調(diào)用引用 ID 指向的 Lua function,并傳入一個字符串作為參數(shù)。
[Java] LuaJavaBridge.callLuaGlobalFunctionWithString(int luaFunctionId, String value)
調(diào)用指定名字的 Lua 全局函數(shù),并傳入一個字符串作為參數(shù)。
[Java] LuaJavaBridge.retainLuaFunction(int luaFunctionId)
增加引用 ID 的計數(shù),確保 Lua function 不會被 Lua 虛擬機自動回收。
[Java] LuaJavaBridge.releaseLuaFunction(int luaFunctionId)
減少引用 ID 的計數(shù),當(dāng)計數(shù)等于 0 時,引用 ID 指向的 Lua function 將被回收。
~
因為我們自己的項目暫時還沒有更復(fù)雜的需求,所以 luaj 目前的實現(xiàn)很簡單。但要在這個基礎(chǔ)上進(jìn)行完善是很容易的事情,luaj 已經(jīng)解決了幾個關(guān)鍵性問題。
未來計劃會增加的主要特性就是支持更多的類型,例如將一個以字符串為鍵名的 Lua table 以 Java Map 集合的形式傳遞給 Java。同樣,從 Java 調(diào)用 Lua 函數(shù)時,也應(yīng)該支持多個參數(shù),以及更多的參數(shù)類型。
至于將 Java 對象傳入 Lua,并在 Lua 中調(diào)用 Java 對象的方法,目前沒這個打算。因為 luaj 的主要目的是為 cocos2d-x 游戲服務(wù),而 cocos2d-x 的多線程模式要求 Lua 和 Java 代碼必須在不同的線程里運行。如果在 Lua 中調(diào)用 Java 對象方法將面對許多復(fù)雜的問題。與其花大量時間去解決這個問題(還不一定能保證最后簡單易用),不如簡單寫一個中間層。
最后,luaj 已經(jīng)被集成到了 quick-cocos2d-x 這個基于 cocos2d-x 的快速游戲開發(fā)引擎中。quick-cocos2d-x 讓開發(fā)者可以使用 Lua 語言開發(fā)高質(zhì)量的商業(yè)游戲,同時又保持 cocos2d-x 的高性能、開放性、可擴展能力。并且 quick-cocos2d-x 使用最新的 LuaJIT 實現(xiàn),可以讓 Lua 腳本獲得數(shù)倍到數(shù)十倍的性能提升。
新聞熱點
疑難解答
圖片精選