- A+
在六章节讲热更的时候,使用了Addressable可寻址进行的热更,已经做好了一键标志地址,这里就说下怎么通过地址加载资源,然后在lua侧实现对象池回收,引用计数等。
一、lua脚本的加载
xlua中自定义的CustomLoader是同步进行加载的,所以我们需要在驱动lua逻辑之前就要进行预加载了,在第六章中标志地址的时候,如果是lua脚本内容会根据目录(模块)添加上一个lable,然后在上一章中,是通过一个配置文件列出了框架和公用的lua目录lable,然后进行预加载缓存起来。场景加载的时候会预加载加载相关场景的lua脚本。后面觉得新建非场景脚本目录(模块),每次都需要去配置文件中添加一下预加载,且脚本内容不是很大不占内存,所以改成lua脚本再创建lua虚拟机之前全部加载出来。只需要在标志地址的时候针对lua脚本添加一个统一的label,再根据这个lable统一去加载就好了。
启动时,在创建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 |
public void ReBoot() { AddressableManager.GetInstance().ClearLuaScriptCache(); AddressableManager.GetInstance().PreLoadLuaScript("frametext", LoadFrameLuaComplete); } private void LoadFrameLuaComplete(object count) { StartCoroutine(RestartLuaEnv()); } 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; } //获取脚本 会在缓存中获取 public byte[] GetLuaScript(string key) { byte[] datas = null; if (frameTextContents.ContainsKey(key)) datas = frameTextContents[key].bytes; return datas; } |
通过label预加载lua脚本的接口,首先通过lable获取到所有的地址,再通过所有的地址获取所有的lua文本内容,我测试过了两个数组的地址是相对应的,缓存为地址key=>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 |
//预加载加载场景的所有Lua脚本资源 public async void PreLoadLuaScript(string label, Action<object> cb = null) { #if UNITY_EDITOR //编辑器下不需要预加载 if (cb != null) cb(null); return; #endif var op = Addressables.LoadResourceLocationsAsync(new List<string> { label }, Addressables.MergeMode.Union); var list = await op.Task; List<string> keys = new List<string>(); foreach (var key in list) { keys.Add(key.PrimaryKey); } ReleaseByHandle(op); var op2 = Addressables.LoadAssetsAsync<TextAsset>(keys, null, Addressables.MergeMode.Union); var luaTextAssets = await op2.Task; if (list.Count != luaTextAssets.Count) { Debug.LogError("检查是否有重复key值的lua资源"); } //缓存 for (int i = 0; i < luaTextAssets.Count; i++) { if(!frameTextContents.ContainsKey(keys[i])) frameTextContents.Add(keys[i], luaTextAssets[i]); } if (cb != null) cb(keys.Count); } |
二、其他资源的加载,场景、通用资源加载接口
这里加载场景的方式是叠加的方式,写死了,如果需要可以添加多一个参数进行控制。这里接口好像也没什么说的,OperationData是一个能提供给lua侧做携程等待的,并且加载完成后获取相应的资源。然后在lua侧实现一个AddressableManager.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 |
//加载场景 public OperationData LoadSceneAsync(string key) { var aOperation = Addressables.LoadSceneAsync(key, LoadSceneMode.Additive, true); return OperationData.Get(aOperation,false); } //卸载场景 public OperationData UnLoadSceneAsync(SceneInstance scene) { var aOperation = Addressables.UnloadSceneAsync(scene,false); return OperationData.Get(aOperation); } //加载单资源 public OperationData LoadObjectAsync(string key, bool autoRelease = false, Action<object> callback = null) { var aOperation = Addressables.LoadAssetAsync<object>(key); return OperationData.Get(aOperation,autoRelease,callback); } //加载多个资源 public OperationData LoadObjectsAsync(List<string> keys, bool autoRelease = false, Action<object> callback = null) { var aOperation = Addressables.LoadAssetsAsync<object>(keys, null, Addressables.MergeMode.Union); return OperationData.Get(aOperation,autoRelease,callback); } //卸载资源 public void ReleaseObject(UnityEngine.Object obj) { Addressables.Release(obj); } |
lua侧的AddressManager.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 |
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 ---加载单一资源并缓存 --可获取进度GetOperation ---key为string function AddressableManager:LoadObjectAsync(key) return self.csAddressableManager:LoadObjectAsync(key) end ---加载多个资源并缓存 --可获取进度GetOperation ---keys为一个Table function AddressableManager:LoadObjectsAsync(keys) return self.csAddressableManager:LoadObjectsAsync(keys) end ---释放资源 ---obj为加载的资源 function AddressableManager:ReleaseObject(obj) self.csAddressableManager:ReleaseObject(obj) end return AddressableManager |
三、场景加载的例子
在章节二的时候,场景管理器使用的是Unity提供的UnityEngine.SceneManagement.SceneManager去加载场景的,目前使用了addressable后就采用addressable提供地址的形式来加载场景。这里直接贴一下最新的场景管理器的代码了,在LoadingScene方法中应用了卸载和加载场景。
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 |
--场景管理 加载 local base = require("Frame.Base.Manager.ManagerBase") local M = Class("Manager.SceneManager",base) local CSceneManager = CS.UnityEngine.SceneManagement.SceneManager function M:Init() self.dont_release_keys = {} local scene = CSceneManager.GetSceneByName("Main") self.mainSceneScript = require("Frame.Scene.MainScene").New() self.mainSceneScript:__StartUp(scene) self.scene_config = nil self.currentSceneInstance = nil self.currentSceneName = nil self.currentSceneScirpt = nil self.loadAson = nil --框架预加载资源 内容不能多 self:PreLoadFrameAsset() end function M:PreLoadFrameAsset() local need_preload_count = #FrameConfig.PreLoads if need_preload_count > 0 then local cur_preload_count = 0 for i,v in ipairs(FrameConfig.PreLoads) do self.dont_release_keys[v] = true GameObjectManager:GetInstance():GetAsset(v, function() cur_preload_count = cur_preload_count + 1 if cur_preload_count >= need_preload_count then self:LoadScene(FrameConst.DefaultSceneName) self.mainSceneScript:HideCanvasView() end end) end else self:LoadScene(FrameConst.DefaultSceneName) self.mainSceneScript:HideCanvasView() end end function M:LoadScene(sceneName) coroutine.start(function() self:LoadingScene(sceneName) end) end function M:LoadingScene(sceneName) if self.currentSceneScirpt ~= nil then --卸载原来场景 UIManager:GetInstance():DestroyAllWindow() --清理所有的窗口 回收 self.currentSceneScirpt:Dispose() GameObjectManager:GetInstance():ClearAllCache(self.dont_release_keys) --干掉 AddressableManager:GetInstance():UnLoadSceneAsync(self.currentSceneInstance) end UIManager:GetInstance():OpenWindow(FrameConfig.UIWindowNames.UILoadingWindow) self.currentSceneName = sceneName local key = string.format("%s/Scene/%s.unity",sceneName,sceneName) self.loadAson = AddressableManager:GetInstance():LoadSceneAsync(key) coroutine.waitforasyncop(self.loadAson,function(co,progress) UIManager:GetInstance():Notify(FrameConfig.UIWindowNames.UILoadingWindow, "ShowProgress", progress) end) self:LoadSceneSuccess() end function M:LoadSceneSuccess() local path = string.format("%s.Configs.SceneConfig", self.currentSceneName) local isSuc, cls = pcall(require, path) if isSuc then self.scene_config = table.deep_copy(cls) else self.scene_config = {} end --预加载内容 进度的话 再考虑吧 if self.scene_config.PreLoads and #self.scene_config.PreLoads > 0 then local need_preload_count = #self.scene_config.PreLoads local cur_preload_count = 0 for i,v in ipairs(self.scene_config.PreLoads) do GameObjectManager:GetInstance():GetAsset(v, function() cur_preload_count = cur_preload_count + 1 if cur_preload_count >= need_preload_count then self:FinishLoadScene() end end) end else self:FinishLoadScene() end end function M:FinishLoadScene() --获取加载的场景 self.currentSceneInstance = self.loadAson:GetAsset() local scene = self.currentSceneInstance.Scene CSceneManager.SetActiveScene(scene) self.loadAson = nil --防止场景逻辑直接就加载下一个场景 导致隐藏了下个场景的加载 local tempSceneName = self.currentSceneName --加载场景脚本 --这里有可能在同一帧下加载下一个场景,所以放最后 self.currentSceneScirpt = require(string.format("%s.%sScene",self.currentSceneName,self.currentSceneName )).New() self.currentSceneScirpt:__StartUp(scene) if tempSceneName == self.currentSceneName then UIManager:GetInstance():CloseWindow(FrameConfig.UIWindowNames.UILoadingWindow) end end function M:ShowMainCanvasView() if self.mainSceneScript then self.mainSceneScript:ShowCanvasView() end end function M:AddGameObjectToCurScene( obj ) local scene = self:GetCurScene() if not scene then return end CSceneManager.MoveGameObjectToScene( obj, scene) end function M:GetCurentSceneScript() return self.currentSceneScirpt end function M:GetSceneName() return self.currentSceneName end function M:GetCurScene() if self.currentSceneInstance then return self.currentSceneInstance.Scene end return nil end function M:dtor() self.loadAson = nil self.unLoadAson = nil self.currentSceneInstance = nil end return M |
四、资源加载对象池
需要实例化的资源例如Prefab,通过GetAssetInstance、CoGetAssetInstance来获取到实例,这里自动记录引用次数,不需要时可以通过RecycleObject进行回收或则通过ReleaseInstance销毁实例,回收的资源在下次加载的时候会优先使用不用再实例化(注意被污染的情况,删除了子物体或则添加了新物体,可以在Debug下添加回收检测,这里没做,注意使用就行),销毁的话会查看引用的次数,如果为0了就会卸载原始资源。
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 167 168 169 170 171 172 173 174 175 176 177 178 179 |
local ManagerBase = require("Frame.Base.Manager.ManagerBase") local GameObjectManager = Class("Manager.GameObjectManager",ManagerBase) local CSGameObject = CS.UnityEngine.GameObject function GameObjectManager:ctor() self.assets = {} self.busykeys = {} self.instance_cache = {} self.instance_count = {} self.instance_key = {} self.cache_root = nil self:Init() end --初始化接收回收物体的地方 function GameObjectManager:Init() if not self.cache_root then local go = CSGameObject("InstanceCacheRoot") CSGameObject.DontDestroyOnLoad( go ) self.cache_root = go.transform end end --回调的方式异步加载资源 需要实例化的资源通过GetAssetInstance加载 function GameObjectManager:GetAsset(key,cb) coroutine.start(function() local asset = self:CoGetAsset(key) cb(asset) end) end --异步加载资源 function GameObjectManager:CoGetAsset(key) if self:IsBusy(key) then coroutine.waituntil(function() return self:CheckCache(key) end) else if not self:CheckCache(key) then self.busykeys[key] = true local async = AddressableManager:GetInstance():LoadObjectAsync(key) coroutine.waitforasyncop(async) local asset = async:GetAsset() self:CacheAsset(key,asset) self.busykeys[key] = false end end return self.assets[key] end --回调的方式获取资源的实例 function GameObjectManager:GetAssetInstance(key,cb) coroutine.start(function() local instance = self:CoGetAssetInstance(key) cb(instance) end) end --异步获取资源的实例 记录资源的引用计数 function GameObjectManager:CoGetAssetInstance(key) if self.instance_cache[key] and #self.instance_cache[key] > 0 then local obj = table.remove(self.instance_cache[key], #self.instance_cache[key]) return obj end local asset = nil if self:CheckCache(key) then asset = self.assets[key] end if asset == nil then asset = self:CoGetAsset(key) end if not IsNull(asset) and asset ~= nil then local instance = CS.UnityEngine.GameObject.Instantiate(asset) if not self.instance_count[key] then self.instance_count[key] = 0 end self.instance_count[key] = self.instance_count[key] + 1 self.instance_key[instance] = key return instance end zprint('CoGetAssetInstance ', key, " is nil") return nil end --实例化资源回收 function GameObjectManager:RecycleObject( obj ) if IsNull(obj) then return end local key = self.instance_key[obj] if not key then return end self.instance_cache[key] = self.instance_cache[key] or {} obj.transform:SetParent(self.cache_root, false) obj:SetActive(false) table.insert(self.instance_cache[key], obj) end --释放实例化资源 function GameObjectManager:ReleaseInstance( obj ) if IsNull(obj) then return end local key = self.instance_key[obj] if not key then return end self.instance_key[obj] = nil CSGameObject.Destroy(obj) self.instance_count[key] = self.instance_count[key] - 1 if self.instance_count[key] <= 0 then self:ReleaseObject(key) end end --释放资源 function GameObjectManager:ReleaseObject( key ) if self.instance_count[key] and self.instance_count[key] > 0 then zprint(string.format("%s instance count > 0", key)) return end local asset = self.assets[key] if asset then AddressableManager:GetInstance():ReleaseObject(asset) self.assets[key] = nil end end --清除所有可以清理的缓存 场景切换调用 function GameObjectManager:ClearAllCache( donts ) donts = donts or {} for k,v in pairs(self.instance_cache) do if not donts[k] then for i,vv in ipairs(v) do self:ReleaseInstance( vv ) end self.instance_cache[k] = nil end end for k,v in pairs(self.assets) do self:ReleaseObject(k) end end function GameObjectManager:CacheAsset(key,asset) self.assets[key] = asset end function GameObjectManager:CheckCache(key) if self.assets[key] then return true end return false end function GameObjectManager:IsBusy(key) if self.busykeys[key] then return true end return false end return GameObjectManager |