Addressables为什么加载会很慢
解决目标
1.可以和常规方案一样,带资源版本号,对于不同的渠道,有不同的资源地址,可以回退版本。对于多个旧版本资源,都可以更新到最新的。
2.热更流程和常规流程一样,首包在包体里,也可以部分在包体里,资源从首包里加载。有热更文件进入游戏走热更预加载,也可以部分文件边玩边下,或者静默下载。
3.带热更大小查看工具,可以查看热更Bundle和资源。
4.资源颗粒度控制工具,自动刷新资源Label工具。
5.本地Host工具,由于Addressable自带的Host工具不好使,自己写了一套本地Host工具。
以上工具不修改Addressable源码,只在其基础上扩展。拿来即用。
Inspector设置
Disable Catalog Update on Startup:禁止Unity一开始自己就去下Catalog文件。
Build Remote Catalog:使用远程目录,勾选。
Player Version Override:填写一个字符串,自己根据这个生成版本。
Max Concurrent Web Requests:最大并发下载AssetBundle数量,不宜设的过大,我这个填了10多个。
首席我们打包位置都设置为远程,这样远程地址有更新后,我们能根据这个地址去下载新的热更文件。然后这个目录是自己定义的:
Addressables为什么加载会很慢_System
打开Addressables Profiles,可以设置路径:
Addressables为什么加载会很慢_Addressables_02
所有AB设为远程非静态包。Can Change Post Release
打AB包额外操作
AddressablePath是一个静态类,可以代码运行时改变这个路径。这个路径可以一开始从服务器获取。这样我们就能实现不同的渠道设置获取不同的CDN地址了。
public class AddressablePath
{
public static string ServerBuild = “ServerData”;
private static string _remoteLoadPath = “http://soccer2res.unparallel.cn:80”;
public static string RemoteLoadPath { get { return _remoteLoadPath; } set {
_remoteLoadPath = value;
}
}
这样我们的包一开始就打到ServerData目录下。但是这些资源不会一开始就放到我们包体里面去。然后我们可以自己写一个编辑器工具,打完AB包就拷贝到Streaming文件夹路径下。
同时我们要往Streaming文件夹里写一个文件,我用的是Dict数据结构,保存的是首包资源里面的每一个文件名。为什么要有这么一个文件呢?这是因为后面我们需要从Streaming文件夹里判断是否有Bundle文件,而在Android平台下是无法直接用File.Exist()判断的。
public class BuildLocalBundleData
{
public static Dictionary<string,byte> BuildLocalBundleNames = new Dictionary<string,byte>();
public static async GTask Init()
{
var requestUrl = $"{AddressablePath.StreamingLoadPath}/{AddressablePath.GetPlatformForAssetBundles(Application.platform)}/BuildLocalBundleData.json";
string result = "";
//Android
if (requestUrl.Contains("://"))
{
using (UnityWebRequest webequest = UnityWebRequest.Get(requestUrl))
{
await GAsync.Get(webequest.SendWebRequest());
if (webequest.error != null)
{
#if UNITY_EDITOR
Debug.Log(webequest.error + requestUrl + “:包体资源不全”);
#else
Debug.LogError(webequest.error+requestUrl+“:包体资源不全”);
#endif
}
else
{
result = webequest.downloadHandler.text;
}
}
}
//iOS
else
{
if(File.Exists(requestUrl))
result = File.ReadAllText(requestUrl);
}
BuildLocalBundleNames = CatJson.JsonParser.ParseJson<Dictionary<string, byte>>(result);
if (BuildLocalBundleNames.Count <= 0)
{
#if UNITY_EDITOR
Debug.Log(requestUrl + “:BuildLocalBundleData 为空”);
#else
Debug.LogError(requestUrl+“:BuildLocalBundleData 为空”);
#endif
}
}
public static bool Index(string key)
{
return BuildLocalBundleNames.ContainsKey(key);
}
}
运行时更改地址
在运行时候要要先转换地址,首先从包体里面加载,如果没有,说明这个文件有更新了,如果缓存下载了,我们从缓存下载里去找,再没有再从远程去下载,根据InternalIdTransformFunc这个方法实现。
private string InternalIdTransformFunc(UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation location)
{
if (location.Data is AssetBundleRequestOptions ab)
{
//Debug.Log(“load primaryKey:”+location.PrimaryKey);
if (BuildLocalBundleData.Index(location.PrimaryKey))
{
var path = Path.Combine(AddressablePath.StreamingLoadPath,AddressablePath.GetPlatformForAssetBundles(Application.platform), location.PrimaryKey);
//Debug.Log($“——–exit:{path}—————”);
return path;
}
else
{
var path = Path.Combine(Caching.currentCacheForWriting.path, ab.BundleName, ab.Hash, “__data”);
if (File.Exists(path))
{
return path;
}
}
}
return location.InternalId;
}
Caching.currentCacheForWriting.path的目录在Andorid平台如下:
Android/data/包名/files/UnityCache/Shared/
Addressable更新流程
1.请求服务器获取到资源服务器地址。
2.请求资源服务器上的catalog.hash文件。如果和本地保存的有变化,则说明有热更。
本地的默认在这个目录
internal const string kCacheDataFolder = “{UnityEngine.Application.persistentDataPath}/com.unity.addressables/”;
1.
要取这个目录把下载的Catalog文件覆盖原来的。
3.有热更的情况下,去下载Catalog.json文件。覆盖本地的。
4.下载热更文件。
热更文件对比工具
可以查看两个版本,那些Bundle文件和资源文件发生了改变:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AddressableToolkit;
using Codice.Client.Common;
using Platform.Editor;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
public class ComFoldEditor : EditorWindow
{
GUIStyle mfileStyle;
private string basPath;
private string updatePath;
long totalSize = 0;
IOrderedEnumerable<KeyValuePair<string, long>> sortResult;
Dictionary<string, List<string>> bundle4Key = new Dictionary<string, List<string>>();
ToggleTreeview globaltree;
TreeViewItem globalRoot;
[MenuItem(“Tools/@Addressable/比较Bundle”)]
public static void OpenWindow()
{
GetWindow<ComFoldEditor>().Show();
}
private void OnEnable()
{
mfileStyle = new GUIStyle();
mfileStyle.normal.background = EditorGUIUtility.FindTexture(“Folder Icon”);
string prefBasPath = EditorPrefs.GetString(“basPath”);
if (Directory.Exists(prefBasPath))
{
basPath = prefBasPath;
}
string prefUpdatePath = EditorPrefs.GetString("updatePath");
if (Directory.Exists(prefUpdatePath))
{
updatePath = prefUpdatePath;
}
}
public void InitBundle4Key(string catalogPath)
{
Action<string, IList<IResourceLocation>> logBunSzie = (x, y) =>
{
foreach (IResourceLocation location in y)
{
var sizeData = location.Data as AssetBundleRequestOptions;
if (sizeData != null)
{
if (!bundle4Key.TryGetValue(location.PrimaryKey, out var collectList))
{
collectList = new List<string>();
bundle4Key.Add(location.PrimaryKey, collectList);
}
if (!collectList.Contains(x))
{
collectList.Add(x);
}
}
break;
}
};
try
{
var conetentData = JsonUtility.FromJson<ContentCatalogData>(File.ReadAllText(catalogPath));
var resMap = conetentData.CreateLocator();
foreach (var item in resMap.Locations)
{
foreach (IResourceLocation location in item.Value.Distinct())
{
if (location.Data is AssetBundleRequestOptions ab)
{
}
else
{
logBunSzie(location.PrimaryKey, location.Dependencies);
}
}
}
}
catch (Exception e)
{
Debug.LogError("热更bundle目录下catalog文件不存在");
Debug.LogError(e);
throw;
}
}
private void InitTreeData()
{
if (globalRoot == null)
{
int id = -1;
globalRoot = new TreeViewItem();
globalRoot.id = id;
globalRoot.depth = -1;
foreach (var l in sortResult)
{
var tItem = new TreeViewItem();
tItem.id = ++id;
tItem.depth = 0;
tItem.displayName = $“{l.Key} {GetFileSize(l.Value)}”;
globalRoot.AddChild(tItem);
if(bundle4Key.TryGetValue(l.Key,out var address))
{
foreach (var VARIABLE in address)
{
TreeViewItem addressName = new TreeViewItem();
addressName.displayName = VARIABLE;
addressName.id = ++id;
addressName.depth = 1;
tItem.AddChild(addressName);
}
}
}
}
if (globaltree == null)
{
globaltree = new ToggleTreeview("", "", position.width, globalRoot, true, true, 18);
// globaltree.CellCallBack += DrawItemCell;
globaltree.Reload();
globaltree.OnDoubleClicked = (item) =>
{
string copy = item.displayName.Split(' ')[0];
GUIUtility.systemCopyBuffer =copy;
GetWindow<ComFoldEditor>().ShowNotification(new GUIContent("已copy"));
};
}
}
private Vector3 mScrollPos;
private void OnGUI()
{
GUILayout.BeginHorizontal();
GUILayout.Label(“底包bundle目录:”,GUILayout.Width(100));
basPath = EditorGUILayout.TextField(basPath, GUILayout.Width(420));
if (GUILayout.Button(“选择”))
{
basPath = EditorUtility.OpenFolderPanel(“bundle目录”, "“, ”");
EditorPrefs.SetString(“basPath”,basPath);
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("热更bundle目录:",GUILayout.Width(100));
updatePath = EditorGUILayout.TextField(updatePath, GUILayout.Width(420));
if (GUILayout.Button("选择"))
{
updatePath = EditorUtility.OpenFolderPanel("bundle目录", "", "");
EditorPrefs.SetString("updatePath",updatePath);
}
GUILayout.EndHorizontal();
if (GUILayout.Button("比较"))
{
string catalogPath = updatePath + "/catalog_base.json";
InitBundle4Key(catalogPath);
CompareFold(basPath,updatePath);
globalRoot = null;
globaltree = null;
InitTreeData();
}
if (totalSize > 0)
{
GUILayout.Label("热更大小:"+GetFileSize(totalSize));
}
if (sortResult !=null && sortResult.Any())
{
GUILayout.BeginArea(new Rect(10,100,this.position.width-20,this.position.height-100));
mScrollPos = GUILayout.BeginScrollView(mScrollPos);
if (globaltree != null)
{
globaltree.OnGUI(new Rect(0, 0, this.position.width - 20, this.position.height - 100));
}
GUILayout.EndScrollView();
GUILayout.EndArea();
}
}
public void CompareFold(string basePath,string updatePath)
{
if (string.IsNullOrEmpty(basePath) || string.IsNullOrEmpty(updatePath))
{
Debug.LogError(“请选择目录!”);
return;
}
List<string> baseBundleFullNames = new List<string>();
CollectAllFilesName(basePath,baseBundleFullNames);
List<string> baseBundleNames = baseBundleFullNames.ConvertAll(Path.GetFileName);
// baseBundleNames.ForEach(Debug.Log);
List<string> updateBundleFullNames = new List<string>();
CollectAllFilesName(updatePath,updateBundleFullNames);
Dictionary<string,long> result = new Dictionary<string,long>();
foreach (var VARIABLE in updateBundleFullNames)
{
//名字一样 没有发生变化
if (baseBundleNames.Contains(Path.GetFileName(VARIABLE)))
{
continue;
}
//新增文件
result.Add(Path.GetFileName(VARIABLE),new FileInfo(VARIABLE).Length);
}
sortResult = from d in result orderby d.Value descending select d;
totalSize = 0;
foreach (var l in sortResult)
{
totalSize += l.Value;
Debug.Log(l.Key +" "+ GetFileSize(l.Value));
}
Debug.Log("total size:"+ GetFileSize(totalSize));
}
public static void CollectAllFilesName(string path, List<string> result)
{
if (result == null)
{
Debug.LogError(“result list is null.”);
}
foreach (var filePath in Directory.GetFiles(path))
{
if (filePath.EndsWith("bundle"))
{
result.Add(filePath);
}
}
foreach (var directory in Directory.GetDirectories(path))
{
CollectAllFilesName(directory,result);
}
}
public static string GetFileSize(long byteCount)
{
string size = “0 B”;
if (byteCount >= 1073741824.0)
size = $“{byteCount / 1073741824.0:##.##}” + “ GB”;
else if (byteCount >= 1048576.0)
size = $“{byteCount / 1048576.0:##.##}” + “ MB”;
else if (byteCount >= 1024.0)
size = $“{byteCount / 1024.0:##.##}” + “ KB”;
else if (byteCount > 0 && byteCount < 1024.0)
size = byteCount.ToString() + “ B”;
return size;
}
}
对比效果如下:
Addressables为什么加载会很慢_System_03
附ToggleTreeview文件:
namespace Platform.Editor
{
public class ToggleTreeview : TreeView
{
SearchField _searchField = new SearchField();
TreeViewItem Root;
public Func<Rect, TreeViewItem, int,bool> CellCallBack;
public Action<TreeViewItem> OnDoubleClicked;
public Action<TreeViewItem> OnContextClicked;
public Action<TreeViewItem> OnClicked;
public Action<int,string> OnRename;
public Func<TreeViewItem, string, bool> SearchEvent;
public Action<IList<int>> OnSelectChange;
internal ToggleTreeview(TreeViewState state, MultiColumnHeaderState mchs, TreeViewItem Root,float rowHeight = 18f, bool showAlternatingRowBackgrounds = true) : base(state, new MultiColumnHeader(mchs))
{
showBorder = false;
this.rowHeight = rowHeight;
this.showAlternatingRowBackgrounds = showAlternatingRowBackgrounds;
this.Root = Root;
this.searchString = "";
}
internal ToggleTreeview(string contentName, string tips, float width, TreeViewItem Root, bool showBorder, bool showAlternatingRowBackgrounds, float rowHeight = 18f) : base(new TreeViewState())
{
this.rowHeight = rowHeight;
this.showBorder = showBorder;
this.showAlternatingRowBackgrounds = showAlternatingRowBackgrounds;
showAlternatingRowBackgrounds = true;
this.Root = Root;
this.searchString = "";
}
protected override TreeViewItem BuildRoot()
{
return Root;
}
public override void OnGUI(Rect rect)
{
Rect screct = rect;
screct.height = 18f;
searchString = _searchField.OnGUI(screct, searchString);
screct.height = rect.height;
screct.height -= 18f;
screct.y += 18f;
base.OnGUI(screct);
}
public void SetRowHeight(float rowHeight)
{
this.rowHeight = rowHeight;
}
protected override bool DoesItemMatchSearch(TreeViewItem item, string search)
{
if (SearchEvent != null)
return SearchEvent.Invoke(item, search);
return base.DoesItemMatchSearch(item, search);
}
protected override void SelectionChanged(IList<int> selectedIds)
{
OnSelectChange?.Invoke(selectedIds);
}
protected override void RowGUI(RowGUIArgs args)
{
if (CellCallBack != null)
{
if (CellCallBack.Invoke(args.rowRect, args.item, args.item.id))
{
return;
}
}
base.RowGUI(args);
}
protected override void SingleClickedItem(int id)
{
var item = FindItem(id, Root);
if (item != null)
{
if (OnClicked != null)
{
OnClicked(item);
}
}
}
protected override void ContextClickedItem(int id)
{
var item = FindItem(id, Root);
if (item != null)
{
if (OnContextClicked != null)
{
OnContextClicked(item);
}
}
}
public TreeViewItem GetItem(int ID)
{
return FindItem(ID,Root);
}
public void SetClickItem(int id)
{
var selectionId = GetItem(id);
if (selectionId != null)
SelectionClick(selectionId, true);
}
protected override bool CanRename(TreeViewItem item)
{
return base.CanRename(item);
}
protected override void RenameEnded(RenameEndedArgs args)
{
OnRename?.Invoke(args.itemID, args.newName);
}
protected override void DoubleClickedItem(int id)
{
var item = FindItem(id, Root);
if (item != null)
{
if (OnDoubleClicked != null)
{
OnDoubleClicked(item);
}
}
}
}
}
x
优化
1.catalog文件太大了,可以用JsonCompress压缩下。