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安装即可。

unity怎么接入admob广告 unity added_App Bundle

目标

如果项目本身加载场景/资源都采用异步加载,也就是SceneManager.LoadSceneAsync和Resources.LoadAsync,并且已经实现了等待过程处理,那这种转换起来还是蛮简单的。如果没有也没关系,因为Addressables可寻址系统也支持同步加载(最开始没有,1.17以上加了)

  •  场景加载API替换
  • Resources.Load加载资源的API替换
  • 实例化逻辑替换
  • 内存释放

请注意: 因为之前Resources.Load不需要考虑内存释放的问题,但是Addressables系统需要自己管理内存释放,否则内存会一直留着,导致内存不足可能会导致闪退情况。

资源可寻址

将插件导入后,需要开始配置可寻址路径,只有进行正确的配置,才能实现快速转换。

目标:

  • Resources文件目录转移(Addressables自行处理),否则也会打包到包体内
  • 场景变为可寻址

 配置:

  1. 打开Window/Asset Management/Addressables/Groups
  2. 将需要分包的资源目录/文件拖到组里面,会自动将资源移动到Resources_moved目录,防止Unity打包到包体内



    注意:加载路径有所修改,后面需要加上.prefab后缀名才能读取到
  3. 对于场景,直接将场景目录或.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_EDITOR
    public 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;
#else
		return 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);
}

内存管理

还有一步重要的设置,就是需要自行管理内存释放,否则随着游戏进程的增加,会导致内存泄露,玩家设备内存不足,会出现闪退等错误情况。

如何知道内存情况呢?

  1. 打开EventView,可以检测内存情况。
    Window/Addressables/Event Viewer
  2. 打开设置,选中配置文件,勾选Send Profiler Events,只有打开这个设置,才会检测资源情况
  3. 资源卸载时可以分析资源是否在列表里,在列表里说明没有释放,还占用着内存

测试

至此,转换工作顺利完成,可以开始测试。

  1. 打包AssetBundle
  2. 打包本体
  3. 打包后运行测试

结语

Addressables转换完成了,资源也分包好了,后面的工作就是进行谷歌aab合并上传。通过Play Asset Delivery可以快速的进行分发。

因为Google提供的插件是针对AssetBundle的,对于Addressables系统还有很多问题,而Unity这方面也没有过多的解释,所以后续还有蛮多工作要做的。

后续我们再细讲。