Addressables加载资源浅析


众所周知,Addressables的资源加载全部都是异步完成的。它主要通过两个接口:Addressables.LoadAssetAsync和Addressables.LoadAssetsAsync。我们先来看一下LoadAssetAsync接口:

public static AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(object key)
{
    return m_Addressables.LoadAssetAsync<TObject>(key);
}

public static AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(IResourceLocation location)
{
    return m_Addressables.LoadAssetAsync<TObject>(location);
}

其中第二个重载的参数是 IResourceLocation,这个我们在另一篇文章讨论。今天我们重点看一下第一个更为通用的接口。第一个接口接收的是一个object类型的参数。它可以是如下几种类型:
• Address: 一个string类型,表示资源地址。
• Label: 一个string类型,表示资源所属的标签。
• AssetReference: 一个AssetReference的实例

可以看到,这个接口实际上是将参数直接传递给了m_Addressables.LoadAssetAsync。这里我们看一下m_Addressables的定义:

internal static AddressablesImpl m_AddressablesInstance = new AddressablesImpl(new LRUCacheAllocationStrategy(1000, 1000, 100, 10));
static AddressablesImpl m_Addressables
{
    get
    {
#if UNITY_EDITOR
        if (EditorSettings.enterPlayModeOptionsEnabled && reinitializeAddressables)
        {
            reinitializeAddressables = false;
            m_AddressablesInstance.ReleaseSceneManagerOperation();
            m_AddressablesInstance = new AddressablesImpl(new LRUCacheAllocationStrategy(1000, 1000, 100, 10));
        }
#endif
        return m_AddressablesInstance;
    }
}

由此可见[color=blue]m_AddressablesInstance[/color]就是AddressablesImpl的一个实例。而它的构造参数则是一个IAllocationStrategy的派生类,顾名思义这是一个内存管理策略对象。
来看一下AddressablesImpl.LoadAssetAsync接口都做了什么

public AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(object key)
{
    if (ShouldChainRequest)	// ShouldChainRequest是AddressablesImpl的一个属性,若当前已经初始化完成则返回false,若需要进行初始化则返回true
	// ChainOperation也是AddressablesImpl的一个属性,负责返回当前所需要在加载操作之前进行的行为
	// LoadAssetWithChain负责构建一个链式操作,该链式操作会在ChainOperation结束之后再一次调用LoadAssetAsync
	// TrackHandle则负责给LoadAssetWithChain返回的AsyncOperationHandle对象设置回调
        return TrackHandle(LoadAssetWithChain<TObject>(ChainOperation, key));

    // EvaluateKey对key进行处理,如果key是IKeyEvaluator类型则取它的RuntimeKey属性,AssetReference则是在这里被转化为AssetReference.m_AssetGUID(string类型)
    key = EvaluateKey(key);

    IList<IResourceLocation> locs;
    // 读取TObject的类型,如果是泛型则读取实际的类型
    var t = typeof(TObject);
    if (t.IsArray)
        t = t.GetElementType();
    else if (t.IsGenericType && typeof(IList<>) == t.GetGenericTypeDefinition())
        t = t.GetGenericArguments()[0];
		
    // 遍历当前所有的ResourceLocator,调用IResourceLocator.Locate接口查询资源路径,获得对应的IResourceLocation列表
    foreach (var locatorInfo in m_ResourceLocators)
    {
        var locator = locatorInfo.Locator;
        if (locator.Locate(key, t, out locs))
        {
            foreach (var loc in locs)
            {
		// 遍历locs(IResourceLocation列表),查询对应的Provider(若不存在则不支持该资源)
                var provider = ResourceManager.GetResourceProvider(typeof(TObject), loc);
                if (provider != null)
		    // 调用ResourceManager.ProvideResource进行加载
		    return TrackHandle(ResourceManager.ProvideResource<TObject>(loc));
            }
        }
    }
	
    // 执行到这里说明没有找到对应该key的资源,返回一个带有异常的已完成的AsyncOperationHandle对象
    return ResourceManager.CreateCompletedOperationWithException<TObject>(default(TObject), new InvalidKeyException(key, t));
}

从上面的代码里可以看到,加载的核心逻辑在[color=blue]ResourceManager.ProvideResource[/color]接口,让我们来看一下:

public AsyncOperationHandle<TObject> ProvideResource<TObject>(IResourceLocation location)
{
    // 调用非泛型版本的ProvideResource
    AsyncOperationHandle handle = ProvideResource(location, typeof(TObject));
    // 将handle转化成泛型版本(带类型)
    return handle.Convert<TObject>();
}

/// <summary>
/// Load the <typeparamref name="TObject"/> at the specified <paramref name="location"/>.
/// </summary>
/// <returns>An async operation.</returns>
/// <param name="location">Location to load.</param>
/// <param name="releaseDependenciesOnFailure">When true, if the operation fails, dependencies will be released.</param>
/// <typeparam name="TObject">Object type to load.</typeparam>
private AsyncOperationHandle ProvideResource(IResourceLocation location, Type desiredType = null, bool releaseDependenciesOnFailure = true)
{
    if (location == null)
        throw new ArgumentNullException("location");
    IResourceProvider provider = null;
    if (desiredType == null)
    {
        // 如果传入的desiredType是空的,则通过GetResourceProvider来查询provider上的默认类型
        provider = GetResourceProvider(desiredType, location);
        if (provider == null)
        {
            var ex = new UnknownResourceProviderException(location);
            return CreateCompletedOperationInternal<object>(null, false, ex, releaseDependenciesOnFailure);
        }
        desiredType = provider.GetDefaultType(location);
    }

    // 检查当前是否已有正在加载的异步操作,若有则增加引用计数并返回
    IAsyncOperation op;
    var key = new LocationCacheKey(location, desiredType);
    if (m_AssetOperationCache.TryGetValue(key, out op))
    {
        op.IncrementReferenceCount();
        return new AsyncOperationHandle(op, location.ToString());;
    }

    // 查找或生成provType
    // provType是ProviderOperation<T>类型
    // ProviderOperation负责实际的加载与解析操作
    // m_ProviderOperationTypeCache是该类型的一个缓冲池,用来防止多次调用MakeGenericType来生成ProviderOperation<T>类型
    Type provType;
    if (!m_ProviderOperationTypeCache.TryGetValue(desiredType, out provType))
        m_ProviderOperationTypeCache.Add(desiredType, provType = typeof(ProviderOperation<>).MakeGenericType(new Type[] { desiredType }));
    // 调用CreateOperation创建一个加载操作
    op = CreateOperation<IAsyncOperation>(provType, provType.GetHashCode(), key, m_ReleaseOpCached);

    // Calculate the hash of the dependencies
    int depHash = location.DependencyHashCode;
    // 如果该请求有依赖项,则调用ProvideResourceGroupCached来加载所有的依赖项,并生成一个依赖操作depOp
    var depOp = location.HasDependencies ?
                ProvideResourceGroupCached(location.Dependencies, depHash, null, null, releaseDependenciesOnFailure) :
                default(AsyncOperationHandle<IList<AsyncOperationHandle>>);
    // 获取类型对应的provider
    if (provider == null)
        provider = GetResourceProvider(desiredType, location);

    // 初始化ProviderOperation
    ((IGenericProviderOperation)op).Init(this, provider, location, depOp, releaseDependenciesOnFailure);

    // 开始加载操作
    var handle = StartOperation(op, depOp);
    handle.LocationName = location.ToString();

    if (depOp.IsValid())
        depOp.Release();

    return handle;
}

可以看到[color=blue]ResourceManager.ProvideResource[/color]内部是通过各个[color=blue]ProviderOperation[/color]来进行实际的加载操作的。那么看完了LoadAssetAsync的加载代码,现在让我们来看一下LoadAssetsAsync,也就是同时加载多个资源的版本。

/// <typeparam name="TObject">The type of the assets.</typeparam>
/// <param name="keys">Key的集合</param>
/// <param name="callback">每加载完一个资源所会调用的回调</param>
/// <param name="mode">MergeMode枚举类型,用来表示多个Key是如何决定加载哪些资源的</param>
/// <param name="releaseDependenciesOnFailure">bool类型,决定了有资源加载失败时,如何处理其他资源及依赖项
/// 当所有资源都加载成功时,该参数会被忽略。
/// 当有资源加载失败时:
/// 若为true,则所有资源及依赖项会被释放,并且AsyncOperationHandle.Result会被设置为null。
/// 若为false,则AsyncOperationHandle.Result会被设置为一个长度跟资源数相同List对象,
/// 其中加载失败的资源会被置为null,成功的会设为一个有效的TObject对象。
/// 当有资源加载失败时,不管releaseDependenciesOnFailure是true还是false,
/// AsyncOperationHandle.Status都会被置为AsyncOperationStatus.Failed
/// 当有资源加载失败时,若releaseDependenciesOnFailure,你不需要释放返回的AsyncOperationHandle实例
/// 否则你不能忘了释放它
public static AsyncOperationHandle<IList<TObject>> LoadAssetsAsync<TObject>(IEnumerable keys, Action<TObject> callback, MergeMode mode, bool releaseDependenciesOnFailure)
{
    return m_Addressables.LoadAssetsAsync(keys, callback, mode, releaseDependenciesOnFailure);
}

这里有一个比较重要的参数“[color=red]mode[/color]”。每一个Key都会解析出一个ResourceLocation集合,当这些集合有互相交叉的部分的话,[color=red]mode[/color]参数就决定了怎么生成最终的结果集。它一共有3个类型:
• [color=red]MergeMode.UseFirst[/color]: 不管后面的集合是什么,永远只选择第一个集合
• [color=red]MergeMode.Union[/color]: 最终的结果将是这些集合的一个并集,包含所有的结果。
• [color=red]MergeMode.Intersection[/color]: 最终的结果是这些集合的交集,只包含所有集合共有的部分。

这些不同的[color=blue]AddressablesImpl.LoadAssetsAsync[/color]重载接口最终都会调用到同一个接口:

public AsyncOperationHandle<IList<TObject>> LoadAssetsAsync<TObject>(IList<IResourceLocation> locations, Action<TObject> callback, bool releaseDependenciesOnFailure)
{
    return TrackHandle(ResourceManager.ProvideResources(locations, releaseDependenciesOnFailure, callback));
}

可以看到这个接口直接调用了[color=blue]ResourceManager.ProvideResources[/color],那么ProvideResources又干了什么呢?继续看代码:

/// <returns>AsyncOperationHandle实例</returns>
/// <param name="locations">所有需要加载资源的ResourceLocation</param>
/// <param name="releaseDependenciesOnFailure">发生失败时是否释放已加载的资源</param>
/// <param name="callback">每加载一个资源都会被调用的回调</param>
/// <typeparam name="TObject">所要加载资源的类型</typeparam>
public AsyncOperationHandle<IList<TObject>> ProvideResources<TObject>(IList<IResourceLocation> locations, bool releaseDependenciesOnFailure, Action<TObject> callback = null)
{
    // 如果locations为空,则创建一个已完成的操作(CompletedOperation)
    if (locations == null)
        return CreateCompletedOperation<IList<TObject>>(null, "Null Location");

    // 创建一个通用的支持AsyncOperationHandle参数的回调
    Action<AsyncOperationHandle> callbackGeneric = null;
    if (callback != null)
    {
        callbackGeneric = (x) => callback((TObject)(x.Result));
    }
    // 调用ProvideResourceGroupCached来加载所有locations对应的资源
    // ProvideResourceGroupCached是个递归加载的过程
    // 它会分别调用ResourceManager.ProvideResource去加载每一个资源(包括它们的依赖项)
    var typelessHandle = ProvideResourceGroupCached(locations, CalculateLocationsHash(locations, typeof(TObject)), typeof(TObject), callbackGeneric, releaseDependenciesOnFailure);
    // 调用CreateChainOperation创建一个ChainOperationTypelessDepedency对象并启动(StartOperation)
    // 该对象会在typelessHandle结束后调用后面的callback参数
    var chainOp = CreateChainOperation<IList<TObject>>(typelessHandle, (resultHandle) =>
    {
        // 从AsyncOperationHandle转化成模板版本
        AsyncOperationHandle<IList<AsyncOperationHandle>> handleToHandles = resultHandle.Convert<IList<AsyncOperationHandle>>();

        var list = new List<TObject>();
        Exception exception = null;
        if (handleToHandles.Status == AsyncOperationStatus.Succeeded)
        {
            // 所有资源加载成功,将结果添加到list中
            foreach (var r in handleToHandles.Result)
                list.Add(r.Convert<TObject>().Result);
        }
        else
        {
            // 有资源加载失败,如果releaseDependenciesOnFailure为false则表示需要添加加载成功的资源到list中
            // 不成功的资源置为default(TObject)
            bool foundSuccess = false;
            if (!releaseDependenciesOnFailure)
            {
                foreach (AsyncOperationHandle handle in handleToHandles.Result)
                {
                    if (handle.Status == AsyncOperationStatus.Succeeded)
                    {
                        list.Add(handle.Convert<TObject>().Result);
                        foundSuccess = true;
                    }
                    else
                        list.Add(default(TObject));
                }
            }

            // 根据是否有加载成功的资源,设置不同的exception,以及是否将list置空
            if (!foundSuccess)
            {
                list = null;
                exception = new ResourceManagerException("ProvideResources failed", handleToHandles.OperationException);
            }
            else
            {
                exception = new ResourceManagerException("Partial success in ProvideResources.  Some items failed to load. See earlier logs for more info.", handleToHandles.OperationException);
            }
        }

        // 创建CompletedOperation并执行
        return CreateCompletedOperationInternal<IList<TObject>>(list, exception == null, exception, releaseDependenciesOnFailure);
    }, releaseDependenciesOnFailure);

    // chain operation holds the dependency
    // 释放掉typelessHandle句柄,因为chainOp已经持有了它的引用
    typelessHandle.Release();
    return chainOp;
}

所以其实[color=blue]ResourceManager.ProvideResources[/color]的本质还是通过[color=blue]ResourceManager.ProvideResourceGroupCached[/color]对每一个[color=red]location[/color]调用了[color=blue]ResourceManager.ProvideResource[/color]来实现对多个资源的加载的,然后最上层使用了一个ChainOperation来负责在所有这些资源加载完之后调用回调来处理结果。

至此,Addressables的加载流程已经全部分析完毕,可以看到在Addressables的加载中,各种Operation和Provider起了很大的作用。那么这些Operation和Provider分别是做什么的呢?让我们另开个文章来具体分析一下。

,

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注