0x3、架构设计
从宏观角度看,文件上传的经历的过程跟车间流水线组装很像,以 袋装薯片 的生产流程为例:
土豆进厂 → 清洗削皮 → 切片烘干 → 350度高温油炸 → 加盐 → 按克分装充入氮气 → 袋装薯片
从土豆经历各种转换,到最后的袋装薯片,类比到我们的单个上传任务:
网络异常,图片无法展示
|
再抽象简化为三个部分:
网络异常,图片无法展示
|
任务构建和任务完成 这种流水线处理任务的方式,就很适合上 责任链模式 了。
传统责任链实现,单向往后传递,一层层拦截,直到有人处理为止。
这里参考下 OkHttp拦截器的实现,双向责任链,大概原理:
- Interceptor实现类调用 intercept(Chain) 往下传递Chain实例(包含此拦截器处理后的requests);
- 最后一个拦截器调用 chain.proceed() 返回Response实例,递归往上传递;
具体讲解可见:《把书读薄 | 《设计模式之美》设计模式与范式(行为型-责任链模式)》
这里可以先前后都是Task,后续再来拆,所以单个任务的组合变成了:
请求前的拦截器若干 → 执行上传请求 → 请求后的拦截器若干
执行上传请求,就交给用户自定义了,提供请求构造及发送请求的方法。成功与否,通过回调告知即可。
这是单个任务上传的情况,多个任务还需要:任务队列、轮询器和线程池。
当发起一个上传任务时,把任务加到队列中,轮询器不断从队列里拿任务(直到没任务),从线程池中拿个线程执行任务。
大概的原理很清晰,接着就是具体的代码实现了,代码注释写得很详尽了,就不再一一解释了~
0x4、库的使用
目前还是 写来玩玩状态(一堆坑,菜鸡边踩边改中~),感兴趣可以star下, 仓库地址:github.com/coder-pig/C…
添加依赖:
allprojects { repositories { ... maven { url 'https://jitpack.io' } } } dependencies { implementation 'com.github.coder-pig:CpLightUpload:v0.0.3' }
① 自定义Task
不同场景可能有不同的需求,按需自定义属性:
class CpImageTask : ImageTask() { var needRotate: Boolean? = null var needCompress: Boolean? = null var compressPercent: Int? = 80 } class CpVideoTask : VideoTask() { var limitSize: Int? = -1 // 视频限制大小 var compressVideoPath: String? = null // 压缩视频路径 var compressVideoMD5: String? = null // 压缩视频MD5 var firstFramePath: String? = null // 视频第一帧路径 var firstFrameMD5: String? = null // 视频第一帧MD5 }
② 自定义上传配置
就是一个上传的默认配置,当上传Task对应项没有配置时,填充默认配置:
class ImageUploadConfig : LightUploadConfig() { var needRotate: Boolean = true // 是否需要旋转纠正 var needCompress: Boolean = true // 是否需要压缩 var compressPercent: Int = 80 // 压缩比例,默认80 } class VideoUploadConfig : LightUploadConfig() { // 按需自定义 }
③ 自定义前拦截器
继承 Interceptor 接口,实现intercept() 方法:
class PictureRotateInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Task { val task = chain.task() val config = task.config as? ImageUploadConfig if (task is CpImageTask) { if(task.needRotate == null) task.needRotate = config?.needRotate "============ 判断是否需要图片翻转 ============".logV() val degree = FileUtils.readPictureDegree(task.filePath!!) if (degree != 0) { "图片旋转修正".logV() FileUtils.rotateToDegrees(task.filePath!!, degree.toFloat()) "图片旋转处理完毕".logV() } else { "不需要旋转修正.".logV() } } // 往下传递 return chain.proceed(task) } } class VideoFrameInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Task { val task = chain.task() if (task is CpVideoTask) { "生成视频缩略图...".logV() // 获取第一帧文件名 val tag = task.compressVideoPath!!.substring(task.compressVideoPath!!.lastIndexOf("/")) val frameFile = File(getExternalVideoPath() + tag + ".jpg") task.firstFramePath = frameFile.absolutePath val mmr = MediaMetadataRetriever() mmr.setDataSource(task.compressVideoPath!!) val frameBitmap = mmr.frameAtTime FileUtils.compressImage(frameBitmap, frameFile, 80) task.firstFrameMD5 = FileUtils.getFileMD5ToString(frameFile) LightUpload.upload(task = CpImageTask().apply { filePath = task.firstFramePath md5 = task.firstFrameMD5 }) frameBitmap?.recycle() } return chain.proceed(task) } }
④ 自定义请求
实现 Upload 抽象类,重写initRequest()和sendRequest()方法,对不同请求结果进行回调:
class HucUpload : Upload() { override fun sendRequest() { "开始文件上传...".logV() var ins: InputStream? = null try { mTask.reqData?.let { req -> val conn = (URL(req.uploadUrl).openConnection() as HttpURLConnection).apply { readTimeout = req.timeout!! connectTimeout = req.timeout!! doInput = true doOutput = true useCaches = false requestMethod = req.requestMethod // 请求头设置 val boundary = UUID.randomUUID() req.headers["Content-Type"] = "multipart/form-data;boundary=${boundary}" for ((k, v) in req.headers) setRequestProperty(k, v) val dos = DataOutputStream(outputStream) val sb = StringBuilder().append("--").append(boundary).append("\r\n") .append("Content-Disposition: form-data; name=\"file\"; filename=\"") .append(mTask.md5).append(mTask.fileName).append("\"") .append("\r\n") .append("Content-Type: application/octet-stream; charset=utf-8") .append("\r\n").append("\r\n") dos.write(sb.toString().toByteArray()) ins = FileInputStream(File(mTask.filePath!!)) val bytes = ByteArray(1024) var len: Int while (ins!!.read(bytes).also { len = it } != -1) { dos.write(bytes, 0, len) } ins!!.close() dos.write("\r\n".toByteArray()) val endData: ByteArray = "--$boundary--\r\n".toByteArray() dos.write(endData) dos.flush() } // 获取响应 val input = BufferedReader(InputStreamReader(conn.inputStream, "UTF-8")) val sb1 = StringBuilder() var ss: Int while (input.read().also { ss = it } != -1) { sb1.append(ss.toChar()) } val result = sb1.toString() "文件上传结束...".logV() mTask.response = Response(conn.responseCode, result) mTask.status = TaskStatus.DONE mCallback?.onSuccess(mTask) } } catch (e: IOException) { e.message?.logE() mTask.status = TaskStatus.FAILURE mTask.throwable = e mCallback?.onFailure(mTask) LightUpload.postTask(mTask) } finally { if (ins != null) { try { ins!!.close() } catch (e: IOException) { e.message?.logE() } } } } }