- A+
在六章节中,讲了一键标志地址、自定义打包、自定义加载方式等把热更放在启动时可以检测,还写了修改资源远程加载的地址。大部分接口和自定义都准备好了,但是没有把流程走完,只是列了下热更的流程,下面把这些流程走完,并且在lua侧去控制热更,还有热更完成后的重启操作等。
更新流程在六章节的基础上,第一步改为拉取一个Version配置文件,确定需要更新了再设置catalog和资源的下载地址,所以流程为:
- 检查版本信息
- 设置加载资源的远程路径,catalog和Bundle
- 检查网络资源表catalog
- 下载网络资源表catalog
- 通过全局资源lable后去所有的ResourceLocation
- 自定义辨别筛选出需要更新Bundle资源的ResourceLocation
- 获取下载的大小
- 更新下载
- 完成后的重启
一、准备
一、Addressable的接口准备
在前面,先列出调用的接口提供给lua层调用,并在AddressableManager.lua中去统一调用,这里的接口主要针对Addressable的热更的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.SceneManagement; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.ResourceLocations; using Addressable; using System.IO; using System.Linq; namespace Manager { public class AddressableManager : MonoBehaviour { private static AddressableManager _instance = null; private Dictionary<string, TextAsset> frameTextContents = new Dictionary<string, TextAsset>(); private IList<IResourceLocation> CheckContentList = new List<IResourceLocation>(); public static AddressableManager GetInstance() { if (_instance == null) { _instance = _Create(); } return _instance; } //设置远程加载地址 public void SetAddressableRemoteResCdnUrl(string remoteUrl) { Debug.Log("SetAddressableRemoteUrl remoteUrl = " + remoteUrl); if (string.IsNullOrEmpty(remoteUrl)) { return; } //设置catalog的请求路径 string newLocation = remoteUrl + "/" + "catalog_999.hash"; Addressables.SetRemoteCatalogLocation(newLocation); //设置location的transfrom func Addressables.InternalIdTransformFunc = (IResourceLocation location) => { string internalId = location.InternalId; if (internalId != null && internalId.StartsWith("http")) { var fileName = Path.GetFileName(internalId); string newInternalId = remoteUrl + "/" + fileName; return newInternalId; } else { return location.InternalId; } }; } //检查网络资源表 public OperationData CheckForCatalogUpdates() { var aOperation = Addressables.CheckForCatalogUpdates(false); return OperationData.Get(aOperation); } //下载对应资源表 public OperationData UpdateCatalogs(List<string> catlogs) { var aOperation = Addressables.UpdateCatalogs(catlogs,false); return OperationData.Get(aOperation); } //通过全局lable获取所有的ResourceLocation public OperationData GetCheckContentList(string label) { CheckContentList.Clear(); var aOperation = Addressables.LoadResourceLocationsAsync(new List<string> { label }, Addressables.MergeMode.Union); return OperationData.Get(aOperation,false, OnGetCheckContentList); } //筛选出需要更新的内容 -1错误 0为没有更新 public int GetUpdateContentList() { if (CheckContentList == null) { Debug.LogError("获取所有的ResourceLocation为空"); return -1; } if (AssetBundleManager.GetInstance().buildInData == null) { #if !UNITY_EDITOR Debug.LogError("获取内置Bundle配置为空"); return -1; #else return 0; #endif } IList<IResourceLocation> updateContent = new List<IResourceLocation>(); foreach (var item in CheckContentList) { string bundleName; if (item.HasDependencies) { foreach (var dep in item.Dependencies) { bundleName = Path.GetFileName(dep.InternalId); if (AssetBundleManager.GetInstance().buildInData.BuildInBundleNames.Contains(bundleName)) { } else if (AssetBundleManager.GetInstance().IsCache(bundleName)) { } else { updateContent.Add(dep); } } } } CheckContentList = updateContent; return CheckContentList.Count; } //获取需要下载的大小 public long GetDownloadSize(string label) { long size = 0; foreach (IResourceLocation location in CheckContentList.Distinct()) { size += Addressables.GetResourceLocationSize(location); } return size; } //下载更新 public OperationData DownloadDependenciesAsync(string label) { var aOperation = Addressables.DownloadDependenciesAsync(CheckContentList); return OperationData.Get(aOperation); } public void OnGetCheckContentList(object list) { CheckContentList = list as IList<IResourceLocation>; } private static AddressableManager _Create() { GameObject go = new GameObject(); go.name = "AddressableManager"; DontDestroyOnLoad(go); return go.AddComponent<AddressableManager>(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
local ManagerBase = require("Frame.Base.Manager.ManagerBase") local AddressableManager = Class("Manager.AddressableManager",ManagerBase) local CSAddressableManager = CS.Manager.AddressableManager function AddressableManager:Init() if not self.csAddressableManager then self.csAddressableManager = CSAddressableManager.GetInstance() end end function AddressableManager:SetRemoteResCdnUrl( url ) self.csAddressableManager:SetAddressableRemoteResCdnUrl( url ) end --检查网络资源表 function AddressableManager:CheckForCatalogUpdates() return self.csAddressableManager:CheckForCatalogUpdates() end ---下载对应资源表 ---catlogs为CheckForCatalogUpdates传回来的List<string> function AddressableManager:UpdateCatalogs(catlogs) return self.csAddressableManager:UpdateCatalogs(catlogs) end --通过全局lable获取所有的ResourceLocation function AddressableManager:GetCheckContentList(label) return self.csAddressableManager:GetCheckContentList(label) end --筛选出需要更新的内容 -1错误 0为没有更新 function AddressableManager:GetUpdateContentList() return self.csAddressableManager:GetUpdateContentList() end ---获取需要下载的大小 ---keys为UpdateCatalogs传回来的List<string> function AddressableManager:GetDownloadSize(label) return self.csAddressableManager:GetDownloadSize(label) end ---下载更新资源 --可获取进度GetOperation ---keys为UpdateCatalogs传回来的List<string> function AddressableManager:DownloadDependenciesAsync(keys) return self.csAddressableManager:DownloadDependenciesAsync(keys) end return AddressableManager |
二、检查版本接口准备
在本地的Resources目录下,放一个文本的配置文件,用json格式记录App版本号和Res版本号,App版本号不对,就进行对应的整包更新。资源版本号对不上就进行资源更新。更新完成后会保存一个新的版本配置文件到黑匣子存储目录下。所以,获取本地版本的方法就是先判断黑匣子中是否有配置文件信息,没有再加载Resources下的。下面是获取和保存版本信息的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static string GetVersionInfo() { string versionPath = Path.Combine(Application.persistentDataPath, "Version.bytes"); if (File.Exists(versionPath)) { return File.ReadAllText(versionPath); } TextAsset infoAsset = Resources.Load<TextAsset>("Version/Version"); return infoAsset.text; } public static void SaveVersionInfo(string version) { string versionPath = Path.Combine(Application.persistentDataPath, "Version.bytes"); File.WriteAllText(versionPath, version); } |
三、重启接口准备
在c#中主要是重启lua虚拟机和释放预加载的lua缓存,重启的方法为ReBoot方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
using UnityEngine; using System; using System.Collections; using System.IO; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using System.Collections.Generic; using System.Linq; using System.Text; using Manager; using MiniJSON; using UnityEngine.Networking; using UnityEngine.SceneManagement; using XLua; public class Main : MonoBehaviour { public LuaEnv luaState = null; public static Main Instance = null; private XLuaMono loop = null; private string tips = ""; private void Awake() { Main.Instance = this; Screen.sleepTimeout = SleepTimeout.NeverSleep; Init(); } private void Update() { if (luaState != null) { luaState.Tick(); if (Time.frameCount % 1000 == 0) { luaState.FullGc(); } } } private async void Init() { AssetBundleManager.GetInstance().Init(); await Addressables.InitializeAsync().Task; ReBoot(); } public void ReBoot() { AddressableManager.GetInstance().ClearLuaScriptCache(); AddressableManager.GetInstance().PreLoadLuaScript("frametext", LoadFrameLuaComplete); } private void LoadFrameLuaComplete(object count) { StartCoroutine(RestartLuaEnv()); } public IEnumerator RestartLuaEnv() { DisposeDelegate(); GC.Collect(); yield return null; DisposeLuaEvn(); InitLuaEvn(); StartUpLua(); StartUpUnity2LuaCallback(); } void StartUpLua() { luaState.DoString(string.Format("require('{0}')", "Frame/Main")); LuaFunction init = luaState.Global.Get<LuaFunction>("Init"); init.Call(); init.Dispose(); init = null; loop = gameObject.AddComponent<XLuaMono>(); loop.OnInit(luaState); } void StartUpUnity2LuaCallback() { Unity2LuaCallback.Instance.Init(); } public void DisposeDelegate() { if (loop != null) { loop.OnDispose(); loop = null; } if (Unity2LuaCallback.Instance != null) { Unity2LuaCallback.Instance.Dispose(); } } public void DisposeLuaEvn() { if (luaState != null) { luaState.Dispose(); luaState = null; } } private void InitLuaEvn() { luaState = new LuaEnv(); luaState.AddLoader(CustomLoader); luaState.AddBuildin("cjson", XLua.LuaDLL.Lua.LoadRapidJson); luaState.AddBuildin("pb", XLua.LuaDLL.Lua.LoadLuaProfobuf); } public static byte[] CustomLoader(ref string filepath) { StringBuilder scriptPath = new StringBuilder(); scriptPath.Append(filepath.Replace(".", "/")); scriptPath.Append(".lua"); #if UNITY_EDITOR var scriptDir = Path.Combine(Application.dataPath, "..", "Src"); var luaPath = Path.Combine(scriptDir, scriptPath.ToString()); // Logger.Log("Load lua script : " + luaPath); return File.ReadAllBytes(luaPath); #endif var txtAddress = scriptPath.Append(".bytes").ToString(); var asset = AddressableManager.GetInstance().GetLuaScript(txtAddress); if (asset != null) { return asset; } return null; } } |
二、lua侧实现热更流程
框架启动之后,会加载Update场景,在Update场景中会打开UpdatePanel的UI窗口界面,热更的流程就在UpdatePanel中完成。
1 2 3 4 5 6 7 8 9 10 11 12 |
local UpdateScene = Class("Scene.UpdateScene",SceneBase) local SceneConfig = RequireCur("SceneConfig") function UpdateScene:Awake() G_SCENE_CONFIG = SceneConfig UIManager:GetInstance():OpenWindow(G_SCENE_CONFIG.UIWindowNames.UpdatePanel) end function UpdateScene:dtor() end return UpdateScene |
更新流程首先是检查的版本号,对于版本号对不上的,无论是远程版本大还是小,都需要进行更新,这样可以做版本回退。而远程地址的拼接,不难看出,远程的目录为“版本号/平台”,远端保存多个版本号的所有资源,可以任意的回退到任何版本。
当下载完成后,在FinishUpdate中,更新版本信息。然后重启,需要关闭所有的界面,这里会产生黑屏,所以我会在Main场景中留一个渲染模式为Scene Space - Overlay的底图,当需要关闭界面的时候先显示这个底图而不至于黑屏。销毁所有资源,因为资源可能是旧的了,最后调用c#提供的方法,重新加载最新的lua脚本和重启lua虚拟机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
local M = Class("Update.UpdatePanel",UIBaseView) local base = UIBaseView M.VIEW_CONFIG = { Layer = "BackgroudLayer", PrefabPath = "Update/Prefabs/UpdatePanel.prefab", } function M:Awake() base.Awake(self) self.img_slider = self:AddComponent(UIImage,"Loading/Mask") self.txt_message = self:AddComponent(UIText,"Text") self.txt_progress = self:AddComponent(UIText,"Loading/Text") end function M:OnEnable() base.OnEnable(self) self:StartCheckUpdate() end function M:StartCheckUpdate() if PlatformUtil.IsEditor() then self:EndUpdate() return end --版本检查 --本地版本 local json_version_info = CS.UnityExtends.GetVersionInfo() local version_info = Json.decode( json_version_info ) if not version_info then zerror("version info is nil") return end zprint("local version", version_info, "__tb") --远程版本 local remove_version_url = FrameConst.DownLoadBaseUrl .. "Version.json" HttpManager:GetInstance():HttpGetDefault(remove_version_url, function(content) if not content then --这里当作没有更新处理 直接进去 TODO self:EndUpdate() return end local remote_version_info = Json.decode(content) if not remote_version_info then --这里当作没有更新处理 直接进去 TODO self:EndUpdate() return end zprint("remote version", remote_version_info, "__tb") if version_info.AppVer ~= remote_version_info.AppVer then --整包下载 TODO return end --版本对不上都更新,可以做回退 if version_info.ResVer ~= remote_version_info.ResVer then local res_cdn_url = string.format("%s%s/%s", FrameConst.DownLoadBaseUrl, remote_version_info.ResVer, PlatformUtil.GetStrPlatformOnlyMobile()) --设置资源更新的地址 AddressableManager:GetInstance():SetRemoteResCdnUrl( res_cdn_url ) coroutine.start(function() self:CoCheckUpdate(content) end) else self:EndUpdate() end end) end function M:CoCheckUpdate(content) self:ShowTips("正在检查网络资源表") local async = AddressableManager:GetInstance():CheckForCatalogUpdates() coroutine.waitforasyncop(async) local list = async:GetAsset() if not IsNull(list) and list.Count > 0 then --有新的资源表就下载,没有的话也要检测更新,因为上一次可能中断,但是却更新了catelog self:ShowTips("正在下载网络资源表") async = AddressableManager:GetInstance():UpdateCatalogs(list) coroutine.waitforasyncop(async) end self.checkUpdateLabel = "default" --所有资源 self:ShowTips("正在检测需要更新的内容") async = AddressableManager:GetInstance():GetCheckContentList(self.checkUpdateLabel) coroutine.waitforasyncop(async) local result = AddressableManager:GetInstance():GetUpdateContentList() if result == -1 then self:ShowTips("更新发生错误") return false elseif result == 0 then self:FinishUpdate(content) return true end self:ShowTips("正在获取更新资源大小") local allDownloadSize = AddressableManager:GetInstance():GetDownloadSize(self.checkUpdateLabel) allDownloadSize = CS.UnityExtends.LongToDouble( allDownloadSize ) --获取到为long值,lua层没转换 zprint(string.format('需要下载的大小为%s',allDownloadSize)) if allDownloadSize <= 0 then self:FinishUpdate(content) return true end self:ShowTips("正在下载资源,总大小为:" .. math.floor(allDownloadSize/1024/1024) .. 'M') async = AddressableManager:GetInstance():DownloadDependenciesAsync(self.checkUpdateLabel) coroutine.waitforasyncop(async,function(co,progress) self:ShowProgress(async.progress) end) self:FinishUpdate(content) end function M:FinishUpdate(content) --更新完成后更新远程版本 CS.UnityExtends.SaveVersionInfo( content ) SceneManager:GetInstance():ShowMainCanvasView() --防止背景空白 UIManager:GetInstance():Dispose() --干掉uimanager 其他管理器需要对场景处理的也干掉 GameObjectManager:GetInstance():ClearAllCache() CS.Main.Instance:ReBoot() end function M:EndUpdate() SceneManager:GetInstance():LoadScene("Hall") end function M:ShowTips(value) zprint( value ) self.txt_message:SetText(value) end function M:ShowProgress(value) self.img_slider:SetFillAmount(value) self.txt_progress:SetText((value - value % 0.01)*100 .. "%") end function M:dtor() end return M |