Creator | Asset Bundle 全解析

【本文参与征文活动】

本文中 Asset Bundle 简称AB包

外部资源 = 不在本AB包目录内的资源(包括内置AB包和其他AB包)

01

AB包的定义

AB包作为资源模块化工具,允许开发者按照项目需求将贴图、脚本、场景等资源划分在多个AB包中,然后在游戏运行过程中,按照需求去加载不同的AB包,以减少启动时需要加载的资源数量,从而减少首次下载和加载游戏时所需的时间,同时可以减少内存占用
AB包可以按需求随意放置,比如可以放在远程服务器、本地、或者小游戏平台的分包,也可以跨项目复用,用于加载子项目中的AB包

02

配置AB包

AB包是以 文件夹 为单位进行配置的,而且 不支持嵌套

配置方法:

① 将项目中的场景、资源、代码等内容按照需求划分到不同的文件夹

② 单击该文件夹,属性检查器 中就会出现一个 配置为 Bundle 的选项,勾选后会出现如下图的配置项

配置项

功能说明

Bundle 名称

AB包构建后的名称,默认会使用这个文件夹的名字,可根据需要修改

Bundle 优先级

Creator 开放了 10 个可供配置的优先级,构建时将会按照优先级 **从大到小 **的顺序对AB包依次进行构建

目标平台

不同平台可使用不同的配置,构建时将根据对应平台的设置来构建AB包

压缩类型

决定AB包最后的输出形式,包括 默认、无压缩、合并所有 JSON、小游戏分包、Zip 五种压缩类型

配置为远程包

是否将AB包配置为远程包,不支持 Web 平台

若勾选了该项,则AB包在构建后会被放到 remote 文件夹,你需要将整个 remote 文件夹放到远程服务器上

配置完成后点击右上方的 应用 按钮,这个文件夹就被配置为AB包了,然后在 构建发布 面板选择对应的平台进行构建

注意:

① Creator 有 4 个 内置AB包,包括 resources、internal、main、start-scene,在设置 Bundle 名称 时请不要使用这四个名称

② 小游戏分包只能放在本地,不能配置为远程包,所以当 压缩类型 设置为 小游戏分包 时,配置为远程包 项不可勾选

③ Zip 压缩类型主要是为了降低网络请求数量,如果放在本地,不用网络请求,则没什么必要,所以要求与 配置为远程包 搭配使用

03

内置AB包

构建后,项目中所有的资源都会被分类放到AB包中, 其中自定义AB包中的资源放到对应的AB包中,其他的资源则会被分类放到 4 个内置AB包中

内置AB包

功能说明

优先级

internal

存放所有内置资源以及其依赖资源

11

main

存放所有在** 构建发布** 面板的 参与构建场景 中勾选的场景以及其依赖资源

7

resources

存放 resources目录下的所有资源以及其依赖资源

8

start-scene

如果在 构建发布 面板中勾选了 初始场景分包,则首场景将会被构建到 start-scene 中

9

注意:

start-scene 目前仅支持小游戏平台,如果在 构建发布 面板中勾选 初始场景分包,则首场景会被放到内置AB包的 start-scene 中,从而实现分离首场景

04

构建

在构建时,配置为AB包的 文件夹中****的资源(包含场景、代码和其他资源)以及 **文件夹外的相关依赖资源 **都会被合并到同一个AB包中

构建完成后,该文件夹会被打包到对应平台发布包目录下的 **assets **文件夹中

但有以下两种特殊情况:

① 配置AB包时,若勾选了 配置为远程包,则这个文件夹会被打包到对应平台发布包目录下的 remote 文件夹中

② 配置AB包时,若设置了 压缩类型小游戏分包,则这个文件夹会被打包到对应平台发布包目录下的 **subpackages **文件夹中

assets、remote、subpackages 这三个文件夹中包含的每个文件夹都是一个AB包

assets:

remote:

05

AB包的构造

在构建时,配置为AB包的文件夹中的所有 代码资源,会进行以下处理:

  • 代码:文件夹中的所有代码会根据发布平台合并成一个 index.js 或 game.js 的入口脚本文件,并从主包中剔除

  • 资源:文件夹中的所有资源以及文件夹外的相关依赖资源都会放到 import 或 native 目录下

  • 资源配置:所有资源的配置信息包括路径、类型、版本信息都会被合并成一个 config.json 文件

构建后生成的AB包目录结构如下图所示:

  • import: 资源描述 json 的存放目录

  • **native **:资源文件的存放目录

  • config.json:所有资源的配置信息,包括路径、类型、版本信息

  • index.js:文件夹中的所有代码

06

AB包的优先级和资源引用关系

当文件夹设置为AB包后,构建后 Creator 会将 **文件夹中的资源 **以及 **文件夹外的相关依赖资源 **都合并到同一个AB包中

假设AB包 A(也可以是内置的AB包)中的 asset X 同时被AB包 B、C、D 引用, 按照刚才所说,构建后,AB包 A、B、C、D 中会分别存放一份 asset X,这明显违背减小包体的原则,那么构建后 asset X 究竟该放到哪个AB包中呢?

此时就需要通过调整AB包的优先级来决定资源的存放位置

Creator 开放了 10 个可供配置的优先级,编辑器在构建时将会按照优先级 从大到小 的顺序对AB包依次进行构建(别忘了 Creator 的内置AB包)

了解了AB包的优先级后我们再来讨论下 asset X 的存放情况

① AB包 **优先级相同 **的情况下,引用外部资源时,构建后,该资源会在每个AB包中复制一份,此时不同的AB包之间没有依赖关系,可按任意顺序加载,但由于该资源会被复制N份,这样会引起包体的增大

② AB包 **优先级不同 **的情况下,引用外部资源时,构建后,该资源会放在 **优先级高 **的AB包(包括内置AB包)中,**优先级低 **的AB包只会存储一条记录信息。此时优先级低的AB包会 **依赖 **优先级高的AB包。如果想在优先级低的AB包中加载此资源,必须在加载优先级低的AB包 之前 先加载(loadBundle)优先级高的AB包

因此项目中那些频繁被其他AB包使用的资源,应该放置在优先级较高的AB包中,比如 内置AB包 main,或者为了减少首包的大小,可以放到 自定义AB包中,然后修改该AB包的 Bundle 优先级为较高的值,在合适的时机调用 cc.assetManager.loadBundle

07

AB包中的脚本

Creator 支持脚本分包,如果AB包中包含脚本文件,则所有脚本会被合并为一个 js 文件,并从主包中剔除,在加载AB包时,就会去加载这个 js 文件

注意:

  1. 有些平台不允许加载远程的脚本文件,例如微信小游戏,在这些平台上,Creator 会将AB包中的代码拷贝到src/scripts 目录下,从而保证正常加载

  2. 不同AB包中的脚本建议最好不要互相引用,否则可能会导致在运行时找不到对应脚本,如果需要引用某些类或变量,可以将该类和变量暴露在一个你自己的全局命名空间中,从而实现共享,类似:cc[“MyBundle”] = MyBundle;

注意:
虽然脚本文件也是资源的一种,但是脚本文件只会合并到本AB包中的 index.js,并不会像其他资源一样复制到其他AB包中,无论优先级是多少

node_modules中的第三方脚本文件只会合并到 **内置AB包 main **中的 index.js

注意:

在通过 API(loadBundle)加载AB包时,就会加载AB包中的 index.js,一旦加载后,就会一直存在内存中,移除AB包(removeBundle)也不会释放,再次加载AB包时也不会重新加载脚本

08

加载AB包

1加载AB包

引擎提供了一个统一的 API cc.assetManager.loadBundle 来加载AB包,加载时需要传入AB包在配置面板中的 Bundle 名称 或者AB包的 url

但当你复用其他项目的AB包时,则只能通过 url进行加载

使用方法如下:

cc.assetManager.loadBundle("MyBundle", (err: Error, bundle: cc.AssetManager.Bundle) => {

通过 url 加载AB包,其过程和 cc.assetManager.loadRemote 相同,加载成功后,该AB包会以 **文件夹 **的形式保存在本地缓存目录,如 win32 模拟器:

**cacheList.json **文件中以 { url: object } 的形式记录远程资源信息,以后再次加载该远程资源时则直接使用缓存中的资源文件

cc.assetManager.loadBundle 还支持传入用户空间中的路径来加载用户空间中的AB包

通过对应平台提供的 **下载 **接口将AB包提前下载到用户空间中,然后再使用 loadBundle 进行加载,开发者就可以完全自己管理AB包的下载与缓存过程,更加灵活

// 提前下载某个 Asset Bundle 到用户空间 pathToBundle 目录下。需要保证用户空间下的 Asset Bundle 和对应原始 Asset Bundle 的结构和内容完全一样

注意:

在配置AB包时,若勾选了 配置为远程包,那么构建时请在 构建发布 面板中填写 资源服务器地址

通过 cc.assetManager.bundles 可以看到当前内存中已加载 bundle 的集合以及 bundle 的具体信息

console.log(cc.assetManager.bundles);

2AB包的版本

AB包在更新上延续了 Creator 的 MD5 方案

当你需要更新远程服务器上的AB包时,请在 构建发布 面板中勾选 MD5 Cache 选项,此时构建出来的AB包中的 config.json 文件名会附带 Hash 值

如图所示:

在加载AB包时 不需要 额外提供对应的 Hash 值,Creator 会在 settings.js中查询对应的 Hash 值,并自动做出调整
但如果你想要将相关版本配置信息存储在服务器上,启动时动态获取版本信息以实现热更新,你也可以手动指定一个版本 Hash 值并传入 loadBundle 中,此时将会以传入的 Hash 值为准:

cc.assetManager.loadBundle("MyBundle", { version: "fbc07" }, (err: Error, bundle: cc.AssetManager.Bundle) => {

这样就能绕过缓存中的老版本文件,重新下载最新版本的AB包

3加载AB包中的资源

在通过 API(loadBundle )加载AB包时,引擎并 **没有加载 **AB包中的所有资源,而是只 **加载 **AB包的 资源清单(config.json),以及包含的 所有脚本(index.js)

即AB包中的脚本会被加载到内存中,但是AB包中的资源并不会加载到内存中,如果需要加载其中的资源,还需要 bundle.load(“prefab”)

当AB包加载完成后,会返回一个 cc.AssetManager.Bundle 类的实例,这个实例就是AB包 API 的主要入口,我们可以通过实例上的 load 方法来加载AB包中的资源,此方法的参数与 cc.resources.load 相同,只需要传入资源相对AB包的路径即可,但需要注意的是,路径的结尾处 不能 包含文件扩展名

// 加载 prefab

AB包还提供了 loadDir 方法来批量加载相同目录下的多个资源,此方法的参数与 cc.resources.loadDir 相似,只需要传入该目录相对AB包的路径即可

// 加载 textures 目录下的所有资源

注意:

cc.resources 和 cc.AssetManager.Bundle 分别提供了 load 和loadDir 接口,但 load 后资源的引用计数为 0,而 loadDir 后资源的引用计数为 1,且都需要我们手动管理

cc.resources.load("HelloWorld",  (err, spriteFrame) => {            

4预加载资源

为了尽可能缩短下载时间,我们可以使用预加载

Asset Manager 中的大部分加载接口包括 load、loadDir、loadScene 都有其对应的预加载版本

加载接口与预加载接口所用的参数是完全一样的,两者的区别在于:

  1. 预加载只会下载资源,不会对资源进行解析和初始化操作

  2. 预加载在加载过程中会受到更多限制,例如最大下载并发数会更小

  3. 预加载的下载优先级更低,当多个资源在等待下载时,预加载的资源会放在最后下载

  4. 因为 预加载没有做任何解析操作,所以当所有的预加载完成时,不会返回任何可用资源

以上优化手段充分 降低了预加载的性能损耗确保了游戏体验顺畅,开发者可以充分利用游戏过程中的网络带宽缩短后续资源的加载时间

因为预加载没有去解析资源,所以需要在预加载完成后配合加载接口进行资源的解析和初始化,来完成资源加载

注意:

加载不需要等到预加载完成后再调用,开发者可以在任何时候进行加载。正常加载接口会直接复用预加载过程中已经下载好的内容,缩短加载时间

预加载只会去 **下载 **必要的资源,并 不会进行资源的反序列化和初始化工作,也就不会将资源放入内存(cc.assetManager.assets)中,所以性能消耗更小,确保了游戏体验流畅

AB包中提供了preload、preloadDir、preloadScene 接口用于预加载AB包中的资源,具体的使用方式和 Asset Manager 一致

5加载场景

AB包提供了 loadScene 方法用于加载指定 bundle 中的场景,你只需要传入 场景名 即可

loadScene 与 cc.director.loadScene 不同的地方在于 loadScene只会加载指定 AB包中的场景,而不会运行场景,你还需要使用 cc.director.runScene 来运行场景

// 加载场景

6获取AB包

当AB包被加载过之后,会被缓存下来,此时开发者可以使用AB包名称来获取该 bundle

let bundle = cc.assetManager.getBundle("MyBundle");

09

释放AB包

1释放AB包中的资源

在资源加载完成后,所有的资源都会被临时缓存到 cc.assetManager 中,以避免重复加载。当然,缓存中的资源也会占用内存,有些资源如果不再需要用到,可以通过以下三种方式进行释放:

① 使用常规的 cc.assetManager.releaseAsset 方法进行释放

bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {

② 使用AB包提供的 release 方法,通过传入路径和类型进行释放,只能释放在AB包中的单个资源,参数可以与 AB包的 load 方法中使用的参数一致

bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {

③使用AB包提供的 releaseAll 方法,此方法与 cc.assetManager.releaseAll 相似,releaseAll 方法会释放所有属于该 bundle 的资源(包括在AB包中的资源以及其外部的相关依赖资源),请慎重使用

bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {

意:

在释放资源时,Creator 会自动处理该资源的依赖资源,开发者不需要对其依赖资源进行管理

2移除AB包

在加载了AB包之后,此 bundle 会一直存在整个游戏过程中,除非开发者手动移除

当手动移除了某个不需要的 bundle,那么此 bundle 的缓存也会被移除,如果需要再次使用,则必须再重新加载一次

let bundle = cc.assetManager.getBundle("MyBundle");

注意:

在移除AB包时,并不会释放该 bundle 中加载过的资源

如果需要释放,请先使用AB包的 release / releaseAll方法:

let bundle = cc.assetManager.getBundle("MyBundle");
31赞

yage 放个demo 看着更清晰 哈哈

我其实就想问问,微信小游戏,脚本如何热更。只有资源可以热更?

提问 我在

toGame2() {
cc.log("================>toGame1")
cc.assetManager.loadBundle(‘Game1’, (err, bundle) => {
cc.log("================>toGame1 loadOver")
bundle.loadScene(‘Game2’, function (err, scene) {
cc.director.runScene(scene);
});
});
}
onClear() {
cc.log(“clear”);
var testBundle = cc.assetManager.getBundle(‘Game1’);
if (!testBundle) {
cc.log("无法清除 ");
return;
}
cc.log(testBundle)
testBundle.releaseAll();
cc.assetManager.removeBundle(testBundle);
}
我点一次onClear 的时候 把这个bundle 释放了 并销毁了 但是 我再去加载 这个bundle 的时候 就回报 404 的错 本地浏览没问题 达成web包就不行了

404? 请求不到包?

是的 bundle资源请求路劲会变成 访问地址的路劲 而不是 远程包的地址路劲

第一次load后,浏览器会做缓存,再次调用 cocos会先检查缓存内是否存在,你这情况看起来像是,缓存列表里有这个文件,所以没有去调用远程的,但是本地文件却被清理过了
我猜测是这么个过程,那问题就是,为什么浏览器缓存会被清理····

顶, 战略性mark

cc.resources 和 cc.AssetManager.Bundle 分别提供了 load 和loadDir 接口,但 load 后资源的引用计数为 0,而 loadDir 后资源的引用计数为 1,且都需要我们手动管理

loadDir后资源引用计数为1?

你可以试试··················

提问,可以配置多个平台的AB包吗,那里配置好像只能选某个平台,假如我选了这个平台,另外一个平台也需要配置是怎么办,重新配置,然后再打包?

敢问为什么loadDir后,资源的refCount会被自动加1?

这个问题,我也想问问····引擎组的大大们

哈哈,秒回啊,刚看到你的公众号,有此疑问

刚好开着·····我也是有这个疑惑的····

我把这个问题拿出来跟朋友讨论了下,他告诉我loadDir不指定资源类型时,会将texture2D和spriteframe一起加载出来,所以texture2D的资源会被spriteframe引用到,所以会成为1,经过测试,确实是这样

不指定类型:
cc_Texture2D _ref: 1
cc_SpriteFrame _ref: 0
指定类型:
都是 0

哈,感谢提醒哈,回头有时间,我把内容改下~

我那朋友比较厉害,嘿嘿

嗯嗯,大佬~学到了~
loadDir不指定类型的时候,会加载文件夹内的所有资源,其中 原生 资源会被 非原生 资源引用,所以引用计数为1

1赞

牛批牛批,mark mark