“愤怒的小鸟”中,lua脚本是运行时部署在应用的data目录,同时做了脚本加密;
但是如果你的lua脚本有了模块包,脚本就会有多级目录,而android的data目录下是不允许有子目录;
当然,lua脚本也可以在运行时拷贝到外置sd卡中,不过既然放置在不安全的公共目录,就会有被第三方删除或篡改的危险,那么每次启动的时候必须做一次文件校验,同时,也必须对脚本进行加密,避免程序逻辑暴露;

lua自定义加载器

lua引擎提供了一个自定义加载器,当lua脚本中调用require时,会回调到自定义加载器的CFunction;利用这样特性,可以尝试取消掉脚本的运行时拷贝,而直接从应用的asset资源目录加载;自定义加载器的原理中,云风的blog中有介绍,以自定义方式加载lua模块
直接贴个代码实现

void addAssetLuaLoader(lua_State *L, lua_CFunction func)
{
   if (!func) return;

   lua_getglobal(L, "package");                   
   lua_getfield(L, -1, "loaders");   
   lua_pushcfunction(L, func);  
   int i=0;
   for ( i = (int)lua_objlen(L, -2) + 1; i > 2; --i)
   {
      lua_rawgeti(L, -2, i - 1);    
      lua_rawseti(L, -3, i);    
   }
   lua_rawseti(L, -2, 2);
   lua_setfield(L, -2, "loaders");                       
   lua_pop(L, 1);
}

在lua引擎启动时,调用addAssetLuaLoader进行初始化,把你的CFunction加入到到lua的package.loader表中;
其中asset lua Loader的逻辑是这样的

    //搜索路径
    std::string searchpath("?.lua;?/init.lua;");
   size_t next = searchpath.find_first_of(";", 0);
   do
   {
      if (next == std::string::npos) next = searchpath.length();
      std::string prefix = searchpath.substr(begin, next-begin);
      if (prefix[0] == '.' && prefix[1] == '/')
      {
         prefix = prefix.substr(2);
      }
      pos = prefix.find("?");
      chunkName = prefix.replace(pos, 1, filename);
      //从asset目录读取字节流
      chunk =  getAssetData(chunkName,&chunkSize); 
      if(chunk){
        break;
      }   
      begin = next + 1;
      next = searchpath.find_first_of(";", begin);
   } while (begin < (int)searchpath.length());

   if (chunk)
   {
    // 解开加密lua
      loadencryrtbuffer(L, (char*)chunk, (int)chunkSize, chunkName.c_str());
      delete []chunk;
   }

当lua脚本中调用require时,asset lua loader就会回调得到一个fileName,将filename增补后缀名,转化成完整的asset相对文件路径,读取文件字节流;
这段字节流可以直接是经过加密的字节流,也可以是未加密的原始lua脚本,然后通过luaL_loadbuffer调用执行;

asset资源的读取

android2.3之后,ndk提供了直接操作asset资源的c api,可以在c中直接读取asset文件

unsigned char* data = 0;
//filename也就是上个代码块中的chunkName,即lua文件在asset的相对目录
    AAsset* asset = AAssetManager_open(gAssetManager ,filename.c_str(), AASSET_MODE_UNKNOWN);
    if(asset){
        off_t fileSize = AAsset_getLength(asset);
        data = (unsigned char*) malloc(fileSize);
        int bytesread = AAsset_read(asset, (void*)data, fileSize);
        if (size)
        {
              *size = bytesread;
        }
        AAsset_close(asset);
    }
    return data;

不过lua脚本的位置移动asset目录后,lua脚本常用的代码热更新机制就遇到些许问题,毕竟不可能将热更新代码重新写入到dex文件中;
就时候,机可以考虑将lua脚本进行模块化拆分,需要热更新的脚本部署在sd卡中,其他固化在APK的资源包中,随版本更新进行迭代;
既然有了自定义加载器,动态代码的热更新实现就简单多了。

Comments