【热更新】游戏热更新方案

article/2025/8/27 6:16:24

游戏热更新方案

      • 热更新演化
      • 热更新方案
        • 【1】 进程切换
          • 1.1 利用fork、exec切换
          • 1.2 利用网关切换
          • 1.3 微服务
          • - 进程切换注意要点
        • 【2】 动态库替换
        • 【3】 脚本语言热更新
          • 热更新探究
            • 最简单的实现热更的方法
            • 最简单的实现热更的方法的局限性
            • 热更新全局替换模块方法的局限性
          • 工程实现
            • 1. 规范写法以确保模块内无状态
            • 2. 交给具体模块解决
            • 3. 标注后全局遍历
            • * 选择合适的热更新范围
      • Skynet热更新
        • 【1】利用独立虚拟机热更新
        • 【2】注入补丁
        • 【3】Lua脚本热更新方案

热更新演化

请添加图片描述
进程切换 -> 动态库 -> 脚本语言热更新

热更新方案

【1】 进程切换

1.1 利用fork、exec切换

利用fork、exec函数实现进程切换,原理:fork和exec函数有一个重要的特性,即可以让复刻后的进程和新开启的进程继承原进程的文件描述符,因此新进程也可以直接访问原进程监听的端口的socket。
请添加图片描述
如上图说明了fork和exec函数实现优雅进程切换的流程,进程1是一个服务端程序,监听8001端口,客户端A正在与服务端进行交互。当需要热更新时,让进程1调用fork函数,系统会复刻一个与进程1一摸一样的进程2,两个进程共同监听8001端口。让进程2调用exec函数运行新版本的程序,新版本程序进程3继承了原有的监听端口。此时,可以让进程1停止接收新连接。客户端A可以继续与进程1进行交互,而新的连接会与进程3交互。待到进程1处理完客户端A的请求后,再让它退出,系统仅剩下进程3。如此便实现了优雅的进程切换。由于进程3和进程1都监听8001端口,因此客户端无须做任何改变。

Nginx热更新

Nginx是一款由C语言编写的Web服务器,它是一个多进程架构的程序。Nginx会开启一个master进程和若干个worker进程(图中开启了1个),其中,master进程负责监听(图中监听了80端口)新连接,当客户端成功连接后,master会把该连接交给某个worker处理。

Nginx采用了进程切换的热更新方式,如图9-18所示,在用户输入热更新指令后,Nginx内部会调用fork和exec函数,开启一组新版本进程,旧连接由旧进程负责处理、新连接由新进程负责处理。在旧进程处理完旧连接后,用户可以输入指令让它们退出。
请添加图片描述
图9-19是从Linux命令行观察Nginx热更新时的输出,热更新之前,Nginx拥有master和worker两个进程,当输入热更新指令“kill -USR2 127”之后,Nginx将拥有4个进程。
请添加图片描述

1.2 利用网关切换

除了使用fork和exec函数,利用网关也能够实现优雅的进程切换。如图9-20所示的是一种带有网关的服务端架构,客户端与网关相连,网关再将消息转发给逻辑进程(图中的game1)。
请添加图片描述
如图9-21所示,需要热更新时,开启一个新版本的逻辑进程(图中的game2),让网关把旧连接的请求转发给game1(图中的①)、把新连接的请求转发给game2(图中的②)。待所有旧连接都处理完毕,再关闭game1。
请添加图片描述
由于引入了网关,因此在切换进程的过程中,客户端的连接不会中断,从而实现了热更新。

1.3 微服务

进程切换热更新有个特点:旧连接由旧进程负责处理,新连接由新进程负责处理。这意味着进程切换热更新更适合于短连接的应用,这是因为旧连接很快就断开,服务端很快就能够全部演化到新版本。短连接适用于非频繁交互的休闲游戏,不适合于强交互类的游戏,但它依然可以作为强交互游戏架构的一部分。

图9-22所示是一款策略游戏(SLG)的服务端架构,游戏只有一张大地图,每位玩家占据一个角落发展自己的军团,且随时会与其他玩家发生战斗。鉴于玩家之间具有强交互性,服务端用一个进程(场景服务器)处理地图逻辑。玩家控制军队前进,需要采用A寻路算法,但A寻路算法的计算量较大,为了保证性能,该算法不宜放在场景服务器中计算。于是,开发者把徐璐功能做成无状态的微服务,场景服务器向寻路服务器请求”从{100,200}走到{300,400}的路径“,寻路服务器回应路径点。
请添加图片描述

- 进程切换注意要点

无论是使用fork和exec函数、还是使用网关实现的热更新,都需要借助多个进程的配合,进程切换热更新是一种架构级别的方法;而且需要做到进程级别的无状态,或者能够在重启时恢复整个进程的状态。

伪代码示例:

unordered_map<int, Player*> players; //进程开启时调用
void onStart(Mode mode){if(mode == MODE.HOTFIX){players = LoadFromDB();}
}//进程退出时调用
void onExit(){if(mode == MODE.HOTFIX){SaveToDB(players);}
}

【2】 动态库替换

进程切换不仅需要多个进程互相配合,还要实现进程级别的无状态,灵活性很差。如果靠单个进程就能实现热更新,那么程序在开发时就能够灵活很多。使用动态库就能实现单进程的热更新,而且只需要达到”库“级别的无状态。

动态库更新的方式是指把程序的某些变量和方法编写到外部的动态库文件中(.so),在程序运行时再动态地加载它们。这种方式可用于热更新动态库中的内容,只需要把动态库替换掉即可。

示例

player.h

struct Player{int x;int y;int coin;
};

handle.c

#include "player.h"void work(struct Player *player){player->coin = player->coin + 1;
}

main.c

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h> //该文件声明了处理动态库的方法dlopen、dlsym、dlclose
#include <player.h>void *handle = NULL;
void (*work)(struct Player *player) = NULL;//需要热更新时调用
int reload(struct Player *player){//打开动态库handle = dlopen("./handle.so", RTLD_LAZY);//从动态库中获取某个方法的地址指针work = dlsym(handle, "work");	return 1;
}void closeHandle(){//关闭动态库dlclose(handle);
}void main(){struct Player player = {0,0,0};reload(&player);while(1){work(&player);printf("player x:%d y:%d coin:%d\n", player.x, player.y, player.coin);sleep(1);}closeHandle();
}

编译:

# ls
handle.c main.c  player.h//生成动态库:  -shared代表要生成的目标文件类型是动态库
# gcc -shared -o handle.so handle.c//查看库中的符号(函数、全局变量等)
# nm handle.so
0000000000004028 b completed.7326w __cxa_finalize@@GLIBC_2.2.5
0000000000001050 t deregister_tm_clones
00000000000010c0 t __do_global_dtors_aux
0000000000003e10 t __do_global_dtors_aux_fini_array_entry
0000000000003e18 d __dso_handle
0000000000003e20 d _DYNAMIC
0000000000001124 T _fini
0000000000001100 t frame_dummy
0000000000003e08 t __frame_dummy_init_array_entry
0000000000002080 r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_w __gmon_start__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000001000 T _initw _ITM_deregisterTMCloneTablew _ITM_registerTMCloneTable
0000000000001080 t register_tm_clones
0000000000004028 d __TMC_END__
0000000000001105 T work# ls
handle.c  handle.so  main  main.c  player.h//生成可执行文件: 因为用到了动态链接库,所以在编译时,需要添加参数"-ldl",编译器才能找到dlopen、dlsym、dlclose这几个方法的具体实现。
# gcc -o main main.c -ldl//查看可执行文件中所有依赖的共享库(ldd)
# ldd main
linux-vdso.so.1 =>  (0x00007ffe943ca000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fcd04139000)
libc.so.6 => /lib64/libc.so.6 (0x00007fcd03d6b000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcd0433d000)//运行
# ./main
player x:0 y:0 coin:1
player x:0 y:0 coin:2
player x:0 y:0 coin:3
player x:0 y:0 coin:4
player x:0 y:0 coin:5
...

【3】 脚本语言热更新

解释型脚本语言(Lua、Python……)的模块重载功能能很好满足灵活的热更方案。

热更新探究
最简单的实现热更的方法

main.lua

local shop = require("shop")local players = {} --玩家列表
players[101] = {coin=1000,l bag={}}--lua热更新
function reload()package.loaded["shop"] = nilshop = require("shop")print("reload succ")
end--用字符输入模拟网络消息
while true dp cmd = io.read()if cmd == "b" then --buyshop.onBuyMsg(players[101], 1001)elseif cmd == "r" then --reloadreload()end
end

shop.lua

local M = {}local goods = {[1001] = {name = "金疮药", price = 10},[1002] = {name = "葫芦", price = 2}
}M.onBuyMsg = function(player, id)local item = goods[id]--扣金币,这里缺少对金币数量书否充足的判定player.coin = player.coin - item.price--增加道具计数player.bag[id] = player.bag[id] or 0player.bag[id] = player.bag[id] + 1--...local tip = string.format("player buy item %d, coin:%d item_num:%d", id, player.coin, player.bag[id])print(tip)	
end
最简单的实现热更的方法的局限性

main.c

local cmdHandle = {b = shop.onBuyMsg,--s = shop.onSellMsg, --出售--w = work.onWorkMsg,r = reload,
}function reload()package.loaded["shop"] = nilshop = require("shop")print("reload succ")
endwhile true do cmd = io.read()cmdHandle[cmd](players[101], 1001)
end

如上代码会导致热更新失败。这是因为在reload方法中,只是用"shop = require(“shop”)" 替换了对shop的引用,尽管shop.onBuyMsg引用了新方法,但cmdHandle.b引用的依然是旧方法。而程序调用的就是cmdHandle.b,因此程序没能实现热更新。如图9-37所示,虚线代表热更新前cmdHandle.b和shop.onBuyMsg的引用指向,实现代表热更新后的引用指向。
请添加图片描述
若要想成功实现热更新,那么我们还需要在reload方法中添加一句"cmdHandle.b = shop.onBuyMsg",让cmdHandle.b引用新方法。

由此可见,热更新的实现与业务的写法有关。要么就遵循严格的代码规范,禁止在业务层使用回调函数、匿名函数,禁用任何未经验证的设计模式;要么就为每个模块单独编写特定的热更新方法。

热更新全局替换模块方法的局限性

一种针对上面案例的做法是:通过一些小技巧来实现全局替换,在热更新时遍历虚拟机中的所有全局变量、局部变量、上值、元表等,替换掉旧方法。如Skynet的注入补丁就是使用debug.setupvalue替换了本地变量。

但全局替换并不是一个通用的热更新方法。由于程序无法得知哪些值需要进行热更新,哪些值需要保留,因此我们无法使用一个通用的热更新方法,每个项目都要做特殊处理。

下面以shop模块为例说明问题。假设现在需要为商城添加限购功能,每天仅出售100瓶金疮药,添加remain表记录商品的剩余数量,每成功购买一个道具,剩余数量减1。

shop.lua

local M = {}local goods = {[1001] = {name = "金疮药", price = 1},[1002] = {name = "葫芦", price = 2}
}local remain = {[1001] = 100, --今日剩余的金创药数量[1002] = 200, --今日剩余的葫芦数量
}M.onBuyMsg = function(player, id)local item = goods[id]--扣金币,这里缺少对金币数量书否充足的判定player.coin = player.coin - item.price--省略对金币和先构数量的判定remain[id] = remain[id] - 1--增加道具计数player.bag[id] = player.bag[id] or 0player.bag[id] = player.bag[id] + 1--...local tip = string.format("player buy item %d, coin:%d remain:%d", id, player.coin, player.remain[id])print(tip)	
end

运行程序将得到如图9-38所示的结果,每购买一次,道具剩余量将减少1。

如果修改商品价格,再执行热更新,则将得到如图9-39所示的失败结果,虽然道具价格从10变成了1,但剩余量却发生了错误的变化。正常情况下,道具剩余量应以99、98、97、96的规律递减,现在却变成了99、98、99、98。也就是说,在热更新后,道具价格成功发生改变,但道具剩余量还原到初始值了。
请添加图片描述
这是因为,新版本的remain表替换了旧remain表,而remain表的值需要保存起来。如图9-40所示,热更新前后的M.onBuyMsg引用了各自的本地变量goods和remain,新remain的默认值是100,正是因为新的onBuyMsg方法引用了新的remain表,才使得热更新失败。本例中,需要用到新的goods表(因为修改了商品价格),但要保留旧的remain表。goods表和remain表都是普通的本地变量,程序无法自动区分它们。
请添加图片描述
根源在于代码没有提供足够的信息量,让程序去判断哪些值需要热更新,哪些值需要保留。

工程实现
1. 规范写法以确保模块内无状态

如果开发者清楚哪些值需要热更新,哪些值不需要,则可以把不需要热更新的变量设为全局变量(注意代码中没有local)。

shop.lua

remain = remain or {[1001] = 100,[1002] = 200,
}

上面使用了一个小技巧“remain = remain or 默认值”,模块第一次加载时,全局变量remain为空,为它赋予默认值;热更新时,让remain继承旧值。

修改之后,程序就可以正常进行热更新了,运行结果如图9-41所示。
图9-42展示了变量的引用关系,热更新前后,onBugMsg方法引用了不同的goods表,但引用了同一个remain表。
请添加图片描述
使用全局变量之前必须做好命令的规划。在shop模块的例子中,如果另外的某个模块也用到了全局变量remain,并将其设计成奇怪的值,则将产生不可预料的后果。

main.lua

while true docmd = io.read()if cmd == "b" then --buyshop.onBuyMsg(players[101], 1001)elseif cmd == "r" then --reloadreload()end	remain = p
end

图9-43所示的是由全局变量冲突引起的报错。remain本来是表结构,而代码9-19却把它改成了数值,结果导致shop模块读表时出现报错。

我们可以为各模块分配不同的全局空间,以避免发生全局变量冲突。如图9-44所示,将shop模块的全局变量放到runtime.shop中,将成就模块的全局变量放到runtime.achieve中,以避免冲突。
请添加图片描述

2. 交给具体模块解决

由于程序无法自动判断哪些值需要保留,因此这部分工作最好是交给具体模块的开发者去处理。如下代码所示,我们可以规定每个模块都必须包含一个reload方法,服务端在热更新该模块时会调用它。开发者需要在reload方法中还原需要保留的值。

shop.lua

local M = {}
...M.reload = function(old_module)remain = old_module.get("remain")
endreturn M
3. 标注后全局遍历

如下代码,我们规定NEED_PRESERVE是一个特殊的标识,表示该值需要保留。再使用全局替换的方法遍历所有的全局变量、局部变量、上值、元表等,由于特殊标识的存在,因此程序可以分辨出需要保留的内容。

shop.lua

local M = {}local goods = {--具体内容略
} local remain = {NEED_PRESERVE= true--具体内容略
}

其实,如果能够明确热更新要替换的内容,那么无论程序有多复杂,我们都能用各种技巧成功实现热更新。

* 选择合适的热更新范围

热更新能力和灵活性就像鱼与熊掌的关系一样,难以兼得。要实现更强的热更新能力,就需要遵循更严格的规范,越严格的规范就意味着越多的培训成本。对于大部分项目,通过少量限制,获取有限的热更新能力是面对实际需求权衡之后的选择。

表9-1列出了服务端热更新能力的五个层次。
请添加图片描述
一般而言,我们认为实现前3个层次的热更新能力是性价比比较高的一种选择,这样做既能满足大部分热更新需求,又不至于增加太多写法限制。

Skynet热更新

【1】利用独立虚拟机热更新

1.skynet热更新的依赖于架构

skynet实现了Actor模型,每个Lua服务开启了独立的虚拟机(如图9-4所示),这种架构为skynet提供了一些热更新能力。
请添加图片描述
我们可以这样理解:开启新服务时,虚拟机需要重新加载Lua代码,所以只要先修改Lua代码,再重启(或新建)服务,新开的服务就会基于新代码运行,实现热更新。

2.清除代码缓存

不过,直接修改Lua代码并不能起作用,这是因为skynet使用了修改版的Lua虚拟机,它会缓存代码。所以在修改Lua代码之后,要先登录调试控制台(debug_console)执行清除缓存的指令clearcache(skynet调试台的clearcache指令虽然被称为“清缓存”,但它并不是真的执行“清理”操作,而是额外加载一份代码,所以频繁执行clearcache会加大内存开销)。
请添加图片描述
3.完成热更新需求

每个客户端对应于一个代理服务,每个代理服务的存活时间是客户端从连接到断开的时间。对于一些休闲类手游,玩家每次游玩的时间不会很长,可以让已在线的玩家按照旧的规则玩(如每次增加1金币),同时让新上线的玩家按照新的规则玩(如每次增加2金币)。

热更新范例程序,仅需执行如下两步操作:
1)修改代码,如将“coin = coin + 1” 改成 “coin = coin + 2”
2)登录调试控制台,执行clearcache指令。

热更新后,旧客户端依然只增加1金币,但新连接的客户端会增加2金币,如图9-6所示。
请添加图片描述
4.适用场景

skynet独立虚拟机热更新的方式适合在“一个客户端对应一个代理服务”的架构下热更新代理服务,以及在“开房间型”游戏中热更新战斗服务。

如图9-7所示的是一种典型的Actor服务端架构,每个客户端对应一个代理服务,每场战斗对应于一个战斗服务(battle)。图中灰色底纹的服务即表示可以通过此方案热更新的服务;白色底纹的网关(gateway)、登陆服务(login)、匹配服务(match)是“固定”的服务,难以通过此方式进行热更新。
请添加图片描述
虽然旧客户端执行的是依然是旧代码,但重新登录就能运行新的版本;虽然旧的比赛执行的依然是旧代码,但新开的比赛就能运行新的版本。由于每个客户端的登录时长有限、每场战斗的持续时间也有限,程序最终会趋向于运行新版本。

【2】注入补丁

1.注入补丁热更新方案

skynet还提供一种称为inject(可翻译为“注入”)的热更新方案,如图9-9所示,写一份补丁文件,把它注入某个服务,就可以单独修复这个服务的Bug。
请添加图片描述
2.编写补丁文件

虽然skynet提供了“注入”的热更新方案,却没有给予足够的支持,补丁文件的写法颇具技巧性。

skynet/examples/hinject.lua

local oldfun = _P.lua._ENV.onMsg
_P.lua._ENV.onMsg = function(data)local _,skynet = debug.getupvalue(oldfun, 1)local _,coin = debug.getupvalue(oldfun, 2)skynet.error("agent recv" .. data)--消息处理if data == "work\r\n" thencoin = coin + 2debug.setupvalue(oldfun, 2, coin)return coin .."\r\n"endreturn "err cmd\r\n"	
end

代码中,“_P”是skynet提供的变量,用于获取旧代码的内容,“_P.lua._ENV.onMsg”即原先的onMsg方法,重新为它赋值,即可换成新的方法。因为新、旧方法的运行环境不同,新方法不能直接读取skynet、coin等外部变量,所以这里还需要依靠一些小技巧,如上代码是通过Lua的调试模块(debug)来获取外部值的。

图9-10是如上代码的简化示意图,将hagent.lua中的onMsg替换为hinject中的newfun,newfun中的skynet、coin依然引用旧代码。其中,newfun代表_P.lua._ENV.onMsg。
请添加图片描述
3.完成热更新需求

写完补丁文件,在调试控制台中输入inject a examples/hinject.lua即可完成热更新。其中
“a”是代理服务的id,可从服务端的输出日志中获取;“examples/hinject.lua”是补丁文件的路径。

4.适用场景

“注入”热更新方案适合于需要紧急修复Bug的情况。补丁文件的写法比较诡异,容易出错,需要在开发环境中做严密测试;Lua调试模块(debug)的运行效率较低,还会破坏语言封装的整体性,因此若不是危机情况则尽量不要使用;skynet只提供了针对某个服务的注入功能,若要热更某类服务(如图9-7中的全部代理服务或战斗服务),则还需自行实现。

【3】Lua脚本热更新方案

如上的Lua脚本热更新方案,共有3种:

  1. 规范写法以确保模块内无状态
  2. 交给具体模块解决
  3. 标注后全局遍历

http://chatgpt.dhexx.cn/article/s6nqEtRX.shtml

相关文章

Addressable热更新

文章目录 前提配置代码实现 前提配置 &#xff08;1&#xff09;勾选AddressableAssetSettings设置的Disable Catalog Update On Startup选项 &#xff08;2&#xff09;相应的热更戏资源分组配置&#xff08;注&#xff1a;此文采用的是动态资源更新&#xff09; Can Change …

Nacos配置热更新的4种方式、读取项目配置文件的多种方式,@value,@RefreshScope,@NacosConfigurationProperties

nacos实现配置文件的热更新&#xff0c;服务不用重启即可读取到nacos配置中心修改发布后的最新值&#xff0c;spring&#xff0c;springboot项目读取本地配置文件的各种方式&#xff1b;文章中介绍了一下注解的使用&#xff1a;NacosConfigurationProperties&#xff0c;NacosP…

Unity 热更新技术 | (一) 热更新的基本概念原理及主流热更新方案介绍

&#x1f3ac; 博客主页&#xff1a;https://xiaoy.blog.csdn.net &#x1f3a5; 本文由 呆呆敲代码的小Y 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;Unity系统学习专栏 &#x1f332; 游戏制作专栏推荐&#xff1a;游戏制作 &…

JAVA热更新

引言 知识储备先看这篇文章&#xff1a;JAVA Instrument 在这个案例中我们会利用Instrument机制实现一个简单的热更新案例。 总体来说&#xff0c;步骤如下&#xff1a; 创建一个带premain方法的jar包。这个方法定时检测某个文件然后进行热更新。命令行启动业务类时使用参数…

热更新 深度解析

APP热更新方案 为什么要做热更新 当一个App发布之后&#xff0c;突然发现了一个严重bug需要进行紧急修复&#xff0c;这时候公司各方就会忙得焦头烂额&#xff1a;重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。 重点是还会有原来的版本遗留…

webpack热更新

什么是模热更新&#xff1f;有什么优点 模块热更新是webpack的一个功能&#xff0c;它可以使得代码修改之后&#xff0c;不用刷新浏览器就可以更新。 在应用过程中替换添加删出模块&#xff0c;无需重新加载整个页面&#xff0c;是高级版的自动刷新浏览器。 优点&#xff1a…

electron 热更新

1. electron自带的整体更新方式 &#xff08;全量更新&#xff09; 这种方式为electron官方的升级更新方式&#xff0c;主要是通过主进程中的autoUpdater模块进行检测升级更新的&#xff0c;此方式也是大家常见的大多数electron应用程序的更新方式。 检测到新版本后从服务器拉…

uniApp实现热更新

热更新 热更新是开发中常见且常用的一种软件版本控制的方式&#xff0c;在uniapp进行使用热更新将软件实现更新操作 思路: 服务器中存储着最新版本号&#xff0c;前端进行查询可以在首次进入应用时进行请求版本号进行一个匹对如果版本号一致则不提示&#xff0c;反之则提示进行…

Android热更新详解

一 前言介绍 正好最近又看到热更新&#xff0c;对以前Android 热修复核心原理&#xff1a;ClassLoader类加载机制做了点补充。 从16年开始开始&#xff0c;热修复技术开始在安卓界流行&#xff0c;它以classloader类加载机制为核心&#xff0c;可以不发布新版本就修复线上 bu…

热更新原理

对于热更新的问题就是了解两个点的问题&#xff1a; 如何加载补丁包&#xff0c;也就是如何加载dex 文件的过程&#xff08;dex是补丁包&#xff0c;更改的文件都在补丁包中&#xff09;修复后的类如何替换掉旧的类 通过这篇文章给大家介绍下我理解的热更新的逻辑&#xff0c…

Cocos Creator 3.x 热更新

前言&#xff1a;游戏做热更新 是基本需求&#xff1b; 好在 cocos-creator 已经为我们做好了方案&#xff0c;相对于 U3D 的热更新方案来说&#xff0c;使用起来很简便&#xff01;&#xff0c;不用关注很多细节 本文使用的是 cocos-creator 3.5.2 版本 官方文档 &#xff1…

热更新原理及实践注意

首先要说明几个概念&#xff0c;不要混用&#xff0c;热部署&#xff0c;热加载&#xff1b; 热部署&#xff1a;就是已经运行了项目,更改之后,不需要重新tomcat,但是会清空内存,重新打包,重新解压war包运行&#xff0c;可能好处是一个tomcat多个项目,不必因为tomcat停止而停止…

热更新你都知道哪些?

热更新系列目录 热更新你都知道哪些&#xff1f;热更新Sophix的爬坑之路腾讯热更新Tinker的故事阿里热更新Sophix的故事 Android热更新 前言1. 什么是热更新&#xff1f;2. 主流热更新方案3. 腾讯系热更新4. 阿里系热更新总结 博客创建时间&#xff1a;2020.05.16 博客更新时间…

热更新技术简易原理及技术推荐

为了照顾萌新童鞋&#xff0c;最开始还是对热更新的概念做一个通俗易懂的介绍。 热更新用通俗的讲就是软件不通过应用商店的软件版本更新审核&#xff0c;直接通过应用自行下载的软件数据更新的行为。在用户下载安装App之后&#xff0c;打开App时遇到的即时更新&#xff0c;是…

热更新及其原理

热更新&#xff1a;是app常用的更新方式&#xff0c;只需下载安装更新部分的代码 工作原理&#xff1a;动态下开发代码&#xff0c;使开发者在不发布新版本的情况下修复bug和发布功能&#xff0c;绕开苹果审核机制&#xff0c;避免长时间的审核以及多次被拒绝造成的成本。 优…

HTML/CSS实现小米官网搜索框效果

效果图&#xff1a; 需求分析&#xff1a; 1、输入框焦点事件 onfocus:成为焦点, 点击输入框的时候&#xff0c;出现闪烁光标&#xff0c;此时可以输入内容。 onblur :失去焦点, 点击页面空白区域&#xff0c;光标消失。此时不可以输入内容。 2、获取元素 3、注册事件 2.1…

html中的搜索代码,Web自动化(3):网页自动搜索功能

unsplash.jpg 写在前面 如果我们需要在期刊中搜索我们想要找的文章,那么我们如何才能达到这个目的。我们首先看一下,手动和自动对比图: 网页搜索.png 其实内容全部一样,我们只是用自动化程序,来代替我们手动操作。 1. 创建webdriver驱动对象,驱动打开网页 # 导入包 from …

java搜索代码_Java实现搜索功能代码详解

首先&#xff0c;我们要清楚搜索框中根据关键字进行条件搜索发送的是get请求&#xff0c;并且是向当前页面发送get请求 //示例代码 请求路径为当前页面路径 "/product" 当我们要实现多条件搜索功能时&#xff0c;可以将搜索条件封装为一个map集合&#xff0c;再根据m…

干货!最全优秀搜索框设计案例(含代码链接)

面对纷繁复杂的网页内容&#xff0c;用户通过查询关键词表达需求&#xff0c;期望在响应的查询结果中快速获取准确的信息和流畅的用户体验。用户与网络世界的万千联系都是从搜索开始的。搜索框之于用户就像是用户与应用或网站之间的对话窗口。小小的搜索框传递着用户与网站、应…

php网页制作中搜索框的代码,在网页里嵌入百度搜索框功能

今天发现某个网站是直接使用百度搜索作为自己网站的搜索功能的&#xff0c;感觉这个挺好玩的&#xff0c;不需要去研究复杂的搜索算法而又直接使用了百度搜索这个强大的搜索引擎为自己撑腰。无论对自己还是对用户来说都是相当不错的选择&#xff0c;下面作者将要和大家分享一下…