Cocos Creator 新资源管理系统剖析

COCOS

共 23131字,需浏览 47分钟

 ·

2021-01-18 21:42

v2.4 开始,Cocos Creator 使用 AssetBundle 完全重构了资源底层,提供了更加灵活强大的资源管理方式,也解决了之前版本资源管理的痛点(资源依赖与引用),本文将带你深入了解 Creator 的新资源底层。

目录

  • 资源与构建
  • 理解与使用 AssetBundle
  • 新资源框架剖析
    • 加载管线
    • 文件下载
    • 文件解析
    • 依赖加载
    • 资源释放

1.资源与构建

1.1 creator 资源文件基础

在了解引擎如何解析、加载资源之前,我们先来了解一下这些资源文件(图片、Prefab、动画等)的规则。

在 creator 项目目录下有几个与资源相关的目录:

  • assets 所有资源的总目录,对应 creator 编辑器的资源管理器
  • library 本地资源库,预览项目时使用的目录
  • build 构建后的项目默认目录

assets 目录下,creator 会为每个资源文件和目录生成一个同名的.meta文件,meta 文件是一个 json 文件,记录了资源的版本、uuid 以及各种自定义的信息(在编辑器的属性检查器中设置),比如 prefab 的 meta 文件,就记录了我们可以在编辑器修改的 optimizationPolicyasyncLoadAssets 等属性。

{
  "ver""1.2.7",
  "uuid""a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
  "optimizationPolicy""AUTO",     // prefab创建优化策略
  "asyncLoadAssets"false,         // 是否延迟加载
  "readonly"false,
  "subMetas": {}
}

library 目录下的 imports 目录,资源文件名会被转换成 uuid,并取 uuid 前2个字符进行目录分组存放,creator 会将所有资源的 uuidassets 目录的映射关系,以及资源和 meta 的最后更新时间戳放到一个名为 uuid-to-mtime.json 的文件中,如下所示:

{
  "9836134e-b892-4283-b6b2-78b5acf3ed45": {
    "asset": 1594351233259,
    "meta": 1594351616611,
    "relativePath""effects"
  },
  "430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
    "asset": 1594351233254,
    "meta": 1594351616643,
    "relativePath""effects\\__builtin-editor-gizmo-line.effect"
  },
  ...
}

assets 目录下的资源相比,library 目录下的资源合并了 meta 文件的信息。文件目录则只在 uuid-to-mtime.json 中记录,library 目录并没有为目录生成任何东西。

1.2 资源构建

在项目构建之后,资源会从 library 目录下移动到构建输出的 build 目录中,基本只会导出参与构建的场景和 resources 目录下的资源,及其引用到的资源。脚本资源会由多个 js 脚本合并为一个 js,各种 json 文件也会按照特定的规则进行打包。

我们可以在 Bundle 的配置界面和项目的构建界面,为 Bundle 和项目设置:

1.2.1 图片、图集、自动图集

  • https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html

导入编辑器的每张图片都会对应生成一个 json 文件,用于描述 Texture 的信息。

如下所示,默认情况下项目中所有的 Texture2Djson 文件会被压缩成一个,如果选择无压缩,则每个图片都会生成一个 Texture2Djson 文件。

{
  "__type__""cc.Texture2D",
  "content""0,9729,9729,33071,33071,0,0,1"
}

如果将纹理的 Type 属性设置为 Sprite,Creator 还会自动生成了 SpriteFrame 类型的 json 文件。

图集资源除了图片外,还对应一个图集 json,这个 json 包含了 cc.SpriteAtlas 信息,以及每个碎图的 SpriteFrame 信息。

自动图集在默认情况下只包含了 cc.SpriteAtlas 信息,在勾选内联所有 SpriteFrame 的情况下,会合并所有 SpriteFrame

1.2.2 Prefab与场景

  • https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/scene-managing.html

场景资源与 Prefab 资源非常类似,都是一个描述了所有节点、组件等信息的 json文件,在勾选内联所有 SpriteFrame 的情况下,Prefab 引用到的 SpriteFrame 会被合并到 prefab 所在的 json 文件中。

如果一个 SpriteFrame 被多个 prefab 引用,那么每个 prefabjson 文件都会包含该 SpriteFrame 的信息。

而在没有勾选内联所有 SpriteFrame 的情况下,SpriteFrame 会是单独的 json文件。

1.2.3 资源文件合并规则

当 Creator 将多个资源合并到一个 json 文件中,我们可以在 config.json 中的 packs 字段找到被打包的资源信息,一个资源有可能被重复打包到多个 json 中。

下面举一个例子,展示在不同的选项下,creator 的构建规则:

  • a.png 一个单独的Sprite类型图片
  • dir/b.png、c.png、AutoAtlas dir目录下包含2张图片,以及一个AutoAtlas
  • d.png、d.plist 普通图集
  • e.prefab 引用了SpriteFrame a和b的prefab
  • f.prefab 引用了SpriteFrame b的prefab

下面是按不同规则构建后的文件,可以看到,无压缩的情况下生成的文件数量是最多的,不内联的文件会比内联多,但内联可能会导致同一个文件被重复包含,比如 ef 这两个 Prefab 都引用了同一个图片,这个图片的 SpriteFrame.json 会被重复包含,合并成一个 json 则只会生成一个文件。

默认选项在绝大多数情况下都是一个不错的选择。

如果是 web 平台,建议勾选内联所有 SpriteFrame 这可以减少网络 io,提高性能。

原生平台则不建议勾选,这可能会增加包体大小以及热更时要下载的内容。

对于一些紧凑的 Bundle(比如加载该 Bundle 就需要用到里面所有的资源),我们可以配置为合并所有的 json

2. 理解与使用 Asset Bundle

2.1 创建 Bundle

Asset Bundle 是 creator 2.4 之后的资源管理方案,简单地说,就是通过目录来对资源进行规划,按照项目的需求将各种资源放到不同的目录下,并将目录配置成 Asset Bundle能够起到以下作用:

  • 加快游戏启动时间
  • 减小首包体积
  • 跨项目复用资源
  • 方便实现子游戏
  • 以Bundle为单位的热更新

Asset Bundle 的创建非常简单,只要在目录的属性检查器中勾选配置为 bundle 即可,其中的选项官方文档都有比较详细的介绍。

其中关于压缩的理解,文档并没有详细的描述,这里的压缩指的并不是 zip 之类的压缩,而是通过 packAssets 的方式,把多个资源的 json 文件合并到一个,达到减少 io 的目的。

在选项上打勾非常简单,真正的关键在于如何规划 Bundle。规划的原则在于减少包体、加速启动以及资源复用。根据游戏的模块来规划资源是比较不错的选择,比如按子游戏、关卡副本、或者系统功能来规划。

Bundle 会自动将文件夹下的资源,以及文件夹中引用到的其它文件夹下的资源打包(如果这些资源不是在其它 Bundle 中),如果我们按照模块来规划资源,很容易出现多个 Bundle 共用了某个资源的情况。

可以将公共资源提取到一个 Bundle 中,或者设置某个 Bundle 有较高的优先级,构建 Bundle 的依赖关系,否则这些资源会同时放到多个 Bundle 中(如果是本地 Bundle,这会导致包体变大)。

2.2 使用 Bundle

  • 关于加载资源 https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html
  • 关于释放资源 https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html

Bundle 的使用也非常简单,如果是 resources 目录下的资源,可以直接使用 cc.resources.load 来加载。

cc.resources.load("test assets/prefab"function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});

如果是其他自定义 Bundle(本地 Bundle 或远程 Bundle 都可以用 Bundle 名加载),可以使用 cc.assetManager.loadBundle 来加载 Bundle,然后使用加载后的 Bundle 对象,来加载 Bundle 中的资源。

对于原生平台,如果 Bundle 被配置为远程包,在构建时需要在构建发布面板中填写资源服务器地址。

cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

原生或小游戏平台下,我们还可以这样使用 Bundle:

如果要加载其它项目的远程 Bundle,则需要使用 url 的方式加载(其它项目指另一个 cocos 工程)

如果希望自己管理 Bundle 的下载和缓存,可以放到本地可写路径,并传入路径来加载这些 Bundle

// 当复用其他项目的 Asset Bundle 时
cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

// 原生平台
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

// 微信小游戏平台
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

其它注意项:

  • 加载  Bundle 仅仅只是加载了 Bundle 的配置和脚本而已,Bundle 中的其它资源还需要另外加载
  • 目前原生的 Bundle 并不支持 zip 打包,远程包下载方式为逐文件下载,好处是操作简单,更新方便,坏处是 io 多,流量消耗大
  • 不同 Bundle 下的脚本文件不要重名
  • 一个 Bundle A 依赖另一个 Bundle B,如果 B 没有被加载,加载 A 时并不会自动加载 B,而是在加载 A 中依赖 B 的那个资源时报错

3. 新资源框架剖析

v2.4重构后的新框架代码更加简洁清晰,我们可以先从宏观角度了解一下整个资源框架,资源管线是整个框架最核心的部分,它规范了整个资源加载的流程,并支持对管线进行自定义。

公共文件:

  • helper.js 定义了一堆公共函数,如 decodeUuidgetUuidFromURLgetUrlWithUuid 等等
  • utilities.js 定义了一堆公共函数,如 getDependsforEachparseLoadResArgs 等等
  • deserialize.js 定义了 deserialize 方法,将 json 对象反序列化为 Asset 对象,并设置其 __depends__ 属性
  • depend-util.js 控制资源的依赖列表,每个资源的所有依赖都放在 _depends 成员变量中
  • cache.js 通用缓存类,封装了一个简易的键值对容器
  • shared.js 定义了一些全局对象,主要是 CachePipeline 对象,如加载好的 assets、下载完的 files 以及 bundles

Bundle 部分:

  • config.js bundle 的配置对象,负责解析 bundleconfig 文件
  • bundle.js bundle 类,封装了 config 以及加载卸载 bundle 内资源的相关接口
  • builtins.js 内建 bundle 资源的封装,可以通过 cc.assetManager.builtins 访问

管线部分:

  • CCAssetManager.js 管理管线,提供统一的加载卸载接口

  • 管线框架

    • pipeline.js 实现了管线的管道组合以及流转等基本功能
    • task.js 定义了一个任务的基本属性,并提供了简单的任务池功能
    • request-item.js 定义了一个资源下载项的基本属性,一个任务可能会生成多个下载项
  • 预处理管线

    • urlTransformer.js parse 将请求参数转换成 RequestItem 对象(并查询相关的资源配置),combine  负责转换真正的 url
    • preprocess.js 过滤出需要进行 url 转换的资源,并调用 transformPipeline
  • 下载管线

    • download-dom-audio.js  提供下载音效的方法,使用 audio 标签进行下载
    • download-dom-image.js 提供下载图片的方法,使用 Image 标签进行下载
    • download-file.js 提供下载文件的方法,使用 XMLHttpRequest 进行下载
    • download-script.js 提供下载脚本的方法,使用 script 标签进行下载
    • downloader.js 支持下载所有格式的下载器,支持并发控制、失败重试、
  • 解析管线

    • factory.js 创建 BundleAssetTexture2D 等对象的工厂
    • fetch.js 调用 packManager 下载资源,并解析依赖
    • parser.js 对下载完成的文件进行解析
  • 其它releaseManager.js 提供资源释放接口、负责释放依赖资源以及场景切换时的资源释放cache-manager.d.ts 在非 WEB 平台上,用于管理所有从服务器上下载下来的缓存pack-manager.js 处理打包资源,包括拆包,加载,缓存等等

3.1 加载管线

creator 使用管线(pipeline)来处理整个资源加载的流程,这样的好处是解耦了资源处理的流程,将每一个步骤独立成一个单独的管道,管道可以很方便地进行复用和组合,并且方便了我们自定义整个加载流程,我们可以创建一些自己的管道,加入到管线中,比如资源加密。

AssetManager 内置了3条管线,普通的加载管线、预加载、以及资源路径转换管线,最后这条管线是为前面两条管线服务的。

    // 正常加载
    this.pipeline = pipeline.append(preprocess).append(load);
    // 预加载
    this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
    // 转换资源路径
    this.transformPipeline = transformPipeline.append(parse).append(combine);

3.1.1 启动加载管线【加载接口】

接下来,我们看一下一个普通的资源是如何加载的,比如最简单的 cc.resource.load,在 bundle.load 方法中,调用了 cc.assetManager.loadAny,在 loadAny 方法中,创建了一个新的任务,并调用正常加载管线 pipelineasync 方法执行任务。

注意要加载的资源路径,被放到了 task.input中、options是一个对象,对象包含了 type、bundle__requestType__ 等字段。

    // bundle类的load方法
    load (paths, type, onProgress, onComplete) {
        var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
        cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH, typetype, bundle: this.name }, onProgress, onComplete);
    },
    
    // assetManager的loadAny方法
    loadAny (requests, options, onProgress, onComplete) {
        var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
        
        options.preset = options.preset || 'default';
        let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
        pipeline.async(task);
    },

pipeline 由两部分组成 preprocessloadpreprocess 由以下管线组成 preprocesstransformPipeline { parse、combine }preprocess 实际上只创建了一个子任务,然后交由 transformPipeline 执行。对于加载一个普通的资源,子任务的 inputoptions 与父任务相同。

    let subTask = Task.create({input: task.input, options: subOptions});
    task.output = task.source = transformPipeline.sync(subTask);

3.1.2 transformPipeline 管线【准备阶段】

transformPipelineparsecombine 两个管线组成,parse 的职责是为每个要加载的资源生成 RequestItem对象并初始化其资源信息(AssetInfo、uuid、config等):

先将 input转换成数组进行遍历,如果是批量加载资源,每个加载项都会生成RequestItem

如果输入的 itemobject,则先将 options 拷贝到 item 身上(实际上每个 item 都会是 object,如果是 string 的话,第一步就先转换成 object了)

  • 对于 UUID 类型的 item,先检查 bundle,并从 bundle 中提取 AssetInfo,对于 redirect 类型的资源,则从其依赖的 bundle 中获取 AssetInfo,找不到 bundle 就报错
  • PATH 类型和 SCENE 类型与 UUID 类型的处理基本类似,都是要拿到资源的详细信息
  • DIR 类型会从 bundle 中取出指定路径的信息,然后批量追加到 input尾部(额外生成加载项)
  • URL 类型是远程资源类型,无需特殊处理

RequestItem 的初始信息,都是从 bundle 对象中查询的,bundle 的信息则是从 bundle 自带的 config.json 文件中初始化的,在打包 bundle 的时候,会将 bundle 中的资源信息写入 config.json 中。

经过 parse 方法处理后,我们会得到一系列 RequestItem,并且很多 RequestItem 都自带了 AssetInfouuid 等信息,combine 方法会为每个 RequestItem 构建出真正的加载路径,这个加载路径最终会转换到 item.url 中。

3.1.3 load管线【加载流程】

load 方法做的事情很简单,基本只是创建了新的任务,在 loadOneAssetPipeline 中执行每个子任务。

loadOneAssetPipeline 如其函数名所示,就是加载一个资源的管线,它分为2步,fetchparse

fetch方法:

用于下载资源文件,由 packManager 负责下载的实现,fetch 会将下载完的文件数据放到 item.file 中。

parse方法:

用于将加载完的资源文件转换成我们可用的资源对象:

对于原生资源,调用 parser.parse 进行解析,该方法会根据资源类型调用不同的解析方法

  • import 资源调用 parseImport 方法,根据 json 数据反序列化出 Asset 对象,并放到 assets
  • 图片资源会调用 parseImageparsePVRTexparsePKMTex方法解析图像格式(但不会创建Texture对象)
  • 音效资源调用 parseAudio 方法进行解析
  • plist 资源调用 parsePlist 方法进行解析

对于其它资源,如果 uuidtask.options.__exclude__ 中,则标记为完成,并添加引用计数;否则,根据一些复杂的条件来决定是否加载资源的依赖。

3.2 文件下载

creator 使用 packManager.load 来完成下载的工作,当要下载一个文件时,有2个问题需要考虑:

该文件是否被打包了?比如由于勾选了内联所有 SpriteFrame,导致 SpriteFramejson 文件被合并到 prefab

当前平台是原生平台还是 web 平台?对于一些本地资源,原生平台需要从磁盘读取。

3.2.1 Web 平台的下载

web 平台的 download 实现如下:

  • 用一个 downloaders 数组来管理各种资源类型对应的下载方式
  • 使用 files 缓存来避免重复下载
  • 使用 _downloading 队列来处理并发下载同一个资源时的回调,并保证时序
  • 支持了下载的优先级、重试等逻辑

downloaders 是一个 map,映射了各种资源类型对应的下载方法,在 web 平台主要包含以下几类下载方法:图片类、文件类、字体类、声音类、视频类等等,具体的实现方式,感兴趣的可以点击「阅读原文」查看详情介绍和代码。

3.2.2 原生平台下载

原生平台的引擎相关文件可以在引擎目录的 resources/builtin/jsb-adapter/engine 目录下,资源加载相关的实现在 jsb-loader.js 文件中,这里的 downloader 重新注册了回调函数。

downloader.register({
    // JS
    '.js' : downloadScript,
    '.jsc' : downloadScript,

    // Images
    '.png' : downloadAsset,
    '.jpg' : downloadAsset,
    ...
});

在原生平台下,downloadAsset 等方法都会调用 download 来进行资源的下载,在资源下载之前会调用 transformUrlurl 进行检测,主要判断该资源是网络资源还是本地资源,如果是网络资源,是否已经下载过了。只有没下载过的网络资源,才需要进行下载。不需要下载的在文件解析的地方会直接读文件。

3.3 文件解析

loadOneAssetPipeline 中,资源会经过 fetchparse 两个管线进行处理,fetch 负责下载而 parse 负责解析资源,并实例化资源对象。在 parse 方法中调用了 parser.parse 将文件内容传入,解析成对应的 Asset 对象,并返回。

3.3.1 Web 平台解析

Web 平台下的 parser.parse 主要做的是对解析中的文件的管理,为解析中、解析完的文件维护一个列表,避免重复解析。同时维护了解析完成后的回调列表,而真正的解析方法在 parsers 数组中。

    parse (id, file, type, options, onComplete) {
        let parsedAsset, parsing, parseHandler;
        if (parsedAsset = parsed.get(id)) {
            onComplete(null, parsedAsset);
        }
        else if (parsing = _parsing.get(id)){
            parsing.push(onComplete);
        }
        else if (parseHandler = parsers[type]){
            _parsing.add(id, [onComplete]);
            parseHandler(file, options, function (err, data) {
                if (err) {
                    files.remove(id);
                } 
                else if (!isScene(data)){
                    parsed.add(id, data);
                }
                let callbacks = _parsing.remove(id);
                for (let i = 0, l = callbacks.length; i < l; i++) {
                    callbacks[i](err, data);
                }
            });
        }
        else {
            onComplete(null, file);
        }
    }

parsers 映射了各种类型文件的解析方法。

注意:在 parseImport 方法中,反序列化方法会将资源的依赖放到 asset.__depends__ 中,结构为数组,数组中每个对象包含3个字段,资源id uuidowner 对象prop 属性。比如一个 Prefab 资源,下面有2个节点,都引用了同一个资源,depends 列表需要为这两个节点对象分别记录一条依赖信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]

3.3.2 原生平台解析

在原生平台下,jsb-loader.js 中重新注册了各种资源的解析方法:

parser.register({
    '.png' : downloader.downloadDomImage,
    '.binary' : parseArrayBuffer,
    '.txt' : parseText,
    '.plist' : parsePlist,
    '.font' : loadFont,
    '.ExportJson' : parseJson,
    ...
});

图片的解析方法竟然是 downloader.downloadDomImage跟踪原生平台调试了一下,确实是调用的这个方法,创建了 Image 对象并指定 src 来加载图片,这种方式加载本地磁盘的图片也是可以的,但纹理对象又是如何创建的呢?

通过 Texture2D 对应的 json 文件,creator 在加载真正的原生纹理之前,就已经创建好了 Texture2D 这个 Asset 对象,而在加载完原生图片资源后,会将 Image 对象设置为 Texture2D 对象的 _nativeAsset,在这个属性的 set 方法中,会调用 initWithDatainitWithElement,这里才真正使用纹理数据创建了用于渲染的纹理对象。

var Texture2D = cc.Class({
    name: 'cc.Texture2D',
    extends: require('../assets/CCAsset'),
    mixins: [EventTarget],

    properties: {
        _nativeAsset: {
            get () {
                // maybe returned to pool in webgl
                return this._image;
            },
            set (data) {
                if (data._data) {
                    this.initWithData(data._data, this._format, data.width, data.height);
                }
                else {
                    this.initWithElement(data);
                }
            },
            override: true
        },

而对于 parseJsonparseTextparseArrayBuffer 等实现,这里只是简单地调用了文件系统读取文件而已。像一些拿到文件内容之后,需要进一步解析才能使用的资源呢?比如模型、骨骼等资源依赖二进制的模型数据,这些数据的解析在哪里呢?

没错,跟上面的 Texture2D 一样,都是放在对应的 Asset 资源本身,有些在_nativeAsset 字段的 setter 回调中初始化,而有些会在真正使用这个资源时才惰性地进行初始化。

像图集、Prefab 这些资源又是怎么初始化的呢?

Creator 还是使用 parseImport 方法进行解析,因为这些资源对应的类型是 import,原生平台下并没有覆盖这种类型对应的 parse 函数,而这些资源会直接反序列化成可用的 Asset 对象。

3.4 依赖加载

creator 将资源分为两大类,普通资源和原生资源,普通资源包括 cc.Asset 及其子类,如 cc.SpriteFramecc.Texture2Dcc.Prefab 等等。

原生资源包括各种格式的纹理、音乐、字体等文件,在游戏中我们无法直接使用这些原生资源,而是需要让 creator 将他们转换成对应的 cc.Asset 对象之后才能使用。

在 creator 中,一个 Prefab 可能会依赖很多资源,这些依赖也可以分为普通依赖和原生资源依赖,creator 的 cc.Asset 提供了 _parseDepsFromJson_parseNativeDepFromJson 方法来检查资源的依赖。loadDepends 通过 getDepends 方法搜集了资源的依赖。

loadDepends 创建了一个子任务来负责依赖资源的加载,并调用 pipeline 执行加载,实际上无论有无依赖需要加载,都会执行这段逻辑,加载完成后执行以下重要逻辑:

  • 初始化 assset :在依赖加载完成后,将依赖的资源赋值到 asset 对应的属性后调用 asset.onLoad
  • 将资源对应的 filesparsed 缓存移除,并缓存资源到 assets 中(如果是场景的话,不会缓存)
  • 执行 repeatItem.callbacks 列表中的回调(在 loadDepends 的开头构造,默认记录传入的 done 方法)

3.4.1 依赖解析

dependUtil 是一个控制依赖列表的单例,通过传入 uuidasset 对象来解析该对象的依赖资源列表,返回的依赖资源列表可能包含以下4个字段:

deps 依赖的 Asset资源nativeDep 依赖的原生资源preventPreloadNativeObject 禁止预加载原生对象,这个值默认是 falsepreventDeferredLoadDependents 禁止延迟加载依赖,默认为 false,对于骨骼动画、TiledMap 等资源为 trueparsedFromExistAsset 是否直接从 asset.__depends__ 中取出。

dependUtil 还维护了 _depends 缓存来避免依赖的重复查询,这个缓存会在首次查询某资源依赖时添加,当该资源被释放时移除。

3.5 资源释放

这一小节重点介绍在 Creator 中释放资源的三种方式以及其背后的实现,最后介绍在项目中如何排查资源泄露的情况。

3.5.1 Creator 的资源释放

Creator 支持以下3种资源释放的方式:

3.5.2 场景自动释放

当一个新场景运行的时候会执行 Director.runSceneImmediate 方法,这里调用了 _autoRelease 来实现老场景资源的自动释放(如果老场景勾选了自动释放资源)。

    runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
        // 省略代码...
        var oldScene = this._scene;
        if (!CC_EDITOR) {
            // 自动释放资源
            CC_BUILD && CC_DEBUG && console.time('AutoRelease');
            cc.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
            CC_BUILD && CC_DEBUG && console.timeEnd('AutoRelease');
        }

        // unload scene
        CC_BUILD && CC_DEBUG && console.time('Destroy');
        if (cc.isValid(oldScene)) {
            oldScene.destroy();
        }
        // 省略代码...
    },

最新版本的 _autoRelease 的实现非常简洁干脆,将持久节点的引用从老场景迁移到新场景,然后直接调用资源的 decRef 减少引用计数,而是否释放老场景引用的资源,则取决于老场景是否设置了 autoReleaseAssets

具体实现方式可戳「阅读原文」查看阅读相关代码。

3.5.3 引用计数和手动释放资源

剩下两种释放资源的方式,本质上都是调用 releaseManager.tryRelease 来实现资源释放,区别在于 decRef 是根据引用计数和 autoRelease 来决定是否调用 tryRelease,而 releaseAsset 是强制释放。

资源释放的完整流程大致如下图所示:

    // CCAsset.js 减少引用
    decRef (autoRelease) {
        this._ref--;
        autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
        return this;
    }

    // CCAssetManager.js 手动释放资源
    releaseAsset (asset) {
        releaseManager.tryRelease(asset, true);
    },

tryRelease 支持延迟释放和强制释放2种模式,当传入 force 参数为 true 时直接进入释放流程,否则 creator 会将资源放入待释放的列表中,并在 EVENT_AFTER_DRAW 事件中执行 freeAssets 方法真正清理资源。不论何种方式,资源会传入到 _free 方法处理,这个方法做了以下几件事情。

  • _toDelete中移除
  • 在非 force释放时,需要检查是否还有其它引用,如果是则返回
  • assets 缓存中移除
  • 自动释放依赖资源
  • 调用资源的 destroy 方法销毁资源
  • dependUtil 中移除资源的依赖记录

3.5.4 资源释放的问题

最后我们来聊一聊资源释放的问题与定位,在加入引用计数后,最常见的问题还是没有正确增减引用计数导致的内存泄露(循环引用、少调用了 decRef 或多调用了 addRef),以及正在使用的资源被释放的问题(和内存泄露相反,资源被提前释放了)。

从目前的代码来看,如果正确使用了引用计数,新的资源底层是可以避免内存泄露等问题的。

这种问题怎么解决呢?

首先是定位出哪些资源出了问题,如果是被提前释放,我们可以直接定位到这个资源,如果是内存泄露,当我们发现问题时程序往往已经占用了大量的内存,这种情况下可以切换到一个空场景,并清理资源,把资源清理完后,可以检查 assets 中残留的资源是否有未被释放的资源。

要了解资源为什么会泄露,可以通过跟踪 addRefdecRef 的调用得到,下面提供了一个示例方法,用于跟踪某资源的 addRefdecRef调用,然后调用资源的 dump方法打印出所有调用的堆栈。

结语

本教程包含详细的代码解读,为了保障手机端的阅读体验,没有全部放进来,欢迎大家点击文末阅读原文按钮前往社区查看!

非常感谢宝爷的无私分享,快来给宝爷点赞吧!

浏览 214
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报