記Android層執(zhí)行Lua腳本的一次實(shí)踐

0. 前言

????最近一直在寫Lua腳本,有時候出了問題,不知道是Lua層的問題,還是上游的問題,不知道從何下手。于是我學(xué)習(xí)了一點(diǎn)C/C++和JNI,把整個解析Lua腳本包、執(zhí)行Lua腳本的流程全部都讀了一遍。熟悉了一遍之后,就萌生了自己封一個Android跑Lua腳本庫的想法。于是就有這篇博文。C/C++和Kotlin我都不熟,所以這次我主要用這兩種語言來寫(所以會很Java Style)。

1. 環(huán)境搭建

????首先現(xiàn)在Lua官網(wǎng)下載Lua的源碼,我用的是5.3.5版本的。然后把源碼導(dǎo)入到Project中,寫好CMakeList:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_definitions(-Wno-deprecated)

add_library( # Sets the name of the library.
        luabridge

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        src/main/jni/lua/lapi.c
        src/main/jni/lua/lauxlib.c
        src/main/jni/lua/lbaselib.c
        src/main/jni/lua/lbitlib.c
        src/main/jni/lua/lcode.c
        src/main/jni/lua/lcorolib.c
        src/main/jni/lua/lctype.c
        src/main/jni/lua/ldblib.c
        src/main/jni/lua/ldebug.c
        src/main/jni/lua/ldo.c
        src/main/jni/lua/ldump.c
        src/main/jni/lua/lfunc.c
        src/main/jni/lua/lgc.c
        src/main/jni/lua/linit.c
        src/main/jni/lua/liolib.c
        src/main/jni/lua/llex.c
        src/main/jni/lua/lmathlib.c
        src/main/jni/lua/lmem.c
        src/main/jni/lua/loadlib.c
        src/main/jni/lua/lobject.c
        src/main/jni/lua/lopcodes.c
        src/main/jni/lua/loslib.c
        src/main/jni/lua/lparser.c
        src/main/jni/lua/lstate.c
        src/main/jni/lua/lstring.c
        src/main/jni/lua/lstrlib.c
        src/main/jni/lua/ltable.c
        src/main/jni/lua/ltablib.c
        src/main/jni/lua/ltm.c
        src/main/jni/lua/lua.c
        #src/main/jni/lua/luac.c
        src/main/jni/lua/lundump.c
        src/main/jni/lua/lutf8lib.c
        src/main/jni/lua/lvm.c
        src/main/jni/lua/lzio.c)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        luabridge

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

????我要跑是 *.lua 類型的腳本,那就留下lua.c并刪掉luac.c,CMakeList里面也要跟著注釋掉。另外,因?yàn)槲野袻ua的源代碼導(dǎo)入進(jìn)來當(dāng)做一個庫,所以也不需要main方法了,把lua.c里面的main方法注釋掉。最后Rebuild一下Project就可以了。

2. Android單向調(diào)用Lua

????先定一個小目標(biāo),Android層調(diào)用Lua層的函數(shù),Lua層做一個加法后把結(jié)果返回給Android層。先寫好Lua腳本:

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

????這個Lua腳本很簡單,把傳過來的a和b相加后返回。現(xiàn)在我們可以開始考慮Native層的實(shí)現(xiàn)。在考慮實(shí)現(xiàn)之前,需要了解Lua虛擬棧和幾個Lua C API。

2.1. Lua虛擬棧

????Lua層和Native層的數(shù)據(jù)交換是通過Lua虛擬棧來完成的。這個虛擬棧和普通的棧略有不同,它可以通過負(fù)值索引來訪問指定元素。如圖:

Lua虛擬棧

????和普通的棧一樣,Lua虛擬棧同樣遵循先進(jìn)后出原則,索引從下往上增加。不同的是Lua虛擬棧支持負(fù)值索引,使用負(fù)值索引可以自棧頂向下索引。

2.2. Lua C APIs

????Lua提供了C APIs,方便Native層和Lua層之間的通訊。下面的Demo會用到這幾個C API。

  • lua_State *luaL_newstate (void);

    新建一個Lua的context。

  • int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);

    編譯一個Lua chunk。如果編譯成功,它會把編譯結(jié)果包裝成一個函數(shù),并把這個函數(shù)推入到棧中;否則,編譯失敗,它會把錯誤信息推入棧中。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    buff const char* 需要加載的Lua腳本buffer
    sz size_t Lua腳本buffer的長度
    name const char* 這個chunk的名稱,可空
  • int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);

    以安全模式調(diào)用一個函數(shù),即使拋出異常也不會崩潰。當(dāng)拋出異常時,如果errfunc為0,Lua虛擬機(jī)會把錯誤信息推入到Lua虛擬棧中,如果errfunc不為0,則錯誤處理會交由Lua虛擬棧中索引為errfunc的函數(shù)處理。執(zhí)行結(jié)束后,Lua虛擬機(jī)會把參數(shù)以及調(diào)用的函數(shù)從棧中彈出。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    nargs int 需要調(diào)用的函數(shù)的參數(shù)個數(shù)
    nresults int 需要調(diào)用的函數(shù)的返回結(jié)果個數(shù)
    errfunc int 錯誤處理函數(shù)在Lua虛擬棧中的索引,如果為0,錯誤信息會推入到Lua虛擬棧中
  • void lua_getglobal (lua_State *L, const char *name);

    獲取名字為name的全局變量,并推入棧中。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    name const char* 變量名稱
  • void lua_pushinteger (lua_State *L, lua_Integer n);

  • 推入一個lua_Integer類型的數(shù)據(jù)到棧中

    參數(shù) 類型 說明
    L lua_State* Lua的context
    n lua_Integer 需要推入的數(shù)字
  • lua_Integer lua_tointeger (lua_State *L, int index);

    將棧中的索引為index的元素轉(zhuǎn)lua_Integer并返回

    參數(shù) 類型 說明
    L lua_State* Lua的context
    index int 指定元素在棧中的索引

????除了這些C API,其他的介紹及其用法可以查看官網(wǎng)的說明

????通過理解Lua虛擬棧和了解一些Lua C API,我們就可以實(shí)現(xiàn)一個簡單的Native層調(diào)用Lua層函數(shù)的功能。

jint startScript(JNIEnv* env, jobject obj, jstring jLuaStr, jint a, jint b) {
    // 創(chuàng)建一個lua context
    lua_State* luaContext = lua_newstate();
    // 初始化lua lib
    luaL_openlibs(luaContext);
    const char* cLuaStr = env->GetStringUTFChars(jLuaStr, NULL);
    
    // 加載buff到內(nèi)存
    int loadStatus = luaL_loadbuffer(luaContext, cLuaStr, strlen(cLuaStr), NULL);
    if (LUA_OK != loadStatus) {
        const char *szError = luaL_checkstring(luaContext, -1);
        Log_e(LOG_TAG, "%s", szError);
        return -1;
    }
    env->ReleaseStringUTFChars(jLuaStr, cLuaStr);
    int callStatus = lua_pcall(luaContext, 0, LUA_MULTRET, 0);
    if (LUA_OK != callStatus) {
        const char *szError = luaL_checkstring(luaContext, -1);
        Log_e(LOG_TAG, "%s", szError);
        return -1;
    }
    
    // 獲取test方法
    lua_getglobal(luaContext, "test");
    if (LUA_TFUNCTION != lua_type(luaContext, -1)) {
        Log_d(LOG_TAG, "can not found func : %s", "test");
        return false;
    }
    
    // 推入?yún)?shù)
    lua_pushinteger(luaContext, a);
    lua_pushinteger(luaContext, b);
    
    // 執(zhí)行test方法
    int callTestStatus = lua_pcall(luaContext, 2, 1, 0);
    if(LUA_OK == callTestStatus) {
        int ret = lua_tointeger(luaContext, 1)
        return ret;
    } else {
        const char* errMsg = lua_tostring(luaContext, 1)
        Log_e(LOG_TAG, "%s", errMsg);
        return -1;
    }
}

????流程如注釋。在這一個過程中,Lua虛擬棧的內(nèi)容變化如圖,從luaL_loadbuffer開始:

Lua虛擬棧內(nèi)容變化

????首先,經(jīng)過luaL_loadbuffer之后,luaL_loadbuffer會把傳過來的*.lua文件的buffer作為一個Lua Chunk,接著編譯它。編譯完后,把編譯結(jié)果包裝成一個function并推入Lua虛擬棧中。經(jīng)過lua_pcall后,Lua虛擬機(jī)會把所執(zhí)行的function及其參數(shù)從Lua虛擬棧中彈出。接著,通過lua_getglobal獲取Lua層的全局變量「test」,lua_getglobal會把這個變量的值推入Lua虛擬棧中。函數(shù)已經(jīng)準(zhǔn)備好,再經(jīng)過lua_pushinteger(a)和lua_pushinteger(b)后,函數(shù)和參數(shù)都已經(jīng)順序推入了,調(diào)用lua_pcall的先決條件已經(jīng)滿足。接下來,調(diào)用lua_pcall后,Lua虛擬機(jī)會根據(jù)調(diào)用lua_pcall是傳入的nresults,將結(jié)果推入Lua虛擬棧中。最后,我們只需要lua_tointeger(index)來獲取執(zhí)行結(jié)果,返回給Android層即可。可以看到,自始至終,Lua虛擬棧充當(dāng)一個數(shù)據(jù)交換的橋梁,是一個十分重要的角色。

????接下來,只需要在Native層Register一下NativeMethods,并在Android層聲明一下native方法就可以使用了。

class LuaExecutor {
    init {
        System.loadLibrary("luabridge")
    }

    external fun startScript(luaString: String): Boolean
}

????然而,上面的實(shí)現(xiàn)只有啟動腳本的功能。在實(shí)際中,我們總不可能啟動腳本之后,就沒有對腳本執(zhí)行流程有一點(diǎn)控制吧。因此有必要加一個停止腳本的功能。如何停止正在執(zhí)行的腳本?先來看看Lua提供的C API:

  • int luaL_error (lua_State *L, const char *fmt, ...);

    拋出一個異常,錯誤信息為fmt。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    fmt const char* 錯誤信息
  • int lua_sethook (lua_State *L, lua_Hook f, int mask, int count);

    設(shè)置一個鉤子函數(shù)。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    f lua_Hook 鉤子函數(shù),包含需要執(zhí)行的語句
    mask int 指定被調(diào)用的時機(jī),取值為常量LUA_MASKCALL,LUA_MASKRET,LUA_MASKLINE和LUA_MASKCOUNT的按位或。
    mask取值 說明
    LUA_MASKCALL 代表鉤子函數(shù)f會在進(jìn)入任意函數(shù)后執(zhí)行
    LUA_MASKRET 代表鉤子函數(shù)在退出任意函數(shù)前執(zhí)行
    LUA_MASKLINE 代表鉤子函數(shù)f會在執(zhí)行函數(shù)內(nèi)一行代碼前執(zhí)行
    LUA_MASKCOUNT 代表鉤子函數(shù)f會在lua解釋器執(zhí)行了count條指令后執(zhí)行

????有了這兩個C API,腳本的停止功能就可以實(shí)現(xiàn)了:

void stopLuaHooker(lua_State *L, lua_Debug *ar) {
    luaL_error(L, "quit Lua");
}

void forceStopLua(lua_State *L) {
    int mask = LUA_MASKCOUNT;
    lua_sethook(L, &stopLuaHooker, mask, 1);
}

????當(dāng)我們調(diào)用forceStopLua時,會為Lua腳本的執(zhí)行設(shè)置一個鉤子函數(shù),這個鉤子函數(shù)的執(zhí)行時機(jī)是:lua_sethook執(zhí)行之后,Lua解釋器執(zhí)行完一條指令時。也就是說,我們在Lua層代碼執(zhí)行到任意地方時調(diào)用forceStopLua后,Lua解釋器會在執(zhí)行完一條指令后,接著執(zhí)行stopLuaHooker,進(jìn)而執(zhí)行l(wèi)ua_error,拋出異常,腳本即終止。因此,腳本的啟動和停止的功能已經(jīng)實(shí)現(xiàn)好了,封到一個類里,叫做LuaEngine:

#ifndef ANDROIDLUA_LUAENGINE_H
#define ANDROIDLUA_LUAENGINE_H

#include <cstring>
#include <string>
#include <jni.h>
#include "lua/lua.hpp"

#include "utils/Log.h"
#include "JniManager.h"

#define LOG_TAG "LuaEngine"

class LuaEngine {
public:
    LuaEngine();

    virtual ~LuaEngine();

    lua_State *getScriptContext() {
        return mScriptContext;
    }

    bool startScript(jstring jBuff, const char *functionName);

    bool isScriptRunning() {
        return scriptRunning;
    }

    bool stopScript();

private:
    lua_State *mScriptContext;
    bool scriptRunning;

    bool loadBuff(jstring jBuff);

    bool runLuaFunction(const char *functionName);
};

void quitLuaThread(lua_State *L);

void quitLuaThreadHooker(lua_State *L, lua_Debug *ar);

#endif //ANDROIDLUA_LUAENGINE_H
#include "LuaEngine.h"

LuaEngine::LuaEngine() {
    mScriptContext = luaL_newstate();
    scriptRunning = false;
}

LuaEngine::~LuaEngine() {
    if (isScriptRunning()) {
        stopScript();
    }
    mScriptContext = nullptr;
}

bool LuaEngine::startScript(jstring jBuff, const char *functionName) {
    scriptRunning = true;
    luaL_openlibs(mScriptContext);
    if (this->loadBuff(jBuff)) {
        Log_d(LOG_TAG, "script start running..");
        bool success = this->runLuaFunction(functionName);
        scriptRunning = false;
        return success;
    } else {
        scriptRunning = false;
        return false;
    }
}

bool LuaEngine::stopScript() {
    if (scriptRunning) {
        quitLuaThread(mScriptContext);
        scriptRunning = false;
        return true;
    } else {
        Log_d(LOG_TAG, "script is Not running");
        return false;
    }
}

bool LuaEngine::loadBuff(jstring jBuff) {
    // 讀取buff
    JNIEnv *env;
    JniManager::getInstance()->getJvm()->GetEnv((void **) &env, JNI_VERSION_1_6);
    const char *cBuff = env->GetStringUTFChars(jBuff, nullptr);
    if (LUA_OK != luaL_loadbuffer(mScriptContext, cBuff, strlen(cBuff), NULL)) {
        const char *szError = luaL_checkstring(mScriptContext, -1);
        Log_e(LOG_TAG, "%s", szError);
        return false;
    }
    // 加載buff到內(nèi)存
    if (LUA_OK != lua_pcall(mScriptContext, 0, LUA_MULTRET, 0)) {
        const char *szError = luaL_checkstring(mScriptContext, -1);
        Log_e(LOG_TAG, "%s", szError);
        return false;
    }
    env->ReleaseStringUTFChars(jBuff, cBuff);
    env->DeleteGlobalRef(jBuff);
    return true;
}

bool LuaEngine::runLuaFunction(const char *functionName) {
    // 獲取errorFunc
    // 錯誤由__TRACKBACK__來處理,可以用來打印錯誤信息,
    // __TRACKBACK__函數(shù)需要自己定義在lua腳本中
    lua_getglobal(mScriptContext, "__TRACKBACK__");
    if (lua_type(mScriptContext, -1) != LUA_TFUNCTION) {
        Log_d(LOG_TAG, "can not found errorFunc : __TRACKBACK__");
        return false;
    }
    int errfunc = lua_gettop(mScriptContext);

    // 獲取指定的方法
    lua_getglobal(mScriptContext, functionName);
    if (lua_type(mScriptContext, -1) != LUA_TFUNCTION) {
        Log_d(LOG_TAG, "can not found func : %s", functionName);
        return false;
    }

    // 跑指定的方法
    return LUA_OK == lua_pcall(mScriptContext, 0, 0, errfunc);
}

void quitLuaThread(lua_State *L) {
    int mask = LUA_MASKCOUNT;
    lua_sethook(L, &quitLuaThreadHooker, mask, 1);
}

void quitLuaThreadHooker(lua_State *L, lua_Debug *ar) {
    luaL_error(L, "quit Lua");
}

3. Lua單向調(diào)用Android

????前面的實(shí)現(xiàn),只允許Android層調(diào)用Lua的方法,而Lua層并不能調(diào)用Android層的方法。可不可以在Lua層調(diào)用Android層的方法?答案是可以的。一個思路是,Lua層調(diào)用Native層的方法,Native層再通過反射調(diào)用Android層的方法。先看看Lua層是怎么調(diào)用Native層的方法。Lua提供了一個C API:lua_register,它的原型是:

  • void lua_register (lua_State *L, const char *name, lua_CFunction f);

    注冊一個CFunction。

    參數(shù) 類型 說明
    L lua_State* Lua的context
    name const char* Lua層全局變量的名稱
    f lua_CFunction C函數(shù)。原型是:int functionXXX(lua_State* L);其返回值的意義代表返回結(jié)果的個數(shù)。

????我們可以用這個C API實(shí)現(xiàn)Lua層調(diào)用Native層的方法:

lua_register(mScriptContext, "getString" , getString);

int getString(lua_State *L) {
    const char *cStr = "String From C Layer";
    lua_pushstring(L, cStr);
    return 1;
}

????上面的代碼很簡單,先注冊一個名字為getString的全局變量,指向C函數(shù)getString。C函數(shù)getString中,先聲明并分配一個字符串cStr,再把這個字符串推入到Lua棧中,并返回結(jié)果個數(shù)。因此,在Lua層,如果執(zhí)行g(shù)etString(),則會得到字符串"String From C Layer",Lua層就可以調(diào)用Native層的方法了。

????然后看看Native層調(diào)用Android層的方法。代碼如下:

int getString(lua_State *L) {
    JNIEnv* env;
    g_pJvm->GetEnv((void **) &env, JNI_VERSION_1_6);
    
    jclass clazz = env->FindClass("com/zspirytus/androidlua/shell/ShellBridge");
    if (!clazz) {
        Log_d(LOG_TAG, "class not found!");
        return 0;
    }
    
    jmethodID methodId = env->GetStaticMethodID(clazz, "getStringFromKotlinLayer", "()Ljava/lang/String;");
    if (!methodId) {
        Log_d(LOG_TAG, "method %s not found!", "getStringFromStaticJavaMethod");
        return 0;
    }
    
    jstring jStr = (jstring) env->CallStaticObjectMethod(clazz, methodId);
    
    const char *cStr = env->GetStringUTFChars(jStr, NULL);
    lua_pushstring(L, cStr);
    env->ReleaseStringUTFChars(jStr, cStr);
    env->DeleteLocalRef(jStr);
    return 1;
}

????解釋一下,首先通過在JNI_OnLoad保存下來的JavaVM指針指針獲得Jni的環(huán)境變量,再用Jni的環(huán)境變量找到class和method,最后通過env、class和method反射調(diào)用Android層的方法獲得返回的jstring,轉(zhuǎn)成C-style的string后推入lua棧中,釋放資源,并返回結(jié)果個數(shù)。

????在Android層,留下一個方法以供調(diào)用:

@Keep
object ShellBridge {

    private val TAG = ShellBridge.javaClass.simpleName

    @Keep
    @JvmStatic
    fun getStringFromKotlinLayer(): String {
        return "String From Android Layer"
    }
}

????至此,Android層與Lua層的交互已經(jīng)實(shí)現(xiàn)了。

4. 避免ANR

????然而上面的實(shí)現(xiàn)可能會導(dǎo)致ANR,原因在于Lua腳本的執(zhí)行可能是耗時的。如果Lua腳本的執(zhí)行時間超過5秒,必然ANR。一個解決方法是,把Lua腳本的執(zhí)行放到子線程當(dāng)中。這個子線程應(yīng)當(dāng)給Native層管理比較好,還是Android層管理比較好?我個人覺得放在Native層比較好,這樣Android層就不需要專為執(zhí)行Lua腳本而新建和管理線程,代碼就不會太復(fù)雜;即使Native層的邏輯比較復(fù)雜,編好了so,一般就會當(dāng)做一個庫來使用,而不會去動它。所以,還是在Native層創(chuàng)建和管理線程。
pthread_create是Unix、Linux等系統(tǒng)創(chuàng)建線程的函數(shù),它的原型是:

  • int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

    參數(shù) 類型 說明
    tidp pthread_t *restrict 線程ID
    attr const pthread_attr_t*restrict 線程屬性,默認(rèn)為NULL
    *(*start_rtn)(void *) void 運(yùn)行在新線程的函數(shù)
    *restrict arg void start_rtn的所需參數(shù)

????因此,我們可以把執(zhí)行Lua腳本的邏輯移到新線程中:

void startWork() {
    pthread_create(&mThreadId, NULL, &startWorkInner, (void*)this);
}

void stopWork() {
    stopScript();
    mThreadId = 0;
}

void* startWorkInner(void *args) {
    startScript();
    return nullptr;
}

????這樣,startScript()就運(yùn)行在新線程中,就不會有ANR的風(fēng)險。我們把它封到一個類中,叫LuaTask,一次Lua腳本的開始與結(jié)束,都由這個類來管理。

#ifndef ANDROIDLUA_LUATASK_H
#define ANDROIDLUA_LUATASK_H

#include <sys/types.h>
#include <pthread.h>
#include <jni.h>

#include "LuaEngine.h"

class LuaTask {

public:
    LuaTask(jstring jBuff);

    virtual ~LuaTask();

    void startWork();

    void stopWork();

    bool isRunning();

private:
    static void *startWorkInner(void *args);

private:
    jstring mLuaBuff;
    pthread_t mThreadId;
    LuaEngine *mLuaEngine;
};

#endif //ANDROIDLUA_LUATASK_H
#include "LuaTask.h"

LuaTask::LuaTask(jstring jBuff) {
    mLuaBuff = jBuff;
    mLuaEngine = new LuaEngine();
    mThreadId = 0;
}

LuaTask::~LuaTask() {
    delete mLuaEngine;
}

void LuaTask::startWork() {
    pthread_create(&mThreadId, NULL, &LuaTask::startWorkInner, (void*)this);
}

void LuaTask::stopWork() {
    mLuaEngine->stopScript();
    mThreadId = 0;
}

void* LuaTask::startWorkInner(void *args) {
    LuaTask* task = (LuaTask*) args;
    task->mLuaEngine->startScript(task->mLuaBuff, "main");
    return nullptr;
}

bool LuaTask::isRunning() {
    return mThreadId != 0;
}

????但是,這是我們新創(chuàng)建的線程,還沒有attach到JavaVM。如果沒有attach到JavaVM,就會找不到JNIEnv,所以必須要attach到JavaVM,這樣才能拿到JavaVM的JNI環(huán)境變量,從而可以調(diào)用到Android層的方法。因此startWorkInner要改進(jìn)一下:

void* startWorkInner(void *args) {
    JNIEnv* env = nullptr;
    JavaVMAttachArgs args{JNI_VERSION_1_6, nullptr, nullptr};
    g_pJvm->AttachCurrentThread(&env, &args);
    startScript()
    g_pJvm->DetachCurrentThread();
    return nullptr;
}

????線程退出之前,記得要和JavaVM detach一下,這樣線程才能正常退出。

5. 運(yùn)行腳本包

????至此,我們完成了能夠隨時開始、停止,出錯能打印堆棧信息的執(zhí)行Lua腳本功能。但實(shí)際上,我們不可能只跑單個腳本,并且腳本可能需要一些資源文件。因此我們一般會把腳本和資源文件打包成一個腳本包。在運(yùn)行腳本之前,先解包,把腳本解析出來后再運(yùn)行。
所以這個解析腳本的邏輯放在Native層還是Android層?我個人覺得放在Android層比較好。有兩點(diǎn)原因:

  1. 腳本包格式不確定,Native層不可能為每種情況進(jìn)行適配,既然如此那就交由使用者來解析。
  2. 單一職責(zé)的原則,Native層負(fù)責(zé)還是只負(fù)責(zé)一種功能比較好。而且為解析腳本包而重新編譯一個so文件又太小題大做,所以解析的任務(wù)就交給使用者吧。

????既然提到腳本包,我就簡單談?wù)勎业膶?shí)現(xiàn)。我的實(shí)現(xiàn)是把lua腳本和資源文件一起壓縮成一個zip文件,在zip文件中有一個config文件,里面寫好了所有l(wèi)ua腳本的相對路徑。在解析的時候,先在內(nèi)存中把config解壓出來,讀出所有l(wèi)ua腳本的相對路徑,然后在內(nèi)存中把所有l(wèi)ua腳本文件都解壓出來后,拼接起來,在交給Native層運(yùn)行。至于資源文件,根據(jù)腳本的運(yùn)行情況進(jìn)行動態(tài)解壓。我簡單的封裝了一下:

private external fun startScript(luaString: String): Boolean
external fun stopScript(): Boolean
external fun isScriptRunning(): Boolean

fun runScriptPkg(scriptPkg: File, configFile: String) {
    mThreadPool?.execute {
        val start = System.currentTimeMillis()
        initScriptPkg(scriptPkg)
        val zipFile = ZipFile(scriptPkg)
        val config = ZipFileUtils.getFileContentFromZipFile(zipFile, configFile)
        val luaScriptPaths = config.split("\r\n")
        val luaScript = ZipFileUtils.getFilesContentFromZipFile(zipFile, luaScriptPaths)
        Log.d("USE_TIME", "${System.currentTimeMillis() - start} ms")
        mHandler?.post {
            startScript(luaScript)
        }
    }
}

object ZipFileUtils {

    fun getFileContentFromZipFile(zipFile: ZipFile, targetFile: String): String {
        var ins: InputStream? = null
        try {
            val ze = zipFile.getEntry(targetFile)
            return if (ze != null) {
                ins = zipFile.getInputStream(ze)
                FileUtils.readInputStream(ins)
            } else {
                ""
            }
        } finally {
            ins?.close()
        }
    }

    fun getFilesContentFromZipFile(zipFile: ZipFile, targetFiles: List<String>): String {
        val stringBuilder = StringBuilder()
        targetFiles.filter { it.isNotEmpty() and it.isNotBlank() }.forEach {
            val content = getFileContentFromZipFile(zipFile, it)
            stringBuilder.append(content).append('\n')
        }
        return stringBuilder.toString()
    }
}

object FileUtils {

    fun readInputStream(ins: InputStream): String {
        return ins.bufferedReader().use(BufferedReader::readText)
    }
}

????至此,我們在原有功能的基礎(chǔ)上,增加了跑腳本包的功能。完整的代碼可以看倉庫

6. 總結(jié)

Android層調(diào)用Lua層方法
Lua層調(diào)用Android層方法

7. 感想

????Android跑Lua腳本這個過程其實(shí)是很簡單的,不是主要難點(diǎn)。這次主要卡住的地方是在JNI部分,因?yàn)槲野l(fā)現(xiàn)我所了解的C語言語法太古老了,跟不上現(xiàn)在的C語言。雖然我的C語言的代碼量也不多,加上我對JNI的一些編程規(guī)范不太了解,所以一路磕磕絆絆,但是總算是寫出來了。Kotlin和C/C++還是要多熟悉熟悉,多練練。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,380評論 2 379