做过客户端文件上传的同学会明白,基于HTTP的文件上传并没有看起来那么简单。按我过去经验,至少有两块工作会比看上去要麻烦一些,第一个是断点续传,第二个是进度展示。断点续传想要优化的好要花不少力气,后面有机会再写,这篇先看上传进度的问题。
首先说现象:我们调用第三方http framework上传文件的时候,都会有API回调告诉我们上传的具体进度,这个进度值都是不准的。
比如下面代码使用alamofire(使用AFNetworking也一样)标准API上传一个图片到服务器:
func doUpload() {
let fileURL = Bundle.main.url(forResource: "test", withExtension: "png")
var sentUnitCount: Int64 = 0
var start: CFTimeInterval = 0
var end: CFTimeInterval = 0
var cur: CFTimeInterval = CACurrentMediaTime()
Alamofire.upload(fileURL!, to: "https://httpbin.org/post")
.uploadProgress { progress in
print("Upload Progress: \(progress.fractionCompleted), sent bytes: \(progress.completedUnitCount-sentUnitCount), totoal sent bytes: \(progress.completedUnitCount/1024)")
sentUnitCount = progress.completedUnitCount
if progress.fractionCompleted == 1 {
start = CACurrentMediaTime();
}
let now = CACurrentMediaTime()
print("elapsed: \(now - cur)")
cur = now
}
.response { (rsp) in
end = CACurrentMediaTime();
print("\(end - start)")
}
}
上面的代码会上传一个大约20KB的图片到目标服务器,如果执行代码,你会发现uploadProgress会在一瞬间到达100%,无论设备的网络是快还是慢(大小为100KB的图片也一样)。我们当然知道一个20KB的图片不可能瞬间抵达服务器,那么这个进度到底准不准,问题在哪呢?
要理解这个uploadProgress的含义,需要理解tcp协议发送数据的方式,略去一些协议的细节,大致的模型可以用下图表示:
我们的客户端(TCP的发送方)会有一个send buffer,这个send buffer会缓存等待发送的数据,alamofire等第三方库会先将数据持续写入这个send buffer,每写一次就会调用一次uploadProgress,实际上这时候数据还老老实实待在send buffer当中,并没有抵达服务器,所以会有上面一调用upload函数进度瞬间到100%的现象,这时候如果你根据progress展示进度条,用户就会发现进度条瞬间跳到100%,之后一直卡住,直到send buffer当中的数据全部真正抵达Server并被Server Ack之后,才会进入response回调。
我拿自己的iPhone6测试1MB大小的文件,alamofire每写4KB数据会进入一次uploadProgress回调,一直写到大概120KB左右才停住,等这120KB传输完成之后,再啪啪啪写入后面的100多KB。
所以根据上面的分析,如果你对文件上传不做任何优化,会有两个奇怪的现象:
- 几十KB的小文件会在一瞬间progress到100%。
- 进度跳到100%之后,会卡住几秒(时长因网络状况而异)再进入response回调。
这种场景当然需要优化,怎么优化呢?有套路的,用个假的进度条就可以了。
解决方案
先不要管上传的进度,做个步调优雅的进度动画,慢慢儿的先滑到50%,看下 if 这时候上传进度有50%,继续往前滑到75%,else 没到50%就继续等上传进度,如此往复直达100%。为了避免100%之后还要等最后send buffer清空,可以在99%的位置等一等。这种假进度条的套路在浏览器加载网页的时候也会用到,使用过微信打开网页等加载完成的就能明白。这个套路广泛的应用于无法获取真实进度,却不得不给产品经理或者用户一个交代的场景。
这个上传进度的坑你踩过没呢?
课后作业:根据上面的分析,下载的进度准不准呢?