Zack.Zhang Game Developer

Addressable Asset System源码解析——Runtime

2022-01-10
zack.zhang

Addressable Asset System是Unity的可定位资源管理系统,不同于AssetBundle的资源管理方式,它提供了更加便捷的方法。本文主要介绍了Addressable Asset System在运行时的一些逻辑。

1. 关键类

1.1. 全局管理类

Addressable Asset System以外的外部代码一般直接调用静态类Addressables的静态方法来使用Addressable Asset System提供的具体功能。调用Addressables类的方法,实际上就是调用AddressablesImpl类的成员方法,因为Addressables类里面主要就是封装了一个AddressablesImpl类的对象。AddressablesImpl类里面包含了一个最核心的管理类ResourceManager,这个类管理了资源的整个生命周期。全局管理类的包含关系如下所示。

Addressables

1.2. 异步操作类

Addressable Asset System运行时的加载过程,全部设计为异步的过程。换句话说,当开发者调用加载接口时,并非阻塞主线程且等待I/O结束立即返回加载结果,而是不阻塞主线程,等加载结束后通知开发者的应用程序。

这个过程可以打个比喻,有一个照相馆,用胶片照相机给顾客拍了一张照片,这个照相馆就好比Addressable Asset System,这个顾客就好比上文中的主线程。我们都知道用胶卷拍了照片之后,冲洗出照片,需要很长的时间。

  • 如果照相馆让顾客拍完照后不许离开,必须在店里等着照片冲洗出来,这个过程就类似同步加载,要阻塞主线程,主线程不能干别的事情,只能死等。这是一种体验很差的做法。

  • 但是如果照相馆老板在顾客拍完照后给了他一张取相片用的凭证,等相片洗好了,老板打电话告诉顾客,顾客凭凭证来取照片即可。这个过程,主线程就可以不用死等,是一种体验比较好的做法。

为了实现以上比喻类似的异步过程,Addressable Asset System在开发者调用了加载接口时,返回给开发者一个AsyncOperationBase类的对象,这个对象就好比上面比喻中的取相片用的凭证。

但是AsyncOperationBase里面有很多并不需要开发者关注的数据和方法,所以在这里,Addressable Asset System使用了代理(Proxy)模式,抽象出一个叫做AsyncOperationHandle的类(实际是个结构体),代替AsyncOperationBase返回给开发者,当做“凭证”。

AsyncOption

1.3. 提供者类

对于Addressable Asset System来说,一切要加载/下载的东西都属于资源,比如AssetBundle、引擎资源(预制体、贴图、模型等)、资源目录等。加载/下载的过程在Addressable Asset System可以叫做“提供”。为了封装这种“提供”行为,需要实现IResourceProvider接口。ResourceProviderBase实现了IResourceProvider接口,几乎所有的提供者类都继承于ResourceProviderBase。对于ResourceProviderBase来说最核心的方法就是Provide(…)方法,该方法在不同实现类有不同的实现方法,他们都用来封装“提供”行为。(也就是说“提供”包含了“下载”和“加载”,甚至还可以包含别的异步过程)

一般来说提供者类会赋值给到ProviderOperation类的成员,ProviderOperation类调用Execute()方法,便会触发到Provide(...)方法。因此,AsyncOperationBase的实现类里面,在资源加载方面ProviderOperation类是比较核心的一个异步操作类。

Provider

2. 资源加载过程

AddressablesImpl类作为Addressable Asset System的接口类,对外提供了很多接口方法,其中加载资源可以调用以下接口方法。

public AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(IResourceLocation location)
{
    return TrackHandle(ResourceManager.ProvideResource<TObject>(location));
}

下面详细看看ResourceManager.ProvideResource里干了什么。

2.1. ResourceManager.ProvideResource

这个方法是资源加载过程中最核心的方法。

private AsyncOperationHandle ProvideResource(IResourceLocation location, Type desiredType = null)
{
    ...
    // #2.1.1.
    IResourceProvider provider = null;
    if (desiredType == null)
    {
        provider = GetResourceProvider(desiredType, location);
        ...
        desiredType = provider.GetDefaultType(location);
    }
    
    // #2.1.2.
    IAsyncOperation op;
    int hash = location.Hash(desiredType);
    if (m_AssetOperationCache.TryGetValue(hash, out op))
    {
        op.IncrementReferenceCount();
        return new AsyncOperationHandle(op);
    }
    
    // #2.1.3.
    Type provType;
    if (!m_ProviderOperationTypeCache.TryGetValue(desiredType, out provType))
        m_ProviderOperationTypeCache.Add(desiredType, provType = typeof(ProviderOperation<>).MakeGenericType(new Type[] { desiredType }));
    
    // #2.1.4.
    op = CreateOperation<IAsyncOperation>(provType, provType.GetHashCode(), hash, m_ReleaseOpCached);
    
    // #2.1.5.
    // Calculate the hash of the dependencies
    int depHash = location.DependencyHashCode;
    var depOp = location.HasDependencies ? ProvideResourceGroupCached(location.Dependencies, depHash, null, null) : default(AsyncOperationHandle<IList<AsyncOperationHandle>>);
    if (provider == null)
        provider = GetResourceProvider(desiredType, location);
    
    // #2.1.6.
    ((IGenericProviderOperation)op).Init(this, provider, location, depOp);
    
    // #2.1.7.
    var handle = StartOperation(op, depOp);
    
    ...
    
    return handle;
}

根据上面代码注释中的标题号,详细解释其中的逻辑。

2.1.1. 获得ResourceProvider

因为IResourceLocation里面有成员ProviderId用来记录提供该资源需要的提供者,所以可以根据这个ID便可以获得对应的ResourceProvider。

这个获得Provider的过程被封装到了GetResourceProvider方法里。

另外,这里根据Provider可以获得期望的资源类型,并赋值给desiredType,供后面逻辑使用。

2.1.2. 获得AsyncOperation

如果AsyncOperation存在于m_AssetOperationCache里面,则说明该资源已经被“提供”过了,这个时候直接返回AsyncOperation的代理AsyncOperationHandle便可。

2.1.3. 获得desiredType对应的AsyncOperation类型

如果没有获取到desiredType对应的AsyncOperation类型,则生成这个AsyncOperation类型。

生成的AsyncOperation需要属于ProviderOperation类型。

2.1.4. 创建AsyncOperation

根据2.1.3.获得/生成的AsyncOperation类型,创建AsyncOperation。这里因为上文提到传入的类型确定是ProviderOperation类型,所以创建的AsyncOperation对象也就是ProviderOperation类型的。

2.1.5. 创建依赖项的AsyncOperation

首先,判断当前要加载的资源是否有依赖项。如果没有依赖项就不用特意处理。

如果有依赖项,由于一个资源大部分时候会依赖其他的很多资源。ResourceManager提供了ProvideResourceGroupCached方法来将所有依赖项放到一个IList里,将这个IList当做要加载的资源,通过异步操作类GroupOperation来处理IList的异步操作:当GroupOperation加载完成组内所有资源,通过回调Complete委托来通知后续逻辑——当前资源可以加载了。这个过程在接下来逻辑里还会有体现。

2.1.6. 初始化AsyncOperation

在2.1.4.里创建出的AsyncOperation还不能直接使用,需要把2.1.1.获取出来的ResourceProvider、2.1.5.创建出来的依赖项AsyncOperation赋值给它,才能进行后续加载逻辑。

public void Init(ResourceManager rm, IResourceProvider provider, IResourceLocation location, AsyncOperationHandle<IList<AsyncOperationHandle>> depOp)
{
    m_ResourceManager = rm;
    m_DepOp = depOp;
    if (m_DepOp.IsValid())
        m_DepOp.Acquire();
    m_Provider = provider;
    m_Location = location;
}

这一步其实就是整合了创建的ResourceProvider、AsyncOperation以及AsyncOperation依赖项。

2.1.7. 开始异步加载

StartOperation(…)方法内部其实是触发了AsyncOperation的Start(…)方法。

internal AsyncOperationHandle StartOperation(IAsyncOperation operation, AsyncOperationHandle dependency)
{
    operation.Start(this, dependency, m_UpdateCallbacks);
    return operation.Handle;
}

而AsyncOperationBase的Start(…)方法从下面代码来看过程也很简单。

internal void Start(ResourceManager rm, AsyncOperationHandle dependency, DelegateList<float> updateCallbacks)
{
    ...
    if (dependency.IsValid() && !dependency.IsDone)
        dependency.Completed += m_dependencyCompleteAction;
    else
        InvokeExecute();
}

从上面代码可以看到,如果依赖项还没加载完,等待依赖项加载完再调用委托m_dependencyCompleteAction;如果依赖项加载完了,直接调用InvokeExecute()。

而委托m_dependencyCompleteAction的赋值代码如下。

m_dependencyCompleteAction = o => InvokeExecute();

也就是说,无论是等待依赖项加载完,还是依赖项已经加载完,最终都是要调用当前资源异步操作对象(AsyncOperation对象)的InvokeExecute()方法。

而这个InvokeExecute()方法内部也很简单,仅仅是调用了Execute()方法。

private void InvokeExecute()
{
    Execute();
    ...
}

前文提到过由于当前异步操作对象(AsyncOperation对象)是ProviderOperation类型的,可以观察一下ProviderOperation的Execute()方法都有什么逻辑。

protected override void Execute()
{
    ...
    m_Provider.Provide(new ProvideHandle(m_ResourceManager, this));
    ...
}

由代码可见,最终其实是调用了当前资源类型对应的Provider类的Provide(…)方法,由于上文提到,该方法其实就封装了该种类型资源的“提供”行为。

例如,如果该种资源类型为AssetBundle,那么对应的Provider类为AssetBundleProvider。AssetBundleProvider的Provide(…)方法里其实主要就干了一件事:

  • 如果当前磁盘里有AssetBundle文件,则调用AssetBundle.LoadFromFileAsync方法去加载;

  • 如果当前磁盘里没有AssetBundle文件,则调用UnityWebRequestAsyncOperation.SendWebRequest方法去CDN请求资源。

资源“提供”完成,Provider类调用ProviderOperation的Complete委托,去通知开发者该资源已经加载完毕了。


Similar Posts

上一篇 GPU架构和渲染

评论