前言
功能实现思优化,程序员的奋斗路漫漫。
最近项目接入了oss文件上传功能,乍一听,这容易啊,文档不已经写的明明白白了。好嘛,文档给的通用方案好使是好使,但具体问题具体分析永远是王道。下面我就谈谈具体做的优化点,还请多多指教。
思路
沿袭一贯的自问自答式思维,捋一捋这次优化的思路。
实现一个基本的上传功能需要哪些步骤?
构造上传请求->设置回调->调用上传接口。有方案就有疑问。上传线程如何被创建?又将在何时被销毁呢?这涉及到app的内存占用,不得不多想一步,于是打开debugger,翻开源码。
调试可见,触发上传操作后多了1,2,3,4,5个线程
顺藤摸瓜,果然在源码里找到了ExecutorService,创建的线程数正是5个。
public static final int DEFAULT_BASE_THREAD_POOL_SIZE = 5;
private static ExecutorService executorService =
Executors.newFixedThreadPool(OSSConstants.DEFAULT_BASE_THREAD_POOL_SIZE, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "oss-android-api-thread");
}
});
查看调用
@Override
public OSSAsyncTask asyncPutObject(
PutObjectRequest request, final OSSCompletedCallback completedCallback) {
return internalRequestOperation.putObject(request, completedCallback);
}
public OSSAsyncTask putObject(
PutObjectRequest request, final OSSCompletedCallback completedCallback) {
··· ···
return OSSAsyncTask.wrapRequestTask(executorService.submit(callable), executionContext);
}
which means,oss内部维护了一个线程池,每次触发上传将从中获得相应线程执行任务。
既然这样,那不如··· ···创建一个远端Service,处于新的进程,用于上传的线程全都依附于这个新的进程,将上传任务真正隔离,既减少了对主进程的影响,逻辑上也更清晰。
于是新建:远端任务管理类RemoteOssTaskManager和远程服务类RemoteService,在onStartCommand中对不同的指令做出响应,如下
switch (msg_type) {
case REMOTE_OSS_UPLOAD_YY:
case REMOTE_OSS_UPLOAD_AC:
// 开启上传
··· ···
break;
case REMOTE_CHECK_STOP:
// 遍历任务列表,全部执行完毕则停止服务
··· ···
break;
}
注意在任务全部上传完毕后停止服务,以降低进程优先级。
实现
大致结构清楚了,聚焦实现,继续思考。
构造上传请求->设置回调->调用上传接口。这个流程中,上传任务都是并发么?如此,失败的任务如何记录?如何定义重试规则?等等
为了解决这些问题,我们来到之所以成为“借题发挥”的重点。
所谓“发挥”,即如下几点;
1.定义RemoteTaskItem对象(注意同管理类解耦),包含如下属性
String fileLocalPath;//文件本地存储路径
String ossSavedPath;// oss上的存储地址
PutObjectRequest request;
int retryCount = 0;// 重试次数
long nextUploadTime = 0;// 下次可执行时间 默认0
long lastUploadTime = 0;// 上次执行时间
2.对上传任务的操作都通过消息发送给handler进行处理,保证单线程和任务串行。处理的消息类型如下:
private static final int MSG_TASK_SUCCESSED = 1;
private static final int MSG_TASK_FAILED = 2;
private static final int MSG_TASK_ADD = 3;
private static final int MSG_TASK_CHECK = 4;
3.轮询任务列表,遍历列表检查任务状态(下次可执行时间)
增加无网延迟检查
遍历任务列表,找到最近时间可执行的任务,等待或直接执行
任务执行中延迟10s检查当前任务执行状态,超过30min没执行完任务挂起
流程如下
private long uploadAndCheck() {
if (!hasTask()) {
// 没有待执行任务 退出轮询
··· ···
return 0;
}
if (网络状况异常) {
return 30000; // 无网状态重试时间30s
}
// 当前任务超过最长等待时间
long currTime = System.currentTimeMillis();
if (mCurrTaskItem != null) {
if (mCurrTaskItem.lastUploadTime + MAX_EXECUTING_TIME < currTime) {
// 任务挂起或移除
··· ···
} else {
// 任务执行中
return 10000; // 每10s检查当前任务执行状态
}
}
// 遍历检查任务队列
long nextUploadTime = Long.MAX_VALUE;
for (RemoteTaskItem item : mRemoteTasks) {
if (item.nextUploadTime <= currTime) {// 任务初次执行或已为可执行状态
// 执行该任务并返回
··· ···
return 10000; // 任务执行中,每10s检查当前任务执行状态
} else if (nextUploadTime > item.nextUploadTime) {
nextUploadTime = item.nextUploadTime;
}
}
return nextUploadTime - currTime;
}
下面重点看一看handler回调方法,主要流程都蕴含其中:
@Override
public boolean handleMessage(Message msg) {
Object obj = msg.obj;
switch (msg.what) {
case MSG_TASK_ADD:
// 新任务加入上传队列
··· ···
break;
case MSG_TASK_CHECK:
// 轮询
long ms = uploadAndCheck();
if (ms > 0) {
sendUploadAndCheckAction(ms);
}
break;
case MSG_TASK_SUCCESSED:
// 上传成功 当前任务置空并从列表中移除
··· ···
break;
case MSG_TASK_FAILED:
// 上传失败 当前执行任务置空若超过最大重试次数或错误不可修复移除任务
··· ···
break;
}
return true;
}
最后
我们写一个功能时除了逻辑层面,必须有业务层面的考量。就oss上传功能而言,oss自身提供了retry接口,支持重试,但是如何设置等待时间,对因网络情况,文件过大或服务器原因等造成的失败如何区别处理,分配不同的优先级?多想一步总会多条思路。