兄弟,如果你正在为上线后修Bug还要等3天审核而头疼,或者因为一个文案错误就要重新发版,那这篇教程就是为你准备的。咱们不整那些虚头巴脑的理论,直接上干货,手把手带你从零搭建一套企业级的Lua热更新系统。我会把这里面的坑、原理、代码全部揉碎了讲清楚,保证你看完就能动手写。
第一章:为什么要用Lua做热更新?先搞懂底层逻辑
在动手写代码之前,你必须明白一个核心问题:为什么是Lua,而不是JavaScript或者C#?
1.1 语言特性的“天选之子”
Lua之所以成为热更新的首选,主要归功于三个特性:
第一,极小的嵌入成本。 Lua解释器只有200KB左右,嵌入到任何平台都毫无压力。相比之下,V8引擎(JavaScript)要几MB,CLR(C#)更是要绑定整个运行时。对于游戏或APP这种对安装包体积敏感的产品,Lua简直是轻骑兵。
第二,与C/C++的天然桥梁。 Lua是专门为嵌入设计的语言。它的栈机制(Stack)虽然初学时有点绕,但一旦掌握,你可以通过它完美地调用iOS的Objective-C/Swift代码和Android的Java/Kotlin代码,反之亦然。这意味着你的核心逻辑可以用Lua写,而底层性能敏感的操作依然由原生代码负责。
第三,动态执行的灵活度。
Lua的代码可以在运行时通过loadstring或load函数动态加载、编译、执行。这正好契合了热更新的本质:下载新脚本 -> 替换内存中的旧脚本 -> 下次触发逻辑时使用新脚本。
1.2 热更新的本质:热更什么?
很多人有个误区,认为热更新就是“替换图片”或“替换音频”。其实,那是资源热更。真正的代码热更新,解决的是逻辑Bug、玩法调整、甚至运营活动配置。
比如:
- 一个抽奖逻辑算错了,需要紧急修复概率算法。
- 发现某个界面文案有错别字,需要立即修改。
- 突然想加一个限时活动,但不想走漫长的审核流程。
这些场景,如果靠发版,至少3-7天。有了Lua热更,10分钟就能覆盖全量用户。
第二章:架构设计——双端通用的灵魂
要同时支持iOS和Android,最痛苦的就是要维护两套原生代码。但Lua的优势在于:业务逻辑完全在Lua侧,原生侧只做“桥梁”。
我们的目标是:
- iOS端:Unity/Cocos/Self-Engine + LuaBridge + LuaRuntime
- Android端:Unity/Cocos/Self-Engine + LuaJNI + LuaRuntime
- Lua层:统一的业务代码库,
require机制加载
2.1 核心组件图解
┌─────────────────────────────────────────────────────────┐
│ 业务层 (Lua) │
│ main.lua -> scene_controller.lua -> logic_module.lua │
│ (所有逻辑统一,无需区分iOS/Android) │
└──────────────────────┬──────────────────────────────────┘
│ require / loadstring
┌──────────────────────▼──────────────────────────────────┐
│ 原生桥梁层 (Native Bridge) │
│ iOS: LuaObjCBridge / Android: LuaJNIBridge │
│ (负责将Lua调用映射到Objective-C/Swift 或 Java/Kotlin) │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 资源下载与热更管理器 │
│ HotUpdateManager (Lua侧) │
│ - 版本检查 │
│ - 差分下载 (可选) │
│ - 解压与文件替换 │
│ - 缓存预热 │
└─────────────────────────────────────────────────────────┘
2.2 关键设计原则
原则一:Lua虚拟机独立。 每个场景或模块可以使用独立的Lua虚拟机,或者共享一个全局VM。推荐共享VM,但要注意全局变量的污染问题。
原则二:沙箱机制。
Lua默认没有沙箱,你需要为它定制一个安全环境。禁止访问os.execute、io.open等危险函数,只开放必要的API(如网络请求、本地存储)。
原则三:版本控制。 每个Lua脚本文件都需要一个版本哈希(MD5/SHA1)。下载时对比版本号,只有不同才下载,节省流量。
第三章:原生端实现——iOS篇
iOS端我们假设使用Unity或Cocos Creator作为载体(原理通用),重点讲如何集成Lua虚拟机并处理热更。
3.1 集成Lua虚拟机
首先,你需要一个Lua库。推荐使用LuaJIT(性能更好)或Lua 5.4。这里以标准Lua 5.4为例,使用CMake或CocoaPods集成。
CMakeLists.txt (如果是原生C++项目):
cmake_minimum_required(VERSION 3.10)
project(LuaHotUpdate)
add_subdirectory(lua-5.4) # Lua源码目录
add_executable(MyApp main.mm)
target_link_libraries(MyApp lua)
集成LuaBridge(如果需要使用C++对象):
#include <lua.hpp>
#include <LuaBridge.h>
void setupLua() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// 注册全局函数,供Lua调用原生功能
lua_register(L, "hello_from_objc", [](lua_State* L) -> int {
NSLog(@"Hello from Lua!");
return 0;
});
lua_close(L);
}
3.2 iOS热更管理器实现
热更的核心是:下载文件 -> 写入沙盒 -> 修改Lua的package.path。
HotUpdateManager.m (iOS原生部分):
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface HotUpdateManager : NSObject
@property (nonatomic, strong) dispatch_queue_t downloadQueue;
+ (instancetype)sharedManager;
- (void)checkUpdateWithCompletion:(void(^)(BOOL hasUpdate, NSString* version))completion;
- (void)downloadAndUpdate;
@end
@implementation HotUpdateManager
static HotUpdateManager* _instance;
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (instancetype)init {
if (self = [super init]) {
_downloadQueue = dispatch_queue_create("com.hotupdate.download", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)checkUpdateWithCompletion:(void(^)(BOOL, NSString*))completion {
// 1. 从服务器获取最新版本号和资源列表
// 2. 对比本地Version.meta和MD5校验和
// 3. 返回结果
NSURL *url = [NSURL URLWithString:@"https://api.yourserver.com/hotupdate/check"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
if (completion) completion(NO, nil);
return;
}
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSString *remoteVersion = json[@"version"];
NSString *localVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"HotUpdateVersion"];
BOOL hasUpdate = ![remoteVersion isEqualToString:localVersion];
if (completion) completion(hasUpdate, remoteVersion);
}];
[task resume];
}
- (void)downloadAndUpdate {
// 1. 创建临时目录
NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hotupdate_temp"];
[[NSFileManager defaultManager] createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:nil];
// 2. 下载zip包
NSURL *url = [NSURL URLWithString:@"https://api.yourserver.com/hotupdate/download"];
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"Download error: %@", error);
return;
}
// 3. 解压到沙盒持久化目录
NSString *persistentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *zipPath = [tempDir stringByAppendingPathComponent:@"patch.zip"];
[[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:zipPath error:nil];
// 使用SSZipArchive或ZipArchive解压
[SSZipArchive unzipFileAtPath:zipPath toDestination:persistentDir];
// 4. 关键步骤:通知Lua层热更完成
// 这里通过LuaBridge或原生回调触发Lua脚本执行reload
self->updateCompleted = YES;
// 清理临时文件
[[NSFileManager defaultManager] removeItemAtPath:tempDir error:nil];
}];
[task resume];
}
@end
3.3 修改Lua路径
这是iOS热更最精妙的一步。Lua加载文件时,会去package.path指定的路径查找。我们需要让它在更新后,优先从沙盒目录加载。
在Lua初始化时注入路径:
-- init.lua
local path = package.path
-- 将沙盒目录加入path最前面,优先级最高
local sandboxPath = "/var/mobile/Containers/Data/Application/Documents/"
package.path = sandboxPath .. "?.lua;" .. sandboxPath .. "?/init.lua;" .. path
-- 加载主逻辑
require "main"
注意: iOS的沙盒路径在每次启动时可能不同,所以最好通过原生端传入路径给Lua,或者在原生端修改package.path后,再通过LuaJIT的API设置。
第四章:原生端实现——Android篇
Android端逻辑与iOS类似,但API不同。我们使用Kotlin/Java。
4.1 集成Lua环境
Android上推荐使用lua-Android或tolua等预编译库。
4.2 Android热更管理器实现
HotUpdateManager.kt (Kotlin):
class HotUpdateManager(private val context: Context) {
private val downloadExecutor = Executors.newSingleThreadExecutor()
fun checkUpdate(callback: UpdateCallback) {
// 异步请求服务器
CoroutineScope(Dispatchers.IO).launch {
val remoteVersion = fetchRemoteVersion()
val localVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName
val hasUpdate = remoteVersion != localVersion
callback.onResult(hasUpdate, remoteVersion)
}
}
fun downloadAndUpdate(onComplete: () -> Unit) {
downloadExecutor.execute {
try {
val url = URL("https://api.yourserver.com/hotupdate/download")
val connection = url.openConnection() as HttpURLConnection
connection.inputStream.use { input ->
// 下载到App私有目录
val file = File(context.filesDir, "patch.zip")
file.outputStream().use { output ->
input.copyTo(output)
}
}
// 解压
val zipFile = File(context.filesDir, "patch.zip")
val destinationDir = context.filesDir
unzip(zipFile.absolutePath, destinationDir.absolutePath)
// 删除zip
zipFile.delete()
// 通知Lua热更完成
onComplete()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun unzip(zipPath: String, destDirectory: String) {
ZipFile(zipPath).use { zip ->
zip.entries().asSequence().forEach { entry ->
val outFile = File(destDirectory, entry.name)
if (entry.isDirectory) {
outFile.mkdirs()
} else {
outFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(outFile).use { output ->
input.copyTo(output)
}
}
}
}
}
}
}
interface UpdateCallback {
fun onResult(hasUpdate: Boolean, version: String?)
}
4.3 Android端Lua路径设置
与iOS类似,Android需要在Lua初始化时修改package.path。
-- 通过原生代码传入沙盒路径
local sandboxPath = lua_getglobal("get_sandbox_path")()
package.path = sandboxPath .. "?.lua;" .. sandboxPath .. "?/init.lua;" .. package.path
或者在Java层:
// Android Java/Kotlin代码
luaState.getGlobal("package");
luaState.getField(-1, "path");
String newPath = sandboxPath + "?.lua;" + sandboxPath + "?/init.lua;" + luaState.toString(-2);
luaState.setField(-3, "path");
luaState.pop(2);
第五章:Lua层热更架构——核心灵魂
原生侧只是“搬运工”,Lua侧的架构才是热更新的精髓。我们需要设计一套版本感知、按需加载、故障降级的机制。
5.1 目录结构规范
为了便于管理,Lua代码必须有严格的目录结构:
/assets/hotfix/ # 热更资源根目录
├── version.meta # 版本文件,格式: {"version":"1.0.1", "files": {...}}
├── main.lua # 入口文件
├── config/
│ ├── game_config.lua # 游戏配置
│ └── server_config.lua
├── module/
│ ├── battle/
│ │ ├── battle_controller.lua
│ │ └── battle_skill.lua
│ └── ui/
│ ├── login_view.lua
│ └── home_view.lua
└── utils/
├── http.lua # 网络请求封装
└── logger.lua # 日志工具
5.2 版本控制文件生成
每次发布热更包,都需要生成一个version.meta。这个文件可以由构建脚本自动生成。
构建脚本 (Python伪代码):
import os
import hashlib
import json
def generate_version_meta(dest_dir, version):
files_info = {}
for root, dirs, files in os.walk(dest_dir):
for file in files:
if file.endswith('.lua'):
filepath = os.path.join(root, file)
relpath = os.path.relpath(filepath, dest_dir)
with open(filepath, 'rb') as f:
content = f.read()
md5 = hashlib.md5(content).hexdigest()
size = len(content)
files_info[relpath] = {"md5": md5, "size": size}
meta = {
"version": version,
"files": files_info,
"timestamp": int(time.time())
}
# 同时生成一个version.txt供原生层对比
with open(os.path.join(dest_dir, "version.txt"), 'w') as f:
f.write(version)
return meta
# 使用示例
meta = generate_version_meta("./out/hotfix", "1.2.0")
5.3 Lua热更管理器实现
这是最核心的部分,我们需要一个HotUpdate.lua来管理下载、校验和加载。
HotUpdate.lua: “`lua local HotUpdate = {} HotUpdate.__index = HotUpdate
– 配置 HotUpdate.SERVER_URL = “https://api.yourserver.com/hotupdate” HotUpdate.LOCAL_VERSION_KEY = “hotfix_version” HotUpdate.RESOURCE_DIR = “hotfix” – 对应原生沙盒下的目录名
function HotUpdate.new()
local self = setmetatable({}, HotUpdate)
self.currentVersion = self:_getLocalVersion()
self.pendingVersion = nil
return self
end
– 获取本地版本号 function HotUpdate:_getLocalVersion()
-- 调用原生接口获取,或者读取本地文件
return tolua.getstring("getLocalVersion", "") or "0.0.0"
end
– 设置本地版本号 function HotUpdate:_setLocalVersion(version)
-- 写入持久化存储
print("Version updated to: " .. version)
end
– 检查更新 function HotUpdate:checkUpdate(callback)
Http.get(self.SERVER_URL .. "/check?version=" .. self.currentVersion, function(success, data)
if success and data then
local remoteVersion = data.version
local files = data.files
if remoteVersion ~= self.currentVersion then
print("Update available: " .. remoteVersion)
self.pendingVersion = remoteVersion
self.pendingFiles = files
-- 触发下载回调,由上层决定是否立即下载
if callback then
callback(true, remoteVersion, files)
end
else
if callback then
callback(false, nil, nil)
end
end
else
if callback then
callback(false, nil, nil)
end
end
end)
end
– 下载并应用更新 function HotUpdate:downloadAndApply(progressCallback, completeCallback, failCallback)
if not self.pendingVersion then
if failCallback then failCallback("No update pending") end
return
end
local tempDir = self.RESOURCE_DIR .. "/temp"
local zipPath = tempDir .. "/patch.zip"
-- 创建临时目录
-- 注意:这里需要原生提供文件或目录操作接口,或者我们直接下载文件到Documents
print("Start downloading update: " .. self.pendingVersion)
