Unity构建详解(7)——AssetBundle格式解析

【文件格式】

文件可以分为文本文件、图片文件、音频文件、视频文件等等,我们常见的这些文件都有行业内的标准格式,其意味着按照一定的规则和规范去保存读取文件,可以获取我们想要的数据。

有些软件会有自己的文件格式,会按照其自己设计的规则和规范去保存读取文件。

我们自己在做开发时,也需要保存和读取一些文件,但我们需要保存的数据一般会比较简单,很少去做设计格式。

例如,要保存一系列角色和怪物的当前血量。为了读取时能区分是角色和怪物,可以先保存角色个数。读取时先读取角色个数n,随后读取n个血量数据,这些都是角色的,随后读取的是怪物的。

个数n就是文件头,血量是文件数据。

一般来说,任何文件都可以分为文件头和文件数据两个部分,文件头用于描述文件数据。如果要保存的数据很复杂,可以进一步将文件头分为文件头(描述整个文件的,例如版本号)和数据头(描述数据部分的)、将文件数据分为主要数据段和次要数据段等。

如果复杂些,例如还需要保存当前的魔力值,那么需要区分当前的值是魔力值还是血量。如果是自己做开发,自己读写,可以默认先是血量,随后的值是魔力值。如果要保存角色的更多属性,那么要保存的不能是单一的数值,而是不同数值组成的数据结构了。

如果是软件,那么采用默认的方式就不合适,随着版本的迭代,要保存的数据会发生变化,必须在文件头中加入版本号和其他信息以描述要保存的数据。要保存的数据越复杂,文件头也就越复杂。

尽管更加复杂,但只要明白保存读取的规则和规范(即文件格式),那么和我们自定义的保存读取的主要逻辑在本质上没有区别。

行业标准格式更为复杂,其会被多个软件使用,可以看看场景的文件格式了解下文件是怎么样构成的:

PNG文件解读(2):PNG格式文件结构与数据结构解读—解码PNG数据-腾讯云开发者社区-腾讯云

MP3文件结构解析(超详细)-CSDN博客

AVI文件格式详解-CSDN博客

【AssetBundle格式】

详细的请看下Unity 的AssetBundle解析视频和AssetStudio读取Bundle的源码,这里只做简要介绍

  • AssetBundleHeader
    • AssetBundleFileHeader
    • StorageBlock[] m_BlocksInfo
    • Node[] m_DirectoryInfo
  • SerializedFile
    • SerializedFileHeader 文件头
    • MetaData
      • Types 每个对象的类型是什么
        • TypeTree
      • Objects 有多少对象,分别是什么,多大,在哪开始读取
      • ScriptTypes 如果有mono脚本的话,脚本的类型是什么
      • Externals 如果包里的对象引用了其他AB里的资产,分别在哪里可以找到
      • RefTypes
    • Object(第一个Object类型为AssetBundle)
    • 一系列的其他Object (包内Object的数据)
  • ResourceFile

下面是相关的数据结构

// AssetBundle文件头结构
public class AssetBundleFileHeader {
            public string signature;
            public uint version;
            public string unityVersion;
            public string unityRevision;
            public long size;
            public uint compressedBlocksInfoSize;
            public uint uncompressedBlocksInfoSize;
            public ArchiveFlags flags;
};

        public class StorageBlock
        {
            public uint compressedSize;
            public uint uncompressedSize;
            public StorageBlockFlags flags;
        }

        public class Node
        {
            public long offset;
            public long size;
            public uint flags;
            public string path;
        }

  public class SerializedFileHeader
    {
        public uint m_MetadataSize;
        public long m_FileSize;
        public SerializedFileFormatVersion m_Version;
        public long m_DataOffset;
        public byte m_Endianess;
        public byte[] m_Reserved;
    }


    public class SerializedType
    {
        public int classID;//unity有个classId的映射表
        public bool m_IsStrippedType;
        public short m_ScriptTypeIndex = -1;
        public TypeTree m_Type;
        public byte[] m_ScriptID; //Hash128
        public byte[] m_OldTypeHash; //Hash128
        public int[] m_TypeDependencies;
        public string m_KlassName;
        public string m_NameSpace;
        public string m_AsmName;
    }

    public class TypeTree
    {
        public List<TypeTreeNode> m_Nodes;
        public byte[] m_StringBuffer;
    }

    public class TypeTreeNode
    {
        public string m_Type;
        public string m_Name;
        public int m_ByteSize;
        public int m_Index;
        public int m_TypeFlags; //m_IsArray
        public int m_Version;
        public int m_MetaFlag;
        public int m_Level;
        public uint m_TypeStrOffset;
        public uint m_NameStrOffset;
        public ulong m_RefTypeHash;
}

    public class ObjectInfo
    {
        public long byteStart;//相对于SerializedFileHeader的偏移
        public uint byteSize;
        public int typeID;
        public int classID;
        public ushort isDestroyed;
        public byte stripped;

        public long m_PathID;
        public SerializedType serializedType;
    }

    public class LocalSerializedObjectIdentifier
    {
        public int localSerializedFileIndex;
        public long localIdentifierInFile;
    }

    public class FileIdentifier
    {
        public Guid guid;
        public int type; //enum { kNonAssetType = 0, kDeprecatedCachedAssetType = 1, kSerializedAssetType = 2, kMetaAssetType = 3 };
        public string pathName;

        //custom
        public string fileName;
    }

public class  AssetBundle : NamedObject
 {
     public PPtr<Object>[] m_PreloadTable;
     public KeyValuePair<string, AssetInfo>[] m_Container;
}

 public class AssetInfo
 {
     public int preloadIndex;
     public int preloadSize;
     public PPtr<Object> asset;
}

我们要关心的核心问题是:如何从Bundle中加载需要的Asset

  • 业务上层会传入一个资源路径
  • 游戏中的资源加载模块会根据资源路径得到其所在的Bundle路径
  • 资源加载模块会调用Unity接口去加载Bundle
  • 先将整个文件头加载到内存中,其以SerializedFile 格式存储在内存中(注意保存文件时也即磁盘上的数据结构和内存中的数据结构不一定一致)
  • 根据传入的路径从Bundle内的Container中找到该Asset对应的AssetInfo
  • 从AssetInfo中拿到PreloadIndex,其是PreloadTable中的索引,PreloadSize是长度,结合两者可以知道该Asset包含了哪些Object,并获取到ObjectInfo(也叫ObjectHeader)
  • 从ObjectInfo中拿到FileID和PathID,PathID是Object在AssetBundle内的标识,如果FileID为0,表明Object在该Bundle内,如果FileID不为0,则说明需要的Object在其他AssetBundle中
  • 从Exteranls中根据FileID找到对应的FileIdentifier,拿到AssetBundle的名字,再根据PathID找到Object 。
    • 在内存中会有其他转换,例如每个SerializedFile都会有个SerializedFileIndex。FileID和SerializedFileIndex不过是同一个Bundle在磁盘和内存上的不同标识而已
  • 从ObjectInfo中拿到byteStart和byteSize即可知道Object数据在整个Bundle文件中的位置,并实现读取
  • 依次将Asset内的所有Object数据读取到内存中
  • 根据读取的Object数据反序列化得到想要加载的资源

【AssetBundle的加载和卸载】

先看一张图

加载时AssetBundleHeader、SerializedFileHeader、MetaData的数据会进入到内存中,也即途中的AssetBundle内存镜像:

  • AssetBundle.LoadFromFile(path):同步加载,path为本地路径
  • AssetBundle.LoadFromFileAsync(path):异步加载,path为本地路径
  • AssetBundle.LoadFromMemory(byte[] binary):从字节数组加载,binary为目标ab二进制流
  • AssetBundle.LoadFromMemoryAsync(byte[] binary):从字节数组异步加载,binary为目标ab二进制流
  • UnityWebRequest.GetAssetBundle(string uri):url为ab文件路径,可为本地,也可为云端,

加载Asset时会从Bundle中加载一系列的Object生成对应的Asset,每个Asset会根据FileID和PathID生成唯一的标识ID:

  • assetBundle.LoadAsset<T>(name):T为目标资产类型,name为资产名称,会返回一个T实例
  • assetBundle.LoadAsset(name,type):name为资产名,type为资产类型
  • assetBundle.LoadAllAssets<T>():T为目标资产类型,会返回一个assetBundle中所有T类型资产数组
  • assetBundle.LoadAllAssets():加载assetBundle中所有资产,返回一个assetBundle中所有资产数组

实例化时,会生成一份新的Asset,其有一个对应的InstanceID,并引用原来的Asset。

【AssetBundle解析工具】

unity官方的WebExtract和Binary2Text

WebExtract路径:

cd进入路径后 输入AssetBundle路径即可 

Binary2Text路径:

AssetStudio

【AssetBundle优化】

IO优化:先了解下读取文件的详细流程

这里说的都是很通用的IO优化方法,不光是读取AssetBundle,读取其他文件也一样:

  • 缓存文件句柄
    • 打开关闭文件是一个耗时的操作,上层要卸载Bundle时,可以先缓存文件句柄,而不是立即关闭释放
  • Object数据重排
    • IO中很大一部分时间消耗是寻找磁道和磁头移动的时间(即寻道时间),为了减少这个时间,需要数据顺序排列,这样可以顺序读取,类似CPU读取数组比List快。在加载Asset时会读取多个Object的数据,如果保证传入的流的Pos是顺序增加或减少,而不是来回横跳,那么可以大幅度减少IO时间。
  • 无锁多线程
    • 一般来说一个成熟软件的读取文件操作会在单独的IO线程中执行,但会涉及到很多加锁操作,可以考虑改成无锁的,这个难度比较大
  • 组合IO请求
    • 一次大的IO请求比多次小的IO请求好,如果多个小的IO请求的位置大致是连续的,即使中间有部分数据可能不是需要的,也可以组合成一次大的IO请求

大小和内存优化

  • 剔除重复数据
    • 保证每个Asset只在一个Bundle内
  • 字符串优化(内存优化,很大一部分是去如何优化字符串)
    • string转Id
      • 例如所有的AssetInfo中的assetName转为int,加载资源时也传入Id
      • 所有标识改为用Id,int64改为int32
  • 合并重复数据,例如TypeTree合并成一份
  • 精简冗余数据
    • 会建立各种映射关系,可以梳理下简化映射,例如PersistentManager里的remapper
    • 有些数据在加载到内存后可能不会用了,从原有的数据结构中拆开,然后释放,例如各种版本信息
    • 动态Buffer,读取文件时一般会有个Buffer缓存数据,读Object时的Buffer是FileCacherRead,可以根据需要动态调整,或者共用Buffer

【参考】

AssetBundle研究报告 | BLOG

Unity如何把一个对象从内存序列化到磁盘 | 矩阵·空间

AssetBundle热更新完整工作流与知识点解析 | 登峰造极者,殊途亦同归。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/534854.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot学习笔记四

SpringBoot学习笔记四-监听机制 1. SpringBoot监听器1.1 无需配置1.1.1 CommandLineRunner使用1.1.2 ApplicationRunner的使用1.1.3 CommandLineRunner与ApplicationRunner的区别 1.2 需要创建META-INF文件&#xff0c;并在其中创建spring.factories&#xff0c;配置相关的信息…

WEB3浪潮下的全新体验:精灵派对链游引领边玩边赚的创新之旅

在当前的数字经济浪潮中&#xff0c;区块链技术以其独特的去中心化特性&#xff0c;正在逐渐改变我们的生活和工作方式。其中&#xff0c;区块链游戏&#xff08;链游&#xff09;作为新兴的领域&#xff0c;正以其独特的优势吸引着全球玩家的目光。在这样一个背景下&#xff0…

Windows系统安装WinSCP结合内网穿透实现公网远程SSH本地服务器

List item 文章目录 1. 简介2. 软件下载安装&#xff1a;3. SSH链接服务器4. WinSCP使用公网TCP地址链接本地服务器5. WinSCP使用固定公网TCP地址访问服务器 1. 简介 ​ Winscp是一个支持SSH(Secure SHell)的可视化SCP(Secure Copy)文件传输软件&#xff0c;它的主要功能是在本…

Win11又来「重大」更新!

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站ai人工智能工具 更多资源欢迎关注 Windows 11预览通道的22635.3420版本迎来了几个比较大的改进&#xff0c;主要有三个方面&#xff1a; …

LeetCode题练习与总结:不同路径--62

一、题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&…

【教学类-51-01】20240411动物皮毛图片的彩色打印PDF制作(一页两张图片,2个表格)

作品展示 背景需求&#xff1a; 为了便于快速做出A4两份图片的效果&#xff0c;设计以下代码&#xff0c;进行图片的PDF合成打印 代码参考&#xff1a; 【教学类-50-06】20240410“数一数”4类星号图片制作PDF学具-CSDN博客文章浏览阅读531次&#xff0c;点赞8次&#xff0c;收…

第3章 存储系统(2)

3.3 主存储器与CPU连接 3.3.1 连接原理 现代计算机的MAR和MDR都在CPU内部。 (1)主存储器通过数据总线,地址总线,控制总线与CPU连接。 (2)数据传输率数据总线宽度*总线频率。 (4)控制总线(读写线)控制读写操作。 3.3.2 主存的扩展 数据总线宽度等于存储字长 1.位扩展法【增加…

性能优化“万金油”:缓存Cache

1、首次请求数据时,先从缓存中获取,如果没有,则继续向数据库中获取。获取到数据后,将数据保存到缓存中。再次请求数据,一样先从缓存中获取,成功获取,“缓存命中”。多次请求中,命中次数占全部请求次数的比例,叫“命中率”。如果数据源的数据发生变化,而缓存中的数据没…

4.11作业

服务器端 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器端类 #include<QMessageBox> //消息对话框 #include<QTcpSocket> //客户端类 #include<QList> //链表容器QT_BEGIN_NAMESPACE namespace Ui { cla…

【JAVASE】抽象类和接口及其抽象类和接口的区别

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;再无B&#xff5e;U&#xff5e;G-CSDN博客 目标&#xff1a; 1. 抽象类 2. 接口 3. Object 类 1. &am…

linux中rpm包与deb包的区别及使用

文章目录 1. rpm与deb的区别2. deb软件包的格式和使用2.1 deb软件包命令遵行如下约定2.2 dpkg命令2.3 apt-命令 3. Unix和Linux的区别Reference 1. rpm与deb的区别 有的系统只支持使用rpm包安装&#xff0c;有的只支持deb包安装&#xff0c;混乱安装会导致系统问题。 关于rpm和…

C语言——实践小游戏(贪吃蛇)代码版

大家好久不见&#xff0c;我是残念我回来了&#xff0c;希望在你看完之后&#xff0c;能对你有所帮助&#xff0c;有什么不足请指正&#xff01;共同学习交流 本文由&#xff1a;残念ing原创CSDN首发&#xff0c;如需要转载请通知 个人主页&#xff1a;残念ing-CSDN博客&#x…

Linux网络名称空间的调试方法全面分析

Linux网络名称空间是一种广泛使用的技术&#xff0c;用于隔离网络环境&#xff0c;特别是在容器化和微服务架构中&#x1f4e6;。然而&#xff0c;随着网络名称空间的广泛应用&#xff0c;开发者和系统管理员可能会遇到需要调试网络名称空间配置和性能的情况&#x1f50d;。本文…

智能驾驶的关键技术:自主泊车轨迹规划

智能驾驶的关键技术&#xff1a;自主泊车轨迹规划 搭载先进的车载传感器、控制器、执行器等装置&#xff0c;具备复杂环境感知、智能化决策等功能的车辆&#xff0c;我们称之其为智能车。智能车的车载决策规划模块用于生成车辆的行驶行为&#xff0c;直接体现车辆行驶的智慧水…

【Tars-go】腾讯微服务框架学习使用01--初始化服务

1 初始INIT-Demo运行 按照官网描述 go get 安装框架依赖 # < go 1.16 go get -u github.com/TarsCloud/TarsGo/tars/tools/tarsgo go get -u github.com/TarsCloud/TarsGo/tars/tools/tars2go # > go 1.16 go install github.com/TarsCloud/TarsGo/tars/tools/tarsgolat…

【网安小白成长之路】6.pkachu、sql-lbas、upload-lbas靶场搭建

&#x1f42e;博主syst1m 带你 acquire knowledge&#xff01; ✨博客首页——syst1m的博客&#x1f498; &#x1f51e; 《网安小白成长之路(我要变成大佬&#x1f60e;&#xff01;&#xff01;)》真实小白学习历程&#xff0c;手把手带你一起从入门到入狱&#x1f6ad; &…

python---3--sort、lambdalen(list1)、sorted_numbers = sorted(numbers)、list.sort()

学习目标&#xff1a; lambda len(list1) sorted_numbers sorted(numbers)list.sort() 目录 学习目标&#xff1a; 学习内容&#xff1a; 匿名函数 lambda表达式 lambda [参数]: 函数 不需要return len(list1) sorted_numbers sorted(numbers) list.sort(keyNone, r…

进程通信(管道)

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 前言 两个进程直接可以进行数据的直接传递吗&#xff1f;答案显然是不可以。 为什么&#xff1f; 我们简单概括就是进程具有独立性&#xff0c;如果说有两个进程&#xff0c;第一个进程可以访问第二个进程的数据&#xff…

ssm“健康早知道”微信小程序

采用技术 ssm“健康早知道”微信小程序的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringMVCMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 需求分析 利用ssm、Java、MyEclipse和mysql数据库等知识点&#xff0c;结合相关设…

【CSDN创作优化2】内嵌图片 `<img>` 标签`height`和`width`属性

【CSDN创作优化2】内嵌图片 标签height和width属性 写在最前面<img> 标签简介控制图像尺寸&#xff1a;height和width属性实例为什么要指定height和width注意事项 使用百分比进行响应式设计小结 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字…