UnityWebRequest多个下载器并行下载AB包

本文介绍基于UnityWebRequest实现AB包多线程并行下载的模块化方案,涵盖下载器池与AB包状态池设计、下载任务动态分配机制、下载完成标记同步、字节进度统计与单位转换逻辑,并强调避免using提前释放WebRequest、确保Update中状态检查与任务分配原子性,以及最终校验总下载字节数一致性。

多个UnityWebRequest并行比单个串行更快,程序更复杂。必须弄一个池管理UnityWebRequest,还需要一个AB包状态池,记录每个包是没下载、下载中、下载完成、下载失败。逻辑是:输入一个AB包信息列表,对UnityWebRequest池循环,如果当前是空闲,对AB包状态池循环,如果还没人下载,则给此下载器下载,并且break循环。

我们可以用Update轮询,也可以让下载器下载完通知管理器再分配任务。

在Update里做,先判断AB包是否全部下载完了,就对AB包状态池循环。那么就先对AB包状态池循环,我们发现

如果有AB包没分配下载,就要分配下载;

如果都分配下载了,没下载完,就等待;

如果都是下载完或失败,则返回结果;

发现需要分配下载是最先知道的,全部处理完毕要最后才知道;

然后我们要做成一个模块化的功能,不依赖外部,就输入一个AB包信息列表,最后执行回调,把结果结构体输入,结果包括结果枚举,还有下载失败的AB包清单。

看了一下API,创建UnityWebRequest就要输入url了,那么UnityWebRequest个数和AB包数量一样,下载器开始下载时UnityWebRequest是当场创建的。那么我们维护的是下载器池。

关于下载过程,之前是从开始到结束全部在一个IEnumerator函数里,

using (UnityWebRequest request = UnityWebRequest.Get(url))
{
    request.downloadHandler =
        new DownloadHandlerFile(savePath);
    var op= request.SendWebRequest();
    while (!op.isDone)
    {
        showBytes?.Invoke(request.downloadedBytes);
        yield return null;
    }
    if (request.result == UnityWebRequest.Result.Success)
    {
        result.ok = true;
    }
    else

现在我们改成开始+Update架构,在Update里查看下载器的进度,那么这个UnityWebRequest不能封在using里,否则cpu下载完前都不会离开这个using。

我们在Update里写两个函数,一个是检查是否同时有未下载文件和空闲下载器,有则分配下载,一个是检查每个下载器的状态,如果下载完毕就把UnityWebRequest Dispose,UnityWebRequestAsyncOperation=null。

然后这样有问题,就是我们想每次的下载分配和下载完成都打印,二者一一对应,但是我们在检查下载器状态里打印下载完毕,分配下载时通过UnityWebRequestAsyncOperation.isDone判断下载器空闲,就有下载器没轮到检查状态,就又分配下载了,导致二者对不上。那么我们需要下载器有一个唯一的入口,管理器检查时发现下载完了,设一个标志isDone,分配时下载器要么从没下载过,要么isDone,才能再分配,保证分配、完成打印对上。就是说不是UnityWebRequest下载完了代表下载器就下载完了,还需要管理器检查,打标记。

显示下载的字节数

如果要每帧一更新下载字节数,逻辑有点复杂,因为在下载过程中管理器需要把已下载完成的+每个下载器当前请求已下载的加起来,有文件下载完成需要把这些字节数加入管理器的已下载字节数。

还有一种比较简单的逻辑是一个下载器在下载完成时更新字节数,UI上看起来更新会不频繁一些,遇到大文件半天不更新。但是这样要处理的累加逻辑、要把字节数翻译成M、k为单位的文本工作量会少很多。

然后最终还要保证下载的字节数加起来和预期的相同。

代码

using MyFW;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
    public class ParallelDownloadMgr:
        MonoSingletonDontDestroy<ParallelDownloadMgr>
    {
        List<DownloadAgent> agents;
        List<ABDownloadState> ABStates;
        public Action<string> debugAction;
        [SerializeField]
        /// <summary>
        /// uint最多表示4G,可能不够
        /// </summary>
        ulong downloadedByteNum;
        public string resFolderUrl;
        public string localFolder;
        /// <summary>
        /// 在一次轮询中已经为某下载器分配了任务
        /// </summary>
        bool assigned;
        bool hasLeft;
        DownloadUIInfo[]UIInfos;
        public void Init(int downloaderNum)
        {
            agents=new(downloaderNum);
            for (int i = 0; i < downloaderNum; i++)
            {
                agents.Add(new DownloadAgent((uint)i));
            }
        }
        public void AssignABList(List<ABInfo> list)
        {
            if(list==null|| list.Count == 0)
            {
                debugAction?.Invoke("传入的AB列表为空");
                return;
            }
            ABStates = new(list.Count);
            for (int i = 0; i < list.Count; i++)
            {
                ABDownloadState state = new(list[i]);
                ABStates.Add(state);
            }
            hasLeft = true;
        }
        private void Update()
        {
            if (ABStates != null && ABStates.Count > 0&&hasLeft)
            {
                CheckAssign();
                foreach (var agent in agents)
                {
                    agent.Update();
                }
            }
        }
        void CheckAssign()
        {
            hasLeft = false;
            assigned= false;
            foreach (var abState in ABStates)
            {
                if (abState.downloadState == DownloadState.NotBegin)
                {
                    for(int i = 0;i<agents.Count;i++)
                    {
                        var agent = agents[i];
                        if (agent.IsIdle())
                        {
                            debugAction?.Invoke($"为{i}号下载器分配{abState.ABInfo.name}");
                            agent.Download(abState);
                            assigned = true;
                            break;
                        }
                    }
                }
                if (abState.downloadState != DownloadState.OK)
                {
                    hasLeft = true;
                }
                if (assigned)
                {
                    break;
                }
            }
            if (!hasLeft)
            {
                debugAction?.Invoke("下载完毕");
                ABStates.Clear();
            }
        }
        public void AddDownloadedByteNum(ulong val)
        {
            downloadedByteNum += val;
        }
        public DownloadUIInfo[] GetInfo()
        {
            if (agents == null || agents.Count == 0)
            {
                return null;
            }
            if (UIInfos == null)
            {
                UIInfos = new DownloadUIInfo[agents.Count];
            }
            for (int i = 0; i < agents.Count; i++)
            {
                UIInfos[i].Id =(uint)i;
                UIInfos[i].progress = agents[i].DownloadedProgress();
                UIInfos[i].downloadedByteNum = agents[i].DownloadedByteNum();
            }
            return UIInfos;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace MyFarm
{
    public class DownloadAgent
    {
        uint Id;
        ABDownloadState _state;
        UnityWebRequestAsyncOperation op;
        UnityWebRequest request;
        public DownloadAgent(uint Id)
        {
            this.Id = Id;
        }
        public void Download(ABDownloadState state)
        {
            if (request != null)
            {
                request.Dispose();
                op=null;
            }
            _state = state;
            _state.downloadState = DownloadState.Downloading;
            string fileName = state.ABInfo.name;
            string url = $"{ParallelDownloadMgr.Single.resFolderUrl}{fileName}";
            string savePath = Path.Combine(Application.persistentDataPath,
                ParallelDownloadMgr.Single.localFolder, fileName);
            AsyncReturn<byte[]> ret = new AsyncReturn<byte[]>();
            request = UnityWebRequest.Get(url);
            request.downloadHandler = new DownloadHandlerFile(savePath);
            op = request.SendWebRequest();
        }
        public void Update()
        {
            if (op != null && op.isDone)
            {
                Debug.Log($"<color=#FFA500>{Id}下载完了{request.uri}</color>");
                ParallelDownloadMgr.Single.AddDownloadedByteNum(request.downloadedBytes);
                request.Dispose();
                _state.downloadState = DownloadState.OK;
                op = null;
            }
        }
        public bool IsIdle()
        {
            return _state == null||(op==null);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
    public class ABDownloadState
    {
        ABInfo _ABInfo;
        public ABInfo ABInfo=>_ABInfo;
        public DownloadState downloadState;
        public ABDownloadState(ABInfo info)
        {
            _ABInfo = info;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
    public enum DownloadState:ushort
    {
        NotBegin,
        Downloading,
        OK,
        Fail,
    }
}

检查器

进度条更新并不灵敏。

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace MyFarm
{
    [CustomEditor(typeof(ParallelDownloadMgr))]
    public class ParalellDownloadEditor:Editor
    {
        ParallelDownloadMgr _Mgr;
        DownloadUIInfo[] UIInfos;
        private void OnEnable()
        {
            _Mgr = (ParallelDownloadMgr)target;
        }
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            UIInfos = _Mgr.GetInfo();
            if (UIInfos != null)
            {
                foreach (var info in UIInfos)
                {
                    EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(false, 20),
                        info.progress, $"下载器{info.Id}已下载{info.downloadedByteNum}byte");
                }
            }
        }
    }
}
namespace MyFarm
{
    public struct DownloadUIInfo
    {
        public uint Id;
        public float progress;
        public ulong downloadedByteNum;
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值