05-07-源码-引用计数与资源生命周期

源码-引用计数机制

篇章 :05-源码深度-运行时核心模块 阅读时间 :约 25 分钟 前置知识:了解 Provider 体系


一、引言

本章将深入解析 YooAsset 引用计数机制的源码实现。引用计数是 YooAsset 资源管理的核心机制,用于管理资源的引用关系和生命周期。深入理解引用计数机制是掌握 YooAsset 资源管理的关键。

引用计数机制的设计目标是确保资源在不再使用时及时释放,避免内存泄漏。本章将从源码层面深入解析引用计数机制的实现。


二、引用计数概述

2.1 引用计数的概念

引用计数是一种资源管理技术:

引用计数的定义:

引用计数记录了资源被引用的次数。每次资源被引用时,引用计数加 1;每次引用被释放时,引用计数减 1。

引用计数的作用:

  • 跟踪资源的引用关系
  • 决定资源的生命周期
  • 避免资源过早释放
  • 避免内存泄漏

引用计数的特点:

  • 实时性:引用计数实时更新
  • 准确性:只要遵守规则,引用计数是准确的
  • 自动管理:资源生命周期自动管理

2.2 YooAsset 中的引用计数

YooAsset 中的引用计数应用于以下对象:

  • Provider:资源提供者
  • BundleLoader:Bundle 加载器
  • AssetBundle:AssetBundle 实例

Provider 引用计数:

记录资源被引用的次数。引用为 0 时,Provider 销毁。

BundleLoader 引用计数:

记录 Bundle 被引用的次数。引用为 0 时,Bundle 销毁。

AssetBundle 引用计数:

记录 AssetBundle 被 Provider 引用的次数。

2.3 引用计数的实现

csharp 复制代码
public abstract class RefCountOperation
{
    private int _refCount = 0;
    public int RefCount => _refCount;
    
    public void IncreaseRefCount()
    {
        Interlocked.Increment(ref _refCount);
    }
    
    public void DecreaseRefCount()
    {
        Interlocked.Decrement(ref _refCount);
    }
}

Interlocked.Increment 详解:

使用 Interlocked.Increment 进行原子操作,确保多线程环境下的引用计数正确性。

Interlocked.Decrement 详解:

使用 Interlocked.Decrement 进行原子操作。


三、Provider 引用计数

3.1 Provider 引用计数的作用

Provider 引用计数记录了资源被引用的次数:

Provider 引用计数的应用场景:

  • 同一资源被多个地方引用
  • 异步加载时多次调用 LoadAssetAsync
  • 长生命周期的对象引用短生命周期的资源

3.2 Provider 引用计数的实现

csharp 复制代码
public abstract class ProviderBase
{
    private int _refCount = 0;
    public int RefCount => _refCount;
    
    public void IncreaseRefCount()
    {
        Interlocked.Increment(ref _refCount);
    }
    
    public void DecreaseRefCount()
    {
        Interlocked.Decrement(ref _refCount);
        if (_refCount <= 0)
        {
            // 引用为 0,销毁 Provider
            Destroy();
        }
    }
}

引用计数增加详解:

LoadAssetAsync 调用时,引用计数加 1。

引用计数减少详解:

UnloadAsset 调用时,引用计数减 1。

销毁触发详解:

引用为 0 时,Provider 自动销毁。

3.3 Provider 引用计数的应用

csharp 复制代码
// 场景 1:基本用法
var handle = package.LoadAssetAsync<GameObject>("prefab");
Instantiate(handle.AssetObject);
handle.Release(); // 引用 -1,销毁资源

// 场景 2:多次加载同一资源
var handle1 = package.LoadAssetAsync<GameObject>("prefab");
var handle2 = package.LoadAssetAsync<GameObject>("prefab");
// handle1 和 handle2 引用同一 Provider
handle1.Release(); // 引用 -1
handle2.Release(); // 引用 -1,销毁资源

// 场景 3:异步加载中的引用
var handle = package.LoadAssetAsync<GameObject>("prefab");
// 异步加载中,handle 还在等待
await handle.Task;
Instantiate(handle.AssetObject);
handle.Release();

基本用法详解:

LoadAssetAsync 加载资源,Release 释放资源。

多次加载详解:

多次加载同一资源会复用同一 Provider,引用计数累加。

异步加载详解:

异步加载中,引用计数保持,确保资源不会被过早释放。


四、BundleLoader 引用计数

4.1 BundleLoader 引用计数的作用

BundleLoader 引用计数记录了 Bundle 被引用的次数:

BundleLoader 引用计数的应用场景:

  • 多个资源引用同一 Bundle
  • Provider 加载时引用 Bundle
  • Provider 卸载时减少引用

4.2 BundleLoader 引用计数的实现

csharp 复制代码
public class BundleLoader
{
    private int _refCount = 0;
    public int RefCount => _refCount;
    
    public void Retain()
    {
        Interlocked.Increment(ref _refCount);
    }
    
    public void Release()
    {
        Interlocked.Decrement(ref _refCount);
        if (_refCount <= 0)
        {
            UnloadBundle();
        }
    }
    
    private void UnloadBundle()
    {
        if (_assetBundle != null)
        {
            _assetBundle.Unload(true);
            _assetBundle = null;
        }
    }
}

引用计数增加详解:

Provider 加载资源时,会调用 BundleLoader.Retain 增加引用。

引用计数减少详解:

Provider 销毁时,会调用 BundleLoader.Release 减少引用。

Bundle 卸载详解:

引用为 0 时,BundleLoader 自动卸载 AssetBundle。

4.3 BundleLoader 引用计数的应用

csharp 复制代码
public class ResourceManager
{
    public BundleLoader LoadBundle(BundleInfo bundleInfo)
    {
        string key = bundleInfo.FileName;
        if (_bundleLoaders.TryGetValue(key, out var loader))
        {
            loader.Retain();
            return loader;
        }
        
        loader = new BundleLoader(bundleInfo);
        loader.LoadAsync();
        _bundleLoaders[key] = loader;
        loader.Retain();
        return loader;
    }
    
    public void UnloadBundle(BundleLoader loader)
    {
        loader.Release();
        if (loader.RefCount <= 0)
        {
            _bundleLoaders.Remove(loader.BundleInfo.FileName);
        }
    }
}

BundleLoader 获取详解:

LoadBundle 方法获取 BundleLoader,如果已存在则复用,否则创建。

引用计数管理详解:

LoadBundle 增加引用,UnloadBundle 减少引用。

自动卸载详解:

引用为 0 时,BundleLoader 自动从缓存中移除。


五、引用计数与依赖管理

5.1 依赖资源管理

资源之间的依赖关系也需要引用计数管理:

csharp 复制代码
public class AssetProvider : ProviderBase
{
    private List<BundleLoader> _dependLoaders;
    
    public override void Provide()
    {
        // 加载资源
        var assetInfo = Package.GetAssetInfo(Location);
        
        // 加载依赖 Bundle
        _dependLoaders = new List<BundleLoader>();
        foreach (var depBundleID in assetInfo.DependIDs)
        {
            var depBundle = Package.GetBundleInfo(depBundleID);
            var loader = Package.ResourceManager.LoadBundle(depBundle);
            _dependLoaders.Add(loader);
        }
    }
    
    public override void UnProvide()
    {
        // 释放依赖 Bundle
        foreach (var loader in _dependLoaders)
        {
            Package.ResourceManager.UnloadBundle(loader);
        }
        _dependLoaders.Clear();
    }
}

依赖加载详解:

加载资源时,同时加载所有依赖的 Bundle。

依赖释放详解:

释放资源时,同时释放所有依赖的 Bundle。

引用计数管理详解:

通过 LoadBundle 和 UnloadBundle 管理依赖的引用计数。

5.2 共享 Bundle 管理

共享 Bundle 被多个资源引用,需要特殊处理:

csharp 复制代码
public class ResourceManager
{
    public BundleLoader GetOrLoadBundle(BundleInfo bundleInfo)
    {
        string key = bundleInfo.FileName;
        if (_bundleLoaders.TryGetValue(key, out var loader))
        {
            // 已存在,复用并增加引用
            loader.Retain();
            return loader;
        }
        
        // 不存在,创建
        loader = new BundleLoader(bundleInfo);
        loader.LoadAsync();
        _bundleLoaders[key] = loader;
        loader.Retain();
        return loader;
    }
}

共享 Bundle 处理详解:

共享 Bundle 通过 BundleLoader 缓存实现共享,避免重复加载。

引用计数作用详解:

引用计数确保共享 Bundle 在所有引用都释放后才卸载。


六、引用计数注意事项

6.1 配对原则

引用计数必须遵守"配对原则":

Load-Release 配对:

每次 Load 必须有对应的 Release。

配对检查:

csharp 复制代码
public class ProviderBase
{
    public void VerifyRefCount()
    {
        if (RefCount < 0)
        {
            Debug.LogError($"Provider {Location} ref count is negative!");
        }
        if (RefCount == 0 && Status != EProviderStatus.Destroyed)
        {
            Debug.LogWarning($"Provider {Location} has zero ref count but not destroyed!");
        }
    }
}

调试方法详解:

通过 VerifyRefCount 方法检查引用计数是否正确。

6.2 异步引用计数

异步加载中需要正确管理引用计数:

csharp 复制代码
// 错误:异步加载中提前释放
var handle = package.LoadAssetAsync<GameObject>("prefab");
handle.Release(); // 错误!异步加载还未完成
Instantiate(handle.AssetObject); // 资源可能已被销毁

// 正确:等待异步加载完成后释放
var handle = package.LoadAssetAsync<GameObject>("prefab");
await handle.Task; // 等待加载完成
Instantiate(handle.AssetObject);
handle.Release(); // 正确释放

异步加载注意事项详解:

异步加载中,引用计数保持,确保资源不会过早释放。

6.3 循环引用

循环引用会导致引用计数永远不为 0:

避免循环引用:

  • 资源之间不应该有循环引用
  • 使用弱引用避免循环引用
  • 定期检查引用计数

循环引用检测:

csharp 复制代码
public class RefCountCycleDetector
{
    public List<ProviderBase> DetectCycles()
    {
        // 使用图算法检测循环引用
        // ...
    }
}

七、总结

本章深入解析了 YooAsset 引用计数机制的源码实现,包括:

  • 引用计数概述:引用计数的概念、YooAsset 中的应用、实现
  • Provider 引用计数:Provider 引用计数的作用、实现、应用
  • BundleLoader 引用计数:BundleLoader 引用计数的作用、实现、应用
  • 引用计数与依赖管理:依赖资源管理、共享 Bundle 管理
  • 引用计数注意事项:配对原则、异步引用、循环引用

引用计数机制是 YooAsset 资源管理的核心,通过深入理解引用计数机制,开发者可以避免内存泄漏,正确管理资源生命周期。


上一篇 :异步体系(OperationSystem) 下一篇 :资源缓存机制

相关推荐
雪庭3 小时前
用 OCCT 造一只啤酒瓶:完整实录
unity3d
SmalBox1 天前
05-06-源码-异步操作体系(OperationSystem)
unity3d·游戏开发
侑虎科技1 天前
深度分析屏幕空间反射算法以及在URP中的实现(一)
unity3d
郝学胜-神的一滴2 天前
游戏设计:原型与开发 08:我们到底在为谁做游戏
程序人生·游戏·游戏引擎·游戏开发·游戏策划
SmalBox2 天前
05-05-源码-资源加载与Provider体系
unity3d·游戏开发
SmalBox3 天前
05-04-源码-资源定位系统
unity3d·游戏开发
Mikko74 天前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
郝学胜_神的一滴4 天前
游戏设计:原型与开发 06:读懂游戏设计最容易被忽略的文化层
游戏·游戏开发
SmalBox4 天前
05-03-源码-运行模式体系(PlayMode)
unity3d·游戏开发