当前位置: 首页 > news >正文

Unity-Xlua热更和AssetBundle详解

从今天开始我们深入Unity的组件,最近的一系列事让我明白学习技术不能只有广度,如走马观花,虽然你可能确实学到了皮毛,但是没有足够深厚的理解,是无法融入自己的东西的,那么你就只是在用别人的工具而不是自己的工具,对于追求技术的人来说,这是无法接受的。

不多废话,我们来到第一个部分

AssetBundle打包加载卸载

在开始具体实现AB包打包的框架之前,我们先要明确一些关于AB包的概念:

AB包的全称是AssetBundle,Asset就是资产而Bundle就是包。如何理解资产呢?在Unity的场景中,所有可以被具象化的资源,都可以成为Asset。换句话说,一张图片,一个音频,都是一个资产,试想这样的资产在一个游戏中有多少,如果我们一个一个地进行保存,光是读取资源并保存这个步骤就得重复成千上万次,所以我们需要AB包:把所有Asset按照功能逻辑分开打包,然后我们以包为单位进行保存即可。

简而言之,AssetBundle就是为了让游戏项目中大量Asset适应实际游戏运行时而被压缩后的一种二进制文件。

关于打包

打包的时候涉及到一些资源的分配问题,最直观地说,如果一个包里东西太多,那么后续我们进行更新时就开销大,且包内东西太多操作不灵活,而如果一个包内东西太少又导致打包次数过多。这个时候就会有几个维度来帮助我们考虑打包的策略了:

资源依赖

首先是资源的依赖:我们的资源往往是嵌套存在的,比如UI的资源里总是有图片,假如我们的A包需要这张图片,B包也需要这张图片,那么我们在A包和B包都分别打包了一次这张图片,这就是一种资源的浪费。这个时候我们可以选择先单独把这个图片进行打包,然后在这个包的基础上把A包的其他内容和B包的其他内容分别打包——但是要基于我们的图片的包,这就是资源依赖的思路:就像把一个数组构建出树的结构一样,我们根据资源的使用情况搭建起包与包之间的关系网,这样可以避免重复打包的情况。

但是这样做就一定好吗?我们写代码的时候也知道,嵌套的代码的好处是可以避免重复,坏处就是我们后期去修改的时候非常难以处理。这也是资源依赖的问题:试想一下,我们可能随便修改了一个作为其他包依赖的包,然后这个时候对这个包有依赖的包是否也要进行更新呢?又比如说我们删除了这个包里的一些资源,那么对这个包的资源有依赖的其他包是否还能正常运作呢?这就是非常麻烦的事。

压缩方式

我们说过了,对于AB包来说压缩是必不可少的一部分,目前主流的压缩方式有两种:

是的,就是我们的LZMA压缩算法和LZ4算法,这两个的区别就在于LZMA算法的压缩率更高(压缩后的包体更小),但是相对的解压速度就更慢且开销更大;不过最大的差异在于:我们的LZMA压缩算法是一次性全部解压的,而LZ4可以基于块来进行解压,那么显然在资源繁多的游戏开发过程中我们选择LZ4压缩算法才是更合理的选择。

Resource

事实上,除了AB包,Unity有自带的保存资源的方法:Resource组件系统,相比起AB包,Resource内部会生成维护一个红黑树来保存各个资源的索引方便我们去查找对应的资源,这个红黑树的内存极大且一旦启动游戏就会一直流在游戏资源的内部,极大地拖慢游戏运行速度。

关于卸载

AB包的卸载有两种方法:

AssetBundle.Unload(bool),其中bool变量为true代表我们会把AB包的内存镜像文件以及所有基于这个镜像文件的实例全部删除,而false我们则只会去卸载这个AB包的内存镜像文件而保留已经被实例化的对象。这里就首先要介绍AB包的加载过程和使用过程内存的变化了:

当我们从服务器上下载AB包之后,整个包会首先存储在硬盘(持久化存储路径)上,当我们的游戏想要读取AB包里的具体资源,Unity会首先去读取AB包的.manifest文件,这个文件记载了AB包中具体的资源的存储分布情况,我们按需去硬盘中读取解压AB包文件,并在内存中生成AB包的镜像文件,之后我们的操作都是基于这个镜像文件来实现的。

综上所述,对于什么时候AssetBundle.Unload(false)和AssetBundle.Unload(true)我们的心里就有数了。因为为true时会把所有实例化的对象也删除,我们当然一般都是达到场景切换的程度才会选择true,否则如果只是一个场景内的资源变换我们一般选择false。需要注意的是一旦选择为true,我们整个场景就不能存在对被删除的AB包资源的引用了,否则一定会报错。

在这里补充一点的是:针对AB包中的资源,我们在具体使用时,遵循“值类型复制,引用类型共享”的思路,当调用 Instantiate(prefab) 时,Unity 会执行 ​浅拷贝(Shallow Copy)

具体代码实例

现在让我们来总结一下AB包从打包到加载到卸载的全过程吧:

首先我们要根据一定的规则对已有游戏的所有资源进行打包,打好包之后我们选择某种压缩方法得到压缩包后把压缩包上传到服务器。这中间其实还涉及热更的一个检查版本信息的过程,涉及到MD5值的比较,我们在热更方面展开介绍。当后续我们从服务器下载包时,我们会先把包放在主机的持久化存储路径下,然后在程序中根据.manifest文件来按需加载AB包,在卸载时根据引用情况来决定是只删除内存镜像文件还是把内存镜像文件和已实例化的所有实例删除。

接下来让我们来分析具体的代码,了解如何实现AB包打包。

Builder

namespace AssetBundleFramework
{public static class Builder{...}
}

我们定义命名空间为AB包框架,声明静态类builder不可实例化,实现全局共享的工具类。

public static readonly Vector2 collectRuleFileProgress = new Vector2(0, 0.2f);
private static readonly Vector2 ms_GetDependencyProgress = new Vector2(0.2f, 0.4f);
private static readonly Vector2 ms_CollectBundleInfoProgress = new Vector2(0.4f, 0.5f);
private static readonly Vector2 ms_GenerateBuildInfoProgress = new Vector2(0.5f, 0.6f);
private static readonly Vector2 ms_BuildBundleProgress = new Vector2(0.6f, 0.7f);
private static readonly Vector2 ms_ClearBundleProgress = new Vector2(0.7f, 0.9f);
private static readonly Vector2 ms_BuildManifestProgress = new Vector2(0.9f, 1f);private static readonly Profiler ms_BuildProfiler = new Profiler(nameof(Builder));
private static readonly Profiler ms_LoadBuildSettingProfiler = ms_BuildProfiler.CreateChild(nameof(LoadSetting));
private static readonly Profiler ms_SwitchPlatformProfiler = ms_BuildProfiler.CreateChild(nameof(SwitchPlatform));
private static readonly Profiler ms_CollectProfiler = ms_BuildProfiler.CreateChild(nameof(Collect));
private static readonly Profiler ms_CollectBuildSettingFileProfiler = ms_CollectProfiler.CreateChild("CollectBuildSettingFile");
private static readonly Profiler ms_CollectDependencyProfiler = ms_CollectProfiler.CreateChild(nameof(CollectDependency));
private static readonly Profiler ms_CollectBundleProfiler = ms_CollectProfiler.CreateChild(nameof(CollectBundle));
private static readonly Profiler ms_GenerateManifestProfiler = ms_CollectProfiler.CreateChild(nameof(GenerateManifest));
private static readonly Profiler ms_BuildBundleProfiler = ms_BuildProfiler.CreateChild(nameof(BuildBundle));
private static readonly Profiler ms_ClearBundleProfiler = ms_BuildProfiler.CreateChild(nameof(ClearAssetBundle));
private static readonly Profiler ms_BuildManifestBundleProfiler = ms_BuildProfiler.CreateChild(nameof(BuildManifest));

密密麻麻的变量定义部分,不全部介绍了,前面的一系列Vector2变量表示打包过程中的进度条,后续的profiler则是自定义的一个类型,主要目的就是展示整个打包的过程。

#if UNITY_IOSprivate const string PLATFORM = "iOS";
#elif UNITY_ANDROIDprivate const string PLATFORM = "Android";
#elseprivate const string PLATFORM = "Windows";

根据平台生成平台名。

//bundle后缀
public const string BUNDLE_SUFFIX = ".ab";
public const string BUNDLE_MANIFEST_SUFFIX = ".manifest";
//bundle描述文件名称
public const string MANIFEST = "manifest";public static readonly ParallelOptions ParallelOptions = new ParallelOptions()
{MaxDegreeOfParallelism = Environment.ProcessorCount * 2
};//bundle打包Options
public readonly static BuildAssetBundleOptions BuildAssetBundleOptions = BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.StrictMode | BuildAssetBundleOptions.DisableLoadAssetByFileName | BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension;

这些代码具体来说就是在构建AB包文件的一些格式。

/// <summary>
/// 打包设置
/// </summary>
public static BuildSetting buildSetting { get; private set; }#region Path/// <summary>
/// 打包配置
/// </summary>
public readonly static string BuildSettingPath = Path.GetFullPath("BuildSetting.xml").Replace("\\", "/");/// <summary>
/// 临时目录,临时生成的文件都统一放在该目录
/// </summary>
public readonly static string TempPath = Path.GetFullPath(Path.Combine(Application.dataPath, "Temp")).Replace("\\", "/");/// <summary>
/// 临时目录,临时文件的ab包都放在该文件夹,打包完成后会移除
/// </summary>
public readonly static string TempBuildPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../TempBuild")).Replace("\\", "/");/// <summary>
/// 资源描述__文本
/// </summary>
public readonly static string ResourcePath_Text = $"{TempPath}/Resource.txt";/// <summary>
/// 资源描述__二进制
/// </summary>
public static string ResourcePath_Binary = $"{TempPath}/Resource.bytes";/// <summary>
/// Bundle描述__文本
/// </summary>
public readonly static string BundlePath_Text = $"{TempPath}/Bundle.txt";/// <summary>
/// Bundle描述__二进制
/// </summary>
public readonly static string BundlePath_Binary = $"{TempPath}/Bundle.bytes";/// <summary>
/// 资源依赖描述__文本
/// </summary>
public readonly static string DependencyPath_Text = $"{TempPath}/Dependency.txt";/// <summary>
/// 资源依赖描述__文本
/// </summary>
public readonly static string DependencyPath_Binary = $"{TempPath}/Dependency.bytes";/// <summary>
/// 打包目录
/// </summary>
public static string buildPath { get; set; }

注释写得都很清楚,AB包在打包流程中需要的一系列设置,主要包含一系列存储的文件路径。

        #region Build MenuItem[MenuItem("Tools/ResBuild/Windows")]public static void BuildWindows(){Build();}[MenuItem("Tools/ResBuild/Android")]public static void BuildAndroid(){Build();}[MenuItem("Tools/ResBuild/iOS")]public static void BuildIos(){Build();}#endregion

这个部分的代码就是让你可以在菜单直接调用Build函数启动打包的过程。

/// <summary>
/// 加载打包配置文件
/// </summary>
/// <param name="settingPath">打包配置路径</param>
private static BuildSetting LoadSetting(string settingPath)
{buildSetting = XmlUtility.Read<BuildSetting>(settingPath);if (buildSetting == null){throw new Exception($"Load buildSetting failed,SettingPath:{settingPath}.");}(buildSetting as ISupportInitialize)?.EndInit();buildPath = Path.GetFullPath(buildSetting.buildRoot).Replace("\\", "/");if (buildPath.Length > 0 && buildPath[buildPath.Length - 1] != '/'){buildPath += "/";}buildPath += $"{PLATFORM}/";return buildSetting;
}private static void Build()
{ms_BuildProfiler.Start();ms_SwitchPlatformProfiler.Start();SwitchPlatform();ms_SwitchPlatformProfiler.Stop();ms_LoadBuildSettingProfiler.Start();buildSetting = LoadSetting(BuildSettingPath);ms_LoadBuildSettingProfiler.Stop();//搜集bundle信息ms_CollectProfiler.Start();Dictionary<string, List<string>> bundleDic = Collect();ms_CollectProfiler.Stop();//打包assetbundlems_BuildBundleProfiler.Start();BuildBundle(bundleDic);ms_BuildBundleProfiler.Stop();//清空多余文件ms_ClearBundleProfiler.Start();ClearAssetBundle(buildPath, bundleDic);ms_ClearBundleProfiler.Stop();//把描述文件打包bundlems_BuildManifestBundleProfiler.Start();BuildManifest();ms_BuildManifestBundleProfiler.Stop();EditorUtility.ClearProgressBar();ms_BuildProfiler.Stop();Debug.Log($"打包完成{ms_BuildProfiler}");
}

这段代码里包含两个函数:第一个是加载我们的打包设置,我们会去检查是否有打包设置,没有的话就返回异常,有的话就进行加载;第二个则是开始执行打包流程,这个Build函数主要就是负责根据流程调用各个函数的函数。

/// <summary>
/// 搜集打包bundle的信息
/// </summary>
/// <returns></returns>private static Dictionary<string, List<string>> Collect()
{//获取所有在打包设置的文件列表ms_CollectBuildSettingFileProfiler.Start();HashSet<string> files = buildSetting.Collect();ms_CollectBuildSettingFileProfiler.Stop();//搜集所有文件的依赖关系ms_CollectDependencyProfiler.Start();Dictionary<string, List<string>> dependencyDic = CollectDependency(files);ms_CollectDependencyProfiler.Stop();//标记所有资源的信息Dictionary<string, EResourceType> assetDic = new Dictionary<string, EResourceType>();//被打包配置分析到的直接设置为Directforeach (string url in files){assetDic.Add(url, EResourceType.Direct);}//依赖的资源标记为Dependency,已经存在的说明是Direct的资源foreach (string url in dependencyDic.Keys){if (!assetDic.ContainsKey(url)){assetDic.Add(url, EResourceType.Dependency);}}//该字典保存bundle对应的资源集合ms_CollectBundleProfiler.Start();Dictionary<string, List<string>> bundleDic = CollectBundle(buildSetting, assetDic, dependencyDic);ms_CollectBundleProfiler.Stop();//生成Manifest文件ms_GenerateManifestProfiler.Start();GenerateManifest(assetDic, bundleDic, dependencyDic);ms_GenerateManifestProfiler.Stop();return bundleDic;
}

这个函数负责获取所有AB包的信息。

        /// <summary>/// 收集指定文件集合所有的依赖信息/// </summary>/// <param name="files">文件集合</param>/// <returns>依赖信息</returns>private static Dictionary<string, List<string>> CollectDependency(ICollection<string> files){float min = ms_GetDependencyProgress.x;float max = ms_GetDependencyProgress.y;Dictionary<string, List<string>> dependencyDic = new Dictionary<string, List<string>>();//声明fileList后,就不需要递归了List<string> fileList = new List<string>(files);for (int i = 0; i < fileList.Count; i++){string assetUrl = fileList[i];if (dependencyDic.ContainsKey(assetUrl))continue;if (i % 10 == 0){//只能大概模拟进度float progress = min + (max - min) * ((float)i / (files.Count * 3));EditorUtility.DisplayProgressBar($"{nameof(CollectDependency)}", "搜集依赖信息", progress);}string[] dependencies = AssetDatabase.GetDependencies(assetUrl, false);List<string> dependencyList = new List<string>(dependencies.Length);//过滤掉不符合要求的assetfor (int ii = 0; ii < dependencies.Length; ii++){string tempAssetUrl = dependencies[ii];string extension = Path.GetExtension(tempAssetUrl).ToLower();if (string.IsNullOrEmpty(extension) || extension == ".cs" || extension == ".dll")continue;dependencyList.Add(tempAssetUrl);if (!fileList.Contains(tempAssetUrl))fileList.Add(tempAssetUrl);}dependencyDic.Add(assetUrl, dependencyList);}return dependencyDic;}

这个函数则专门负责针对特定的文件去搜索依赖项并返回依赖文件路径。

BundleManager

然后我们来看我们的BundleManager类,这个类专门提供AB包的 同步/异步加载、引用计数、依赖管理、延迟卸载​功能。

成员变量如下:

public readonly static BundleManager instance = new BundleManager();//单例模式/// <summary>
/// 加载bundle开始的偏移
/// </summary>
internal ulong offset { get; private set; }//只可读/// <summary>
/// 获取资源真实路径回调
/// </summary>
private Func<string, string> m_GetFileCallback;//Func是委托,接受一个string参数返回一个string参数,允许外部的函数//来具体实现寻找资源路径功能而BundleManager类内部不关注具体实现/// <summary>
/// bundle依赖管理信息
/// </summary>
private AssetBundleManifest m_AssetBundleManifest;//AssetBundleManifest是Unity自带的类/// <summary>
/// 所有已加载的bundle
/// </summary>
private Dictionary<string, ABundle> m_BundleDic = new Dictionary<string, ABundle>();//存储AB包名称和AB包的字//典//异步创建的bundle加载时候需要先保存到该列表
private List<ABundleAsync> m_AsyncList = new List<ABundleAsync>();//异步加载的表/// <summary>
/// 需要释放的bundle
/// </summary>
private LinkedList<ABundle> m_NeedUnloadList = new LinkedList<ABundle>();

在这众多变量之中,除了AssetBundleManifest类是Unity自带的类以外,其他的类要么是C#自带的数据结构要么是我们自己定义的。其中我们可以看到有两个比较显眼的我们自定义的类型:ABundle与ABundleAsync。我们后续再来详细地介绍这两个类,我们先把BundleManager类的方法也全部介绍一遍。

/// <summary>
/// 初始化
/// </summary>
/// <param name="platform">平台</param>
/// <param name="getFileCallback">获取资源真实路径回调</param>
/// <param name="offset">加载bundle偏移</param>
internal void Initialize(string platform, Func<string, string> getFileCallback, ulong offset)
{m_GetFileCallback = getFileCallback;this.offset = offset;string assetBundleManifestFile = getFileCallback.Invoke(platform);//getFileCallback是一个委托实例AssetBundle manifestAssetBundle = AssetBundle.LoadFromFile(assetBundleManifestFile);//AssetBundle是封装//好的类,LoadFromFile//则是专门用于读取文件//的方法Object[] objs = manifestAssetBundle.LoadAllAssets();//加载所有assetsif (objs.Length == 0){throw new Exception($"{nameof(BundleManager)}.{nameof(Initialize)}() AssetBundleManifest load fail.");}m_AssetBundleManifest = objs[0] as AssetBundleManifest;//安全地尝试将objs[0]转换成AssetBundleManifest类
}

我们首先来介绍关键字internal:这个关键字修饰的内容只能在这个程序集内被调用,有人要问了,什么是程序集呢?简单地说,我们都知道C#生成可执行文件需要预编译、编译、汇编和链接几个步骤,而经过编译后的中间语言加上元数据以及一些其他的信息就是我们的程序集。

初始化函数中,我们把传入参数中的委托和偏差给到Manager类,然后我们获取AB包的manifest文件名称并读取,这里有几个值得说的点:首先是我们的getFileCallback.Invoke(platform),我们根据平台名调用Invoke函数,调用这个函数的getFileCallback是一个委托实例, 更准确地说,Func是一个带返回值的泛型委托:

namespace System
{public delegate TResult Func<in T, out TResult>(T arg);
}

那么问题来了,Invoke在哪里呢?为什么可以直接调用呢?

简单地说,C#中的所有委托类型都是继承自MulticastDelegate 类,所有委托都隐式地包含图中的这三个函数。

至于AssetBundle,就是一个Unity自带的类,专门用来表示AB包的,具体使用方法可以查询手册。

最后的 m_AssetBundleManifest = objs[0] as AssetBundleManifest;中我们可以看到一个as关键字,这个关键字的作用就是安全地尝试转换,如果转换失败的话会返回NULL。

/// <summary>
/// 获取bundle的绝对路径
/// </summary>
/// <param name="url"></param>//url是统一资源定位符的意思,在这里如果是本地文件则url是文件路径,否则是网址
/// <returns>bundle的绝对路径</returns>
internal string GetFileUrl(string url)
{if (m_GetFileCallback == null){throw new Exception($"{nameof(BundleManager)}.{nameof(GetFileUrl)}() {nameof(m_GetFileCallback)} is null.");}//交到外部处理return m_GetFileCallback.Invoke(url);
}

获取文件的URL方法,内容非常的简单,如果我们的委托实例为空则抛出异常,反之我们执行委托,这里我有添加关于URL的注释。

        /// <summary>/// 同步加载bundle/// </summary>/// <param name="url">asset路径</param>internal ABundle Load(string url){return LoadInternal(url, false);}/// <summary>/// 异步加载bundle/// </summary>/// <param name="url">asset路径</param>internal ABundle LoadAsync(string url){return LoadInternal(url, true);}

同步和异步加载AB包,执行的都是LoadInternal()函数,只是修改了某个参数。

/// <summary>
/// 内部加载bundle
/// </summary>
/// <param name="url">asset路径</param>
/// <param name="async">是否异步</param>
/// <returns>bundle对象</returns>
private ABundle LoadInternal(string url, bool async)//加载AB包
{ABundle bundle;if (m_BundleDic.TryGetValue(url, out bundle))//AB包的字典,尝试根据键获得值,这里用out修饰//bundle表示我们可以不初始化bundle但输出的bundle//必须是有值的{if (bundle.reference == 0)//之前的引用为0,但此时我们要去加载这个包,所以将其从需要移除的//包列表中移除{m_NeedUnloadList.Remove(bundle);}//从缓存中取并引用+1bundle.AddReference();return bundle;}//创建abif (async)//如果是异步加载{bundle = new BundleAsync();bundle.url = url;m_AsyncList.Add(bundle as ABundleAsync);//加入异步加载的AB包表}else{bundle = new Bundle();bundle.url = url;}m_BundleDic.Add(url, bundle);//字典加入该包,注意是url作为键//加载依赖string[] dependencies = m_AssetBundleManifest.GetDirectDependencies(url);//获取直接依赖,输入Urlif (dependencies.Length > 0){bundle.dependencies = new ABundle[dependencies.Length];for (int i = 0; i < dependencies.Length; i++){string dependencyUrl = dependencies[i];ABundle dependencyBundle = LoadInternal(dependencyUrl, async);bundle.dependencies[i] = dependencyBundle;}}bundle.AddReference();bundle.Load();return bundle;
}

具体的作用其实我都写在注释里了,这里反而没有太多好说的,没有陌生的语法也没有陌生的概念,不过我们在这里介绍一下同步加载AB包和异步加载AB包的区别:

最大的区别就在于会不会阻塞主线程。

/// <summary>
/// 卸载bundle
/// </summary>
/// <param name="bundle">要卸载的bundle</param>
internal void UnLoad(ABundle bundle)
{if (bundle == null)throw new ArgumentException($"{nameof(BundleManager)}.{nameof(UnLoad)}() bundle is null.");//引用-1bundle.ReduceReference();//引用为0,直接释放if (bundle.reference == 0){WillUnload(bundle);}
}
/// <summary>
/// 即将要释放的资源
/// </summary>
/// <param name="resource"></param>
private void WillUnload(ABundle bundle)
{m_NeedUnloadList.AddLast(bundle);//添加到链表的尾节点
}

卸载函数,核心就是减少引用计数并且在引用计数为0时直接启动真正的释放函数。(卸载函数是单独地卸载某个AB包,释放函数则是检测当前场景中没有AB包引用后直接将AB包从内存中释放),释放函数的内容就是把当前包丢进需要释放的列表。

        public void Update(){for (int i = 0; i < m_AsyncList.Count; i++){if (m_AsyncList[i].Update())//如果异步任务列表中某个任务完成则从列表中移除{m_AsyncList.RemoveAt(i);i--;}}}

update的生命周期函数中,不断轮询异步任务列表中是否有任务以及完成,完成的话则移除。注意这里有两个update,但是其实这个异步任务(ABundleAsync)的update函数是自定义的,返回bool值表示异步任务是否完成。

public void LateUpdate()
{if (m_NeedUnloadList.Count == 0)return;while (m_NeedUnloadList.Count > 0){ABundle bundle = m_NeedUnloadList.First.Value;m_NeedUnloadList.RemoveFirst();if (bundle == null)continue;m_BundleDic.Remove(bundle.url);if (!bundle.done && bundle is BundleAsync)//is关键字用来判断该对象是否为某个类或者某个类的派生类{BundleAsync bundleAsync = bundle as BundleAsync;if (m_AsyncList.Contains(bundleAsync))m_AsyncList.Remove(bundleAsync);}bundle.UnLoad();//依赖引用-1if (bundle.dependencies != null){for (int i = 0; i < bundle.dependencies.Length; i++){ABundle temp = bundle.dependencies[i];UnLoad(temp);}}}
}

主要就是通过引用计数和依赖递归机制确保资源安全释放。

现在让我们介绍之前说到的ABundle类和ABundleAsync类:

ABundle类和ABundleAsync类

internal abstract class ABundle
{/// <summary>/// AssetBundle/// </summary>internal AssetBundle assetBundle { get; set; }/// <summary>/// 是否是场景/// </summary>internal bool isStreamedSceneAssetBundle { get; set; }/// <summary>/// bundle url/// </summary>internal string url { get; set; }/// <summary>/// 引用计数器/// </summary>internal int reference { get; set; }//引用计数//bundle是否加载完成internal bool done { get; set; }//是否完成/// <summary>/// bundle依赖/// </summary>internal ABundle[] dependencies { get; set; }//AB包依赖/// <summary>/// 加载bundle/// </summary>internal abstract void Load();//定义抽象方法Load()以加载/// <summary>/// 卸载bundle/// </summary>internal abstract void UnLoad();//定义抽象方法UnLoad()以卸载/// <summary>/// 异步加载资源/// </summary>/// <param name="name">资源名称</param>/// <param name="type">资源Type</param>/// <returns>AssetBundleRequest</returns>internal abstract AssetBundleRequest LoadAssetAsync(string name, Type type);//加载异步资源,AssetBundleRequest 是用于//​异步加载 AssetBundle 中特定资源​ 的核心类。它属于//UnityEngine 命名空间,通常配合协程(Coroutine)或//async/await 使用,用于在运行时动态加载资源//(如模型、纹理、场景等),而不会阻塞主线程。/// <summary>/// 加载资源/// </summary>/// <param name="name">资源名称</param>/// <param name="type">资源Type</param>/// <returns>指定名字的资源</returns>internal abstract Object LoadAsset(string name, Type type);//加载资产/// <summary>/// 增加引用/// </summary>internal void AddReference()//增加引用指数{//自己引用+1++reference;}/// <summary>/// 减少引用/// </summary>internal void ReduceReference()//减少引用指数{//自己引用-1--reference;if (reference < 0){throw new Exception($"{GetType()}.{nameof(ReduceReference)}() less than 0,{nameof(url)}:{url}.");}}
}

ABundle类作为一个抽象类,主要就是定义一系列抽象方法,作为基类让其他的子类实现。

ABundleAsync的实现则是:

using System;
using UnityEngine;
using Object = UnityEngine.Object;//利用using关键字让Object变成了UnityEngine.Object的别名namespace AssetBundleFramework
{internal abstract class ABundleAsync : ABundle//抽象类{internal abstract bool Update();//抽象函数}
}

是的,注意我们的ABundleAsync已经继承了ABundle类,但是他没有负责实现抽象方法而是继续作为抽象类,这样的话就把问题抛给了下一个非抽象的子类,ABundleAsync类唯一定义的抽象方法就是这个返回bool的Update函数。

Bundle类

Bundle类就是一个继承了ABundle类的类,简单地说,他重写了ABundle类的:

每个函数的内容如下:

        /// <summary>/// 加载AssetBundle/// </summary>internal override void Load(){if (assetBundle){throw new Exception($"{nameof(Bundle)}.{nameof(Load)}() {nameof(assetBundle)} not null , Url:{url}.");}string file = BundleManager.instance.GetFileUrl(url);#if UNITY_EDITOR || UNITY_STANDALONEif (!File.Exists(file)){throw new FileNotFoundException($"{nameof(Bundle)}.{nameof(Load)}() {nameof(file)} not exist, file:{file}.");}
#endifassetBundle = AssetBundle.LoadFromFile(file, 0, BundleManager.instance.offset);isStreamedSceneAssetBundle = assetBundle.isStreamedSceneAssetBundle;done = true;}

存在ab包则获取文件路径,文件路径没错的话调用LoadFromFile()函数,这里我们可以看到有一个参数是isStreamedSceneAssetBundle,这个也是AssetBundle内部自带的一个成员bool变量,表示:

所谓的流式场景

        /// <summary>/// 卸载bundle/// </summary>internal override void UnLoad(){if (assetBundle)assetBundle.Unload(true);assetBundle = null;done = false;reference = 0;isStreamedSceneAssetBundle = false;}

卸载函数非常简单。

        /// <summary>/// 异步加载资源/// </summary>/// <param name="name">资源名称</param>/// <param name="type">资源Type</param>/// <returns>AssetBundleRequest</returns>internal override AssetBundleRequest LoadAssetAsync(string name, Type type){if (string.IsNullOrEmpty(name))throw new ArgumentException($"{nameof(Bundle)}.{nameof(LoadAssetAsync)}() name is null.");if (assetBundle == null)throw new NullReferenceException($"{nameof(Bundle)}.{nameof(LoadAssetAsync)}() Bundle is null.");return assetBundle.LoadAssetAsync(name, type);}

异步加载函数的内容本质上来说依然是在调用Unity的AssetBundle类内封装的LoadAssetAsync,我们只是加了一些检测,这里可以看到我们返回的是AssetBundleRequest类型,这个类型显然也是Unity自带的类。

        /// <summary>/// 加载资源/// </summary>/// <param name="name">资源名称</param>/// <param name="type">资源Type</param>/// <returns>指定名字的资源</returns>internal override Object LoadAsset(string name, Type type){if (string.IsNullOrEmpty(name))throw new ArgumentException($"{nameof(Bundle)}.{nameof(LoadAsset)}() name is null.");if (assetBundle == null)throw new NullReferenceException($"{nameof(Bundle)}.{nameof(LoadAsset)}() Bundle is null.");return assetBundle.LoadAsset(name, type);}

同步加载和异步加载的最大区别就在于调用的AssetBundle的加载函数不同,以及返回的类型不同,注意这里的Object实际上是我们在头文件引用中:

using System;
using System.IO;
using UnityEngine;
using Object = UnityEngine.Object;

这里就是using的另外一个用法:赋予别名,我们把UnityEngine.Object赋予另一个别名Object,这样我们调用UnityEngine.Object就直接写Object即可。

那么问题来了,为什么我们重写的两个函数中异步加载返回AssetBundleRequest一个返回Object呢?这两个类型有什么区别呢?

简单地说,我们的同步加载就是直接返回加载好的资源,异步加载则返回AssetBundleRequest追踪加载状态。

BundleAsync类

和Bundle类继承自ABundle类似,BundleAsync类也是继承自ABundleAsync类,当然,如果还有人记得的话,我们的ABundleAsync其实也继承自ABundle类,还多加了一个返回Bool变量的Update的抽象函数。

也是同样的重写了Load、UnLoad、LoadAsset、LoadAssetAsync四个函数,当然,还有我们ABundleAsync类新加入的Update函数。

事实上,这个类中的LoadAsset、LoadAssetAsync函数与Bundle类中的同名的两个函数内容一模一样,我就不多赘述了,主要来说说有区别的部分。

后续的还有很多函数,但是大体的逻辑与结构核心就是这几个类。

具体运行时我们在Tools中点击对应的平台进行AB包打包即可,效果如图:

Xlua热更

我先来展示一下使用流程:

既然涉及到热更,我们当然需要自己创建一个简单的服务器,我们使用NetBox来实现一个简单的window服务器。

我们来搭建一个小服务器:

Dim httpd
Shell.Service.RunService "NBWeb", "NetBox Web Server", "NetBox Http Server Sample"
'---------------------- Service Event ---------------------
Sub OnServiceStart()
Set httpd = NetBox.CreateObject("NetBox.HttpServer")
If httpd.Create("", 5858) = 0 Then
Set host = httpd.AddHost("", "\Web")
host.EnableScript = true
host.AddDefault "index.htm"
host.AddDefault "index.html"
host.AddDefault "index.asp"
host.AddDefault "default.htm"
host.AddDefault "default.asp"
httpd.Start
else
Shell.Quit 0
end if
End Sub
Sub OnServiceStop()
httpd.Close
End Sub
Sub OnServicePause()
httpd.Stop
End Sub
Sub OnServiceResume()
httpd.Start
End Sub

虽然服务器的代码并不是我们的关键(VBScript),但我还是大致解释一下这个服务器代码的意思:

Dim httpd
Shell.Service.RunService "NBWeb", "NetBox Web Server", "NetBox Http Server Sample"

这两段的意思Dim即定义一个变量httpd,然后我们运行一个变量名为NBWeb的Window服务,显示名为NetBox Web Server,描述为NetBox Http Server Sample。

Sub OnServiceStart()
Set httpd = NetBox.CreateObject("NetBox.HttpServer")
If httpd.Create("", 5858) = 0 Then
Set host = httpd.AddHost("", "\Web")

OnServiceStart看名字也知道是标记服务开始运行时,然后我们把之前定义的httpd赋值NetBox 提供的 HTTP 服务器对象(NetBox.HttpServer),If httpd.Create("", 5858) = 0表示我们的httpd试图绑定端口5858,如果成功(返回0)则执行Then之后的内容,否则返回报错。而绑定端口成功后我们就要去配置我们的虚拟主机,地址在当前文件路径(这个.box文件路径)下的Web文件中。

Set host = httpd.AddHost("", "\Web")
host.EnableScript = true
host.AddDefault "index.htm"
host.AddDefault "index.html"
host.AddDefault "index.asp"
host.AddDefault "default.htm"
host.AddDefault "default.asp"
httpd.Start

这一段其实干的事就是启动脚本的支持并配置一系列的默认文件。

ElseShell.Quit 0  ' 退出服务
End If

显然绑定失败就直接退出服务。

else
Shell.Quit 0
end if
End Sub
Sub OnServiceStop()
httpd.Close
End Sub
Sub OnServicePause()
httpd.Stop
End Sub
Sub OnServiceResume()
httpd.Start
End Sub

这里还多了几个有关服务进行的方法,分别是停止服务,暂停服务以及恢复服务时我们的http主机httpd的操作。

大体上来说,就是写了一个服务器运作的逻辑,对我们来说最重要的有两个东西:绑定的端口号以及我们的虚拟主机的文件路径。

现在让我们先在Unity里写两个测试的Lua脚本:

我们有两个脚本,分别的输出是Hello和Test Old。

我们稍微做一个修改:

然后我们取到Unity界面,Tools->AB包->创建AB包->window。

我们生成了新的AB包,这个AB包的位置在当地的AssetBundlesEncrypt文件夹下:

我们将这些东西复制粘贴到我们的虚拟主机下(其实就是我们的服务器)。

现在让我们来观赏实现这一切的核心代码:我们的热更新管理器。

热更新管理器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System.IO;
using System.Text;public class HotUpdateMgr : MonoSingletonBase<HotUpdateMgr>
{/// <summary>/// _sBaseUrl下载网址/// </summary>
#if UNITY_EDITOR || UNITY_STANDALONE_WINpublic static string _sBaseUrl = "http://127.0.0.1:5858";
#elif UNITY_ANDROIDpublic static string _sBaseUrl = "http://192.168.255.10:5858";
#elif UNITY_IPHONEpublic static string _sBaseUrl = "http://192.168.255.10:5858";
#endifprivate string _sABVersionName = "";/// <summary>/// 本地版本信息缓存路径/// </summary>private string _sVersionLocalFilePath = "";/// <summary>/// 同时下载的最大数量/// </summary>private int _nMaxDownloader = 5;/// <summary>/// 当前需要下载的AB包数据/// </summary>List<ABPackInfo> _list_allNeedABPack = new List<ABPackInfo>();/// <summary>/// 所需下载资源总大小/// </summary>private float _nDownloadTotalSize = 0;/// <summary>/// 当前已下载资源的大小/// </summary>private float _nCurDownloadedSize = 0;/// <summary>/// AB包下载器/// </summary>private List<ABDownloader> _list_allABDownloader = new List<ABDownloader>();/// <summary>/// 客户端的AB版本数据/// </summary>private Dictionary<string, ABPackInfo> _dict_clientABInfoList = null;protected override void Awake(){string sPlatformStr = ABPackUtils.GetABPackPathPlatformStr();_sABVersionName = sPlatformStr + ABPackUtils.sABVersionName;_sVersionLocalFilePath = Application.persistentDataPath + _sABVersionName;IOUtils.CreateDirectroryOfFile(_sVersionLocalFilePath);}/// <summary>/// 开始热更/// </summary>public void StartHotUpdate(){Debug.Log("开始热更 >>>>>> ");StartCoroutine(DownloadAllABPackVersion());}/// <summary>/// 解析版本文件,返回一个文件列表/// </summary>/// <param name="sContent"></param>/// <returns></returns>public Dictionary<string, ABPackInfo> ConvertToAllABPackDesc(string sContent){Dictionary<string, ABPackInfo> dict_allABPackDesc = new Dictionary<string, ABPackInfo>();string[] arrLines = sContent.Split('\n');//用回车 字符 \n 分割每一行foreach (string item in arrLines){string[] arrData = item.Split(' ');//用空格分割每行数据的三个类型if (arrData.Length == 3){ABPackInfo obj_ABPackData = new ABPackInfo();obj_ABPackData.sABName = arrData[0]; // 名称即路径obj_ABPackData.sMd5 = arrData[1]; // md5值obj_ABPackData.nSize = int.Parse(arrData[2]); // AB包大小//Debug.Log(string.Format("解析的路径:{0}\n解析的MD5:{1}\n解析的文件大小KB:{2}", obj_ABPackData.sABName, obj_ABPackData.sMd5, obj_ABPackData.nSize));dict_allABPackDesc.Add(obj_ABPackData.sABName, obj_ABPackData);}}return dict_allABPackDesc;}/// <summary>/// 获取服务端的AB包版本信息/// </summary>/// <returns></returns>IEnumerator DownloadAllABPackVersion(){string sVersionUrl = _sBaseUrl + @"/" + _sABVersionName;//Debug.Log("下载版本数据路径:" + sVersionUrl);using (UnityWebRequest uObj_versionWeb = UnityWebRequest.Get(sVersionUrl)){yield return uObj_versionWeb.SendWebRequest(); // 等待资源下载if (uObj_versionWeb.isNetworkError || uObj_versionWeb.isHttpError){Debug.LogError("获取版本AB包数据错误: " + uObj_versionWeb.error);yield break;}else{string sVersionData = uObj_versionWeb.downloadHandler.text;//Debug.Log("成功获取到版本相关数据 >>>> \n" + sVersionData);CheckNeedDownloadABPack(sVersionData);}}}/// <summary>/// 检测需要下载/// </summary>/// <param name="sServerVersionData"></param>void CheckNeedDownloadABPack(string sServerVersionData){//Debug.Log("运行平台:" + Application.platform);//Debug.Log("本地版本文件路径是:" + _sVersionLocalFilePath);Dictionary<string, ABPackInfo> dict_serverDownList = ConvertToAllABPackDesc(sServerVersionData); // 服务端获取的资源下载列表if (File.Exists(_sVersionLocalFilePath)){//Debug.Log("存在本地,对比服务器版本信息");string sClientVersionData = File.ReadAllText(_sVersionLocalFilePath); // 本地版本信息_dict_clientABInfoList = ConvertToAllABPackDesc(sClientVersionData); // 客户端本地缓存的资源下载列表//遍历服务器文件foreach (ABPackInfo obj_itemData in dict_serverDownList.Values){// 存在对应已下载文件,对比Md5值是否一致if (_dict_clientABInfoList.ContainsKey(obj_itemData.sABName)){// md5值不一致,则更新文件if (_dict_clientABInfoList[obj_itemData.sABName].sMd5 != obj_itemData.sMd5){_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;//Debug.Log("MD5 值不一样,资源存在变更,增加文件 >>>>> " + obj_itemData.sABName);}}else{_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;}}}else // 如果说不存在本地缓存,那就直接下载所有的AB包{foreach (ABPackInfo obj_itemData in dict_serverDownList.Values){_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;//Debug.Log("所需下载文件 >>>>> " + obj_itemData.sABName);}}StartDownloadAllABPack();}/// <summary>/// 开始下载所有所需下载的AB包资源/// </summary>/// <param name="list_allABPack"></param>void StartDownloadAllABPack(){int nMaxCount = _list_allNeedABPack.Count;if (nMaxCount <= 0) {HotUpdateEnd();return;}int nNeedCount = Mathf.Min(nMaxCount, _nMaxDownloader);for (int i = 0; i < nNeedCount; i++){ABPackInfo obj_ABPackDesc = _list_allNeedABPack[0];ABDownloader obj_downloader = new ABDownloader();_list_allABDownloader.Add(obj_downloader);StartCoroutine(obj_downloader.DownloadABPack(obj_ABPackDesc));_list_allNeedABPack.RemoveAt(0);}}/// <summary>/// 切换下载下一个AB包/// </summary>/// <param name="obj_ABDownloader">需要切换的下载器</param>public void ChangeDownloadNextABPack(ABDownloader obj_ABDownloader){//Debug.Log("切换下载下一个 AB 包");_nCurDownloadedSize += obj_ABDownloader.GetDownloadResSize();if (_list_allNeedABPack.Count > 0) // 还存在需要下载的资源,下载器切换资源,继续下载{StartCoroutine(obj_ABDownloader.DownloadABPack(_list_allNeedABPack[0]));_list_allNeedABPack.RemoveAt(0);}else{bool bIsDownloadSuc = true; // 资源是否全部下载完成foreach(ABDownloader obj_downloader in _list_allABDownloader){if(obj_downloader.bIsDownloading) // 存在一个下载中,即表示当前还有未下载完成的部分{bIsDownloadSuc = false;break;}}if (bIsDownloadSuc) // 已完成全部下载{HotUpdateEnd();}}}/// <summary>/// 更新本地缓存的AB包版本数据/// </summary>/// <param name="obj_ABPackDecs"></param>public void UpdateClientABInfo(ABPackInfo obj_ABPackDecs){if (_dict_clientABInfoList == null){_dict_clientABInfoList = new Dictionary<string, ABPackInfo>();}_dict_clientABInfoList[obj_ABPackDecs.sABName] = obj_ABPackDecs;StringBuilder obj_sb = new StringBuilder();foreach (ABPackInfo obj_temp in _dict_clientABInfoList.Values){obj_sb.AppendLine(ABPackUtils.GetABPackVersionStr(obj_temp.sABName, obj_temp.sMd5, obj_temp.nSize.ToString()));}IOUtils.CreatTextFile(_sVersionLocalFilePath, obj_sb.ToString());}/// <summary>/// 热更新结束,进入下一个阶段/// </summary>private void HotUpdateEnd(){// TODO 进入下一个阶段Debug.Log("热更新: 已完成所有的AB包下载, 进入下一个阶段 TODO");HotUpdateTest.GetInstance().RunLua();HotUpdateTest.GetInstance().InitShow();}
}

可以看到非常冗长的代码啊,我们来一点一点介绍:

首先我们要注意的是这个MonoSingletonBase<HotUpdateMgr>,这个是泛型单例基类的意思,也就是同时包含了泛型和单例模式的一个基类,这两个概念就不用我多做介绍了吧。

    /// <summary>/// _sBaseUrl下载网址/// </summary>
#if UNITY_EDITOR || UNITY_STANDALONE_WINpublic static string _sBaseUrl = "http://127.0.0.1:5858";
#elif UNITY_ANDROIDpublic static string _sBaseUrl = "http://192.168.255.10:5858";
#elif UNITY_IPHONEpublic static string _sBaseUrl = "http://192.168.255.10:5858";
#endif

这个就是根据我们的平台来确定服务器的网址,这样我们才能去服务器下载AB包。

private string _sABVersionName = "";/// <summary>
/// 本地版本信息缓存路径
/// </summary>
private string _sVersionLocalFilePath = "";/// <summary>
/// 同时下载的最大数量
/// </summary>
private int _nMaxDownloader = 5;/// <summary>
/// 当前需要下载的AB包数据
/// </summary>
List<ABPackInfo> _list_allNeedABPack = new List<ABPackInfo>();/// <summary>
/// 所需下载资源总大小
/// </summary>
private float _nDownloadTotalSize = 0;/// <summary>
/// 当前已下载资源的大小
/// </summary>
private float _nCurDownloadedSize = 0;/// <summary>
/// AB包下载器
/// </summary>
private List<ABDownloader> _list_allABDownloader = new List<ABDownloader>();/// <summary>
/// 客户端的AB版本数据
/// </summary>
private Dictionary<string, ABPackInfo> _dict_clientABInfoList = null;

我们在这里自定义了一系列变量,包括AB包版本信息,本地版本信息缓存路径,同时下载AB包的最大数量,当前需要下载的AB包数据,所需资源的总大小与已下载的总大小,AB包下载器与客户端的AB包版本信息。

    protected override void Awake(){string sPlatformStr = ABPackUtils.GetABPackPathPlatformStr();_sABVersionName = sPlatformStr + ABPackUtils.sABVersionName;_sVersionLocalFilePath = Application.persistentDataPath + _sABVersionName;IOUtils.CreateDirectroryOfFile(_sVersionLocalFilePath);}

Awake函数中,我们定义了AB包版本信息的构成方式:由平台名加上固定的版本信息拼接而成,获得平台名的函数如下所示,根据不同的平台(如Windows)来生成不同的平台名。然后我们将Unity的持久化数据目录与版本文件名拼接,生成版本文件的完整本地路径,最后根据文件路径自动创建所有必要的父目录(即使路径中部分目录不存在)。

    public static string GetABPackPathPlatformStr(){RuntimePlatform obj_platform = Application.platform;string sPlatformStr = "/AssetBundles/";if (obj_platform == RuntimePlatform.WindowsEditor || obj_platform == RuntimePlatform.WindowsPlayer){sPlatformStr += "StandaloneWindows/";}else if (obj_platform == RuntimePlatform.Android){sPlatformStr += "Android/";}else if (obj_platform == RuntimePlatform.IPhonePlayer){sPlatformStr += "iOS/";}return sPlatformStr;}
    /// <summary>/// 开始热更/// </summary>public void StartHotUpdate(){Debug.Log("开始热更 >>>>>> ");StartCoroutine(DownloadAllABPackVersion());}

没啥好说,开始热更,启动协程执行DownloadAllABPackVersion()函数。

/// <summary>
/// 解析版本文件,返回一个文件列表
/// </summary>
/// <param name="sContent"></param>
/// <returns></returns>
public Dictionary<string, ABPackInfo> ConvertToAllABPackDesc(string sContent)
{Dictionary<string, ABPackInfo> dict_allABPackDesc = new Dictionary<string, ABPackInfo>();string[] arrLines = sContent.Split('\n');//用回车 字符 \n 分割每一行foreach (string item in arrLines){string[] arrData = item.Split(' ');//用空格分割每行数据的三个类型if (arrData.Length == 3){ABPackInfo obj_ABPackData = new ABPackInfo();obj_ABPackData.sABName = arrData[0]; // 名称即路径obj_ABPackData.sMd5 = arrData[1]; // md5值obj_ABPackData.nSize = int.Parse(arrData[2]); // AB包大小//Debug.Log(string.Format("解析的路径:{0}\n解析的MD5:{1}\n解析的文件大小KB:{2}", obj_ABPackData.sABName, obj_ABPackData.sMd5, obj_ABPackData.nSize));dict_allABPackDesc.Add(obj_ABPackData.sABName, obj_ABPackData);}}return dict_allABPackDesc;
}

这一段主要是用来解析我们读取到的AB包版本文件,按换行符来份行,每一行分为名字,MD5值以及AB包的尺寸大小。我们的函数会把这些信息全部塞进字典中,字典的键是AB包的名称,而值则是AB包的所有信息。

    /// <summary>/// 获取服务端的AB包版本信息/// </summary>/// <returns></returns>IEnumerator DownloadAllABPackVersion(){string sVersionUrl = _sBaseUrl + @"/" + _sABVersionName;//Debug.Log("下载版本数据路径:" + sVersionUrl);using (UnityWebRequest uObj_versionWeb = UnityWebRequest.Get(sVersionUrl)){yield return uObj_versionWeb.SendWebRequest(); // 等待资源下载if (uObj_versionWeb.isNetworkError || uObj_versionWeb.isHttpError){Debug.LogError("获取版本AB包数据错误: " + uObj_versionWeb.error);yield break;}else{string sVersionData = uObj_versionWeb.downloadHandler.text;//Debug.Log("成功获取到版本相关数据 >>>> \n" + sVersionData);CheckNeedDownloadABPack(sVersionData);}}}

这个函数负责读取服务器的AB包版本信息,先用服务器的网址和AB包版本信息拼接出一个新的版本网址,然后我们调用http中的GET请求从服务器处获取数据,注意这里我们在一开始我们使用了一个using。

这是using的一个用法之一,我们下载过程中如果报错就break,否则我们将下载到的版本数据进行比对判断是否需要下载新的AB包。

/// <summary>
/// 检测需要下载
/// </summary>
/// <param name="sServerVersionData"></param>
void CheckNeedDownloadABPack(string sServerVersionData)
{//Debug.Log("运行平台:" + Application.platform);//Debug.Log("本地版本文件路径是:" + _sVersionLocalFilePath);Dictionary<string, ABPackInfo> dict_serverDownList = ConvertToAllABPackDesc(sServerVersionData); // 服务端获取的资源下载列表if (File.Exists(_sVersionLocalFilePath)){//Debug.Log("存在本地,对比服务器版本信息");string sClientVersionData = File.ReadAllText(_sVersionLocalFilePath); // 本地版本信息_dict_clientABInfoList = ConvertToAllABPackDesc(sClientVersionData); // 客户端本地缓存的资源下载列表//遍历服务器文件foreach (ABPackInfo obj_itemData in dict_serverDownList.Values){// 存在对应已下载文件,对比Md5值是否一致if (_dict_clientABInfoList.ContainsKey(obj_itemData.sABName)){// md5值不一致,则更新文件if (_dict_clientABInfoList[obj_itemData.sABName].sMd5 != obj_itemData.sMd5){_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;//Debug.Log("MD5 值不一样,资源存在变更,增加文件 >>>>> " + obj_itemData.sABName);}}else{_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;}}}else // 如果说不存在本地缓存,那就直接下载所有的AB包{foreach (ABPackInfo obj_itemData in dict_serverDownList.Values){_list_allNeedABPack.Add(obj_itemData);_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;//Debug.Log("所需下载文件 >>>>> " + obj_itemData.sABName);}}StartDownloadAllABPack();
}

检查AB包是否需要下载的代码非常冗长,我们首先把上一个函数中从服务器获得的AB包版本信息转换成字典,然后检查本地是否已经有AB包缓存地址,有的话说明我们已经从服务器处下载过AB包了,这时候我们就去比对本地的AB包的版本信息与服务器的AB包的版本信息,尤其是其中的MD5的值,如果发现不同说明我们的服务器的AB包有更改过,我们需要重新下载。对于本地缓存中不包含的AB包,我们需要下载,当然,如果我们连本地的缓存都没有,那当然需要下载所有的AB包。

/// <summary>
/// 开始下载所有所需下载的AB包资源
/// </summary>
/// <param name="list_allABPack"></param>
void StartDownloadAllABPack()
{int nMaxCount = _list_allNeedABPack.Count;if (nMaxCount <= 0) {HotUpdateEnd();return;}int nNeedCount = Mathf.Min(nMaxCount, _nMaxDownloader);for (int i = 0; i < nNeedCount; i++){ABPackInfo obj_ABPackDesc = _list_allNeedABPack[0];ABDownloader obj_downloader = new ABDownloader();_list_allABDownloader.Add(obj_downloader);StartCoroutine(obj_downloader.DownloadABPack(obj_ABPackDesc));_list_allNeedABPack.RemoveAt(0);}
}

下载AB包的代码并不复杂,我们有一个AB包数量的上限,在数量达到上限之前,我们实现了一个异步下载AB包的流程。

    /// <summary>/// 切换下载下一个AB包/// </summary>/// <param name="obj_ABDownloader">需要切换的下载器</param>public void ChangeDownloadNextABPack(ABDownloader obj_ABDownloader){//Debug.Log("切换下载下一个 AB 包");_nCurDownloadedSize += obj_ABDownloader.GetDownloadResSize();if (_list_allNeedABPack.Count > 0) // 还存在需要下载的资源,下载器切换资源,继续下载{StartCoroutine(obj_ABDownloader.DownloadABPack(_list_allNeedABPack[0]));_list_allNeedABPack.RemoveAt(0);}else{bool bIsDownloadSuc = true; // 资源是否全部下载完成foreach(ABDownloader obj_downloader in _list_allABDownloader){if(obj_downloader.bIsDownloading) // 存在一个下载中,即表示当前还有未下载完成的部分{bIsDownloadSuc = false;break;}}if (bIsDownloadSuc) // 已完成全部下载{HotUpdateEnd();}}}

这段代码负责更新是否还存在需要下载的资源,如果存在则启动下载的协程,否则更新资源下载情况,资源全部下载之后我们停止热更。

/// <summary>
/// 更新本地缓存的AB包版本数据
/// </summary>
/// <param name="obj_ABPackDecs"></param>
public void UpdateClientABInfo(ABPackInfo obj_ABPackDecs)
{if (_dict_clientABInfoList == null){_dict_clientABInfoList = new Dictionary<string, ABPackInfo>();}_dict_clientABInfoList[obj_ABPackDecs.sABName] = obj_ABPackDecs;StringBuilder obj_sb = new StringBuilder();foreach (ABPackInfo obj_temp in _dict_clientABInfoList.Values){obj_sb.AppendLine(ABPackUtils.GetABPackVersionStr(obj_temp.sABName, obj_temp.sMd5, obj_temp.nSize.ToString()));}IOUtils.CreatTextFile(_sVersionLocalFilePath, obj_sb.ToString());
}/// <summary>
/// 热更新结束,进入下一个阶段
/// </summary>
private void HotUpdateEnd()
{// TODO 进入下一个阶段Debug.Log("热更新: 已完成所有的AB包下载, 进入下一个阶段 TODO");HotUpdateTest.GetInstance().RunLua();HotUpdateTest.GetInstance().InitShow();
}v

更新本地缓存和执行下一个代码,都没啥可说的。

看似很长的代码,具体来说就干了几件事:获取服务器和客户端的AB包版本信息,发现不同后就开始下载,更新本地缓存。

热更新测试

热更新测试是用来干嘛的呢?其实就是用来检查我们的热更是否正常运行,也就是我们实现了热更这个过程吗。

using UnityEngine;public class HotUpdateTest : MonoSingletonBase<HotUpdateTest>
{void Start(){HotUpdateMgr.GetInstance().StartHotUpdate();}public void RunLua(){LuaInterpreter.GetInstance().RequireLua("HelloWorld");LuaInterpreter.GetInstance().RequireLua("Test");}public void InitShow(){GameObject obj_cube = AssetBundleMgr.GetInstance().LoadABPackRes<GameObject>("mode.ab", "Cube");Debug.Log("实例化 Cube");}
}

在start函数中直接调用热更新管理器的开始热更函数,之后的RunLua函数中则是从Lua解释器中声明需要的Lua脚本,最后还实例化了一个立方体。

关于热更新的逻辑在这里就差不多结束了,我们现在运行代码之后可以看到:

可以看到我们的整个热更新过程已经执行成功了,该有的打印信息也都有。

热更新原理

本身热更新这个过程并不难理解,难的是其中的原理,因为这里涉及到C#,Lua互相转换。

我们都知道C#是编译型语言而Lua是解释性语言,Lua的轻量化让他可以被替换后不再重新编译就生效,简单奔放的语法让他易于更改,但是世上没有免费的午餐。

我们先来聊聊C#和Lua脚本互相转换的过程吧:

C#调用Lua的过程中,C#会生成一个Bridge文件(中间层),这个文件可以去调用Lua的.dll文件(其实就是一些C的API)来使用Lua语言(Lua的虚拟机使用C/C++实现),而反过来Lua调用C#时会首先生成一个wrap文件,这个文件会把C#的各种字段方法注册到Lua的虚拟机中变成Lua可以识别的类型,这样Lua就可以在虚拟机中调用C#的方法了。

这里会涉及到一个经典的问题就是:为什么Lua调用C#这么慢?

大体上来说,C#调用Lua本质上就是去调用Lua的C接口,直接操作Lua的虚拟机,还有一部分开销来自于C#和Lua的类型转换;而Lua调用C#还涉及到一个C#的字段方法反射到Lua虚拟机的过程,而这个过程是非常耗时的。至于优化的方法,比如我们的XLua就可以为Lua生成可识别的C#类和方法,大体思路就是去减少反射的次数与减少数据传递的频率。

让我们来聊聊Unity,我们都知道我们通过C#控制Unity引擎,但其实Unity引擎本身是用C++写成的,我们的C#本身只是去调用Unity底层的各种C++代码,而如果我们想要实现热更的效果,那么我们就避免不了C#和Lua脚本的转换。

更新时,我们的C++写成的Unity底层代码并不能更改,我们更改的只是Lua脚本写成的这一部分代码,这些Lua脚本在C#脚本中通过某些手段(如XLua)能快速地实现转换。在真正的热更流程中,我们会在服务器发布新的Lua脚本,然后客户端会检查Lua脚本的版本信息(MD5值或shader64值)决定是否需要更新,需要的话就会下载新的Lua脚本替换原脚本。

有了AB包和Lua脚本,我们就可以大致上实现了热更的整体逻辑:针对简单的脚本交互逻辑,我们用lua脚本实现,针对场景内的资源,我们用AB包进行打包,二者都可以实现热更。 

相关文章:

Unity-Xlua热更和AssetBundle详解

从今天开始我们深入Unity的组件&#xff0c;最近的一系列事让我明白学习技术不能只有广度&#xff0c;如走马观花&#xff0c;虽然你可能确实学到了皮毛&#xff0c;但是没有足够深厚的理解&#xff0c;是无法融入自己的东西的&#xff0c;那么你就只是在用别人的工具而不是自己…...

【企业级数据安全】掌握高性能Log4j2敏感信息脱敏方案

前言 在数据安全合规日益严格的今天&#xff0c;日志中的敏感信息保护已成为企业IT建设的必备环节。本文带您深入了解如何打造一套高性能、可实时配置的Log4j2日志脱敏插件&#xff0c;轻松应对各类敏感数据保护需求&#xff0c;让您的系统既满足合规要求&#xff0c;又不牺牲…...

力扣刷题——606.根据二叉树创建字符串

给你二叉树的根节点 root &#xff0c;请你采用前序遍历的方式&#xff0c;将二叉树转化为一个由括号和整数组成的字符串&#xff0c;返回构造出的字符串。 空节点使用一对空括号对 "()" 表示&#xff0c;转化后需要省略所有不影响字符串与原始二叉树之间的一对一映…...

图像处理中的 Gaussina Blur 和 SIFT 算法

Gaussina Blur 高斯模糊 高斯模糊的数学定义 高斯模糊是通过 高斯核(Gaussian Kernel) 对图像进行卷积操作实现的. 二维高斯函数定义为 G ( x , y , σ ) 1 2 π σ 2 e − x 2 y 2 2 σ 2 G(x, y, \sigma) \frac{1}{2\pi \sigma^2} e^{-\frac{x^2 y^2}{2\sigma^2}} G(x…...

AWS区块链游戏场景技术解决方案:全球节点与去中心化架构实践

一、区块链游戏的技术挑战与架构需求 区块链游戏作为Web3领域的重要应用场景&#xff0c;其技术架构需要满足以下核心需求&#xff1a; 分布式账本的高效同步与共识验证 智能合约的安全执行环境 全球玩家的低延迟交互体验 动态扩展的节点网络支持 海量NFT资产的可靠存储 …...

AWS VPC深度解析:构建安全可靠的云网络基础设施

1. 引言 在云计算时代,网络基础设施的重要性不言而喻。Amazon Web Services (AWS) 的Virtual Private Cloud (VPC)为用户提供了一个强大而灵活的网络环境,使他们能够在AWS云中构建安全、可扩展的应用程序。本文将全面剖析AWS VPC的核心特性,帮助读者深入理解如何利用VPC构建高…...

青少年编程与数学 02-016 Python数据结构与算法 08课题、图

青少年编程与数学 02-016 Python数据结构与算法 08课题、图 一、图1. 图的基本概念1.1 定义1.2 顶点和边1.3 图的分类1.4 特殊术语 2. 图的表示方法1. 邻接矩阵&#xff08;Adjacency Matrix&#xff09;2. 邻接表&#xff08;Adjacency List&#xff09;3. 边列表&#xff08;…...

微信小程序:动态表格实现,表头单元格数据完全从data中获取,宽度自定义,自定义文本框,行勾选,样式效果,横向滚动表格(解决背景色不足的问题)等

一、样式效果 二、代码 1、wxml <view class"line flex flex-center"><view class"none" wx:if"{{info.length 0}}">暂无料号</view><view wx:else class"table-container"><!-- 动态生成表头 -->&…...

MySQL学习笔记集--游标

游标 在MySQL中&#xff0c;游标&#xff08;Cursor&#xff09;是一种数据库对象&#xff0c;它允许您逐行处理查询结果集。游标通常与存储过程一起使用&#xff0c;因为它们需要在存储过程或函数中声明和操作。游标的使用涉及几个步骤&#xff1a;声明游标、打开游标、从游标…...

Microsoft Defender Antivirus Service服务占用CPU过高

下载火绒安全&#xff0c;用它替代 Microsoft Defender&#xff0c;并关闭 Microsoft Defender 两步禁用Windows Defender Antivirus Service_microsoft defender antivirus service-CSDN博客 Windows10/11家庭版 关闭方法 按 ‘Win键R’&#xff0c;输入 “regedit”&#…...

Ansible(7)——管理机密与事实

目录 一、管理机密&#xff1a; 1、Ansible Vault &#xff1a; 2、ansible-vault 命令行工具&#xff1a; &#xff08;1&#xff09;创建加密文件&#xff1a; &#xff08;2&#xff09;查看加密文件&#xff1a; &#xff08;3&#xff09;编辑现有加密文件&#xf…...

consul服务注册与发现(go)-学习笔记

参考博客 1、服务实例接口与默认实现 type ServiceInstance interface {// 获取服务实例的唯一IDGetInstanceId() string// 获取服务IDGetServiceId() string// 获取服务实例的主机名或IP地址GetHost() string// 获取服务实例的端口号GetPort() int// 判断服务实例是否使用HT…...

golang-defer延迟机制

defer延迟机制 defer是什么 defer是go中一种延迟调用机制。 执行时机 defer后面的函数只有在当前函数执行完毕后才能执行。 执行顺序 将延迟的语句按defer的逆序进行执行&#xff0c;也就是说先被defer的语句最后被执行&#xff0c;最后被defer的语句&#xff0c;最先被执…...

字符串哈希算法详解:原理、实现与应用

字符串哈希是一种高效处理字符串匹配和比较的技术&#xff0c;它通过将字符串映射为一个唯一的数值&#xff08;哈希值&#xff09;&#xff0c;从而在O(1)时间内完成子串的比较。本文将结合代码实现&#xff0c;详细讲解前缀哈希法的工作原理&#xff0c;并通过流程图逐步解析…...

python-Leetcode 65.搜索旋转排序数组

题目&#xff1a; 整数数组nums按升序排列&#xff0c;数组中的值互不相同 在传递给函数之前&#xff0c;nums在预先未知的某个小标K上进行了旋转&#xff0c;使数组变为[nums[k], nums[k1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]&#xff0c;小标从0开始计数。…...

蓝桥杯 C/C++ 组历届真题合集速刷(二)

一、0ASC - 蓝桥云课 &#xff08;单位换算&#xff09;算法代码&#xff1a; #include <iostream> using namespace std; int main() {printf("%d",L);return 0; } 二、0时间显示 - 蓝桥云课 &#xff08;单位换算&#xff09;算法代码&#xff1a; #inclu…...

react的redux总结

目录 一、Antd 1.1、基本使用 1.2、自定义主题 二、Redux 2.1、工作流程 2.2、理解react-redux 2.3、优化 2.3.1、简写mapDispatch 2.3.2、Provider组件 2.4、数据共享 2.4.1、编写Person组件 2.4.2、Person组件的reducer 2.4.3、完成数据共享 2.5、求和案例 2.…...

MySQL视图

一、视图的本质与分类 1. 定义 虚拟表&#xff1a;视图不存储数据&#xff0c;本质是保存的查询语句&#xff08;SELECT&#xff09;&#xff0c;每次访问视图时动态执行查询并返回结果。 逻辑抽象&#xff1a;基于一个或多个基表&#xff08;或视图&#xff09;创建&#xf…...

程序化广告行业(69/89):电商素材制作与展示策略解析

程序化广告行业&#xff08;69/89&#xff09;&#xff1a;电商素材制作与展示策略解析 在如今数字化营销的浪潮中&#xff0c;程序化广告成为众多企业精准触达目标客户的有力武器。作为一名在广告技术领域摸爬滚打多年的从业者&#xff0c;深知学习是不断进步的阶梯&#xff…...

【PCB工艺】发光二极管的原理

你真的知道发光二极管为什么会发光吗&#xff1f; 而为什么另一部分二极管不会发光呢&#xff1f; 这篇文章解释元器件发光二极管&#xff08;LED&#xff09;的底层原理。 发光二极管&#xff08;LED, Light Emitting Diode&#xff09; 是一种能够将电能转换为光能的半导体…...

探秘 DeepSeek:开源生态如何推动 AI 技术普惠?

探秘 DeepSeek:开源生态如何推动 AI 技术普惠? 引言 在人工智能(AI)领域,技术的快速发展和广泛应用正在深刻改变我们的生活。然而,AI 的发展往往伴随着资源和技术的集中化问题,大型科技公司凭借其雄厚的资金和人才优势占据了主导地位,而中小企业、研究机构和个人开发…...

远程主机可能不符合glibc和libstdc++ VS Code服务器的先决条件

这是因为我最近更新了vscode&#xff0c; 服务器中有个GLIBC库&#xff0c;VSCode>1.86.0版本对 低于v2.28.0版本的GLIBC不再满足需求。 解决办法 回退到之前能够连接服务器的版本。我之前用的是January 2025 (version 1.97) vscode旧版本下载地址...

JVM性能调优:参数配置×内存诊断×GC调优实战

&#x1f680;前言 “你的Java应用是否还在经历莫名卡顿&#xff1f;半夜被OOM报警惊醒&#xff1f;GC日志像天书看不懂&#xff1f; 本文将用20个真实案例50个关键参数&#xff0c;带你掌握&#xff1a; 参数调优&#xff1a;如何用-XX:UseG1GC让GC暂停从秒级降到毫秒级&…...

pg_waldump 使用方法和输出验证

目录 pg_waldump 使用方法和输出验证一、pg_waldump 基础用法二、验证输出文件正确性三、关键参数 -p 的作用四、验证示例五、注意事项 pg_waldump 使用方法和输出验证 一、pg_waldump 基础用法 命令格式 pg_waldump [选项] [WAL文件路径]-p, --pgdataDIR&#xff1a;指定 Pos…...

Android 定制飞行模式和通话中设置菜单置灰

业务背景 定制需求实现 目标&#xff1a;通话中禁用移动网络设置中的网络模式和APN入口。 Google原生行为分析 在原生Android中&#xff1a; 飞行模式&#xff1a; 无法在通话中开启&#xff1a;系统会自动阻止&#xff0c;因飞行模式会断开通话所需的射频。APN/网络模式修改…...

C# System.Text.Json 中 ReferenceHandling 使用详解

总目录 一、什么是 ReferenceHandling&#xff1f; 1. 概述 ReferenceHandling 是 System.Text.Json 中用于处理对象引用&#xff08;循环引用或重复引用&#xff09;的选项。它允许开发者在序列化和反序列化时控制如何处理对象之间的引用关系。 默认情况下&#xff0c;Syst…...

【开发经验】调试OpenBMC Redfish EventService功能

EventService功能是Redfish规范中定义的一种事件日志的发送方式。用户可以设置订阅者信息(通常是一个web服务器)&#xff0c;当产生事件日志时&#xff0c;OpenBMC可以根据用户设置的订阅者信息与对日志的筛选设置&#xff0c;将事件日志发送到订阅者。 相比于传统的SNMPTrap日…...

【AI工具】FastGPT:开启高效智能问答新征程

前言 在人工智能飞速发展的当下&#xff0c;各类 AI 工具如雨后春笋般涌现。FastGPT 作为一款基于大语言模型&#xff08;LLM&#xff09;的知识图谱问答系统&#xff0c;凭借其强大的数据处理和模型调校能力&#xff0c;为用户带来了便捷的使用体验。今天&#xff0c;就让我们…...

4.8学习总结 贪心算法+Stream流

贪心算法&#xff1a; 找到局部最优->从而推导全局最优。 Java练习&#xff1a; 获取随机验证码&#xff1a; import java.util.*; import java.util.function.BiConsumer; public class test {public static void main(String[] args) {System.out.println(createCode(…...

入选ICLR‘25 Spotlight!深度强化学习(DRL)迎来新突破!

近年来&#xff0c;深度强化学习相关的成果在顶会顶刊上接受度普遍较高&#xff0c;经常上榜ICLR、Nature、Science等。比如ICLR 2025上的一篇Spotlight&#xff0c;由清华团队提出&#xff0c;介绍了一种SmODE网路&#xff0c;让深度强化学习的控制更加丝滑&#xff01; 另外…...

【学习笔记】HTTP和HTTPS的核心区别及工作原理

一、基础概念 HTTP&#xff08;超文本传输协议&#xff09;&#xff1a;明文传输数据&#xff0c;默认端口80&#xff0c;容易被窃听或篡改。 HTTPS&#xff08;HTTP SSL/TLS&#xff09;&#xff1a;通过加密传输数据&#xff0c;默认端口443&#xff0c;保障安全性。 二、…...

gbase8s之数据字典导出脚本(完美)

有时我们需要将表结构转换成数据库设计文档&#xff08;WORD或者其他格式&#xff09;&#xff0c;这时需要使用脚本将表结构导出&#xff0c;转换成可用格式。 该脚本适用于GBase 8s小版本号在3.0之后的版本&#xff08;含有syscolumnsext、syscomments以及syscolcomments表&a…...

java整合socket通信全流程

前言 大家好,由于工作上业务的需要,在java项目中引入了socket通信,特此记录一下,用以备份,本文章中的socket通信实现了,服务端与客户端的双向通讯,以及二者之间的心跳通信,服务端重启之后,客户端的自动重连功能。 原理 Socket通信是计算机网络中常用的一种通信机制…...

【scikit-learn基础】--『预处理』之 正则化

数据的预处理是数据分析&#xff0c;或者机器学习训练前的重要步骤。 通过数据预处理&#xff0c;可以 提高数据质量&#xff0c;处理数据的缺失值、异常值和重复值等问题&#xff0c;增加数据的准确性和可靠性整合不同数据&#xff0c;数据的来源和结构可能多种多样&#xff…...

WHAT - React 使用 Hook 分离计算逻辑与渲染逻辑

目录 原始代码如何优化1. 函数式简洁风格2. hook 封装&#xff08;重点&#xff09;3. 性能优化 原始代码 const GoodList ({ goods }) > {if (goods.length 0) {return <>暂无数据</>;}let totalCount 0;let totalPrice 0;goods.forEach((good) > {tot…...

AI比人脑更强,因为被植入思维模型【49】冰山理论思维模型

giszz的理解&#xff1a;冰山一角&#xff0c;冰山理论并不深奥&#xff0c;就是这个意思。对我启发比较大的&#xff0c;就是人的一个行为&#xff0c;背后可能藏着行为、应对方式、感受、观点、期待、渴望、自我七个层次。更有一个扩展&#xff0c;就是每个人的自我&#xff…...

【Linux】Git的简单使用

&#x1f4dd;前言&#xff1a; 这篇文章我们来讲讲版本控制器Git&#xff0c;主要掌握一些简单的本地仓库与远端仓库之间的文件传输操作。 &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;Linux &#x1f380;CSDN主页 愚润求学 &#x1f30…...

【WebRTC】开源项目Webrtc-streamer介绍

WebRTC-Streamer 这是一个用于通过简单的信令机制&#xff08;参见 api&#xff09;流式传输 WebRTC 媒体源的实验项目&#xff0c;支持以下媒体源&#xff1a; 捕获设备 屏幕捕获 mkv 文件 RMTP/RTSP 源 同时该项目也兼容 WHEP 接口。 注意 * 在线演示已停止&#xff0c…...

Bigemap pro制作行政区域图

Bigemap pro制作行政区域图 第一步&#xff1a;打开bigemap pro软件&#xff0c;右上角加载更多矢量到地图上&#xff0c;加载出来需要的矢量数据&#xff0c;以北京市为例&#xff0c;如图所示&#xff1a; 第二步&#xff1a;在我的矢量图层&#xff0c;点击右键&#xff0c…...

Kotlin 和 spring-cloud-function 兼容问题

错误&#xff1a; [ERROR] Failed to execute goal org.jetbrains.kotlin:kotlin-maven-plugin:1.9.25:compile (compile) on project springdoc-openapi-starter-common: Compilation failure [ERROR] /opt/repository/org/springframework/cloud/spring-cloud-function-conte…...

OpenVINO是什么

OpenVINO&#xff08;Open Visual Inference and Neural Network Optimization&#xff09;是由英特尔&#xff08;Intel&#xff09;开发的一个开源工具套件&#xff0c;用于优化和加速深度学习模型的推理过程&#xff0c;特别是在计算机视觉、自然语言处理和生成式 AI 等领域…...

【学Rust写CAD】38 over_in 函数(alpha256补充方法)

源码 #[inline] // 内联优化标记 pub fn over_in(self, src: Argb, dst: Argb) -> Argb {// 计算目标alpha因子 self * src的alpha通道let dst_alpha self * src.alpha_t();// 预乘源和目标的颜色分量let src_rb src.rb() * self.0; // 源的红蓝分量乘以alpha因子let …...

球类(继承和多态)

父类Ball&#xff0c;设置为抽象类&#xff0c;调用get和set方法创建对象&#xff0c;将子类重写的功能函数抽象化。 // 抽象球类 abstract class Ball {private String name;private double radius; // 半径private double weight; // 重量private double price; // 价格// 构…...

苍穹外卖(1)-部分环境配置(git、数据库)

首先配置git 创建好本地仓库之后 把项目弄到远程仓库里去 先进行提交 &#xff0c;后进行推送 &#xff0c;然后gitee创建一个仓库 把这个url复制好 推送后会出来一个 点击推送&#xff0c;会让你输入gitee账号密码&#xff0c;输入自己的账号密码&#xff0c;就可以连接远程仓…...

避免误用strncmp与memcmp,strcpy与memcpy

(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 注&#xff1a;使用说明部分参考豆包ai 1. 字符串与二进制流认知 许多时候&#xff0c;我们作为软件研发人员&#xff0c;会觉得 一段内存就是一串字符串&#xff1b;字符串就是一段内存&#xff1b; 概念上&#xff…...

华为欧拉系统安装docker

华为欧拉系统安装docker cat /etc/openEuler-release sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo vi /etc/yum.repos.d/docker-ce.repo dnf makecache dnf install https://download.docker.com/linux/centos…...

windows11怎么把notepad++添加到鼠标右键菜单?

在Windows 11中将Notepad添加到鼠标右键菜单&#xff0c;可通过以下两种方法实现&#xff1a; ​​方法一&#xff1a;手动修改注册表&#xff08;推荐&#xff09;​​ ​​打开注册表编辑器​​ 按下 Win R&#xff0c;输入 regedit 并回车 1 2 3 。 ​​定位注册表路径​​…...

HTML5笔记: 什么是HTML

HTML的全称为超文本标记语言&#xff0c;是一种标记语言。它包括一系列标签&#xff0c;通过这些标签可以将网络上的文档格式统一&#xff0c;使分散的Internet资源连接为一个逻辑整体。HTML文本是由HTML命令组成的描述性文本&#xff0c;HTML命令可以说明文字&#xff0c;图形…...

【WRF理论第十五期】WPS中输入geogrid二进制格式

WPS中输入geogrid二进制格式 基本概念&#xff1a;Geogrid二进制格式支持的数据类型 geotiff→tiff的规则说明类型1&#xff1a;主导类别字段&#xff08;Dominant Category Field&#xff09;类型2&#xff1a;连续字段&#xff08;Continuous Field&#xff09;类型3&#xf…...

《UNIX网络编程卷1:套接字联网API》第8章:基本UDP套接字编程深度解析

《UNIX网络编程卷1&#xff1a;套接字联网API》第8章&#xff1a;基本UDP套接字编程深度解析&#xff08;8000字图文实战&#xff09; 一、UDP协议核心特性与编程模型 1.1 UDP协议设计哲学 UDP&#xff08;User Datagram Protocol&#xff09; 是面向无连接的传输层协议&…...