|
之前基于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, &#34;ProcessAllGroups&#34;))
{
var errorString = ProcessAllGroups(aaContext);
if (!string.IsNullOrEmpty(errorString))
result = AddressableAssetBuildResult.CreateResult<TResult>(null, 0, errorString);
}
/////START 初始化buildInData
var targetPath = &#34;&#34;;
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, &#34;Resources_Internal/Resources&#34;);
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
targetDir = Path.Combine(targetDir, &#34;Version&#34;);
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
targetPath = Path.Combine(targetDir, &#34;BuildInBundleName.bytes&#34;);
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() : &#34;&#34;,
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() : &#34;&#34;,
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(&#34;_unitybuiltinshaders.bundle&#34;) || info.FileName.EndsWith(&#34;_monoscripts.bundle&#34;)))
{
outputBundles = ConstructAssetBundleName(null, schema, info, outputBundles);
}
else
{
int extensionLength = Path.GetExtension(outputBundles).Length;
string[] deconstructedBundleName = outputBundles.Substring(0, outputBundles.Length - extensionLength).Split(&#39;_&#39;);
string reconstructedBundleName = string.Join(&#34;_&#34;, deconstructedBundleName, 1, deconstructedBundleName.Length - 1) + &#34;.bundle&#34;;
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(&#34;http:\\&#34;))
dataEntry.InternalId = dataEntry.InternalId.Replace(&#34;http:\\&#34;, &#34;http://&#34;).Replace(&#34;\\&#34;, &#34;/&#34;);
if (dataEntry.InternalId.StartsWith(&#34;https:\\&#34;))
dataEntry.InternalId = dataEntry.InternalId.Replace(&#34;https:\\&#34;, &#34;https://&#34;).Replace(&#34;\\&#34;, &#34;/&#34;);
}
else
{
Debug.LogWarningFormat(&#34;Unable to find ContentCatalogDataEntry for bundle {0}.&#34;, 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, &#34;/catalog_&#34; + 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(&#34;Remote Build and/or Load paths are not set on the main AddressableAssetSettings asset, but &#39;Build Remote Catalog&#39; is true. Cannot create remote catalog. In the inspector for any group, double click the &#39;Addressable Asset Settings&#39; object to begin inspecting it. &#39;&#34; + remoteBuildFolder + &#34;&#39;, &#39;&#34; + remoteLoadFolder + &#34;&#39;&#34;);
}
else
{
///加入版本号概念
int resVersion = 0;
string resPath = remoteBuildFolder + &#34;/resVersion&#34;;
if (File.Exists(resPath))
{
int.TryParse(File.ReadAllText(resPath), out resVersion);
}
//对比hash变化了才会加1
var remoteOldHashBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.hash&#34;;
bool hasChange = false;
if (File.Exists(remoteOldHashBuildPath))
{
if (File.ReadAllText(remoteOldHashBuildPath) != contentHash)
{
resVersion++;
hasChange = true;
}
}
else {
hasChange = true;
}
var remoteJsonBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.json&#34;;
var remoteHashBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.hash&#34;;
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+ &#34;_UpdateCatalog_&#34;+ resVersion, needUpdateBundleStr.ToString());
}
dependencyHashes = new string[((int)ContentCatalogProvider.DependencyHashIndex.Count)];
dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote] = ResourceManagerRuntimeData.kCatalogAddress + &#34;RemoteHash&#34;;
dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache] = ResourceManagerRuntimeData.kCatalogAddress + &#34;CacheHash&#34;;
var remoteHashLoadPath = remoteLoadFolder + versionedFileName + &#34;.hash&#34;;
var remoteHashLoadLocation = new ResourceLocationData(
new[] { dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote] },
remoteHashLoadPath,
typeof(TextDataProvider), typeof(string));
remoteHashLoadLocation.Data = catalogLoadOptions.Copy();
locations.Add(remoteHashLoadLocation);
var cacheLoadPath = &#34;{UnityEngine.Application.persistentDataPath}/com.unity.addressables&#34; + versionedFileName + &#34;.hash&#34;;
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(&#34;My AssetBundle Provider&#34;)]
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=&#34;location&#34;>The location of the asset to release</param>
/// <param name=&#34;asset&#34;>The asset in question</param>
public override void Release(IResourceLocation location, object asset)
{
if (location == null)
throw new ArgumentNullException(&#34;location&#34;);
if (asset == null)
{
Debug.LogWarningFormat(&#34;Releasing null asset bundle from location {0}. This is an indication that the bundle failed to load.&#34;, 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, &#34;&#34;);
// 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&#39;s async call to get the content.
if (AssetBundleManager.Instance.IsBuildIn(bundleName))//本地资源,内置包
{
string streamPath = UnityEngine.AddressableAssets.Addressables.PlayerBuildData1Path + &#34;/&#34; + bundleName;
Debug.Log(&#34;LoadOne:&#34; + 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(&#34;LoadTwo:&#34; + cachePath);
var crc = m_Options == null ? 0 : m_Options.Crc;
CompleteBundleLoad(AssetBundle.LoadFromFile(cachePath));
}
else if (ResourceManagerConfig.ShouldPathUseWebRequest(path)) //真正需要下载的Bundle
{
AddressableManager.Log(&#34;DownloadThree:&#34; + 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(&#34;Invalid path in AssetBundleProvider: &#39;{0}&#39;.&#34;, 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, &#34;&#34;);
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(&#34;Web request {0} failed with error &#39;{1}&#39;, retrying ({2}/{3})...&#34;, 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(
&#34;RemoteAssetBundleProvider unable to load from url {0}, result=&#39;{1}&#39;.&#34;, 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, &#34;&#34;);
// Need to use webrequest&#39;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 = &#34;Version/BuildInBundleName&#34;;
private string cachePath = &#34;&#34;;
public BuildInBundleData buildInData = new BuildInBundleData();
public void Init()
{
cachePath = Path.Combine(Application.persistentDataPath, &#34;ab&#34;);
if (!Directory.Exists(cachePath))
{
Directory.CreateDirectory(cachePath);
}
#if APP_CONTAIN_RES
var json = Resources.Load<TextAsset>(&#34;Version/BuildInBundleName&#34;);
if (json)
{
buildInData = JsonUtility.FromJson<BuildInBundleData>(json.text);
}
else {
Debug.LogError(&#34;缺少内置资源目录文件:&#34;+ 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 &#34;&#34;;
int index = bundlename.LastIndexOf(&#34;_&#34;);
return bundlename.Substring(0, index);
}
}
2、资源目录支持版本号,可以做到版本更新和回退
这里支持版本号的方式就是,将每次生成的remoteCatalogHash与上一次比较,如果不同就加1生成新的版本号. 然后remoteCatalog文件加上版本号保存,而不是直接覆盖.
创建目录文件
MyBuildScriptPackedMode.CreateRemoteCatalog
///加入版本号概念
int resVersion = 0;
string resPath = remoteBuildFolder + &#34;/resVersion&#34;;
if (File.Exists(resPath))
{
int.TryParse(File.ReadAllText(resPath), out resVersion);
}
//对比hash变化了才会加1
var remoteOldHashBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.hash&#34;;
bool hasChange = false;
if (File.Exists(remoteOldHashBuildPath))
{
if (File.ReadAllText(remoteOldHashBuildPath) != contentHash)
{
resVersion++;
hasChange = true;
}
}
else {
hasChange = true;
}
}
var remoteJsonBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.json&#34;;
var remoteHashBuildPath = remoteBuildFolder + versionedFileName + &#34;_&#34; + resVersion + &#34;.hash&#34;; 远端目录和资源地址
游戏启动的时候会读取服务器上运维设置的版本号,然后设置下远端资源目录的路径, 这里也支持了动态切换cdn资源路径了:
Dictionary<string, string> InternalIdURLMap = new Dictionary<string, string>();
public void SetAddressableRemoteResCdnUrl(string remoteUrl)
{
Debug.Log(&#34;SetAddressableRemoteUrl remoteUrl = &#34; + remoteUrl);
string cataLogName = &#34;catalog_1_&#34; + VersionMgr.Instance.resVersion + &#34;.hash&#34;;
string oldUrl = Addressables.GetRemoteCatalogUrl();
if (string.IsNullOrEmpty(remoteUrl))
{
remoteCatalogUrl = oldUrl + &#34;/&#34;;
Addressables.SetRemoteCatalogLocation1(cataLogName);
return;
}
remoteCatalogUrl = remoteUrl + &#34;/&#34;;
//设置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(&#34;http&#34;))
{
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+ &#34;_UpdateCatalog_&#34;+ resVersion, needUpdateBundleStr.ToString()); 4、动态修改remote地址功能
动态切换cdn比较简单, 利用Addressables.InternalIdTransformFunc 覆盖就资源路径即可,条目2已经展示
5、自动group工具
先自定义创建group创建资源规则 这里用了odin插件优化inspactor界面
BuildGroupConfig
[Serializable, Toggle(&#34;isShow&#34;, CollapseOthersOnExpand = false)]
public class BuildGroupConfig
{
[LabelText(&#34;组名&#34;), Space(10)]
public string groupName;
[LabelText(&#34;Asset相对目录&#34;), LabelWidth(80), HorizontalGroup(&#34;1&#34;)]
public string assetFolders;
[LabelText(&#34;是否使用二级目录拆分&#34;), LabelWidth(80), HorizontalGroup(&#34;1&#34;)]
public bool useSubFolders;
[LabelText(&#34;文件后缀&#34;), HorizontalGroup(&#34;2&#34;), ValueDropdown(&#34;fileSuffixs&#34;)]
public string filesuffix;
[LabelText(&#34;Unity后缀&#34;), HorizontalGroup(&#34;2&#34;), ValueDropdown(&#34;extraSuffixs&#34;)]
public string suffix;
[LabelText(&#34;排除文件&#34;), SuffixLabel(&#34;使用;相隔&#34;)]
public string excludePath;
[LabelText(&#34;更新设置&#34;)]
public AddressableAssetGroupTemplate groupSchema;
public bool isShow = true;
private static IEnumerable extraSuffixs = new ValueDropdownList<string>()
{
{ &#34;&#34;, &#34;&#34; },
{ &#34;scene&#34;, &#34;scene&#34; },
{ &#34;shader&#34;, &#34;shader&#34; },
//{ &#34;lua&#34;, &#34;lua&#34; },
{ &#34;prefab&#34;, &#34;prefab&#34; },
};
private static IEnumerable fileSuffixs = new ValueDropdownList<string>()
{
{ &#34;&#34;, &#34;&#34; },
{ &#34;bytes&#34;, &#34;bytes&#34; },
{ &#34;shader&#34;, &#34;shader&#34; },
{ &#34;lua&#34;, &#34;lua&#34; },
};
}
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 = &#34;Custom/Build/AddressableGroupSetter&#34;, fileName = &#34;AddressableGroupSetter&#34;)]
public class AddressableGroupSetter : ScriptableObject
{
private static AddressableGroupSetter _instance;
public static AddressableGroupSetter Instance { get {
if (_instance == null) {
_instance = AssetDatabase.LoadAssetAtPath<AddressableGroupSetter>(&#34;Assets/AddressableAssetsData/AddressableGroupSetter.asset&#34;);
}
return _instance;
} }
[ListDrawerSettings(Expanded = true), LabelText(&#34;远程资源&#34;)]
public List<BuildGroupConfig> GroupSettingList;
[LabelText(&#34;首包资源&#34;), HideIf(&#34;@true&#34;)]
public StaticGroupConfig staticConfig;
AddressableAssetSettings Settings
{
get { return AddressableAssetSettingsDefaultObject.Settings; }
}
[Button(&#34;Auto Build Groups&#34;)]
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 + &#34;_&#34; + Path.GetFileName(subFolder);
subCofig.assetFolders = subFolder;
ResetGroup(subCofig);
}
}
else {
ResetGroup(config);
}
}
}
//静态资源
//CreateStaticGroup();
AssetDatabase.SaveAssets();
}
/// <summary>
/// 重置某分组
/// </summary>
/// <typeparam name=&#34;T&#34;>资源类型</typeparam>
/// <param name=&#34;groupName&#34;>组名</param>
/// <param name=&#34;assetFolder&#34;>资源目录</param>
/// <param name=&#34;getAddress&#34;>通过 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($&#34;Reset group finished, group: {config.groupName}, asset folder: {config.assetFolders}, count: {assets.Length}&#34;);
}
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 + &#34;_Lua&#34;;
string uiDestStataiclabel = staticConfig.groupName + &#34;_UIDesc&#34;;
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(&#34;.lua&#34;))
{
entry.SetLabel(luaStataiclabel, true, false, false);
}
if (assetPath.EndsWith(&#34;.bytes&#34;))
{
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($&#34;Reset static group finished, group: {staticConfig.groupName}, count: {assets.Count}&#34;);
if (sb.Length > 0) {
Debug.LogError($&#34;有问题的资源{sb}&#34;);
}
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(&#39;/&#39;);
if (ss.Length > 2) {
assetFolder = ss[0] + &#34;/&#34; + ss[1];
}
string assetkey = assetPath.Replace(fileFullName, &#34;&#34;).Replace(assetFolder, &#34;&#34;)+ fileName;
if (assetkey.StartsWith(&#34;/&#34;))
{
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(&#34;无法创建entry:&#34;+ assetPath);
return null;
}
entry.address = address;
entry.SetLabel(group.Name, true, false, false);
return entry;
}
/// <summary>
/// 查找资源
/// </summary>
/// <param name=&#34;assetPath&#34;></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(&#34;folder&#34;);
folder = folder.TrimEnd(&#39;/&#39;).TrimEnd(&#39;\\&#39;);
string suffix = config.suffix;
if (!String.IsNullOrEmpty(suffix))
{
suffix = &#34;t:&#34;+ 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(&#39;;&#39;);
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(&#34;Assets/Resources&#34;)) {
//resource下的资源移动至Resources_Internal目录中
string newPath = path.Replace(&#34;Assets/Resources&#34;, &#34;Assets/Resources_Internal/Resources&#34;);
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(&#34;没有创建对应的group:&#34;+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(&#34;isShow&#34;, CollapseOthersOnExpand = false)]
public class BuildGroupConfig
{
[LabelText(&#34;组名&#34;), Space(10)]
public string groupName;
[LabelText(&#34;Asset相对目录&#34;), LabelWidth(80), HorizontalGroup(&#34;1&#34;)]
public string assetFolders;
[LabelText(&#34;是否使用二级目录拆分&#34;), LabelWidth(80), HorizontalGroup(&#34;1&#34;)]
public bool useSubFolders;
[LabelText(&#34;文件后缀&#34;), HorizontalGroup(&#34;2&#34;), ValueDropdown(&#34;fileSuffixs&#34;)]
public string filesuffix;
[LabelText(&#34;Unity后缀&#34;), HorizontalGroup(&#34;2&#34;), ValueDropdown(&#34;extraSuffixs&#34;)]
public string suffix;
[LabelText(&#34;排除文件&#34;), SuffixLabel(&#34;使用;相隔&#34;)]
public string excludePath;
[LabelText(&#34;更新设置&#34;)]
public AddressableAssetGroupTemplate groupSchema;
public bool isShow = true;
private static IEnumerable extraSuffixs = new ValueDropdownList<string>()
{
{ &#34;&#34;, &#34;&#34; },
{ &#34;scene&#34;, &#34;scene&#34; },
{ &#34;shader&#34;, &#34;shader&#34; },
//{ &#34;lua&#34;, &#34;lua&#34; },
{ &#34;prefab&#34;, &#34;prefab&#34; },
};
private static IEnumerable fileSuffixs = new ValueDropdownList<string>()
{
{ &#34;&#34;, &#34;&#34; },
{ &#34;bytes&#34;, &#34;bytes&#34; },
{ &#34;shader&#34;, &#34;shader&#34; },
{ &#34;lua&#34;, &#34;lua&#34; },
};
}
[Serializable]
public class StaticGroupConfig {
[LabelText(&#34;组名&#34;), Space(10)]
public string groupName;
[LabelText(&#34;必备资源&#34;), AssetsOnly]
public List<UnityEngine.Object> includes;
[LabelText(&#34;更新设置&#34;)]
public AddressableAssetGroupTemplate groupSchema;
} |
|