文章目錄
- 零、前言
- 一、我做的熱更新Demo
- 1、效果演示
- 2、流程圖
- 3、工程原始碼
- 二、為什么要有熱更新
- 三、Unity如何支持熱更新
- 1、熱更C#代碼
- 2、熱更lua代碼與資源
- 四、Unity中集成tolua框架: LuaFramewrk
- 1、下載tolua框架: LuaFramewrk
- 2、打開tolua框架專案:LuaFramework_UGUI
- 3、生成注冊檔案:生成Wrap類
- 4、Generate All選單
- 5、解決報錯問題
- 5.1、GetElementType()為空報錯
- 5.2、UnityEngine_ParticleSystemWrap報錯
- 5.3、特定的Wrap移動到BaseType中
- 5.4、LightWrap和MeshRendererWrap報錯
- 五、tolua框架的作業流程
- 1、Main.cs:入口腳本
- 2、StartUp:啟動游戲框架
- 3、LuaManager:Lua管理器
- 3.1、LuaState:lua虛擬機
- 3.2、LuaLoader:lua檔案加載器
- 3.3、LuaLooper:lua生命周期控制
- 4、GameManager:游戲管理器
- 4.1、釋放資源
- 4.2、更新資源
- 4.3、執行lua代碼
- 4.4、lua業務代碼的結構
- 六、我的熱更Demo的一些介紹說明
- 1、Web服務器
- 2、代碼結構:Scripts目錄
- 3、資源目錄結構:RawAssets目錄、GameRes目錄
- 4、資源配置:resources.bytes、ResourcesCfg.cs
- 5、資源管理器:ResourceMgr.cs
- 6、界面管理器:PanelMgr.cs、BasePanel.cs
- 7、熱更新邏輯:HotUpdater.cs
- 8、下載器:Downloader.cs
- 9、檔案解壓和壓縮
- 10、AES對稱加密解密
- 11、打整包
- 12、打熱更包
- 七、完畢
零、前言
嗨,大家好,我是新發,
有同學私信我,問我能不能寫一篇關于ToLua熱更新的教程,

今天,我就來好好講講,內容會比較長,建議大家收藏后慢慢看,
一、我做的熱更新Demo
我花了一些時間做了一個Demo,采用的是Unity + tolua,實作完整的熱更流程,包括版本管理、資源打包、資源加載、lua代碼加密解密、熱更包下載、斷點續傳等功能,
1、效果演示
效果如下,下載多個增量包:

跳過大版本更新:

斷點續傳:

2、流程圖
對應的流程圖如下(圖片可放大):

首先是版本管理器,記錄當前的最新版本;
啟動時顯示更新界面,這里就涉及到界面資源的加載,我封裝了資源管理器和界面管理器,資源管理器優先從熱更目錄(persistentDataPath的update)中查找資源,如果找不到才去包內的StreamingAssets目錄找資源;
接著執行熱更邏輯,先去Web服務器請求更新串列,判斷版本號,是整包更新還是增量更新,是否是強制更新;
根據更新串列執行下載,這里我使用獨立執行緒下載,這樣不會卡住UI主執行緒的進度更新顯示;
下載程序支持斷點續傳,這樣可以避免下載程序中網路斷開或強殺行程后需要從頭開始下載;
下載完增量包后校驗MD5是否正確,如果MD5不正確則重新下載;
校驗MD5正確后解壓到persistentDataPath的update目錄中;
啟動lua框架前,先預加載lua的bundle:lua.bundle和lua_update.bundle;
最后啟動lua框架,顯示登錄界面,
另外,我單獨寫一套簡單的打包工具,方便打AssetBundle、APP整包和增量包,
打APP整包之前會先生成一份原始的lua檔案的MD5串列,打lua、配置、資源等的AssetBundle,最終才生成APP整包;
另外,打包lua時我先對lua做了加密,這樣可以防止被別人直接拿到lua明文檔案,

3、工程原始碼
我的熱更新Demo工程以上傳到CODE CHINA,地址:https://codechina.csdn.net/linxinfa/UnityHotUpdateFramework
感興趣的同學可自行下載下來學習,另外,我使用的Unity版本為2021.1.7f1c1,如果你使用的版本與我的不同,可能打開工程會報錯,

關于我這個Demo的一些介紹說明,可以跳到文章第六節,接下來,我先花一點篇幅講講熱更新和tolua框架,
二、為什么要有熱更新
關于為什么要有熱更新,我簡單啰嗦幾句,
假設你開發了一個游戲,上架到應用市場,之后用戶反饋了一個嚴重BUG,你緊急修復后,需要重新打包APP,重新提審應用市場,經過焦急地等待,終于過審了,接著玩家需要重新下載APP,重新安裝,整個流程可想而知,無法做到快速高效,而且一旦需要重新下載和安裝,用戶很可能就流失了,
所以我們需要有一種可以不重新安裝APP就可以修復BUG的方式,那就是熱更新,我們一般也叫增量更新,
事實上,熱更新不僅僅應用于修復BUG,也經常用于線上的小版本迭代,實際的游戲專案開發節奏是很快的,一般分為大版本迭代和小版本迭代,大版本會設計比較多的開發內容,周期長,一般在兩周到一個月左右;然而在同類游戲競品的激烈競爭下,你不得不小步快跑地迭代新內容,持續給玩家新的游戲內容,拉高留存,提升活躍度,所以在大版本周期中,就會設計一些小版本迭代,以熱更的方式把內容更新到線上版本,這樣既不需要重新提審APP到應用商店,又不需要玩家重新下載APP和安裝,一舉多得,
三、Unity如何支持熱更新
熱更新的內容包括代碼和資源,代碼有C#代碼、lua代碼,資源包括配置表、預設、音樂音效、影片、字體、圖片、材質等等,

1、熱更C#代碼
Unity默認的開發語言是C#,我們寫的C#代碼最侄訓被編譯成dll由Unity引擎來加載,所以可以把部分C#代碼編譯成一個獨立的dll,上傳到Web服務器,啟動游戲時從服務器下載dll檔案,在運行時重新加載dll,通過這種方式來達到熱更新的目的,不過這種方式被視為是危險操作,因為鬼知道你重新加載的dll的代碼里是不是病毒,如果你的專案上架了應用市場,使用這種dll的熱更操作,大概率會被應用市場視為違規操作而下架,

注:順便說一下,如果你使用
IL2CPP方式打包,則你的C#代碼會被轉成C++代碼,
2、熱更lua代碼與資源
說到游戲的熱更新,就不得不提lua,lua這門語言是運行時動態解釋的,它沒運行時就是一個普通的文本檔案,我們可以把它看成是資源檔案,所以lua代碼熱更和資源熱更本質是一樣的,一般都是打成AssetBundle放在Web服務器,客戶端從Web服務器下載最新的AssetBundle到本地,
市面上的lua框架有很多,比如tolua、xlua、ulua、slua等等,本質都是在Unity環境里內嵌一個lua虛擬機(使用c語言實作的虛擬機),游戲運行時動態決議lua腳本并執行,所以我們就可以把一些邏輯用lua來實作,然后再通過Web服務器下載lua腳本(一般是lua原始碼做加密后再打成AssetBundle檔案,或者是使用luac將lua原始碼編譯成位元組碼然后再打成AssetBundle檔案),從而實作熱更的目的,

四、Unity中集成tolua框架: LuaFramewrk
1、下載tolua框架: LuaFramewrk
tolua的GitHub地址:https://github.com/topameng/tolua
如果有同學無法訪問GitHub,也可以通過Code China的鏡像源來下載,
地址:https://codechina.csdn.net/mirrors/topameng/tolua
我們可以看到它提供了兩個版本的框架:LuaFramework_NGUI和LuaFramework_UGUI,
我們下載UGUI版本的:https://codechina.csdn.net/mirrors/jarjin/LuaFramework_UGUI

2、打開tolua框架專案:LuaFramework_UGUI
下載下來后,我們在Unity Hub中添加它,可以看到它是使用Unity5版本做的,我使用Unity2021.1.7f1c1版本打開它,

可想而知,肯定會有一些兼容問題的報錯,不要怕,我都幫你一一解決了,Unity2021.1.7f1c1版本的LuaFramework_UGUI我已上傳到CODE CHINA,如果你也是使用2021版本的Unity,可以直接使用我的版本,地址:https://codechina.csdn.net/linxinfa/LuaFramework_UGUI_2021

不過,為了然你了解一些細節,我還是把我的解決程序寫出來吧,
我建議大家往下看我是如何解決這些報錯問題的,這對于你理解LuaFramework的作業原理是有幫助的,授人以魚不如授人以漁,你是要魚還是漁呢?
瀟灑地點擊確定按鈕,

3、生成注冊檔案:生成Wrap類
經過幾分鐘的載入等待,彈出了下面這個框,點擊確定,它會將Unity常用的C#類生成Wrap類并注冊到lua虛擬機中,這樣我們就可以在lua中使用這些c#類了,

上面點擊確定按鈕,等效于點擊選單Lua / Gen Lua Wrap Files,所以如果你不小心點擊了取消按鈕,可以在選單這里執行,

生成Wrap成功后,我們可以在Assets/LuaFramework/ToLua/Source/Generate目錄中看到很多Wrap類,

自問:它是怎么知道要生成哪些類的Wrap類的呢?
答案就在CustomSettings.cs腳本中,如果你打開CustomSettings.cs腳本,你可以看到很多_GT(typeof(XXXXX)),如下:

它就是根據這里來生成對應的Wrap類的,如果你想在lua中使用你自己寫的類,則需要在這里加上_GT的呼叫,例:
_GT(typeof(MyClass)),
注:
GT就是Genrate Table的意思,在lua中,類其實就是table
4、Generate All選單
上面我們只是生成了Wrap類,事實上,還要生成Lua Delegates和LuaBinder,你可以在選單中看到,

生成的Wrap類需要在LuaBinder中注冊到lua虛擬機中,生成的lua委托需要在DelegateFactory中注冊到lua虛擬機中,當然,這些都是自動生成的,我們只需執行選單即可,
一般我們都是直接點擊選單Lua / Generate All,

它會做三件事情:
1 根據CustomSettings中的customDelegateList,生成lua委托并在DelegateFactory中注冊到lua虛擬機中;
2 根據CustomSettings中的customTypeList,生成Wrap類;
3 在LuaBinder中生成Wrap類的注冊邏輯,

5、解決報錯問題
5.1、GetElementType()為空報錯
我們點擊Generate All選單后,報了如下錯:

定位到代碼處:ToLuaExport.cs第295行,GetElementType()可能回傳空,

我們加上判空,這是ToLuaExport.cs工具的問題,

5.2、UnityEngine_ParticleSystemWrap報錯
重新點擊Generate All選單,報了新的錯,

定位到代碼處:UnityEngine_ParticleSystemWrap.cs腳本,可以看到是生成的Wrap類的ParticleSystem的SetParticles的異參多載函式的生成有問題,

事實上,這個SetParticles這個方法我們基本不用在lua代碼中使用到,所以簡單粗暴把它注釋掉就可以了,

同理,解決掉UnityEngine_ParticleSystemWrap類的同類報錯,
5.3、特定的Wrap移動到BaseType中
問題來了,因為Wrap是工具生成的,上面我們這樣修改Wrap類,下次重新生成的時候會被覆寫回去,就又會報錯了,
解決辦法是把它移到Assets / LuaFramework / ToLua / BaseType目錄中,如下

記得在CustomSettings.cs中把對應的類的_GT呼叫注釋掉,

我們重新點擊Generate All選單,可以看到不會幫我們重新生成UnityEngine_ParticleSystemWrap類了,不過新的問題來了,在LuaBinder中會自動幫我們注冊生成的Wrap類,現在我們沒有指定生成UnityEngine_ParticleSystemWrap,自然在LuaBinder中就不會幫我們生成注冊的邏輯了,

沒關系,BaseType中的也有一些Wrap類,它們肯定也是要注冊到lua虛擬機中的,我們只需要隨便找一個看看它是在哪里參考的就可以啦,

通過參考查找,我們跳到了LuaState.cs腳本中,可以看到那些BasetType中的Wrap類是在LuaState的OpenBaseLibs函式中執行注冊的,

我們只需要在這里添加上Register呼叫就可以了,不過需要注意命名空間,它是以BeginModule和EndModule來包裹的,多層命名空間可以嵌套,例:
BeginModule("System");
BeginModule("Generic");
System_Collections_Generic_ListWrap.Register(this);
System_Collections_Generic_DictionaryWrap.Register(this);
System_Collections_Generic_KeyValuePairWrap.Register(this);
EndModule();//Generic
EndModule();//end System
我們的ParticleSystem是在UnityEngine命名空間下的,所以放在BeginModule("UnityEngine");和EndModule();之間,如下:

5.4、LightWrap和MeshRendererWrap報錯
我們看到它沒有報錯了,打開main場景,

運行,閃一下,報了一個Error,如下:

我是Windows平臺,所以我點擊 Build Windows Resource選單,

報了下面新的錯誤:

一個是UnityEngine_LightWrap類,一個是UnityEngine_MeshRendererWrap類,我們使用上面類似UnityEngine_ParticleSystemWrap的方法來處理即可,
我們重新執行選單Generate All和 Build Windows Resource,
可以在Assets / StreamingAssets目錄中生成了很多AssetBundle檔案,說明資源打包成功了,

此時我們重新運行,即可看到可以正常運行了,

五、tolua框架的作業流程
上面我們看到運行后出現了一個UI界面,這個UI界面是在lua代碼中創建的,那么,Unity是如何加載并執行lua代碼的呢?下面我來一步步講,希望你耐心看完,
1、Main.cs:入口腳本
我們看回main場景的Hierarchy視圖,有個GameManager,

它身上掛著一個Main腳本,明顯,這就是入口腳本,

2、StartUp:啟動游戲框架
我們打開Main.cs腳本,如下,那句StartUp就是最關鍵的呼叫,

里面會發送一個START_UP訊息,

觸發StartUpCommand類執行Execute,我們可以看到在這里添加了很多管理器,

這些管理器會掛到GameManager物體上,

當然,我們可以根據自己的需求添加新的管理器,也可以把不需要的管理器刪掉,特別是你是專案中途繼承tolua框架的話,很多管理器可能你本身專案中就已經有了,比如界面管理器、資源管理器、聲音管理器、網路管理器、執行緒管理器等等,如果你想成為一位架構師,我建議你自己嘗試去寫這些管理器,
上面那些管理器中,最核心最關鍵的就是LuaManager,我這里要重點講一下LuaManager,
3、LuaManager:Lua管理器
LuaManager是整個tolua框架的核心,它的三個核心成員如下:

// lua虛擬機
private LuaState lua;
// lua檔案加載器
private LuaLoader loader;
// lua生命周期控制
private LuaLooper loop;
下面我挨個講解他們各自做的事情,
3.1、LuaState:lua虛擬機
我們的lua代碼需要經過lua解釋器進行解釋才能執行,lua解釋器是使用c語言寫的,它在各個平臺下有對應的庫檔案,我之前寫過一篇文章:《【游戲開發進階篇】教你在Windows平臺編譯tolua runtime的各個平臺庫(Unity | 熱更新 | tolua | 交叉編譯)》,里面我詳細講解了各個平臺的tolua庫檔案的編譯,感興趣的同學可以去看看,
庫函式的宣告在LuaDLL.cs中,

LuaState中封裝了很多對LuaDLL的調度,比如呼叫某個lua的方法,

LuaState又是由LuaManager來調度的,所以調度關系為LuaManager -> LuaState -> LuaDLL,
啟動框架時,主要是調度LuaState做了下面這些事情:

3.2、LuaLoader:lua檔案加載器
LuaLoader是檔案加載器,它繼承LuaFileUtils,主要提供lua檔案的讀取、查找功能,
核心成員變數:
public bool beZip = false;
protected List<string> searchPaths = new List<string>();
protected Dictionary<string, AssetBundle> zipMap = new Dictionary<string, AssetBundle>();
當beZip為false時,在searchPaths中查找讀取lua文件;否則從外部設定過來bundel檔案中讀取lua檔案,我們可以重寫ReadFile方法,根據自己的設計去加載lua檔案,比如你對lua檔案做了加密,則需要在加載這里先做解密,

3.3、LuaLooper:lua生命周期控制
在Unity中,MonoBehaviour是有生命周期的,
可以參見Unity官方檔案的說明:https://docs.unity3d.com/Manual/ExecutionOrder.html

我們的tolua為了實作類似的生命周期的功能,封裝了一些API,
// LuaDLL.cs
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_update(IntPtr L, float deltaTime, float unscaledDelta);
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_lateupdate(IntPtr L);
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_fixedupdate(IntPtr L, float fixedTime);
這些API就是由LuaLooper來調度的,

注:如果沒有
LuaLooper,則lua的協程會無法正常執行,
4、GameManager:游戲管理器
框架中幫我們提供了GameManager:游戲管理器,這個我們可以自己寫一個,不是用框架中的GameManager,不過我這里講一下框架中的GameManager做了什么事情,
4.1、釋放資源

GameManager啟動時,會先檢測資源路徑(Util.DataPath)中是否有lua檔案,如果沒有,則將StreamingAssets目錄中的files.txt檔案拷貝到資源路徑(Util.DataPath)中,其中files.txt記錄了StreamingAssets目錄中所有lua檔案和資源檔案的md5,
遍歷files.txt檔案,把StreamingAssets目錄中的lua檔案和資源檔案拷貝到資源路徑(Util.DataPath)中,這個程序叫做釋放資源,
4.2、更新資源

根據AppConst.UpdateMode決定要不要執行更新資源,
如果需要更新,則訪問Web服務器地址AppConst.WebUrl,下載最新的files.txt,
然后遍歷最新的files.txt,檢查本地檔案是否缺少或者MD5是否不相等,然后去Web服務器下載lua代碼或資源,下載使用了執行緒管理器啟動獨立執行緒進行下載,
4.3、執行lua代碼
更新完lua代碼和資源后會呼叫GameManager的OnInitialize,到這里就可以啟動lua虛擬機執行lua代碼了,
啟動lua虛擬機:
LuaManager.InitStart();
執行lua代碼:
-- 加載Game.lua腳本
LuaManager.DoFile("Logic/Game");
-- 執行lua的Game.OnInitOK方法
Util.CallMethod("Game", "OnInitOK");
我們在場景中看到的界面,

就是在Game.OnInitOK里面創建出來的,

4.4、lua業務代碼的結構

lua業務代碼的結構是這樣的,以Demo中的界面為了例,Prompt是提示界面,

對應一個PromptCtrl.lua腳本(界面互動邏輯,類似Android的Activity腳本)和PromptPanel.lua腳本(界面UI物件系結,類似于Android的layout布局檔案),CtrlManager.lua就是管理和調度Ctrl腳本的,

先在define.lua中定義Ctrl和Panel的名字,
-- define.lua
CtrlNames = {
Prompt = "PromptCtrl",
Message = "MessageCtrl"
}
PanelNames = {
"PromptPanel",
"MessagePanel",
}
然后所有的Ctrl在CtrlManager中注冊,
-- CtrlManager.lua
function CtrlManager.Init()
logWarn("CtrlManager.Init----->>>");
ctrlList[CtrlNames.Prompt] = PromptCtrl.New();
ctrlList[CtrlNames.Message] = MessageCtrl.New();
return this;
end
通過CtrlManager獲取對應的Ctrl物件,呼叫Awake()方法,
-- CtrlManager.lua
local ctrl = CtrlManager.GetCtrl(CtrlNames.Prompt);
if ctrl ~= nil then
ctrl:Awake();
end
Ctrl中,Awake()方法中呼叫C#的PanelManager的CreatePanel方法,
-- PromptCtrl.lua
function PromptCtrl.Awake()
logWarn("PromptCtrl.Awake--->>");
panelMgr:CreatePanel('Prompt', this.OnCreate);
end
C#的PanelManager的CreatePanel方法去加載界面預設,并掛上LuaBehaviour腳本,

這個LuaBehaviour腳本,主要是管理Panel的生命周期,呼叫lua中Panel的Awake,獲取UI元素物件,
-- PromptPanel.lua
local transform;
local gameObject;
PromptPanel = {};
local this = PromptPanel;
--啟動事件--
function PromptPanel.Awake(obj)
gameObject = obj;
transform = obj.transform;
this.InitPanel();
logWarn("Awake lua--->>"..gameObject.name);
end
--初始化面板--
function PromptPanel.InitPanel()
this.btnOpen = transform:Find("Open").gameObject;
this.gridParent = transform:Find('ScrollView/Grid');
end
--單擊事件--
function PromptPanel.OnDestroy()
logWarn("OnDestroy---->>>");
end
界面創建后會回呼Ctrl的OnCreate(),在Ctrl中對UI元素物件添加一些事件和控制,
-- PromptCtrl.lua
--啟動事件--
function PromptCtrl.OnCreate(obj)
gameObject = obj;
transform = obj.transform;
panel = transform:GetComponent('UIPanel');
prompt = transform:GetComponent('LuaBehaviour');
logWarn("Start lua--->>"..gameObject.name);
prompt:AddClick(PromptPanel.btnOpen, this.OnClick);
resMgr:LoadPrefab('prompt', { 'PromptItem' }, this.InitPanel);
end
六、我的熱更Demo的一些介紹說明
1、Web服務器
Web服務器我是使用小皮客戶端,直接啟動一個Apache的Web服務器,

實際專案會使用阿里云、騰訊云這些云服作為Web服務器,
增量包放在Web服務器跟目錄中,
update_list.json是更新串列檔案,里面記錄每個增量包的版本號、md5、大小和url,例:
[
{
"appVersion": "1.0.0.0",
"appUrl": "https://blog.csdn.net/linxinfa",
"updateList":
[
{
"resVersion": "1.0.0.2",
"md5": "206933991b0fd0275695e302b9fa0839",
"size": 916897,
"url": "http://localhost:7890/res_1.0.0.2.zip"
},
{
"resVersion": "1.0.0.1",
"md5": "6d71d1648247546b43197d1ddd832ad6",
"size": 4737,
"url": "http://localhost:7890/script_1.0.0.1.zip"
}
]
}
]
2、代碼結構:Scripts目錄
我的代碼結構如下:

3、資源目錄結構:RawAssets目錄、GameRes目錄
生肉資源放在RawAssets目錄中,比如影片、字體、圖片等,這些資源會被預設依賴,預設是熟肉資源,相對的,這些就是生肉資源,

熟肉資源放在GameRes目錄中,其中BaseRes放熱更之前就要使用的資源,其他目錄的資源都是熱更后才加載的,

4、資源配置:resources.bytes、ResourcesCfg.cs
我把資源路徑配置在resources.bytes中,如下:
[
{ "id":1, "editor_path":"UIPrefabs/LoginPanel.prefab", "desc":"登錄界面" },
{ "id":2, "editor_path":"UIPrefabs/PlazaPanel.prefab", "desc":"大廳界面" },
{ "id":3, "editor_path":"UIPrefabs/TipsFly.prefab", "desc":"提示語" }
]
editor_path是相對GameRes的路徑,它的第一級目錄將會作為AssetBundle的名字,比如上面三個資源的一級目錄都是UIPrefab,所以他們會一起打在一個叫uiprefab.bundle的AssetBundle檔案中,
我封裝了ResourcesCfg腳本來讀取resources.bytes,你可以通過GetResCfg方法來獲取配置,
// ResourcesCfg.cs
public ResourcesCfgItem GetResCfg(int resId)
例:
var resCfg = ResourcesCfg.instance.GetResCfg(1);
5、資源管理器:ResourceMgr.cs
配置了資源后,可以通過資源管理器來加載資源,我封裝了兩個介面:
// ResourceMgr.cs
public T LoadAsset<T>(int resId) where T : UObject
public T LoadAsset<T>(string resPath) where T : UObject
你可以通過資源id來加載資源,
例:
var loginPanelObj = ResourceMgr.instance.LoadAsset<GameObject>(1);
也可以通過相對路徑來加載資源,
例:
var loginPanelObj = ResourceMgr.instance.LoadAsset<GameObject>("UIPrefabs/LoginPanel.prefab");
不過如果要顯示界面,建議使用PanelMgr來調度和統一管理,
6、界面管理器:PanelMgr.cs、BasePanel.cs
為了方便管理界面,我封裝了界面基類BasePanel,由它來調度界面的生命周期,它有一個panelName成員,初始化時會去查找與panelName同名的lua腳本,調度生命周期相關的函式,界面的創建和銷毀由PanelMgr來統一呼叫,
畫個圖:

7、熱更新邏輯:HotUpdater.cs
熱更新路基我封裝在HotUpdater中,它做的事情如下:
1 請求更新串列;
2 根據版本號計算真正需要下載的檔案,最終決定是否需要更新,是強制更新還是可選更新;
3 執行下載,調度Downloader類完成下載任務;
4 在Update中監聽下載事件,并根據事件呼叫界面更新的委托函式,實作界面狀態更新;
5 下載完成后執行MD5校驗,如果校驗不通過,重新執行下載;
6 MD5校驗通過后,執行檔案解壓,解壓到persistentDataPath/update目錄中;
7 解壓完畢后洗掉zip檔案;
8 下載下一個增量包,知道全部下載完畢;
9 下載完畢后,回呼actionAllDownloadDone委托函式,
8、下載器:Downloader.cs
Downloader主要就是執行下載任務,使用的是HttpWebRequest來請求Web服務器,
var httpReq = HttpWebRequest.Create(url) as HttpWebRequest;
要支持斷點續傳,需要判斷HttpWebResponse的StatusCode是否為HttpStatusCode.PartialContent,如果是才支持斷點續傳,否則要重新從頭下載,
var response = (HttpWebResponse)httpReq.GetResponse();
if (response.StatusCode != HttpStatusCode.PartialContent)
{
// 不能斷點續傳,要重新下載
}
斷點續傳的核心就是本地檔案Seek到檔案末尾,HttpWebRequest.AddRange到要續傳的位置,如下:
m_fs = new FileStream(savePath, FileMode.OpenOrCreate, FileAccess.Write);
var lastDownloadSize = fs.Length;
m_fs.Seek(lastDownloadSize, SeekOrigin.Current);
httpReq.AddRange(lastDownloadSize);
下載檔案的寫檔案比較耗時,使用獨立的執行緒來執寫檔案,
// 開啟一個獨立的寫檔案執行緒
if (null == m_thread)
{
m_stopThread = false;
m_thread = new Thread(WriteThread);
m_thread.Start();
}
寫檔案的邏輯就是從HttpWebResponse的Stream流中讀取資料然后寫到本地的檔案中,
var readSize = m_ns.Read(m_buff, 0, m_buff.Length);
if (readSize > 0)
{
m_fs.Write(m_buff, 0, readSize);
curDownloadSize += readSize;
Thread.Sleep(0);
}
else
{
// 完畢
m_stopThread = true;
state = DownloadState.End;
Dispose();
}
9、檔案解壓和壓縮
增量包我是在打包工具中執行了壓縮,壓成.zip檔案,客戶端熱更新時下載后會執行解壓,
壓縮和解壓我使用的庫是Ionic.Zip.Unity.dll,

壓縮檔案:
// using Ionic.Zip;
using (ZipFile zip = new ZipFile())
{
// 設定壓縮密碼
// zip.Password = "123456";
zip.AddDirectory(Application.dataPath + "/TestDir", "./TestDir");
zip.AddFile(Application.dataPath + "/Test1.txt", "./");
zip.Save(Application.dataPath + "/result.zip");
}
解壓檔案:
using (ZipFile zip = new ZipFile(Application.dataPath + "/result.zip"))
{
// 設定解壓密碼
// zip.Password = "123456";
// 直接解壓所有檔案
// zip.ExtractAll(Application.dataPath + "/UnZip");
foreach (var entity in zip)
{
// 挨個檔案解壓
entity.Extract(Application.dataPath + "/UnZip");
}
}
10、AES對稱加密解密
lua代碼打包成AssetBundle時,我先把lua代碼拷貝到一個臨時目錄中并做加密,然后才執行AssetBundle打包,
我使用的加密演算法是AES,對應的腳本是AESEncrypt.cs,

解密介面:
public static byte[] Encrypt(byte[] toEncryptArray)
解密介面:
public static byte[] Decrypt(byte[] toDecryptArray)
我們可以使用AssetStudio對打出來的lua的AssetBundle進行逆向,可以看到逆向出來的是亂碼,說明我們的加密生效了,

注:
AssetStudio下載地址:https://codechina.csdn.net/mirrors/perfare/assetstudio
11、打整包
點擊選單Build / 打包APP,

設定你要打的整包的版本號,點擊Save,然后點擊Build APP即可,生成的APP會放在Assets同級目錄的Bin目錄中,

以Windows平臺為例,如下

會自動生成一個LuaFrameworkFiles_版本號.json檔案,里面記錄了打包時的LuaFramework的檔案的md5,方便后續打增量包是進行MD5對比,
12、打熱更包
點擊選單Build / 打熱更包,

設定增量包的版本號,設定要讀取的LuaFrameworkFiles_xxxx.json檔案的版本號,根據需要添加要打進增量包的資源檔案,最后點擊打熱更包按鈕即可,

生成的熱更包會存放在Bin目錄中,

我們只需將其拷貝到Web服務器,

并配置到update_list.json即可,如下:
[
{
"appVersion": "1.0.0.0",
"appUrl": "https://blog.csdn.net/linxinfa",
"updateList":
[
{
"resVersion": "1.0.0.1",
"md5": "50d550486de1a72bd0caaa560128a9ad",
"size": 908405,
"url": "http://localhost:7890/res_1.0.0.1.zip"
}
]
}
]
七、完畢
好了,就先寫這么多吧~
希望可以幫助到對熱更新有困惑的同學,
我是林新發:https://blog.csdn.net/linxinfa
原創不易,若轉載請注明出處,感謝大家~
喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/293056.html
標籤:其他
下一篇:《Learning Spatio-Temporal Representation with Pseudo-3D Residual Networks》演算法詳解
