creator-泄漏检测之资源篇

creator-泄漏检测之资源篇


前篇

  • 执行相关测试打快照前, 一定要多 gc js 几次

    image-20230329152312826


相关 dump 信息的代码

  • dump

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
        public dumpPath(path: string) {
    let bundleName = "resources"

    let bundle = assetManager.getBundle(bundleName);
    let asset01 = bundle.get(path);
    let uuid = asset01._uuid

    this.dumpUuidRecur(uuid)
    }

    private dumpUuidRecur(uuid: string, depth: number = 1) {
    let pre = "---".repeat(depth)
    var asset01 = assetManager.assets.get(uuid)!;

    var uuids: string[] = assetManager.dependUtil.getDepsRecursively(uuid)!;
    LogUtil.D(`${pre} dumpUuidRecur: ${uuid}, refCnt: ${asset01.refCount}\nasset:`, asset01, "\nuuid:", uuids)

    uuids.forEach(uuid => {
    this.dumpUuidRecur(uuid, ++depth)
    });
    }

    public dump() {
    assetManager.assets.forEach((value: Asset, key: string) => {
    console.log(assetManager.assets.get(key));
    })
    console.log(`当前资源总数:${assetManager.assets.count}`);
    }



    ---

    ### 静态加载: 多个预制引用同一张图

    1. 制作了两个最简单的预制, 里面引用同一个图片

    ![image-20230329145021401](https://pic05.wilker.cn/20230329145034-040.webp)

    2. 运行动态 load 出来

    3. 查看一下这两个预制路径的资源信息

    ![image-20230329145353550](https://pic05.wilker.cn/20230329145355-621.webp)

    - 发现 spriteFrame 就有维护引用计数, texture2d 和 imageAsset 就没维护引用计数

    根据管饭文档: https://docs.cocos.com/creator/manual/zh/asset/release-manager.html#%E8%B5%84%E6%BA%90%E7%9A%84%E9%9D%99%E6%80%81%E5%BC%95%E7%94%A8

    我们只需要维护 预制的引用计数 -1, 引擎会去维护 spriteFrame 引用技术, 进而维护 texture2d 和 imageAsset, spriteFrame 就相当于右图中的 Material

    ![image-20230329145540060](https://pic05.wilker.cn/20230329145542-140.webp)



    ---

    #### 销毁预制时引用计数 -1

    1. 未加载 预制情况下, 总数量是 55

    ![image-20230329145900957](https://pic05.wilker.cn/20230329145903-006.webp)

    2. 加载 预制情况下, 总数量是 60

    ![image-20230329145917606](https://pic05.wilker.cn/20230329145920-557.webp)

    3. 销毁预制, 并且预制引用计数-1, 总数量回到 55
    资源正常

    ![image-20230329145937661](https://pic05.wilker.cn/20230329145939-689.webp)



    ---

    ### 动态加载: 正确

    1. 未加载 预制情况下, 总数量是 55

    ![image-20230329150045997](https://pic05.wilker.cn/20230329150048-939.webp)

    2. 加载预制后, 在动态加载一张 big002 的图片并且引用计数+1, 总数 63

    ![image-20230329150122949](https://pic05.wilker.cn/20230329150126-292.webp)

    3. 销毁预制, 并且预制引用计数-1, 并且将动态加载出来的资源引用计数-1, 总数量回到 55
    资源正常

    !!!这里很关键的就是要自己去维护 动态加载的资源的 引用计数!!!

    ![image-20230329150147216](https://pic05.wilker.cn/20230329150149-837.webp)



    ---

    ### 动态加载: 错误

    1. 未加载 预制情况下, 总数量是 55

    ![image-20230329150301368](https://pic05.wilker.cn/20230329150304-376.webp)

    2. 加载预制后, 在动态加载一张 big002 的图片 (不操作引用计数), 总数 63

    ![image-20230329150322070](https://pic05.wilker.cn/20230329150324-938.webp)

    3. 销毁预制, 总数量没有回到 55
    资源不正常

    ![image-20230329150345262](https://pic05.wilker.cn/20230329150347-681.webp)

    看到 big002 spriteFrame 引用计数虽然为 0, 但是为什么还没释放呢? 导致关联的 texture2d 和 imageAsset 也没有被释放

    cocos 触发是放是由 decRef 减引用计数的方法里触发, 所有还是需要手动调用, 才能进行释放

    - 3.7.1 相关源码

    ![image-20230329150427482](https://pic05.wilker.cn/20230329150430-525.webp)



    ---

    ### 资源追踪自动维护引用计数

    > 摸清了释放规则后, 就可以在自定义的资源管理器中, 加入一个 追踪器, 让加载出来的资源, 根据 节点 的生命周期结束时, 自动去维护相关加载出来的资源, 让开发人员不用去关心引用计数问题, 只关心节点的生成和销毁



    1. 其实原理很简单, 就是在加载出来时往 节点 身上挂个组件去记录资源, 从而维护引用计数.

    - *AssetTracker.ts*

    ```typescript
    import { _decorator, Component, Node, Asset, SpriteFrame } from 'cc';
    import { LogUtil } from '../log/LogUtil';
    const { ccclass, property } = _decorator;

    @ccclass('AssetTracker')
    export class AssetTracker extends Component {

    public static trace(go: Node, ast: Asset) {
    let at = go.getComponent(AssetTracker)
    if (!at) {
    at = go.addComponent(AssetTracker)
    }
    at.traceInner(ast)
    }

    private _astArr = new Array<Asset>()

    traceInner(ast: Asset) {
    ast.addRef()
    this._astArr.push(ast)
    }

    onDestroy() {
    // LogUtil.D(`--- onDestroy, cnt: ${this._astArr.length}, _astArr:\n`, this._astArr)
    this._astArr.forEach((ast, idx, arr) => {
    ast.decRef()
    })
    this._astArr = null
    }

    debugDump() {
    this._astArr.forEach((ast, idx, arr) => {
    LogUtil.D("", ast)
    })
    }
    }
  1. 相关加载接口示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // ------------------------------------ 对外接口 begin
    public async instantiateAsync(prefabPath: string, parent?: Node) {
    return new Promise<Node>((resolve) => {
    this.load(prefabPath, (err: Error, asset: Prefab) => {
    if (err) {
    LogUtil.E(`--- instantiateAsync error, path: ${prefabPath}, err:`, err)
    resolve(null)
    return
    }

    let go = instantiate(asset);
    AssetTracker.trace(go, asset) // 资源计数追踪
    if (parent)
    go.parent = parent
    resolve(go)
    })
    })
    }

    // refGo 挂点, 最好是资源要依附的节点
    public async loadAssetAsync<T extends Asset>(assetPath: string, refGo: Node) {
    return new Promise<T>((resolve) => {
    this.load(assetPath, (err: Error, asset: T) => {
    if (err) {
    LogUtil.E(`--- loadAssetAsync error, path: ${assetPath}, err:`, err)
    resolve(null)
    return
    }

    AssetTracker.trace(refGo, asset) // 资源计数追踪
    resolve(asset)
    })
    })
    }

    // refGo 挂点, 最好是资源要依附的节点
    public async loadRemoteAsync<T extends Asset>(url: string, opts: IRemoteOptions, refGo: Node) {
    return new Promise<T>((resolve) => {
    this.loadRemote(url, opts, (err: Error, asset: T) => {
    if (err) {
    LogUtil.E(`--- loadRemoteAsync error, url: ${url}, err:`, err)
    resolve(null)
    return
    }

    AssetTracker.trace(refGo, asset) // 资源计数追踪
    resolve(asset)
    })
    })
    }
    // ------------------------------------ 对外接口 end

实测

  1. 未加载 预制情况下, 总数量是 55, 内存和图片缓存如下

    image-20230329152013866

  2. 加载 预制 并且动态加载 图片 情况下, 总数量是 61, 内存和图片缓存如下

    image-20230329152036689

  3. 销毁预制节点, 总数量回到 55, 内存和图片缓存也回到了初始值, 说明正常释放资源

    image-20230329152054504