Addressable优化解决方案

解决目标

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 Profiles,可以设置路径:
在这里插入图片描述
所有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/";

要取这个目录把下载的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;
   }
}

对比效果如下:

在这里插入图片描述

附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);
                }
            }
        }
    }
}

优化

1.catalog文件太大了,可以用JsonCompress压缩下。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KindSuper_liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值