本文主要是介绍Unity发布Android App Bundle详解(三)快速转换Addressables,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
2021.12.28更新
经群友提醒,目前Unity官方已经支持直接不进行任何更改打出aab包了。
支持的Unity版本:
- 2021 → 2021.2.0b4 以上
- 2020 → 2020.3.15f2 以上
- 2019 → 2019.4.29f1 以上
将Split Application Binary选项勾选
在ProjectSettings → Android → Publish Settings 最底下有个 Split Application Binary,将其勾选
原本这是会让 APK 产生 APK Expansion Files (.oob) 的选项,但在build target选AAB的情况下会变成使用Play Asset Delivery。
详细操作方式按照官方文档操作即可
也可以查看该文章解决:https://medium.com/akatsuki-taiwan-technology/unity-play-asset-delivery-1d468fd90c2d
-----------------------------------------------------------------------------------------------------
概述
对于项目本身就使用AssetBundle的来说,打包新格式aab是很容易的,上篇文章已经详细说过了。
对于项目之初没有考虑AssetBundle热更新的项目怎么办呢?
项目都是采用Resources加载,并且是同步加载的,unity场景资源也较多,没有做好分包设计的怎么办呢?
这篇文章我们讲怎么处理。
难点
- 场景采用同步/异步加载
SceneManager.LoadScene ("xx",LoadSceneMode.Single);
- 资源采用Resources.Load加载
GameObject prefab = Resources.Load<GameObject> (path);
方案
采用Unity自带的可寻址系统插件Addressables(该插件已经替代AssetBundle作为Unity推荐的热更新方案)
采用Addressables的方式将项目快速转换为热更新。
此方案改动较少,对于之前的文件路径也不需要修改太多逻辑,几乎能完美移植。
插件导入
在项目中使用Package Manager,找到Addressables安装即可。
目标
如果项目本身加载场景/资源都采用异步加载,也就是SceneManager.LoadSceneAsync和Resources.LoadAsync,并且已经实现了等待过程处理,那这种转换起来还是蛮简单的。如果没有也没关系,因为Addressables可寻址系统也支持同步加载(最开始没有,1.17以上加了)
-
场景加载API替换
-
Resources.Load加载资源的API替换
-
实例化逻辑替换
-
内存释放
请注意: 因为之前Resources.Load不需要考虑内存释放的问题,但是Addressables系统需要自己管理内存释放,否则内存会一直留着,导致内存不足可能会导致闪退情况。
资源可寻址
将插件导入后,需要开始配置可寻址路径,只有进行正确的配置,才能实现快速转换。
目标:
- Resources文件目录转移(Addressables自行处理),否则也会打包到包体内
- 场景变为可寻址
配置:
- 打开Window/Asset Management/Addressables/Groups
- 将需要分包的资源目录/文件拖到组里面,会自动将资源移动到Resources_moved目录,防止Unity打包到包体内
注意:加载路径有所修改,后面需要加上.prefab后缀名才能读取到 - 对于场景,直接将场景目录或.unity文件直接拖入即可。(名字可以自行更改)
代码配置
如果想通过代码配置,Addressables官方没有提供简单的方式,但是我们也可以通过这种方式实现。
为此我自己封装了这个类,大家调用接口即可通过代码方式把资源变为可寻址。
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AddressableAssets.Settings;
#endif
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;public class AddressablesUtils
{public static T LoadAsset<T>(string path){var request = LoadAssetAsync<T>(path);return request.WaitForCompletion();}public static AsyncOperationHandle<T> LoadAssetAsync<T>(string path){try{var request = Addressables.LoadAssetAsync<T>(path);return request;}catch (Exception e){Debug.Log(e.Message);}return default;}public static AsyncOperationHandle<SceneInstance> LoadSceneAsync(string level,LoadSceneMode loadSceneMode){var async = Addressables.LoadSceneAsync(level, loadSceneMode);return async;}public static GameObject Instantiate(string path, Transform parent){var request = InstantiateAsync(path, parent);return request.WaitForCompletion();}public static AsyncOperationHandle<GameObject> InstantiateAsync(string path,Transform parent){try{return Addressables.InstantiateAsync(path, parent);}catch (Exception e){Debug.Log(e.Message);}return default;}public static void ReleaseInstance(GameObject instance){Addressables.ReleaseInstance(instance);}public static void Release<T>(T target){Addressables.Release(target);}#if UNITY_EDITORpublic static void AddToAddressable(string path,string address="",string groupName="PlayAssetDelivery",bool isAdd=true){if (string.IsNullOrEmpty(path)) return;var assetSettingPath = "Assets/AddressableAssetsData/AddressableAssetSettings.asset";AddressableAssetSettings addressableAssetSettings = AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(assetSettingPath);SetAaEntry(addressableAssetSettings, groupName, path, isAdd, address);}static void SetAaEntry(AddressableAssetSettings aaSettings,string groupName, string path, bool create,string address=""){AddressableAssetGroup assetGroup = null;if (!string.IsNullOrEmpty(groupName)){assetGroup = aaSettings.FindGroup(groupName);}else{assetGroup = aaSettings.DefaultGroup;}if (create && assetGroup.ReadOnly){Debug.LogError("Current default group is ReadOnly. Cannot add addressable assets to it");return;}Undo.RecordObject(aaSettings, "AddressableAssetSettings");var guid = string.Empty;//if (create || EditorUtility.DisplayDialog("Remove Addressable Asset Entries", "Do you want to remove Addressable Asset entries for " + targets.Length + " items?", "Yes", "Cancel")){var entriesAdded = new List<AddressableAssetEntry>();var modifiedGroups = new HashSet<AddressableAssetGroup>();Type mainAssetType;guid = AssetDatabase.AssetPathToGUID(path);if (create){var e = aaSettings.CreateOrMoveEntry(guid, assetGroup, false, false);if (!string.IsNullOrEmpty(address)) e.address = address;entriesAdded.Add(e);modifiedGroups.Add(e.parentGroup);}else{aaSettings.RemoveAssetEntry(guid);}if (create){foreach (var g in modifiedGroups)g.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesAdded, false, true);aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesAdded, true, false);}}}
#endif
}
增加到Unity右键菜单
选中目录,直接添加可寻址,并去除后缀
因为unity通过Resources.Load加载的资源路径不需要后缀的,而拖文件夹的方式后缀名又无法去除,所以写了个菜单,自动将选中的目录下的文件加到可寻址,并去除后缀名。
static List<string> SelectsPath{get{List<string> pathList = new List<string>();var selectionList = Selection.assetGUIDs;foreach (var guid in selectionList){var assetPath = AssetDatabase.GUIDToAssetPath(guid);var fullPath = SVNProjectPath + "/" + assetPath;pathList.Add(fullPath);}List<string> result = new List<string>(pathList);for (int i = 0; i < pathList.Count; i++){string svnPath = pathList[i];//移除掉所有比自己长并且部分完全包含自己的路径(也就是移除所有子路径)result.RemoveAll((path) =>{return path.Length > svnPath.Length && path.StartsWith(svnPath);});}return result;}}
public static string SVNProjectPath{get{System.IO.DirectoryInfo parent = System.IO.Directory.GetParent(Application.dataPath);return parent.ToString();}}
[MenuItem("Assets/Add To Addressables", false)]static void AddToAddressables(){List<string> selectPath = SelectsPath;foreach (var path in selectPath){if (FileControll.FolderExist(path)){var files = FileControll.GetFolderFiles(path,true);int totalLength = files.Count;int i = 0;foreach (var filePath in files){i++;float progress = ((float)i / (float)totalLength);bool isCancel = EditorUtility.DisplayCancelableProgressBar (string.Format("正在处理..."), string.Format("处理资源({0}/{1})",i,totalLength), progress);if (isCancel){EditorUtility.ClearProgressBar();break;}if (filePath.EndsWith(".meta")) continue;string fPath = FileControll.GetRelativePath(filePath, Application.dataPath.Replace("Assets",""));string address = fPath.Replace(new FileInfo(fPath).Extension,"");address = FileControll.MakePathPerfect(address);if (address.StartsWith("Assets/Game/Resources_moved")){address = address.Replace("Assets/Game/Resources_moved/", "");}//Debug.Log(fPath+"==="+Application.dataPath);AddressablesUtils.AddToAddressable(fPath,address);}EditorUtility.ClearProgressBar();}}}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using System.Text;public static class FileControll{/// <summary>/// 创建文件/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static bool CreateFile(string filePath){if (!FileExist (filePath)) {File.Create (filePath);return FileExist(filePath);}return false;}/// <summary>/// 创建目录/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static bool CreateFolder(string folderPath){if (!FolderExist(folderPath)) {DirectoryInfo info = Directory.CreateDirectory (folderPath);return info.Exists;}return false;}/// <summary>/// 删除文件/// </summary>/// <param name="filePath"></param>public static void DeleteFile(string filePath){if (FileExist (filePath)) {File.Delete (filePath);}}/// <summary>/// 删除目录/// </summary>/// <param name="folderPath"></param>public static void DeleteFolder(string folderPath){DeleteFolder (folderPath, true);}/// <summary>/// 删除目录/// </summary>/// <param name="folderPath"></param>/// <param name="recursive">是否递归删除</param>public static void DeleteFolder(string folderPath,bool recursive){if(FolderExist(folderPath)){Directory.Delete (folderPath, recursive);}}/// <summary>/// 目录是否存在/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static bool FolderExist(string folderPath){return Directory.Exists (folderPath);}/// <summary>/// 文件是否存在/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static bool FileExist(string filePath){return File.Exists (filePath);}/// <summary>/// 获取文件的目录/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static string GetFileFolder(string filePath){FileInfo fileInfo = new FileInfo (filePath);return fileInfo.Directory.ToString ();}/// <summary>/// 获取子目录/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static List<string> GetSubFolders(string folderPath){List<string> result = new List<string> ();GetSubFolders (folderPath, ref result);return result;}/// <summary>/// 获取子目录/// </summary>/// <param name="path"></param>/// <param name="result"></param>static void GetSubFolders(string path,ref List<string> result){result.Add (path);if (Directory.Exists(path)){foreach (string sub in Directory.GetDirectories(path)) {GetSubFolders (sub + "/", ref result);}}}/// <summary>/// 获取对应路径的相对路径(如absolutePath=E:/AB/c.txt,relativeTo=E:/AB/,输出c.txt)/// </summary>/// <param name="absolutePath"></param>/// <param name="relativeTo"></param>/// <returns></returns>public static string GetRelativePath(string absolutePath,string relativeTo){var fileInfo = new FileInfo(relativeTo);var fullFileInfo = new FileInfo(absolutePath);string absoluteName = fullFileInfo.FullName;absoluteName = MakePathPerfect(absoluteName);string relative = fileInfo.FullName;relative = MakePathPerfect(relative);string result = absoluteName.Replace(relative, "");return result;}/// <summary>/// 获取目录下的所有文件/// </summary>/// <param name="folderPath"></param>/// <param name="recursive">是否递归获取</param>/// <param name="endWith"></param>/// <returns></returns>public static List<string> GetFolderFiles(string folderPath, bool recursive,string endWith=""){List<string> fileList;if (recursive){fileList = new List<string>();List<string> subFolderList = GetSubFolders (folderPath);foreach (var subFolder in subFolderList){List<string> subFileList = GetFolderFiles(subFolder, endWith);fileList.AddRange (subFileList);}}else{fileList = GetFolderFiles(folderPath, endWith);}return fileList;}/// <summary>/// 获取目录下的所有文件/// </summary>/// <param name="folderPath"></param>/// <param name="endWith"></param>/// <returns></returns>public static List<string> GetFolderFiles(string folderPath,string endWith=""){List<string> result = new List<string> ();if (Directory.Exists(folderPath)){foreach (string file in Directory.GetFiles(folderPath)){if (!string.IsNullOrEmpty(endWith) && !file.ToLower().EndsWith(endWith.ToLower())) continue;result.Add (file);}}return result;}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="bytes"></param>public static void WriteFile(string path,byte[] bytes){WriteFile (path, bytes, FileMode.Create);}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="bytes"></param>/// <param name="fileMode"></param>public static void WriteFile(string path,byte[] bytes,FileMode fileMode){
#if UNITY_EDITOR || (!UNITY_WINRT)try {FileStream fs = new FileStream(path, fileMode);fs.Write (bytes, 0, bytes.Length);fs.Close();} catch (Exception ex) {Debug.LogError ("文件写入失败" + path + ":" + ex.Message);}
#endif}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="append"></param>/// <param name="infos"></param>public static void WriteFile(string path,bool append,List<string> infos){try {StreamWriter sw = new StreamWriter (path, append);if (infos!=null) {foreach (string info in infos) {sw.WriteLine(info); } }sw.Close ();sw.Dispose ();} catch (Exception ex) {Debug.LogError ("文件写入失败" + path + ":" + ex.Message);}}/// <summary>/// 写入Txt文件/// </summary>/// <param name="path"></param>/// <param name="content"></param>/// <param name="encoding"></param>public static void WriteTxtFile(string path, string content,Encoding encoding){File.WriteAllText(path,content,encoding);}/// <summary>/// 写入Txt文件/// </summary>/// <param name="path"></param>/// <param name="content"></param>public static void WriteTxtFile(string path, string content){File.WriteAllText(path,content);}/// <summary>/// 复制文件到/// </summary>/// <param name="path"></param>/// <param name="toPath"></param>/// <param name="overwrite">是否覆盖</param>public static void CopyFile(string path,string toPath,bool overwrite){try {File.Copy(path,toPath,overwrite);} catch (Exception ex) {Debug.LogError ("拷贝文件失败:"+ex.Message);}}/// <summary>/// 复制目录到/// </summary>/// <param name="from"></param>/// <param name="to"></param>public static void CopyFolder(string from, string to){if (!Directory.Exists(to))Directory.CreateDirectory(to);// 子文件夹foreach (string sub in Directory.GetDirectories(from))CopyFolder(sub + "/", to + Path.GetFileName(sub) + "/");// 文件foreach (string file in Directory.GetFiles(from)){try {File.Copy(file, to + Path.GetFileName(file), true);} catch (Exception ex) {Debug.LogWarning ("拷贝失败:" + ex.Message);}}}/// <summary>/// 读取文件/// </summary>/// <param name="path"></param>/// <returns></returns>public static byte[] ReadFile(string path){
#if UNITY_EDITOR || (!UNITY_WINRT)if (!File.Exists (path))return null;FileStream fs = new FileStream(path, FileMode.Open);long size = fs.Length;byte[] array = new byte[size];//将文件读到byte数组中fs.Read(array, 0, array.Length);fs.Close();return array;
#elsereturn null;
#endif}/// <summary>/// 读取Txt数据/// </summary>/// <param name="path"></param>/// <returns></returns>public static string ReadTxtFile(string path){if (!File.Exists (path))return null;return File.ReadAllText (path);}/// <summary>/// 读取Txt文件/// </summary>/// <param name="path"></param>/// <param name="encoding"></param>/// <returns></returns>public static string ReadTxtFile(string path,System.Text.Encoding encoding){if (!File.Exists (path))return null;return File.ReadAllText (path, encoding);}/// <summary>/// 读取Txt行/// </summary>/// <param name="path"></param>/// <returns></returns>public static List<string> ReadTxtFileLine(string path){if (!File.Exists (path))return null;try{using(StreamReader sr = new StreamReader (path)){List<string> dataList=new List<string>();string line;while ((line = sr.ReadLine()) != null) {dataList.Add(line);}return dataList;}}catch(Exception e){Debug.Log("文件未能读取"+e.Message);return null;}}/// <summary>/// 检测并矫正CSV格式/// </summary>/// <param name="path">csv文件路径</param>/// <param name="fileEncoding"></param>/// <returns>是否矫正</returns>public static bool CheckAndCollectCSVFormat(string path,Encoding fileEncoding=null){if (fileEncoding == null){TextUtil.EncodingType encodingType = TextUtil.GuessFileEncoding(path);if (encodingType == TextUtil.EncodingType.Unknown){fileEncoding = TextUtil.ANSI_CHINESE; //默认用ANSI编码}else fileEncoding = TextUtil.GetEncoding(encodingType);}if (!fileEncoding.CodePage.Equals(Encoding.UTF8.CodePage)){var content = File.ReadAllText(path, fileEncoding);File.WriteAllText(path,content,Encoding.UTF8);return true;}return false;}/// <summary>/// 合并路径/// </summary>/// <param name="folderPath"></param>/// <param name="fileName"></param>/// <returns></returns>public static string CombineFilePath(string folderPath, string fileName){if (string.IsNullOrEmpty(folderPath)){return "";}DirectoryInfo directoryInfo=new DirectoryInfo(folderPath);if (!directoryInfo.Exists){Debug.LogError("目录不存在:"+folderPath);return "";}return directoryInfo.FullName + "\\" + fileName;}/// <summary>/// 使路径规范化(都变成这样的格式:Assets/Game/Source)/// 如Assets\Game\Source变成Assets/Game/Source/// </summary>/// <param name="path"></param>/// <returns></returns>public static string MakePathPerfect(string path){return path.Replace("\\", "/");}/// <summary>/// 移动文件/// </summary>/// <param name="from"></param>/// <param name="to"></param>public static void MoveFile(string @from, string to){@from = from.Replace('\\', '/');@to = to.Replace('\\', '/');if (Directory.Exists(to)){FileControll.CopyFolder(@from, to);FileControll.DeleteFolder(@from);Debug.Log($"覆盖文件夹:{@from} ===> {to}");}else if (File.Exists(to)){FileControll.CopyFile(@from, to, true);FileControll.DeleteFile(@from);Debug.Log($"覆盖文件:{@from} ===> {to}");}else{string fileLastPath = to;if (to.Contains(".")){fileLastPath=to.GetFileLastPath();}if (!Directory.Exists(fileLastPath)){Directory.CreateDirectory(fileLastPath);}File.Move(@from, to);Debug.Log($"移动文件:{@from} ===> {to}");}}
}
场景加载API替换
代码示例
void LoadScene(string sceneName)
{SceneManager.LoadSceneAsync(sceneName,LoadSceneMode.Single);
}
替换后:
void LoadScene(string sceneName)
{Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
}
注意:
对于LoadSceneMode.Single,Addressables系统会在切换到其他场景时自动卸载内存,所以无需内存管理。而对于LoadSceneMode.Additive模式需要卸载场景时调用Addressables.UnloadSceneAsync卸载场景释放内存。
Resources资源API替换
代码示例
T LoadAsset<T>(string path)where T:Object
{return Resources.Load<T>(path);
}
替换后
使用request.WaitForCompletion可以将线程卡死,等待加载完成释放(该行为有风险,可能会导致一些意外情况)相当于是同步过程。
参考官方文档:Synchronous Workflow | Addressables | 1.17.17
T LoadAsset<T>(string path)where T:Object
{var request = Addressables.LoadAssetAsync<T>(path);request.WaitForCompletion();return request.Result;
}
注意:
- 加载后的资源需要在恰当的时候释放掉,否则会导致内存泄露问题
释放接口如下:
//释放实例
public static void ReleaseInstance(GameObject instance)
{Addressables.ReleaseInstance(instance);
}//释放加载的对象
public static void Release<T>(T target)
{Addressables.Release(target);
}
实例化逻辑替换
对于使用Instantiate实例化预制,需要修改,否则释放不了
代码示例
void InstancePrefab(string path,Transform parent)
{var prefab=Resources.Load<GameObject>(path);var go = GameObject.Instantiate(prefab, parent);
}
替换后
void InstancePrefab(string path,Transform parent)
{var request = Addressables.InstantiateAsync(path);var go=request.WaitForCompletion();
}
注意:
只有通过这种方式Instance出来的对象,才能进行释放
public static void ReleaseInstance(GameObject instance)
{Addressables.ReleaseInstance(instance);
}
内存管理
还有一步重要的设置,就是需要自行管理内存释放,否则随着游戏进程的增加,会导致内存泄露,玩家设备内存不足,会出现闪退等错误情况。
如何知道内存情况呢?
- 打开EventView,可以检测内存情况。
Window/Addressables/Event Viewer - 打开设置,选中配置文件,勾选Send Profiler Events,只有打开这个设置,才会检测资源情况
- 资源卸载时可以分析资源是否在列表里,在列表里说明没有释放,还占用着内存
测试
至此,转换工作顺利完成,可以开始测试。
- 打包AssetBundle
- 打包本体
- 打包后运行测试
结语
Addressables转换完成了,资源也分包好了,后面的工作就是进行谷歌aab合并上传。通过Play Asset Delivery可以快速的进行分发。
因为Google提供的插件是针对AssetBundle的,对于Addressables系统还有很多问题,而Unity这方面也没有过多的解释,所以后续还有蛮多工作要做的。
后续我们再细讲。
上一篇:Unity发布Android App Bundle详解(二)Play Asset Delivery介绍
下一篇:Unity发布Android App Bundle详解(四)Addressables+Play Asset Delivery分发
系列文章索引
Unity发布Android App Bundle详解(一)Unity .aab支持情况
Unity发布Android App Bundle详解(二)Play Asset Delivery介绍
Unity发布Android App Bundle详解(三)快速转换Addressables
Unity发布Android App Bundle详解(四)Addressables+Play Asset Delivery分发
这篇关于Unity发布Android App Bundle详解(三)快速转换Addressables的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!