找回密码
 立即注册
查看: 372|回复: 1

基于addressable的热更系统

[复制链接]
发表于 2022-7-15 10:05 | 显示全部楼层 |阅读模式
之前基于assetbundle实现过一版热更系统. 该系统可以实现增量更新,即使同一个资源迭代多次也不会重复下载,也支持回滚资源版本.
        原理如下:整个过程分为打包,下载,和加载三部分.首先每一个版本都会有一个文件夹,里面存放与上一个版本变化的资源和当前版本的目录文件.目录文件中每条目录会记录资源名字以及该资源当前最新的版本号.
       打包流程:先按分类打出assetbundle资源并计算这些assetbundle的md5, 基于上一个目录文件创建新的目录文件,对比如果有新增的资源或者md5变化的资源则更新资源对应的版本号,同时吧改资源放入新版本文件夹. 对比完毕后将新目录保存值新版本文件夹,然后将新版本增量资源同步到cdn服务器. 运维在后台更新客户端资源版本. 此处可以通过脚本批量完成和发送钉钉微信通知
        更新流程:先获取服务器设置的资源版本号,然后对比本地已经下载的版本号, 如有变化就将一个或者多个迭代版本下载.此处可以加上断线重连等优化功能.  
        加载流程: 将要加载的资源先从目录里面查找,如果有找到就加载当前版本的资源,如果没有那么就是首包带入的资源.去streamassets和resources目录查找和加载.
        addressable系统则是基于assetbundle整理了资源加载流程.通过门面模式抽象了本地加载和远端加载.让加载资源的代码保持统一而不用关心具体资源在何处.其可以将资源分为带入版本的local资源和远端remote资源, local资源如果有更新会重新生成remote组.  
针对当下运营的需求, 首包需要带入初始资源,然后运营活动和玩法需要经常迭代.如果使用local方式组织资源,那么后续会有大量的增量组,维护比较麻烦, 使用romote方式,不会在首包.第一次进入回加载较大的资源.而且没有版本概念所有无法做到回退版本,同时每次变化也没有增量文件提示,会导致即使只有一个小文件比变化,也需要同步整个资源目录至cdn.
那么我们需要增加如下功能:
1、remote资源可以带入首包中
2、资源目录支持版本号,可以做到版本更新和回退
3、统计每次打资源的增量变化,减少cdn同步大小
4、因为会有发行的变化, cdn地址会有变化,需要支持动态修改remote地址功能
5、自动group工具,方便开发的时候对于导入新资源或者删除资源的时候能够自动更新group组

废话不多说,魔改addressable代码如下:
1、remote资源带入首包中

如果要实现remote资源带入首包,那么就需要将构建的remote资源打包的时候放入streamasset文件夹中,同时将这部分资源保存在一个首包资源目录中.这里自定义构建类MyBuildScriptPackedMode.cs继承BuildScriptPackedMode实现重写BuildDataImplementation加了首包资源目录文件的处理, 重写PostProcessBundles将首包资源放入streamasset中,增加了一个目录路径Addressables.PlayerBuildData1Path,用来存放首包资源的.
  构建

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.IO;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Build.DataBuilders;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.Initialization;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.AddressableAssets.ResourceProviders;
using UnityEngine.ResourceManagement.ResourceProviders;
using LPCFramework;
using Debug = UnityEngine.Debug;

[CreateAssetMenu(fileName = "MyBuildScriptPackedMode.asset", menuName = "Addressables/Content Builders/MyBuildScriptPackedMode")]
public class MyBuildScriptPackedMode : BuildScriptPackedMode
{
    public static bool APP_CONTAIN_RES = false;
    public static bool BUILD_APP = false;
     public override string Name
     {
         get { return "My Build"; }//自定义Build的名字
     }
    //需要上传到远端的bundle,全部上传速度非常慢
    public List<string> needUpdateBundles = new List<string> { };

    protected override TResult BuildDataImplementation<TResult>(AddressablesDataBuilderInput builderInput)
    {
        TResult result = default(TResult);
        needUpdateBundles.Clear();
        var timer = new Stopwatch();
        timer.Start();
        InitializeBuildContext(builderInput, out AddressableAssetsBuildContext aaContext);

        using (m_Log.ScopedStep(UnityEditor.Build.Pipeline.Interfaces.LogLevel.Info, "ProcessAllGroups"))
        {
            var errorString = ProcessAllGroups(aaContext);
            if (!string.IsNullOrEmpty(errorString))
                result = AddressableAssetBuildResult.CreateResult<TResult>(null, 0, errorString);
        }

        /////START 初始化buildInData
        var targetPath = "";
        if (APP_CONTAIN_RES && BUILD_APP)
        {
            m_Linker.AddTypes(new Type[] { typeof(MyAssetBundleProvider), typeof(MyAssetBundleRequestOptions) });
            AssetBundleManager.Instance.buildInData = new BuildInBundleData();
            var targetDir = Path.Combine(Application.dataPath, "Resources_Internal/Resources");
            if (!Directory.Exists(targetDir))
                Directory.CreateDirectory(targetDir);

            targetDir = Path.Combine(targetDir, "Version");
            if (!Directory.Exists(targetDir))
                Directory.CreateDirectory(targetDir);

            targetPath = Path.Combine(targetDir, "BuildInBundleName.bytes");
            if (File.Exists(targetPath))
            {
                File.Delete(targetPath);
            }
        }
        /////END 初始化buildInData


        if (result == null)
        {
            result = DoBuild<TResult>(builderInput, aaContext);
        }

        if (result != null)
            result.Duration = timer.Elapsed.TotalSeconds;

        ////START 序列化保存本次打包的内置包列表
        if (APP_CONTAIN_RES && BUILD_APP)
        {
            var BuildInJson = JsonUtility.ToJson(AssetBundleManager.Instance.buildInData);
            File.WriteAllText(targetPath, BuildInJson);
        }
         ////END 序列化保存本次打包的内置包列表

         return result;
     }

     public override void PostProcessBundles(AddressableAssetGroup assetGroup, List<string> buildBundles, List<string> outputBundles, IBundleBuildResults buildResult, ResourceManagerRuntimeData runtimeData, List<ContentCatalogDataEntry> locations, FileRegistry registry, Dictionary<string, ContentCatalogDataEntry> primaryKeyToCatalogEntry, Dictionary<string, string> bundleRenameMap, List<Action> postCatalogUpdateCallbacks)
     {
         var schema = assetGroup.GetSchema<BundledAssetGroupSchema>();
         if (schema == null)
             return;

         var path = schema.BuildPath.GetValue(assetGroup.Settings);
         if (string.IsNullOrEmpty(path))
             return;
        bool isUseMyAssetBundle = schema.AssetBundleProviderType.Value == typeof(LPCFramework.MyAssetBundleProvider);
         for (int i = 0; i < buildBundles.Count; ++i)
         {
            if (primaryKeyToCatalogEntry.TryGetValue(buildBundles, out ContentCatalogDataEntry dataEntry))
            {
                var info = buildResult.BundleInfos[buildBundles];
                var requestOptions = new AssetBundleRequestOptions
                {
                    Crc = schema.UseAssetBundleCrc ? info.Crc : 0,
                    UseCrcForCachedBundle = schema.UseAssetBundleCrcForCachedBundles,
                    UseUnityWebRequestForLocalBundles = schema.UseUnityWebRequestForLocalBundles,
                    Hash = schema.UseAssetBundleCache ? info.Hash.ToString() : "",
                    ChunkedTransfer = schema.ChunkedTransfer,
                    RedirectLimit = schema.RedirectLimit,
                    RetryCount = schema.RetryCount,
                    Timeout = schema.Timeout,
                    BundleName = Path.GetFileNameWithoutExtension(info.FileName),
                    AssetLoadMode = schema.AssetLoadMode,
                    BundleSize = GetFileSize(info.FileName),
                    ClearOtherCachedVersionsWhenLoaded = schema.AssetBundledCacheClearBehavior == BundledAssetGroupSchema.CacheClearBehavior.ClearWhenWhenNewVersionLoaded
                };
                if (isUseMyAssetBundle)
                {
                    requestOptions = new LPCFramework.MyAssetBundleRequestOptions
                    {
                        Crc = schema.UseAssetBundleCrc ? info.Crc : 0,
                        UseCrcForCachedBundle = schema.UseAssetBundleCrcForCachedBundles,
                        UseUnityWebRequestForLocalBundles = schema.UseUnityWebRequestForLocalBundles,
                        Hash = schema.UseAssetBundleCache ? info.Hash.ToString() : "",
                        ChunkedTransfer = schema.ChunkedTransfer,
                        RedirectLimit = schema.RedirectLimit,
                        RetryCount = schema.RetryCount,
                        Timeout = schema.Timeout,
                        BundleName = Path.GetFileNameWithoutExtension(info.FileName),
                        AssetLoadMode = schema.AssetLoadMode,
                        BundleSize = GetFileSize(info.FileName),
                        ClearOtherCachedVersionsWhenLoaded = schema.AssetBundledCacheClearBehavior == BundledAssetGroupSchema.CacheClearBehavior.ClearWhenWhenNewVersionLoaded
                    };
                }
                dataEntry.Data = requestOptions;

                if (assetGroup == assetGroup.Settings.DefaultGroup && info.Dependencies.Length == 0 && !string.IsNullOrEmpty(info.FileName) && (info.FileName.EndsWith("_unitybuiltinshaders.bundle") || info.FileName.EndsWith("_monoscripts.bundle")))
                {
                    outputBundles = ConstructAssetBundleName(null, schema, info, outputBundles);
                }
                else
                {
                    int extensionLength = Path.GetExtension(outputBundles).Length;
                    string[] deconstructedBundleName = outputBundles.Substring(0, outputBundles.Length - extensionLength).Split('_');
                    string reconstructedBundleName = string.Join("_", deconstructedBundleName, 1, deconstructedBundleName.Length - 1) + ".bundle";
                    outputBundles = ConstructAssetBundleName(assetGroup, schema, info, reconstructedBundleName);
                }

                dataEntry.InternalId = dataEntry.InternalId.Remove(dataEntry.InternalId.Length - buildBundles.Length) + outputBundles;
                dataEntry.Keys[0] = outputBundles;
                ReplaceDependencyKeys(buildBundles, outputBundles, locations);

                if (!m_BundleToInternalId.ContainsKey(buildBundles))
                    m_BundleToInternalId.Add(buildBundles, dataEntry.InternalId);

                if (dataEntry.InternalId.StartsWith("http:\\"))
                    dataEntry.InternalId = dataEntry.InternalId.Replace("http:\\", "http://").Replace("\\", "/");
                if (dataEntry.InternalId.StartsWith("https:\\"))
                    dataEntry.InternalId = dataEntry.InternalId.Replace("https:\\", "https://").Replace("\\", "/");
            }
            else
            {
                Debug.LogWarningFormat("Unable to find ContentCatalogDataEntry for bundle {0}.", outputBundles);
            }

            if (APP_CONTAIN_RES && BUILD_APP)
            {
                //UnityEngine.Debug.Log(outputBundles);
                if (!AssetBundleManager.Instance.buildInData.BuildInBundleNames.Contains(outputBundles))
                {
                    //添加打包的Bundle记录,内置Bundle
                    AssetBundleManager.Instance.buildInData.BuildInBundleNames.Add(outputBundles);
                }
            }

            var targetPath = Path.Combine(path, outputBundles);
            var srcPath = Path.Combine(assetGroup.Settings.buildSettings.bundleBuildPath, buildBundles);

            if (assetGroup.GetSchema<BundledAssetGroupSchema>()?.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash)
                outputBundles = StripHashFromBundleLocation(outputBundles);

            bundleRenameMap.Add(buildBundles, outputBundles);
            if (MoveFileToDestinationWithTimestampIfDifferent(schema, srcPath, targetPath, dataEntry, m_Log)) {
                //记录变化的bundle,减少上传压力
                needUpdateBundles.Add(targetPath);
            }

             AddPostCatalogUpdatesInternal(assetGroup, postCatalogUpdateCallbacks, dataEntry, targetPath, registry);

            //复制到Library 打包到包里面
            if (APP_CONTAIN_RES && BUILD_APP)
            {
                string RuntimePath = UnityEngine.AddressableAssets.Addressables.PlayerBuildData1Path;
                string destPath = Path.Combine(System.Environment.CurrentDirectory, RuntimePath, outputBundles);
                if (!Directory.Exists(Path.GetDirectoryName(destPath)))
                    Directory.CreateDirectory(Path.GetDirectoryName(destPath));
                if (File.Exists(destPath))
                {
                    File.Delete(destPath);
                }
                File.Copy(targetPath, destPath);
            }

            registry.AddFile(targetPath);
         }

     }

    protected override string[] CreateRemoteCatalog(string jsonText, List<ResourceLocationData> locations, AddressableAssetSettings aaSettings, AddressablesDataBuilderInput builderInput, ProviderLoadRequestOptions catalogLoadOptions)
    {
        string[] dependencyHashes = null;

        var contentHash = HashingMethods.Calculate(jsonText).ToString();

        var versionedFileName = aaSettings.profileSettings.EvaluateString(aaSettings.activeProfileId, "/catalog_" + builderInput.PlayerVersion);
        var remoteBuildFolder = aaSettings.RemoteCatalogBuildPath.GetValue(aaSettings);
        var remoteLoadFolder = aaSettings.RemoteCatalogLoadPath.GetValue(aaSettings);

        if (string.IsNullOrEmpty(remoteBuildFolder) ||
            string.IsNullOrEmpty(remoteLoadFolder) ||
            remoteBuildFolder == AddressableAssetProfileSettings.undefinedEntryValue ||
            remoteLoadFolder == AddressableAssetProfileSettings.undefinedEntryValue)
        {
            Addressables.LogWarning("Remote Build and/or Load paths are not set on the main AddressableAssetSettings asset, but 'Build Remote Catalog' is true.  Cannot create remote catalog.  In the inspector for any group, double click the 'Addressable Asset Settings' object to begin inspecting it. '" + remoteBuildFolder + "', '" + remoteLoadFolder + "'");
        }
        else
        {
            ///加入版本号概念
            int resVersion = 0;
            string resPath = remoteBuildFolder + "/resVersion";
            if (File.Exists(resPath))
            {
                int.TryParse(File.ReadAllText(resPath), out resVersion);
            }
            //对比hash变化了才会加1
            var remoteOldHashBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".hash";
            bool hasChange = false;
            if (File.Exists(remoteOldHashBuildPath))
            {
                if (File.ReadAllText(remoteOldHashBuildPath) != contentHash)
                {
                    resVersion++;
                    hasChange = true;
                }
            }
            else {
                hasChange = true;
            }
            var remoteJsonBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".json";
            var remoteHashBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".hash";

            WriteFile(remoteJsonBuildPath, jsonText, builderInput.Registry);
            WriteFile(remoteHashBuildPath, contentHash, builderInput.Registry);
            File.WriteAllText(resPath, resVersion.ToString());
            if (hasChange) {
                StringBuilder needUpdateBundleStr = new StringBuilder();
                needUpdateBundleStr.AppendLine(remoteJsonBuildPath);
                needUpdateBundleStr.AppendLine(remoteHashBuildPath);
                foreach (string p in needUpdateBundles) {
                    needUpdateBundleStr.AppendLine(p);
                }
                needUpdateBundles.Clear();
                File.WriteAllText(resPath+ "_UpdateCatalog_"+ resVersion, needUpdateBundleStr.ToString());
            }

            dependencyHashes = new string[((int)ContentCatalogProvider.DependencyHashIndex.Count)];
            dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote] = ResourceManagerRuntimeData.kCatalogAddress + "RemoteHash";
            dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache] = ResourceManagerRuntimeData.kCatalogAddress + "CacheHash";

            var remoteHashLoadPath = remoteLoadFolder + versionedFileName + ".hash";
            var remoteHashLoadLocation = new ResourceLocationData(
                new[] { dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote] },
                remoteHashLoadPath,
                typeof(TextDataProvider), typeof(string));
            remoteHashLoadLocation.Data = catalogLoadOptions.Copy();
            locations.Add(remoteHashLoadLocation);

            var cacheLoadPath = "{UnityEngine.Application.persistentDataPath}/com.unity.addressables" + versionedFileName + ".hash";
            var cacheLoadLocation = new ResourceLocationData(
                new[] { dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache] },
                cacheLoadPath,
                typeof(TextDataProvider), typeof(string));
            cacheLoadLocation.Data = catalogLoadOptions.Copy();
            locations.Add(cacheLoadLocation);
        }

        return dependencyHashes;
    }
}

      打包处理之后我们还需要处理下下载和加载流程.因为默认的代码流程还是会从远端下载, 然后缓存在本地, 这里我们重新实现MyAssetBundleProvider 继承AssetBundleProvider, 这里AssetBundleManager 用于记录首包资源目录,判断文件是否本地缓存, 这里使用自定义缓存下载, 此处需要UnityWebRequest.Get创建下载句柄,使用UnityWebRequestAssetBundle无法读取数据自定义保存.  同时需要重新计算下下载资源大小,自定义MyAssetBundleRequestOptions继承AssetBundleRequestOptions, 判断是否是首包资源和是否已经缓存。
  加载

using UnityEngine;
using System.Collections;
using UnityEngine.ResourceManagement.ResourceProviders;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.Exceptions;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.Util;
using UnityEngine.Serialization;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement;
using AsyncOperation = UnityEngine.AsyncOperation;

namespace LPCFramework {

[DisplayName("My AssetBundle Provider")]
public class MyAssetBundleProvider : AssetBundleProvider
{
    public override void Provide(ProvideHandle providerInterface)
    {
        new MyAssetBundleResource().Start(providerInterface, DataStreamProcessor);
    }
    /// <summary>
    /// Releases the asset bundle via AssetBundle.Unload(true).
    /// </summary>
    /// <param name="location">The location of the asset to release</param>
    /// <param name="asset">The asset in question</param>
    public override void Release(IResourceLocation location, object asset)
    {
        if (location == null)
            throw new ArgumentNullException("location");
        if (asset == null)
        {
            Debug.LogWarningFormat("Releasing null asset bundle from location {0}.  This is an indication that the bundle failed to load.", location);
            return;
        }
        var bundle = asset as MyAssetBundleResource;
        if (bundle != null)
        {
            bundle.Unload();
            return;
        }
    }
}

public class MyAssetBundleResource : AssetBundleResource
{
#if APP_CONTAIN_RES

        public override void Start(ProvideHandle provideHandle, IDataConverter dataProc)
        {
            m_dataProc = dataProc;
            m_Retries = 0;
            m_AssetBundle = null;
            m_downloadHandler = null;
            m_RequestOperation = null;
            m_WebRequestCompletedCallbackCalled = false;
            m_ProvideHandle = provideHandle;
            m_Options = m_ProvideHandle.Location.Data as MyAssetBundleRequestOptions;
            m_BytesToDownload = -1;
            m_ProvideHandle.SetProgressCallback(PercentComplete);
            m_ProvideHandle.SetDownloadProgressCallbacks(GetDownloadStatus);
            m_ProvideHandle.SetWaitForCompletionCallback(WaitForCompletionHandler);
            BeginOperation();
        }


    public override void BeginOperation()
    {
        string path = m_ProvideHandle.Location.InternalId;
        var url = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location);
        string bundleName = url.Replace(AddressableUpdateContent.Instance.remoteCatalogUrl, "");
        // if a path starts with jar:file, it is an android embeded resource. The resource is a local file but cannot be accessed by
        // FileStream(called in LoadWithDataProc) directly
        // Need to use webrequest's async call to get the content.
        if (AssetBundleManager.Instance.IsBuildIn(bundleName))//本地资源,内置包
        {
            string streamPath = UnityEngine.AddressableAssets.Addressables.PlayerBuildData1Path + "/" + bundleName;
            Debug.Log("LoadOne:" + streamPath);
            var crc = m_Options == null ? 0 : m_Options.Crc;
            CompleteBundleLoad(AssetBundle.LoadFromFile(streamPath));
        }
        else if (AssetBundleManager.Instance.IsCache(bundleName)) //已经下载过 缓存到本地的Bundle
        {
            string cachePath = Path.Combine(AssetBundleManager.Instance.GetBundleCachePath(), bundleName);
            AddressableManager.Log("LoadTwo:" + cachePath);
            var crc = m_Options == null ? 0 : m_Options.Crc;
            CompleteBundleLoad(AssetBundle.LoadFromFile(cachePath));
        }
        else if (ResourceManagerConfig.ShouldPathUseWebRequest(path)) //真正需要下载的Bundle
        {
            AddressableManager.Log("DownloadThree:" + url);
            var req = CreateWebRequest(m_ProvideHandle.Location);
            req.disposeDownloadHandlerOnDispose = false;
            m_WebRequestQueueOperation = WebRequestQueue.QueueRequest(req);
            if (m_WebRequestQueueOperation.IsDone)
            {
                m_RequestOperation = m_WebRequestQueueOperation.Result;
                m_RequestOperation.completed += WebRequestOperationCompleted;
            }
            else
            {
                m_WebRequestQueueOperation.OnComplete += asyncOp =>
                {
                    m_RequestOperation = asyncOp;
                    m_RequestOperation.completed += WebRequestOperationCompleted;
                };
            }
        }
        else
        {
            m_RequestOperation = null;
            m_ProvideHandle.Complete<MyAssetBundleResource>(null, false, new Exception(string.Format("Invalid path in AssetBundleProvider: '{0}'.", path)));
        }
    }


    public override void WebRequestOperationCompleted(AsyncOperation op)
    {
        UnityWebRequestAsyncOperation remoteReq = op as UnityWebRequestAsyncOperation;
        var webReq = remoteReq.webRequest;
        m_downloadHandler = webReq.downloadHandler;
        if (!UnityWebRequestUtilities.RequestHasErrors(webReq, out UnityWebRequestResult uwrResult))
        {
            var url = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location);
            string bundleName = url.Replace(AddressableUpdateContent.Instance.remoteCatalogUrl, "");

            AssetBundleManager.Instance.CacheBundle(bundleName, m_downloadHandler.data);//主要是在这里加了一个保存到本地的方法

            if (!m_Completed)
            {
                m_ProvideHandle.Complete(this, true, null);
                m_Completed = true;
            }
        }
        else
        {
            m_downloadHandler.Dispose();
            m_downloadHandler = null;
            string message = string.Format("Web request {0} failed with error '{1}', retrying ({2}/{3})...", webReq.url, webReq.error, m_Retries, m_Options.RetryCount);

            if (m_Retries < m_Options.RetryCount)
            {
                Debug.LogFormat(message);
                BeginOperation();
                m_Retries++;
            }
            else
            {
                var exception = new Exception(string.Format(
                    "RemoteAssetBundleProvider unable to load from url {0}, result='{1}'.", webReq.url,
                    webReq.error));
                m_ProvideHandle.Complete<MyAssetBundleResource>(null, false, exception);
            }
        }
        webReq.Dispose();
    }

        public override UnityWebRequest CreateWebRequest(string url)
        {
            UnityWebRequest webRequest = UnityWebRequest.Get(url);

            if (webRequest == null)
                return webRequest;

            if (m_Options != null) {
                if (m_Options.Timeout > 0)
                    webRequest.timeout = m_Options.Timeout;
                if (m_Options.RedirectLimit > 0)
                    webRequest.redirectLimit = m_Options.RedirectLimit;
#if !UNITY_2019_3_OR_NEWER
                webRequest.chunkedTransfer = m_Options.ChunkedTransfer;
#endif
            }
            if (m_ProvideHandle.ResourceManager.CertificateHandlerInstance != null)
            {
                webRequest.certificateHandler = m_ProvideHandle.ResourceManager.CertificateHandlerInstance;
                webRequest.disposeCertificateHandlerOnDispose = false;
            }

            m_ProvideHandle.ResourceManager.WebRequestOverride?.Invoke(webRequest);
            return webRequest;
        }
    #endif
}

    [Serializable]
    public class MyAssetBundleRequestOptions : AssetBundleRequestOptions {
            public override long ComputeSize(IResourceLocation location, ResourceManager resourceManager)
    {
        var id = resourceManager == null ? location.InternalId : resourceManager.TransformInternalId(location);
        if (!ResourceManagerConfig.IsPathRemote(id))
            return 0;
        var url = Addressables.ResourceManager.TransformInternalId(location);
        string bundleName = url.Replace(AddressableUpdateContent.Instance.remoteCatalogUrl, "");
        // Need to use webrequest's async call to get the content.
        if (AssetBundleManager.Instance.IsBuildIn(bundleName))//本地资源,内置包
        {
            return 0;
        }
        else if (AssetBundleManager.Instance.IsCache(bundleName)) //已经下载过 缓存到本地的Bundle
        {
            return 0;
        }
        return BundleSize;
    }
    }
}AssetBundleManager

这是用于首包目录管理
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;

[Serializable]
public class BuildInBundleData
{
    public List<string> BuildInBundleNames = new List<string>();
}

public class AssetBundleManager : Singleton<AssetBundleManager>
{
    const string BuildInBundleFileName = "Version/BuildInBundleName";
    private string cachePath = "";
    public BuildInBundleData buildInData = new BuildInBundleData();
   

    public void Init()
    {
        cachePath = Path.Combine(Application.persistentDataPath, "ab");
        if (!Directory.Exists(cachePath))
        {
            Directory.CreateDirectory(cachePath);
        }
#if APP_CONTAIN_RES
        var json = Resources.Load<TextAsset>("Version/BuildInBundleName");
        if (json)
        {
            buildInData = JsonUtility.FromJson<BuildInBundleData>(json.text);
        }
        else {
            Debug.LogError("缺少内置资源目录文件:"+ BuildInBundleFileName);
        }
#endif
    }

    public bool IsBuildIn(string bundleName)
    {
        return buildInData.BuildInBundleNames.Contains(bundleName);
    }

    public String GetBundleCachePath()
    {
        return cachePath;
    }

    public bool IsCache(string bundleName)
    {
        string filePath = Path.Combine(GetBundleCachePath(), bundleName);
        return File.Exists(filePath);
    }

    public void CacheBundle(string bundlename, byte[] bytes)
    {
        string filePath = Path.Combine(GetBundleCachePath(), bundlename);
        if (File.Exists(filePath))
            File.Delete(filePath);
        string dir = Path.GetDirectoryName(filePath);
        if (!Directory.Exists(dir)) {
            Directory.CreateDirectory(dir);
        }

        File.WriteAllBytes(filePath, bytes);
    }

    public string GetRealBundleName(string bundlename)
    {
        if (string.IsNullOrEmpty(bundlename))
            return "";

        int index = bundlename.LastIndexOf("_");
        return bundlename.Substring(0, index);
    }
}
2、资源目录支持版本号,可以做到版本更新和回退

      这里支持版本号的方式就是,将每次生成的remoteCatalogHash与上一次比较,如果不同就加1生成新的版本号. 然后remoteCatalog文件加上版本号保存,而不是直接覆盖.
创建目录文件

  MyBuildScriptPackedMode.CreateRemoteCatalog  
///加入版本号概念
int resVersion = 0;
string resPath = remoteBuildFolder + "/resVersion";
if (File.Exists(resPath))
{
    int.TryParse(File.ReadAllText(resPath), out resVersion);
}
//对比hash变化了才会加1
var remoteOldHashBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".hash";
bool hasChange = false;
if (File.Exists(remoteOldHashBuildPath))
{
     if (File.ReadAllText(remoteOldHashBuildPath) != contentHash)
     {
        resVersion++;
        hasChange = true;
        }
     }
     else {
       hasChange = true;
     }
}
var remoteJsonBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".json";
var remoteHashBuildPath = remoteBuildFolder + versionedFileName + "_" + resVersion + ".hash";   远端目录和资源地址

游戏启动的时候会读取服务器上运维设置的版本号,然后设置下远端资源目录的路径, 这里也支持了动态切换cdn资源路径了:
Dictionary<string, string> InternalIdURLMap = new Dictionary<string, string>();
        public void SetAddressableRemoteResCdnUrl(string remoteUrl)
        {
            Debug.Log("SetAddressableRemoteUrl remoteUrl = " + remoteUrl);
            string cataLogName = "catalog_1_" + VersionMgr.Instance.resVersion + ".hash";
            string oldUrl = Addressables.GetRemoteCatalogUrl();
            if (string.IsNullOrEmpty(remoteUrl))
            {
                remoteCatalogUrl = oldUrl + "/";
                Addressables.SetRemoteCatalogLocation1(cataLogName);
                return;
            }
            remoteCatalogUrl = remoteUrl + "/";
            //设置catalog的请求路径
            string newLocation = remoteCatalogUrl + cataLogName;//后缀对应为AddressableAssetSettings的PlayerVersionOverried
            Addressables.SetRemoteCatalogLocation(newLocation);
            //设置location的transfrom func
            Addressables.InternalIdTransformFunc = (IResourceLocation location) =>
            {
                string internalId = location.InternalId;
                if (internalId != null && internalId.StartsWith("http"))
                {
                    if (!InternalIdURLMap.TryGetValue(internalId, out string newInternalId)) {
                        newInternalId = internalId.Replace(oldUrl, remoteUrl);
                        InternalIdURLMap.Add(internalId, newInternalId);
                    }
                    return newInternalId;
                }
                else
                {
                    return location.InternalId;
                }
            };
        }3、统计每次打资源的增量变化,减少cdn同步大小

      减少cdn同步,就是在打包的脚本里面,记录下那些变化的文件,这里直接修改下MoveFileToDestinationWithTimestampIfDifferent, 返回变化值,同时记录这个文件名. 打包结束的时候把变化的文件名列表统一保存, cdn上传的时候读区这个文件,然后依次上传:
MyBuildScriptPackedMode.PostProcessBundles 中

if (MoveFileToDestinationWithTimestampIfDifferent(schema, srcPath, targetPath, dataEntry, m_Log)) {
     //记录变化的bundle,减少上传压力
     needUpdateBundles.Add(targetPath);
}  MyBuildScriptPackedMode.CreateRemoteCatalog中写入实际更新的文件列表
StringBuilder needUpdateBundleStr = new StringBuilder();
needUpdateBundleStr.AppendLine(remoteJsonBuildPath);
needUpdateBundleStr.AppendLine(remoteHashBuildPath);
foreach (string p in needUpdateBundles) {
     needUpdateBundleStr.AppendLine(p);
}
needUpdateBundles.Clear();
File.WriteAllText(resPath+ "_UpdateCatalog_"+ resVersion, needUpdateBundleStr.ToString()); 4、动态修改remote地址功能

      动态切换cdn比较简单, 利用Addressables.InternalIdTransformFunc 覆盖就资源路径即可,条目2已经展示
5、自动group工具

     先自定义创建group创建资源规则 这里用了odin插件优化inspactor界面
    BuildGroupConfig


[Serializable, Toggle("isShow", CollapseOthersOnExpand = false)]
public class BuildGroupConfig
{
    [LabelText("组名"), Space(10)]
    public string groupName;
    [LabelText("Asset相对目录"), LabelWidth(80), HorizontalGroup("1")]
    public string assetFolders;
    [LabelText("是否使用二级目录拆分"), LabelWidth(80), HorizontalGroup("1")]
    public bool useSubFolders;
    [LabelText("文件后缀"), HorizontalGroup("2"), ValueDropdown("fileSuffixs")]
    public string filesuffix;
    [LabelText("Unity后缀"), HorizontalGroup("2"), ValueDropdown("extraSuffixs")]
    public string suffix;
    [LabelText("排除文件"), SuffixLabel("使用;相隔")]
    public string excludePath;
    [LabelText("更新设置")]
    public AddressableAssetGroupTemplate groupSchema;
    public bool isShow = true;
    private static IEnumerable extraSuffixs = new ValueDropdownList<string>()
    {
        { "", "" },
        { "scene", "scene" },
        { "shader", "shader" },
        //{ "lua", "lua" },
        { "prefab", "prefab" },
    };
    private static IEnumerable fileSuffixs = new ValueDropdownList<string>()
    {
        { "", "" },
        { "bytes", "bytes" },
        { "shader", "shader" },
        { "lua", "lua" },
    };
}
AddressableGroupSetter 中创建和更新group


using System.Linq;
using System.IO;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor.AddressableAssets.GUI;
using UObject = UnityEngine.Object;

using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Demos.RPGEditor;
using System.Collections;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine.ResourceManagement.Util;
using System.Text;
using System.Reflection;

//自动构建group
[System.Serializable]
[CreateAssetMenu(menuName = "Custom/Build/AddressableGroupSetter", fileName = "AddressableGroupSetter")]
public class AddressableGroupSetter : ScriptableObject
{
    private static AddressableGroupSetter _instance;
    public static AddressableGroupSetter Instance { get {
            if (_instance == null) {
                _instance =  AssetDatabase.LoadAssetAtPath<AddressableGroupSetter>("Assets/AddressableAssetsData/AddressableGroupSetter.asset");
            }
            return _instance;
        } }

    [ListDrawerSettings(Expanded = true), LabelText("远程资源")]
    public List<BuildGroupConfig> GroupSettingList;
    [LabelText("首包资源"), HideIf("@true")]
    public StaticGroupConfig staticConfig;
    AddressableAssetSettings Settings
    {
        get { return AddressableAssetSettingsDefaultObject.Settings; }
    }


    [Button("Auto Build Groups")]
    public void ResetGroups()
    {
        /* string[] groupNames = Settings.groups.Select(g => g.Name).ToArray();
        foreach (string groupName in groupNames)
        {
            AddressableAssetGroup group = Settings.FindGroup(groupName);
            if (!group.Default)
            {
                Settings.RemoveGroup(group);
            }
        } */
        foreach (BuildGroupConfig config in GroupSettingList)
        {
            if (config.isShow) {
                if (config.useSubFolders)
                {
                    string[] subFolders = Directory.GetDirectories(config.assetFolders);
                    foreach (string subFolder in subFolders) {
                        var subCofig = DeepCopy<BuildGroupConfig>(config);
                        subCofig.groupName = config.groupName + "_" + Path.GetFileName(subFolder);
                        subCofig.assetFolders = subFolder;
                        ResetGroup(subCofig);
                    }
                }
                else {
                    ResetGroup(config);
                }
            }
        }
        //静态资源
        //CreateStaticGroup();
        AssetDatabase.SaveAssets();
    }

    /// <summary>
    /// 重置某分组
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="groupName">组名</param>
    /// <param name="assetFolder">资源目录</param>
    /// <param name="getAddress">通过 asset path 得到地址名</param>
    void ResetGroup(BuildGroupConfig config)
    {
        string[] assets = GetAssets(config);
        AddressableAssetGroup group = CreateGroup(config);
        List<AddressableAssetEntry> newAddressAssetEntries = new List<AddressableAssetEntry>(assets.Length);
        foreach (var assetPath in assets)
        {
            if (!Directory.Exists(assetPath)) {
                string address = GetAddress(assetPath, config.assetFolders);
                newAddressAssetEntries.Add(AddAssetEntry(group, assetPath, address));
            }
        }
        List<AddressableAssetEntry> needRemoveList = new List<AddressableAssetEntry>();
        foreach (var entry in group.entries.ToArray()) {
            if (!newAddressAssetEntries.Contains(entry)) {
                needRemoveList.Add(entry);
            }
        }
        group.RemoveAssetEntries(needRemoveList);
        EditorUtility.SetDirty(group);

        Debug.Log($"Reset group finished, group: {config.groupName}, asset folder: {config.assetFolders}, count: {assets.Length}");
    }

    void CreateStaticGroup() {
        var assets = new List<UnityEngine.Object>(staticConfig.includes);
        AddressableAssetGroup group = Settings.FindGroup(staticConfig.groupName);
        if (group == null)
        {
            List<UnityEditor.AddressableAssets.Settings.AddressableAssetGroupSchema> schema = new List<UnityEditor.AddressableAssets.Settings.AddressableAssetGroupSchema>() {
             ScriptableObject.CreateInstance<BundledAssetGroupSchema>(),
             staticConfig.groupSchema.GetSchemaByType(typeof(ContentUpdateGroupSchema)) ,
            
             //ScriptableObject.CreateInstance<UnityEditor.AddressableAssets.Settings.GroupSchemas.PlayerDataGroupSchema>(),
            };
            group = Settings.CreateGroup(staticConfig.groupName, false, false, false, schema);
        }
        Settings.AddLabel(staticConfig.groupName, false);
        string luaStataiclabel = staticConfig.groupName + "_Lua";
        string uiDestStataiclabel = staticConfig.groupName + "_UIDesc";
        Settings.AddLabel(luaStataiclabel, false);
        Settings.AddLabel(uiDestStataiclabel, false);
        List<AddressableAssetEntry> newAddressAssetEntries = new List<AddressableAssetEntry>(assets.Count);
        StringBuilder sb = new StringBuilder();
        foreach (var asset in assets)
        {
            if (asset)
            {
                var assetPath = AssetDatabase.GetAssetPath(asset);
                AddressableAssetEntry entry = FindAssetEntry(assetPath);
                if (entry != null)
                {
                    entry.SetLabel(group.Name, true, false, false);
                    if (assetPath.EndsWith(".lua"))
                    {
                        entry.SetLabel(luaStataiclabel, true, false, false);
                    }
                    if (assetPath.EndsWith(".bytes"))
                    {
                        entry.SetLabel(uiDestStataiclabel, true, false, false);
                    }
                    newAddressAssetEntries.Add(entry);
                }
                else {
                    sb.AppendLine(assetPath);
                }
            }
        }
        List<AddressableAssetEntry> needRemoveList = new List<AddressableAssetEntry>();
        foreach (var entry in group.entries.ToArray())
        {
            if (!newAddressAssetEntries.Contains(entry))
            {
                needRemoveList.Add(entry);
            }
        }
        group.RemoveAssetEntries(needRemoveList);
        Settings.MoveEntries(newAddressAssetEntries, group, false, false);
        Debug.Log($"Reset static group finished, group: {staticConfig.groupName}, count: {assets.Count}");
        if (sb.Length > 0) {
            Debug.LogError($"有问题的资源{sb}");
        }
        EditorUtility.SetDirty(group);
    }

    // 创建分组
    AddressableAssetGroup CreateGroup(BuildGroupConfig config)
    {
        AddressableAssetGroup group = Settings.FindGroup(config.groupName);
        if (group == null)
        {
            List<UnityEditor.AddressableAssets.Settings.AddressableAssetGroupSchema> schema = config.groupSchema.SchemaObjects;
            group = Settings.CreateGroup(config.groupName, false, false, false, schema);
        }
      
        Settings.AddLabel(config.groupName, false);
        return group;
    }

    string GetAddress(string assetPath, string assetFolder) {
        string fileName = Path.GetFileNameWithoutExtension(assetPath);
        string fileFullName = Path.GetFileName(assetPath);
        var ss = assetFolder.Split('/');
        if (ss.Length > 2) {
            assetFolder = ss[0] + "/" + ss[1];
        }
        string assetkey = assetPath.Replace(fileFullName, "").Replace(assetFolder, "")+ fileName;
        if (assetkey.StartsWith("/"))
        {
            assetkey = assetkey.Substring(1);
        }
        return assetkey;
    }

    // 给某分组添加资源
    AddressableAssetEntry AddAssetEntry(AddressableAssetGroup group, string assetPath, string address)
    {
        string guid = AssetDatabase.AssetPathToGUID(assetPath);

        AddressableAssetEntry entry = group.entries.FirstOrDefault(e => e.guid == guid);
        if (entry == null)
        {
            entry = Settings.CreateOrMoveEntry(guid, group, false, false);
        }
        if (entry == null) {
            Debug.LogError("无法创建entry:"+ assetPath);
            return null;
        }
        entry.address = address;
        entry.SetLabel(group.Name, true, false, false);
        return entry;
    }
    /// <summary>
    /// 查找资源
    /// </summary>
    /// <param name="assetPath"></param>
    /// <returns></returns>

    AddressableAssetEntry FindAssetEntry(string assetPath)
    {
        string guid = AssetDatabase.AssetPathToGUID(assetPath);
        foreach (AddressableAssetGroup group in Settings.groups) {
            AddressableAssetEntry entry = group.entries.FirstOrDefault(e => e.guid == guid);
            if (entry != null) {
                return entry;
            }
        }

        return null;
    }
    /// <summary>
    /// 获取指定目录的资源
    /// </summary>

    public static string[] GetAssets(BuildGroupConfig config)
    {
        string folder = config.assetFolders;
        if (string.IsNullOrEmpty(folder))
            throw new ArgumentException("folder");

        folder = folder.TrimEnd('/').TrimEnd('\\');

        string suffix = config.suffix;
        if (!String.IsNullOrEmpty(suffix))
        {
            suffix = "t:"+ suffix;
        }
        string[] guids = AssetDatabase.FindAssets(suffix, new string[] { folder });
        bool isNeedCheckExclude = !String.IsNullOrEmpty(config.excludePath);
        Func<string, bool> checkExcluedFunc = (path) => {
            if (isNeedCheckExclude ) {
                string[] excludePaths = config.excludePath.Split(';');  
                for (int i = 0; i < excludePaths.Length; i++) {
                    if (path.Contains(excludePaths)) {
                        return true;
                    }
                }
            }
            return false;
        };

        List<string> paths = new List<string>();
        for (int i = 0; i < guids.Length; i++)
        {
            string path = AssetDatabase.GUIDToAssetPath(guids);
            if (Directory.Exists(path)) {
                continue;
            }
            if (checkExcluedFunc(path)) {
                if (path.StartsWith("Assets/Resources")) {
                    //resource下的资源移动至Resources_Internal目录中
                    string newPath = path.Replace("Assets/Resources", "Assets/Resources_Internal/Resources");
                    var dirInfo = new FileInfo(newPath).Directory;
                    if (dirInfo != null && !dirInfo.Exists)
                    {
                        dirInfo.Create();
                        //生成meta文件
                        AssetDatabase.Refresh();
                    }
                    string errorMsg = AssetDatabase.MoveAsset(path, newPath);
                    if(!string.IsNullOrEmpty(errorMsg))
                    {
                        Debug.LogError(errorMsg);
                    }
                }
                continue;
            }
            if (!String.IsNullOrEmpty(config.filesuffix) && !path.EndsWith(config.filesuffix))
            {
                continue;
            }
            paths.Add(path);
        }
        return paths.ToArray();
    }

    //自动化内容
    public static void AutoSetAssetGroup(string str)
    {
        var Settings = AddressableGroupSetter.Instance.Settings;
        foreach (BuildGroupConfig config in AddressableGroupSetter.Instance.GroupSettingList)
        {
            if (str.Contains(config.assetFolders)) {
                AddressableAssetGroup group = Settings.FindGroup(config.groupName);
                if (group)
                {
                    string address = AddressableGroupSetter.Instance.GetAddress(str, config.assetFolders);
                    AddressableGroupSetter.Instance.AddAssetEntry(group, str, address);
                }
                else {
                    Debug.LogError("没有创建对应的group:"+config.groupName);
                }
                return;
            }
        }
    }

    public static T DeepCopy<T>(T _object)
    {
        Type t = _object.GetType();
        T o = Activator.CreateInstance<T>();
        PropertyInfo[] PI = t.GetProperties();
        for (int i = 0; i < PI.Length; i++)
        {
            PropertyInfo P = PI;
            P.SetValue(o, P.GetValue(_object));
        }
        FieldInfo[] FI = t.GetFields();
        for (int i = 0; i < FI.Length; i++)
        {
            FieldInfo F = FI;
            F.SetValue(o, F.GetValue(_object));
        }
        return o;
    }
}

[Serializable, Toggle("isShow", CollapseOthersOnExpand = false)]
public class BuildGroupConfig
{
    [LabelText("组名"), Space(10)]
    public string groupName;
    [LabelText("Asset相对目录"), LabelWidth(80), HorizontalGroup("1")]
    public string assetFolders;
    [LabelText("是否使用二级目录拆分"), LabelWidth(80), HorizontalGroup("1")]
    public bool useSubFolders;
    [LabelText("文件后缀"), HorizontalGroup("2"), ValueDropdown("fileSuffixs")]
    public string filesuffix;
    [LabelText("Unity后缀"), HorizontalGroup("2"), ValueDropdown("extraSuffixs")]
    public string suffix;
    [LabelText("排除文件"), SuffixLabel("使用;相隔")]
    public string excludePath;
    [LabelText("更新设置")]
    public AddressableAssetGroupTemplate groupSchema;

    public bool isShow = true;
    private static IEnumerable extraSuffixs = new ValueDropdownList<string>()
    {
        { "", "" },
        { "scene", "scene" },
        { "shader", "shader" },
        //{ "lua", "lua" },
        { "prefab", "prefab" },
    };
    private static IEnumerable fileSuffixs = new ValueDropdownList<string>()
    {
        { "", "" },
        { "bytes", "bytes" },
        { "shader", "shader" },
        { "lua", "lua" },
    };

}
[Serializable]
public class StaticGroupConfig {
    [LabelText("组名"), Space(10)]
    public string groupName;
    [LabelText("必备资源"), AssetsOnly]
    public List<UnityEngine.Object> includes;
    [LabelText("更新设置")]
    public AddressableAssetGroupTemplate groupSchema;

}
发表于 2022-7-15 10:14 | 显示全部楼层
可以放出来一个例子工程吗?看着有点乱
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-13 08:27 , Processed in 0.093752 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表