由OSS文件上传“借题发挥”

前言

功能实现思优化,程序员的奋斗路漫漫。
最近项目接入了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接口,支持重试,但是如何设置等待时间,对因网络情况,文件过大或服务器原因等造成的失败如何区别处理,分配不同的优先级?多想一步总会多条思路。

你可能感兴趣的:(由OSS文件上传“借题发挥”)