Unity热更新
1,热更新的概念与作用
app更新通常分为两类,一种是整包更新(换包),一种是热更新(不换包,通过网络下载,动态更新资源等)。
- 整包更新,是指在需要更新时,需要用户手动到应用商店或官方网站下载新版本安装包并重新安装的一种更新方式,该方式成本较高,一般只有在无法热更新时才使用。
- 热更新,是指在不需要重新编译发布应用程序的情况下,通过远程更新服务器向客户端推送程序代码、资源文件等数据的一种技术手段,以修复程序漏洞、优化游戏性能、更新游戏内容等。
热更新又分资源热更新和代码热更新,资源热更新较为简单,一般的app都可实现,而代码热更新,由于考虑到安全性,代码编译等问题,实现起来较为困难,一种实用的方法就是把代码当成资源。Unity热更新就是把代码(如Lua代码)打包成AssetBundle,达到和其它资源一样的更新效果。
在当今快节奏时代,app更新频繁,尤其是游戏app,如果每次更新都需要换包,十分影响用户体验,极易造成用户流失,代价成本实在太高,因此app热更新是十分必要的。
如果有了热更新,会带来什么好处呢?
- 提高用户体验。热更新可以实现及时修复bug和添加新功能等,减少了玩家等待更新的时间和下载流量,提高了用户的体验感。
- 降低开发成本。热更新可以在不重新打包的情况下实现游戏的更新,避免了频繁发布新版本的成本和风险。
- 提高迭代效率。热更新可以快速地进行游戏内容的调整和修改,加快了游戏迭代周期,提高了开发效率。
2,热更新原理
热更新,是通过把最新的资源或代码放到网络服务器,app检测到需要更新版本时,通过网下载资源或代码到本地包,将新的代码或资源加载到应用程序中,以替换旧的代码或资源。
Unity以C#为主要开发语言,如何能做到代码的热更新呢?
C#是编译型语言,Unity在打包后,会将C#编译成一种中间代码IL,后续对这些IL的编译方式不同可以分为AOT和JIT,最终编译为各平台的NativeCode,在没有特殊处理的情况下,无法直接通过替换NativeCode,来达成热更新的。
一种理想化的C#热更新流程是:
- 把需要更新的代码编译成动态链接库
- 游戏启动时加载新的动态链接库
- 用反射的形式获取动态链接库中的实例或方法
这种模式在PC和Android平台是可以的,但在IOS平台是不可行的。因为IOS对申请的内存禁止了可执行权限,所以运行时创建/加载的NativeCode是无法执行的。
为了解决IOS上的热更新问题,有两个主流方案:ILRuntime 和 HybridCLR。
ILRuntime
Unity会把C#代码打包成DLL,ILRuntime在运行时用自己的解释器来解释IL并执行,而不是直接调用.NET FrameWork或Mono虚拟机来运行代码。它借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。
但是ILRuntime会有不少限制
- ILRuntime和原始的 compiler是两套东西,也就是说你的热更DLL和主工程的DLL实质是不互通的(如热更DLL中一个类要继承主工程DLL的一个类),所以就存在跨域问题,需要写委托适配器,委托转换器。在发布版本后这些不能热更,使用之前一定要预留好可能会使用的
- 部分 C# 语法不支持:由于 ILRuntime 是基于 Mono 实现的,而 Mono 不支持所有 C# 语法,所以 ILRuntime 在某些 C# 语法方面也有限制,比如属性、泛型委托、可选参数等
- 需要特殊处理的代码:由于 ILRuntime 的实现方式,一些特殊的代码需要进行特殊处理,比如反射、LINQ、协程等
- 性能问题:由于 ILRuntime 需要动态解析和执行代码,相对于编译时静态绑定的方式,其性能会有一定程度的下降。同时,在使用过程中也需要注意避免频繁的跨域调用和反射操作,以免影响性能
- ILRuntime对多线程Thread不兼容,在热更代码里使用多线程会导致Unity崩溃闪退
HybridCLR
是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
IL2CPP是一个纯静态的AOT运行时,不支持运行时加载dll,因此不支持热更新。HybridCLR扩充了IL2CPP的代码,使其由纯AOT Runtime变成“AOT+Interpreter”混合Runtime,进而原生支持动态加载Assembly,使得基于IL2CPP打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行。
HybridCLR是近年来一种划时代的Unity原生C#热更新技术,见https://hybridclr.doc.code-philosophy.com/
相比于直接热更新C#代码,使用C#+Lua脚本的热更新方案是目前最主流的实现方式。
Lua是一种跨平台的脚本语言,它主要依赖解释器和虚拟机实现跨平台功能,Lua是解释型语言,并不需要事先编译,而是运行时动态解释执行的。这样Lua就和普通的游戏资源如图片,文本没有区别。由于解释器和虚拟机都是跨平台的,lua脚本也就可以在不同的平台上运行了。
本质上就是利用相关插件(如ulua、slua、tolua、xlua等)提供一个Lua的运行环境(虚拟机),为Unity提供Lua编程的能力,让C#和Lua可以相互调用和访问。
3,xLua热更新方案
xLua是腾讯一个开源项目,xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。
xLua在功能、性能、易用性都有不少突破,这几方面分别最具代表性的是:
- 可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;
- 编辑器下无需生成代码,开发更轻量;
- 出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;
下载地址:https://github.com/Tencent/xLua
4,xLua的简单使用
4.1,xLua安装使用
xLua下载后,将xLua文件中的Assets文件夹下的文件放到项目中的Assets文件下,就完成了XLua的安装。
新建C#代码LuaManager.cs
using UnityEngine;
using XLua;public class LuaManager : MonoBehaviour
{LuaEnv m_luaEnv;void Start(){m_luaEnv = new LuaEnv();m_luaEnv.DoString("print('Hello World')");}
}
新建场景,挂上LuaManager.cs,运行,看到打印 Hello World ,则安装成功了
4.2,自定义Lua加载器
要想执行lua文件,就要用上Lua加载器了,修改LuaManager.cs
using System;
using System.IO;
using UnityEngine;
using XLua;public class LuaManager : MonoBehaviour
{public static string LuaDir = "src"; // 存放lua文件的位置,Assets根目录下LuaEnv m_luaEnv;Action m_startAction;Action m_updateAction;void Start(){m_luaEnv = new LuaEnv();m_luaEnv.AddLoader(new LuaEnv.CustomLoader(this.LuaLoaderFromRes));// 请求执行src下的Main.lua文件m_luaEnv.DoString("require('Main')", "chunk");LuaTable luaTable = this.m_luaEnv.Global.Get<LuaTable>("Main");if (luaTable != null){m_startAction = luaTable.Get<Action>("Start");m_updateAction = luaTable.Get<Action>("Update");}// 执行Main.lua Start方法m_startAction?.Invoke();}void Update(){// 执行Main.lua Update方法m_updateAction?.Invoke();}private byte[] LuaLoaderFromRes(ref string filePath){filePath = filePath.Replace('.', '/');if (!filePath.EndsWith(".lua")){filePath += ".lua";}#if UNITY_EDITORstring path = Application.dataPath + "/" + LuaDir + "/" + filePath;if (File.Exists(path)){//读取路径下的文件的值以字节形式返回return File.ReadAllBytes(path);}
#endif// TODO// android ios 等平台读取lua文件return null;}
}
在Assets目录下新建文件夹src,src文件夹下新建文件Main.lua
Main = {}
setmetatable(Main, {__index = _G})
local _ENV = Mainfunction Start()print("Lua Start")
endfunction Update()-- TODO
endreturn Main
运行,看到打印 Lua Start ,表示成功,Update()可增加每帧的逻辑,可在src下继续增加其它lua文件
4.3,Lua调用C#
[LuaCallCSharp],在C#类加上标签[LuaCallCSharp],就可在Lua中访问了
新建C#代码GameTest.cs
using UnityEngine;
using XLua;namespace MyGame
{[LuaCallCSharp] // 建立Lua调用C#的映射public class GameTest : MonoBehaviour{public string Name;void Start(){Debug.Log("Name:" + Name);}public void CallTest(string text){Debug.Log("Lua Call:" + text);}}
}
修改Main.lua
Main = {}
setmetatable(Main, {__index = _G})
local _ENV = Mainfunction Start()print("Lua Start")-- 访问C#的类,使用CS + 命名空间 + 类名local go = CS.UnityEngine.GameObject("LuaGameObject")local test = go:AddComponent(typeof(CS.MyGame.GameTest))test.Name = "Game Test"-- 调用方法,使用:test:CallTest("666")
endfunction Update()-- TODO
endreturn Main
如果不想在每个类中加标签[LuaCallCSharp],也可以参考XLua/Editor/ExampleConfig,集中配置。
注意,如果需要打包,需提前生成Wrap文件,执行菜单命令:XLua/Generate Code
至于C#调用Lua,4.2代码已有了,更详细的参考官方例子
推荐一个基于xLua的Unity游戏纯lua客户端完整框架:https://github.com/smilehao/xlua-framework
5,xLua可热更规则:
- 进入lua层后的一切逻辑、资源都可热更
- app中基本所有的资源(图片、声音、3d模型、动作、特效、文本文件)、lua代码都可热更
- C#层代码不可热更(也不完全不能,xlua.hotfix可以修改C#代码的执行,替换原来的逻辑,但这是lua代码,不是直接修改C#)
- 需要新增或修改的代码必须是C#代码则不可热更
6,热更新流程
6.1,更新前准备
- 打包AssetBundle,打包程序会比较Unity所有资源,与上次打包后对比实现增量打包,生成md5信息文件(assetbundlemd5.txt),版本信息文件(version.txt),递增资源版本号
- 上传AssetBundle,assetbundlemd5.txt,version.txt到网络服务器(cdn)
- 停服或后台通知用户在线更新
6.2,更新流程
- 启动app,下载版本信息文件version.txt
- 版本号的比较,如果版本号不同才继续以下流程
- 下载资源服务器上的md5对比文件(assetbundlemd5.txt)
- 确定下载列表,将最新下载的md5对比文件和本地旧md5对比文件对比,记录缺少或不同的文件。(assetbundlemd5.txt中的md5码实现此步骤)
- 根据下载列表,下载所需的资源。(一般放在Application.persistentDataPath)
- 保证下载成功后,用最新的md5对比文件覆盖本地的md5对比文件(更新assetbundlemd5.txt),记录最新的版本号
7,Unity热更新实现
版本信息文件version.txt
{"code":0,"data":{"isUpdateClient":0,"isUpdateRes":1,"version":"1.0","resVersion":"1.0.0.1","clientUrl":"https://aa.bb.cc.com/game/client.apk","resUrl":"https://aa.bb.cc.com/game/res/"}
}
这是version.txt的结构参考,JSON格式,字段说明:
- isUpdateClient:是否强制更新整包,视情况是否打开
- isUpdateRes:是否更新资源开头,无特殊情况都是打开
- version:客户端版本号,如果此版本号对比不一样,需考虑更新整包
- resVersion:资源版本号,如果此版本号对比不一样,则进行热更新,每次打包递增
- clientUrl:客户端整包的更新地址,可根据后缀,跳转网站,或直接下载安装
- resUrl:热更新资源的地址
App启动,下载version.txt,版本比较代码
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;public class GameStart : MonoBehaviour
{public string VersionUrl = "https://aa.bb.cc.com/game/version.txt"; // version.txt网络服务器地址public string appVersion = "1.0"; // 当前客户端版本号public string currentResVersion = "1.0.0.1"; // 当前最新资源版本号void Start(){// app 启动前逻辑,如读取客户端版本号,最新资源版本号//currentResVersion = PlayerPrefs.GetString("currentResVersion");StartCoroutine(RequestVersionInfo());}IEnumerator RequestVersionInfo(){// 加上时间戳,确保下载的是最新文件UnityWebRequest request = new UnityWebRequest(VersionUrl + "?time=" + System.DateTime.Now.Ticks);request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return request.SendWebRequest();if (request.error == null){string text = request.downloadHandler.text;LitJson.JsonData versionInfo = LitJson.JsonMapper.ToObject(text);int isUpdateClient = (int)versionInfo["data"]["isUpdateClient"];int isUpdateRes = (int)versionInfo["data"]["isUpdateRes"];string version = (string)versionInfo["data"]["version"];string clientUrl = (string)versionInfo["data"]["clientUrl"];string resUrl = (string)versionInfo["data"]["resUrl"];string resVersion = (string)versionInfo["data"]["resVersion"];if (isUpdateClient == 1){if (compareResVersion(version, appVersion)){// 提示客户端更新// Application.OpenURL(clientUrl);}}if (isUpdateRes == 1){if (compareResVersion(resVersion, currentResVersion)){// 进入热更新;//StartHotUpdate(resUrl);}}}request.Dispose();}public bool compareResVersion(string resVersion1, string resVersion2){var arr1 = resVersion1.Split('.');var arr2 = resVersion2.Split('.');for (int i = 0; i < arr1.Length; i++){if (int.Parse(arr1[i]) > int.Parse(arr2[i])){return true;}}return false;}}
热更新流程代码
IEnumerator StartHotUpdate(string resUrl)
{bool downloadFailed = false;// 下载网络服务器最新md5信息文件string md5Url = resUrl + "assetbundlemd5.txt";UnityWebRequest md5Request = new UnityWebRequest(md5Url + "?version=" + currentResVersion); // 加上版本号,确保下载的是最新文件md5Request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return md5Request.SendWebRequest();if (md5Request.error == null){AssetBundleMD5Infos remoteMd5_info = new AssetBundleMD5Infos(md5Request.downloadHandler.data); // 网络服务器最新md5信息文件AssetBundleMD5Infos tmpMd5_info; // 由于出错中断暂时保存的md5信息文件string dirPath = Application.persistentDataPath + "/" + Utility.GetPlatformName();if (!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}dirPath = dirPath + "/";if (File.Exists(dirPath + "assetbundlemd5.tmp")){byte[] fileContent = File.ReadAllBytes(dirPath + "assetbundlemd5.tmp");tmpMd5_info = new AssetBundleMD5Infos(fileContent);}else{tmpMd5_info = new AssetBundleMD5Infos(null);}List<string> needUpdateAbs = new List<string>(); // 需要下载更新的ab文件列表foreach (var abName in remoteMd5_info.m_AssetBundleMD5.Keys){string remoteMd5 = remoteMd5_info.GetAssetBundleMD5(abName);// 与网络服务器最新md5比较,不同则加载下载更新列表,AssetBundleManager.GetAssetBundleMD5(abName)本地最新md5if (tmpMd5_info.GetAssetBundleMD5(abName) != remoteMd5 && remoteMd5 != AssetBundleManager.GetAssetBundleMD5(abName)){needUpdateAbs.Add(abName);}}// 下载更新的ab文件foreach (string abName in needUpdateAbs){UnityWebRequest abRequest = new UnityWebRequest(resUrl + abName + "?version=" + currentResVersion); // 加上版本号,确保下载的是最新文件abRequest.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return abRequest.SendWebRequest();if (abRequest.error == null){// 保存到最新的ab文件到本地File.WriteAllBytes(dirPath + abName, abRequest.downloadHandler.data);tmpMd5_info.AddAssetBundleMD5(abName, remoteMd5_info.GetAssetBundleMD5(abName), remoteMd5_info.GetAssetBundleSize(abName), remoteMd5_info.GetAssetBundleMiniGameId(abName));} else{downloadFailed = true;}abRequest.Dispose();}if (needUpdateAbs.Count > 0){if (!downloadFailed){// 保存最新的md5文件remoteMd5_info.SerializeToFile(dirPath + "assetbundlemd5.txt");File.Delete(dirPath + "assetbundlemd5.tmp");}else{tmpMd5_info.SerializeToFile(dirPath + "assetbundlemd5.tmp"); // 出错中断保存临时的md5,避免下次更新重新下载}}}else{downloadFailed = true;}md5Request.Dispose();if (downloadFailed){// 出错重新执行更新流程StartCoroutine(StartHotUpdate(resUrl));}
}
相关文章:
Unity热更新
1,热更新的概念与作用 app更新通常分为两类,一种是整包更新(换包),一种是热更新(不换包,通过网络下载,动态更新资源等)。 整包更新,是指在需要更新时&#x…...
如何用维格云搭建和一键训练你的钧瓷AI机器人?
大禹智库 第69期(总第400期) 2023年11月4日 如何用维格云搭建和一键训练你的钧瓷AI机器人? 钧瓷私有数据聊天机器人是一种能够根据预设的数据集进行智能对话的机器人。通过维格云,我们可以轻松地搭建自己的钧瓷私有数据聊天机器人。本文将以钧道机器人为例,详细介绍如何…...
整理的一些Java细节问题
1. 为什么要有无参构造? 在 Java 中,如果一个类没有显式定义构造方法,编译器会自动生成一个默认的无参构造方法(也称为默认构造方法)。无参构造方法是一个没有任何参数的构造方法。 无参构造方法的存在有几个重要原因…...
初识AUTOSAR网络管理
文章目录 目的模式时间参数T_REPEAT_MESSAGET_NM_TIMEOUTT_WAIT_BUS_SLEEPT_START_Tx_AppFrameT_NM_ImmediateCycleTimeT_NM_MessageCycleN_ImmediateNM_TIMEST_START_NM_TXT_WakeUp跳转状态NM_1NM_2NM_3NM_4NM_5NM_6NM_7...
Flink SQL Hive Connector使用场景
目录 1.介绍 2.使用 2.1注册HiveCatalog 2.2Hive Read 2.2.1流读关键配置 2.2.2示例...
【Docker】联合探讨Docker:容器化技术的革命性应用
前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 📕作者简介:热…...
dirhunt使用手册,中文版
“dirhunt” 的命令行工具的帮助信息,用于目录扫描和网站内容分析。以下是这个命令的使用方法和示例: 命令格式: dirhunt [OPTIONS] [URLS]… [URLS]…:一个或多个域名或 URL,可以加载来自文件的 URL,使用…...
【从0到1设计一个网关】如何设计一个稳定的网关?
文章目录 高可用分析软件架构心跳检测自动恢复熔断降级接口重试隔离压测和预案多机房灾备以及双活数据中心异常处理机制重试主备服务自动切换动态剔除或恢复异常机器超时时间的考虑服务设计这篇文章并没有具体的业务实现,而只是对于如何设计一个高可用,稳定的网关列举出了一些…...
chromedp库编写程序
步骤1:首先,我们需要导入chromedp库,以便使用它来下载网页内容。 import chromedp 步骤2:然后,我们需要创建一个函数,该函数接受一个URL作为参数,并使用chromedp库下载该URL的内容。 func do…...
pngquant failed to build, make sure that libpng-dev is installed 问题
第一个参考方案失败 :npm install -g windows-build-tools4.0.0 安装失败,提示 依赖不在支持 第二个方案,降低node 版本 失败 第三种方案,成功 先执行,下面两行代码,再按照依赖 npm install imagemin-pn…...
进程控制(二):进程等待
文章目录 进程控制(二)进程等待wait函数waitpid函数wait/waitpid获取子进程状态码的过程进程等待相关的宏 总结 进程控制(二) 延续对于上文进程结束,我们继续对于进程控制进行学习,本文我们主要是对于进程…...
SWAT-MODFLOW地表水与地下水耦合模型的建模及应用
目录 第一讲 模型原理与层次结构 第二讲 QGIS软件 第三讲 基于QSWATMOD的SWAT-MODFLOW模拟 第四讲 QSWAT模型介绍与建模 第五讲 基于QGIS的数据制备 第六讲 基于CUP的SWAT参数率定 第七讲 MODFLOW模型讲解 第八讲 结果分析 更多应用 耦合模型被应用到很多科学和工程领…...
使用navicat操纵数据库
<1>连接数据库 打开Navicat,点击“连接”,选择“MySQL”,这边是本机安装的mysql,主机为localhost,输入root密码。 使用Navicat创建数据库并导入SQL文件 SQL查询 普通SQL查询 USE demo; SELECT * FROM t_emp;SELECT emp…...
websocket入门
一,什么是websocket WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议。 WebSocket有…...
Word里MathType插件符号表消失了
场景再现 在word里面编辑数学公式,结果符号表跑到屏幕外面了,找不到; 解决办法 在其中找到视图->工具栏,点击即可: 还嫌弃它跑到外边了,那就可以点击符号表的边框: 双击左边边框&#x…...
利用MySQL玩转数据分析之基础篇
知识无底,学海无涯,到今天进入MySQL的学习4天了,知识点虽然简单,但是比较多,所以写一篇博客将MySQL的基础写出来,方便自己以后查找,还有就是分享给大家。 1、SQL简述 1)SQL的概述 S…...
【ML】分类问题
分类问题 classification:根据已知样本特征,判断输入样本属于哪种已知样本类。 常用入门案例:垃圾邮件检测、图像分类、手写数字识别、考试通过预测。 分类问题和回归问题的明显区别: 分类问题的结果是非连续型标签,…...
python @classmethod装饰器作用 与 使用 类方法 实例方法
1 表示是类方法, 类方法可以修改类变量, 实例方法不能修改类变量 类方法可以访问和修改类变量(也称为类属性)。这是实例方法做不到的,因为实例方法只能访问和修改实例变量(也称为实例属性) 1.1 例子 class MyClass:…...
layui form 中input输入框长度的统一设置
Layui.form中使用class"layui-input-inline"就可轻松将元素都放到一行,但如果元素过多,就会自动换行。那就需要手动设置input框的长度。 像这种情况: 其实只需要添加css样式就可修改了 .layui-form-item .layui-input-inline {wid…...
【WSL/WSL 2-Redis】解决Windows无法安装WSL Ubuntu子系统与Redis安装
前言 在现代计算环境中,开发人员和技术爱好者通常需要在不同的操作系统之间切换,以便利用各种工具和应用程序。在这方面,Windows用户可能发现WSL(Windows Subsystem for Linux)是一个强大的工具,它允许他们…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)
升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点,但无自动故障转移能力,Master宕机后需人工切换,期间消息可能无法读取。Slave仅存储数据,无法主动升级为Master响应请求ÿ…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
