专栏首页循迹漫聊虚幻引擎UE热更新:资源的二进制补丁方案

UE热更新:资源的二进制补丁方案

先前介绍了一系列UE中热更新工程实践的文章,能够实现基于原始工程的资源的版本比对与差异更新。但默认情况下资源的更新是基于文件的更新,某个资源产生了变动,就要将该资源的文件完整地打包进去,而UE的资源变动在Cook之后并不会造成整个文件的全部更新,序列化时只变动了某些bytes。在这种情况下,基于文件的Patch机制能够大幅度减少补丁的大小,本篇文章对二进制补丁的生成和加载方案做一个概述,以HotPatcher为基础,可以方便地实现二进制补丁的生成。 为了实现这个需求,我将HDiffPatch移植到了UE内,将其作为HotPatcher默认的二进制差异补丁的DIFF/PATCH算法,基于Modular Feature方式也可以方便地扩展其他的算法。

概览

基于资源的二进制差异的补丁实现,在有些游戏中已经经过了大规模地应用,如英雄联盟就在自己的更新机制中实现了二进制补丁的方案。 有些实现方案是基于完整包的差异,这种机制在UE中不合适,因为UE打包出来的资源是Pak,打包Pak时资源的排列顺序并不固定,并不能保证相同的排列方式,所以直接对于Pak的比对不合理,表现也很差,所以我的方案是对资源Cook后的文件,未打包进Pak时进行DIFF,将差异结果再打包至Pak中。

在UE中,我们修改的文件最终更新到玩家设备上的uasset,都是Cook之后的文件。如下图为例:

同一个资源在修改前后,对其进行Cook之后的对比。可以看到Cook之后的文件只是变动了一些数据,但是大部分的数据是一致的,在这种情况下,对整个文件的更新就显得比较浪费,二进制补丁收益巨大。

所以目标是:

  1. 打包时基于对UAsset的Cook结果进行二进制的patch生成
  2. 将patch的文件替换原始的Cooked的资源文件
  3. 加载资源时进行patch

针对这三个需求,需要介入打包的流程以及修改引擎的代码。

资源的二进制补丁

UE打包的uasset,都是Cooked之后的文件,也是游戏打包之后能读取的文件,实现uasset的二进制DIFF/PATCH,就是要获取基础包内的Cooked文件与最新资源Cooked的文件进行差异。

得益于HotPatcher的机制,可以很方便地得到当前Patch的uasset信息,将Patch列表中的文件从基础包的Pak中解包出来,与最新Cooked的文件进行DIFF。

所以整个资源进行DIFF的步骤和流程为:

  1. 获取到当前Patch中的资源列表
  2. Cook Patch中的资源
  3. 从基础包Pak中解包当前Patch列表中的资源
  4. 使用HDiffPatch创建新资源与旧资源的二进制补丁
  5. 将原始的Cooked的文件从Patch信息中剔除,使用patch替代

HotPatcher插件中已经提供了这个机制,可以在Patch配置页面的Binaries Patch中启用。

可以指定某个平台基础包的Pak,以及解密的Key,也可以做一些过滤操作,支持文件规则匹配(大小、类型)等。 补丁大小还是非常可观的:

二进制DIFF/PATCH算法

我移植了HDiffPatch到UE中,以插件的形式实现,可以从github上下载:hxhb/HDiffPatchUE,与HotPacther放入同一个工程中即可在Patch使用。

二进制的DIFF/PATCH算法,我目前选择的是HDiffPatch,相较于BsDiff有性能优势:

我将HDiffPatch的代码移植为了UE的一个Module,并针对跨平台的问题做了一些修正,将其放到游戏工程中,启动HotPatcher就可以在Binaries Patch-Binaries Patch Type中选择。

我将其的代码封装了两个函数,可以方便地调用进行DIFF/PATCH的行为。

UFUNCTION(BlueprintCallable)
static bool CreateCompressedDiff(const TArray<uint8>& NewData, const TArray<uint8>& OldData, TArray<uint8>& OutPatch);
UFUNCTION(BlueprintCallable)
static bool PatchCompressedDiff(const TArray<uint8>& OldData, const TArray<uint8>& PatchData, TArray<uint8>& OutNewData);

当然,前面也提到了,如果不想使用HDiffPatch作为创建二进制的算法,也可以自己实现其他算法的集成,因为是基于Modular Feature的实现,可以非侵入式地实现扩展,只需要添加一个实现以下接口的模块,在StartupModule中注册至BINARIES_DIFF_PATCH_FEATURE_NAME的Modular Features中即可。

struct IBinariesDiffPatchFeature: public IModularFeature
{
	virtual ~IBinariesDiffPatchFeature(){};
	virtual bool CreateDiff(const TArray<uint8>& NewData, const TArray<uint8>& OldData, TArray<uint8>& OutPatch) = 0;
	virtual bool PatchDiff(const TArray<uint8>& OldData, const TArray<uint8>& PatchData, TArray<uint8>& OutNewData) = 0;
	virtual FString GetFeatureName()const = 0;
};

运行时PATCH

注意:改造PakFile模块的实现,并没有支持PakCache,所以需要将其关闭。

经过前面的步骤,已经实现了二进制的补丁,并且将其打包成了Pak。

对于创建了二进制补丁的资源,在运行时的加载和使用也需要通过Patch之后才能正常访问。本节提供一个实现思路,但不是完整的方案,这部分内容涉及过多并且需要修改引擎,我提供一个粗暴的实践方案,目的是验证二进制补丁的可行性,在实际的生产中还需要根据项目需求进行大量的优化。

UE在加载Pak中的文件时,根据PakOrder进行优先级排序,这也是UE热更新实现的基础,从Pak中读取文件的实现是在IPlatformPakFile.cpp中,它位于UE的PakFile模块。

原始的Pak加载流程:

  1. 读取A文件
  2. 从Pak中读取A的原始Cooked的文件
  3. 返回文件Handle

但是我们经过创建了Patch之后,Pak内并不包含原始的Cooked之后的文件,所以就需要先在旧资源的基础上执行Patch操作,将最新的文件恢复出来:

  1. 读取A文件
  2. 从Pak中读取旧的A文件+A的Patch文件
  3. 运行时Patch,恢复出源文件
  4. 返回源文件Handle

Patch的操作通常是Pre Patch的流程,因为在运行时在内存中实时Patch的话也能实现,但是会有很大的性能损失。所以,可以在进入游戏之前,在热更流程中,对所有下载的资源执行Patch操作,这样在运行时只是读取,只是从Pak读还是从磁盘读的区别。

想要实现这个流程,需要改造UE的PakFile模块。不过,建议不要直接在PakFile模块上改,可以实现一个继承自FPakPlatformFile的类:

class PATCHPAKFILE_API FPatchPakPlatformFile: public FPakPlatformFile
{
public:
	virtual IFileHandle* OpenRead(const TCHAR* Filename, bool bAllowWrite = false) override;
	virtual IAsyncReadFileHandle* OpenAsyncRead(const TCHAR* Filename)override;
	virtual const TCHAR* GetName() const override { return TEXT("PatchPakFile"); }
};

从Pak中读取文件,就是通过调用OpenReadOpenAsyncRead两个接口实现的,在这两个接口中可以实现上面的流程。

核心流程伪代码如下:

IFileHandle* FPatchPakPlatformFile::OpenRead(const TCHAR* Filename, bool bAllowWrite)
{
	IFileHandle* Handle = NULL;
	if(HasPatch(Filename))
	{
		Handle = GetLowerLevel()->OpenRead(GetPatchedAssetPath(Filename),bAllowWrite);
	}
	return !Handle ? FPakPlatformFile::OpenRead(Filename, bAllowWrite) : Handle;
}

异步也是相同的思路。

在实现完毕FPatchPakPlatformFile之后,可以将其放入引擎的Runtime中,需要修改引擎的代码,将引擎中的PakFile模块替换成PatchPakFile,这样才能使我们的代码生效。

需要修改Launch模块LaunchEngineLoop.cpp文件中的LaunchCheckForFileOverride函数:

// From
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("PakFile"), CurrentPlatformFile, CmdLine);
// To
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("PatchPakFile"), CurrentPlatformFile, CmdLine);

这样就能在从Pak中读取文件时能走到自己实现的读取流程,实现Patch的行为。

结语

本篇文章介绍了在UE中创建二进制资源补丁的一个方案,在HotPatcher的基础上实现基于HDiffPatch的二进制资源补丁,并且能够在运行时实时、或PrePatch的方法,验证了可行性,应用于实际的项目中还需要优化,因为恢复出来的文件是Cooked之后的原始文件,并没有经过加密,会有资源安全问题,也需要改造处理。有时间的话我会再详细补充关于Patch的性能提升、Patched的资源加密等实现。

为了方便测试,我提供了一个ThirdPerson的Demo,可以点击链接下载:CompressionLab_WindowsNoEditor.7z

红框中的两个文件,一个是经过了BinariesPatch的,一个是原始Cooked文件,他们可以实现相同的作用,放入CompressionLab/Content/Paks即可启动测试。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android热更新方案Robust开源,新增自动化补丁工具

    我们在之前的博客文章中介绍了高兼容性、高稳定性的实时热更新解决方案Robust之后,业内反响强烈,不断有读者咨询我们什么时候开源。今天我们非常高兴地宣布,Rob...

    美团技术团队
  • UE热更新:Create Shader Patch

    之前的热更新系列文章中介绍了UE热更新的流程和打包细节,其实有一些热更补丁优化的工程实践我觉得也可以详细介绍。

    查利鹏
  • UE热更新:Shader更新策略

    在之前的一些文章中,介绍了UE热更新丢失Shader使用默认材质的处理问题,详情可见:UE热更新:Questions & Answers#热更的资源没有效果/材...

    查利鹏
  • APP 热修复都懂了,你会 SDK 热修复吗?最全方案在这里!

    某日,解决完一个线上 bug 后,我冒出了一个念头:让我们的 SDK 也具有热修复的能力呗!

    CCCruch
  • APP 热修复都懂了那你会 SDK 热修复吗?最全的方案在这里!

    某日,解决完一个线上 bug 后,我冒出了一个念头:让我们的 SDK 也具有热修复的能力呗!

    Android技术干货分享
  • 【Dev Club 分享】微信热补丁 Tinker 的实践演进之路

    Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是经过审核的移动开发工程师。每周都会举行嘉宾分享,话题讨论等活动。 本期,我们邀请了腾...

    腾讯Bugly
  • UE热更新:资产管理与审计工具

    在前面的文章中,介绍了基础包的拆分规则和实现,在基础的打包规则稳定之后,日常开发中的关注重点就转向侧重于项目的资产管理和包体资源审计、分析项目中的资产大小和冗余...

    查利鹏
  • 全面了解 Android 热修复技术

    走马观花地看一遍各家的热修复方案并不能找到答案,所以写下本文,希望从一个不同的角度来了解热修复技术,权当抛砖引玉,如有不足,欢迎指正。

    WeTest质量开放平台团队
  • 微信Android热补丁实践演进之路

    继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著名的有淘宝的Dexposed、支付宝的AndFix以及Qzon...

    微信终端开发团队
  • UE热更新:拆分基础包

    在之前的几篇文章中,分别介绍了UE热更新的实现机制,以及热更的自动化流程,近期打算继续写几篇文章介绍下UE里热更新中资源包管理的流程和规则。

    查利鹏
  • Android热修复技术原理详解(最新最全版本)

    本文框架 什么是热修复? 热修复框架分类 技术原理及特点 Tinker框架解析 各框架对比图 总结   通过阅读本文,你会对热修复技术有更深的认知,本文会列出各...

    用户1155943
  • 微信Android热更新Tinker使用详解(星空武哥)

    Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tink...

    砸漏
  • 微信Android热补丁实践演进之路

    继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著名的有淘宝的Dexposed、支付宝的AndFix以及Qzon...

    张绍文
  • 全面了解Android热修复技术

    热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现。国内大部分成熟的主流APP都拥有自己的热修复技术,像手淘、支付宝、...

    WeTest质量开放平台团队
  • UE工具集:我的开源项目介绍

    工欲善其事必先利其器,本文主要介绍在我在使用UE的过程中开发的一些开源的工具和插件,能够方便地在项目中使用,提高开发效率。之前简单罗列在资源页面里,今天做一个详...

    查利鹏
  • 实现iOS图片等资源文件的热更新化(四): 一个最小化的补丁更新逻辑

    简介 ? 以前写过一个补丁更新的文章,此处会做一个更精简的最小化实现,以便于集成.为了使逻辑具有通用性,将剥离对AFNetworking和ReativeCoco...

    ios122
  • Android热修复技术总结

    插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本...

    xiangzhihong
  • Android热修复技术总结

    插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本...

    xiangzhihong
  • UE性能分析:内存优化

    在开发游戏时,程序性能是需要着重考虑的问题,因为要尽可能覆盖最多的用户群体,就要考虑那些中低端设备的运行效果,兼容非常多配置差异的硬件,在这种情况下,怎么样分析...

    查利鹏

扫码关注云+社区

领取腾讯云代金券