目录
正文
下载篇(7)是获取边下边玩的AB包列表,这一步只是获取列表,并没有下载。
本篇文章,也就是下载篇(9)启动边下边玩流程,可以在热更结束后的任意时刻开始下载。毕竟是边玩边下流程,和启动热更流程混到一起就不合适了。
//开启下载
private IEnumerator Download()
{
// 计算下载文件的总大小
totalDownloadCount = _patcher.InGameDownloadList.Count;
if (totalDownloadCount == 0)
{
PatchEventDispatcher.SendInGameWebFileDownloadOverMsg();
_patcher.Switch(EPatchStates.InGameDownloadOver.ToString());
yield break;
}
//先检查一遍temp目录有没有下载完的, 计算这次实际需要下载的个数和size
IEnumerator innerCoroutineEnumerator = CheckDownloaded(_patcher.InGameDownloadList, false);
while(innerCoroutineEnumerator.MoveNext())
yield return innerCoroutineEnumerator.Current;
// 开始下载列表里的所有资源
PatchHelper.Log(ELogLevel.Log, $"Begine download web files : {totalDownloadCount-currentDownloadCount}");
PatchEventDispatcher.SendPatchStatesChangeMsg(EPatchStates.InGameDownloadWebFiles);
var startTime = Time.realtimeSinceStartup;
var newDownloaded = new List<PatchElement>(_patcher.InGameDownloadList.Count);
foreach (var element in _patcher.InGameDownloadList)
{
if (element.SkipDownload) continue;
newDownloaded.Add(element);
}
//是否开启多任务下载
if (useMultiRequest)
{
yield return DownloadWithMultiTask(newDownloaded);
}
else
{
yield return DownloadWithSingleTask(newDownloaded);
}
if (failedOnDownload)
{
yield break;
}
MotionLog.Log(ELogLevel.Log, $"<color=#ff0000>Downloading {newDownloaded.Count} files cost {Time.realtimeSinceStartup-startTime} sec.</color>");
//全部下载完成后把这次新下载的文件再校验一遍,如果有文件失败就退出
IEnumerator coroutineEnumerator = CheckDownloaded(newDownloaded, true);
while(coroutineEnumerator.MoveNext())
yield return coroutineEnumerator.Current;
if(failedOnCheckDownload)
{
yield break;
}
// 最后清空下载列表
_patcher.InGameDownloadList.Clear();
_patcher.SwitchNext();
}
//检查是否有已完成或下载中断的文件。
private IEnumerator CheckDownloaded(List<PatchElement> list, bool isPostCheck)
{
PatchEventDispatcher.SendCheckDownloadedFileMd5();
var sw = new Stopwatch();
sw.Start();
var buf = new byte[10240];
this.totalDownloadSizeKB = 0;
this.currentDownloadSizeKB = 0;
this.currentDownloadCount = 0;
using (var md5 = System.Security.Cryptography.MD5.Create())
{
foreach (var ele in list)
{
this.totalDownloadSizeKB += ele.SizeKB;
string savePath = AssetPathHelper.MakeDownloadTempPath(ele.Name);
if (!File.Exists(savePath))
{
//下载后的检查需要抛出异常,下载前的检查跳过不存在文件
if (isPostCheck)
{
PatchHelper.Log(ELogLevel.Error, $"[checking md5] file is not existed: {ele.Name}");
failedOnCheckDownload = true;
yield break;
}
else
{
continue;
}
}
using (var fs = new FileStream(savePath, FileMode.Open, FileAccess.Read))
{
int byteRead;
md5.Initialize();
while ((byteRead = fs.Read(buf, 0, buf.Length)) > 0)
{
md5.TransformBlock(buf, 0, byteRead, null, 0);
if (sw.ElapsedMilliseconds > 20)
{
yield return null;
sw.Restart();
}
}
md5.TransformFinalBlock(buf, 0, 0);
fs.Close();
string localMd5 = BitConverter.ToString(md5.Hash).Replace("-", "");
if (string.Equals(ele.MD5, localMd5, StringComparison.OrdinalIgnoreCase))
{
//MotionLog.Log(ELogLevel.Log, StringFormat.Format("skip download existed file: {0}", savePath));
ele.SkipDownload = true;
this.currentDownloadSizeKB += ele.SizeKB;
this.currentDownloadCount++;
}
else if (isPostCheck)
{
PatchHelper.Log(ELogLevel.Error, $"Web file md5 verification error : {ele.Name}");
PatchHelper.Log(ELogLevel.Error, $"local md5 is : {localMd5}");
PatchHelper.Log(ELogLevel.Error, $"md5 in manifest is : {ele.MD5}");
PatchEventDispatcher.SendWebFileMD5VerifyFailedMsg(ele.Name);
failedOnCheckDownload = true;
File.Delete(savePath);
yield break;
}
}
}
}
}
单任务下载
IEnumerator DownloadWithSingleTask(List<PatchElement> newDownloaded)
{
foreach (var element in newDownloaded)
{
string url = _patcher.GetWebDownloadURL(element.Name);
string savePath = AssetPathHelper.MakeDownloadTempPath(element.Name);
PatchHelper.CreateFileDirectory(savePath);
// 创建下载器
using (var download = new WebFileRequest(url, savePath, OnDownloadProgress))
{
yield return download.DownLoad();
//PatchHelper.Log(ELogLevel.Log, $"Web file is download : {savePath}");
// 检测是否下载失败
if (download.States != EWebRequestStates.Success)
{
PatchEventDispatcher.SendWebFileDownloadFailedMsg(url, element.Name);
failedOnDownload = true;
yield break;
}
}
currentDownloadSizeKB += element.SizeKB;
currentDownloadCount++;
element.SavePath = savePath;
}
}
多任务下载
private class DownloadJob
{
public UnityWebRequest req;
public PatchElement info;
}
IEnumerator DownloadWithMultiTask(List<PatchElement> downloadList)
{
var maxJobs = 2;
var runningJobs = new List<DownloadJob>(maxJobs);
var lastRecordTime = Time.realtimeSinceStartup;
int assignedIdx = 0;
while (assignedIdx < downloadList.Count)
{
while (runningJobs.Count < maxJobs && assignedIdx < downloadList.Count)
{
var element = downloadList[assignedIdx];
var url = _patcher.GetWebDownloadURL(element.Name);
string savePath = AssetPathHelper.MakeDownloadTempPath(element.Name);
PatchHelper.CreateFileDirectory(savePath);
var job = UnityWebRequest.Get(url);
job.downloadHandler = new DownloadHandlerFile(savePath) { removeFileOnAbort = true };
job.SendWebRequest();
runningJobs.Add(new DownloadJob()
{
req = job,
info = element
});
assignedIdx++;
}
yield return new WaitUntil(() => CheckAnyDownloadFinished(runningJobs, ref lastRecordTime));
if (failedOnDownload) yield break;
}
yield return new WaitUntil(() => CheckAllDownloadFinished(runningJobs, ref lastRecordTime));
//clear up all request jobs
foreach(var job in runningJobs)
{
LogIfFailed(job);
job.req.Dispose();
}
runningJobs.Clear();
}
//检查是否有任务完成
bool CheckAnyDownloadFinished(List<DownloadJob> jobs, ref float lastRecordTime)
{
long downloadedBytes = 0;
int finished = 0;
for(var i=jobs.Count-1; i>=0; i--)
{
var job = jobs[i];
if(job.req.isDone)
{
finished++;
LogIfFailed(job);
jobs.RemoveAt(i);
downloadedBytes += job.info.SizeKB;
job.req.Dispose();
}
}
if (finished>0)
{
var deltaTime = Time.realtimeSinceStartup - lastRecordTime;
currentDownloadCount += finished;
currentDownloadSizeKB += downloadedBytes;
OnDownloadProgress(downloadedBytes, downloadedBytes / deltaTime);
lastRecordTime += deltaTime;
}
return finished > 0;
}
//检查所有任务是否完成
bool CheckAllDownloadFinished(List<DownloadJob> jobs, ref float lastRecordTime)
{
long downloadedBytes = 0;
foreach(var job in jobs)
{
if (!job.req.isDone) return false;
downloadedBytes += job.info.SizeKB;
}
var deltaTime = Time.realtimeSinceStartup - lastRecordTime;
if(deltaTime > 0)
{
currentDownloadCount += jobs.Count;
currentDownloadSizeKB += downloadedBytes;
OnDownloadProgress(downloadedBytes, downloadedBytes / deltaTime);
lastRecordTime += deltaTime;
}
return true;
}
请注意,InGame模式的AB包下载和热更AB包没有区别,甚至这两步的代码几乎一模一样,虽然代码分开写的,封装在一起没有任何问题,下载完成后,记得把临时文件夹的AB包剪切到persistentDataPath文件夹内就好啦。