利用策略模式结合alibaba/alpha框架优化你的图片上传功能

2,632 阅读10分钟

图片上传作为一个App经常用到的功能,项目中可以使用各种成熟的框架去完成,但往往实际的情况比想象的复杂。假设我们的上传功能需要满足下面的情况:

  1. 支持上传一张图
  2. 支持上传多张图
  3. 上传多张图时能获取到每张图的上传进度,成功或失败的状态还有上传时间,失败原因等
  4. 上传多图时获取到总的上传进度
  5. 上传多图时尽可能缩短总时间
  6. 在项目的迭代中图片上传框架可能会被替换,所以要支持方便地切换上传框架,但上传的业务逻辑不受影响。

上面几点要求其实是比较普遍的,让我们一点一点来看吧。 上传一张图好像没什么好说的。最后一点支持框架一键替换这里选择使用策略模式去封装刚好比较符合要求。 主要是中间那几点,多图情况下的上传封装优化。

在我看过的项目中一般上传多张图片大概有三种写法:

  1. for 循环上传
  2. 递归上传
  3. 放到一个队列中上传

显然第一种很容易出错且不可取,第二三种是上传完一张再上传下一张,这种方式的缺点就是时间太长了,而且同样也容易出错,我们需要并发上传。

有没有一种数据结构能满足上面的要求呢,答案是 PERT图 结构,如果不知道的大家可以百度一下这个东西就知道了,但是这个结构该如何用代码实现,阿里的 alpha 框架已经帮我们实现好了,虽然它的介绍是用于 app 初始化的,但我觉得它也很适合这种情况。

下面开始撸代码(alpha 的源码请自己去了解,这里不是重点,下面整个思路很简单,就是策略模式加框架的使用):

第一步

因为 alpha 的任务是同步运行的,由于上传是一个异步操作,直接使用会导致有时序问题,所以要修改一下源码,自己添加一个异步方法:

alpha:Task#start() 方法:

修改为:

就是在 Task 的构造方法里面添加多一个参数 isRunAsynchronous 判断是否需要异步,异步的话无非就是加多个回调监听,等回调回来的时候再往下执行。 (主要修改的地方是这里,详细代码可以自己看看 alpha 框架,其他代码见文末地址)

第二步,接下来开始写策略模式了:

1. 首先定义一个接口,定义一下下载功能

public interface IUploadImageStrategy {
    void uploadImageWithListener(int sequence, String imagePath, UploadOptions options,
                                 OnUploadTaskListener taskListener);

    interface OnUploadTaskListener {
        //每一张图片上传的进度
        void onProcess(int sequence, long current, long total); 
        //每一张图片的上传失败回调
        void onFailure(int sequence, int errorCode, String errorMsg); 
        //每一张图片的上传成功回调
        void onSuccess(int sequence, String imageUrl); 
    }
}

可以看到定义了一个带监听的上传方法,sequence 代表当前第几张图片,imagePath 图片路径,UploadOptions 是封装了一些其他的上传参数的一个类,这里的监听是内部用的,暴露在外面给我们使用的回调是另一个,下面会讲到,之所以分开两个监听是因为不想太耦合。

2. 然后继承接口实现具体上传功能(这里以七牛上传为例,如果是其他框架,同样也是继承接口实现方法即可)

public class QiNiuUploader implements IUploadImageStrategy {

    private static final String TAG = "QiNiuUploader";
    private UploadManager uploadManager;

    QiNiuUploader() {
        Configuration config = new Configuration.Builder()
                .zone(FixedZone.zone0)       
                .build();
        uploadManager = new UploadManager(config);
    }

    @Override
    public void uploadImageWithListener(int sequence, String imagePath, UploadOptions options,
                                        OnUploadTaskListener taskListener) {
       //七牛上传具体实现,吧啦吧啦吧一堆代码...
    }
}

3. 看看上面说到的 UploadOptions类,对其他上传参数的封装,Builder 模式

public class UploadOptions {
    private boolean isSingle; //是否上传单张图片
    String type; //七牛参数
    String operation; //七牛参数
    boolean isShowProgress; //是否展示进度提示
    String progressTip;     //提示内容
    private boolean isCanDismissProgress; //是否可取消提示
    IUploadListener mUploadListener; //对外的回调监听
    private ProgressDialog mProgressDialog; //提示弹出
    
    //上面这些参数都是通过建造者模式去构建,这里省略Bilder构建参数的一堆方法...
    
    //上传方法
    public void go() {
        //单张图片
        if (isSingle) {
            UploadImageManager.getInstance().loadOptionsAtOneImage(this);
        }else{
            //多张图片
            UploadImageManager.getInstance().loadOptionsAtMoreImage(this);
        }
    }
}

UploadOptions 类的作用主要是封装好上传参数,然后传给管理类去上传,可以有隔离的作用,里面的参数可以根据具体情况来添加或删除。这里的 go() 方法就相当于 Builder 中最后那个 build() 方法一样。

4. 对外的回调监听 IUploadListener

public interface IUploadListener {
    //多张或单张图片时每一张上传进度
    void onProcess(int sequence, long current, long total); 
    //多张图片时总的上传进度
    void onTotalProcess(long current, long total); 
    //每一张的上传失败回调
    void onFailure(int sequence, int errorCode, String errorMsg); 
    //每一张的上传成功回调
    void onSuccess(int sequence, String imageUrl, String imageName, String bucket); 
    //多张图片时总的上传成功回调
    void onTotalSuccess(int successNum, int failNum, int totalNum); 
}

其实这个跟上面提到的 OnUploadTaskListener 差不多,不过这里做了更细的划分而已。

5. 接下来关键在于 ImageUploadManager 图片上传管理类(代码略长一点点,有注释):

// 上传图片管理类,单张图片直接上传,多张图片扔到PERT图中上传
public class ImageUploadManager {
    //单例模式
    private static volatile UploadImageManager sInstance; 
    //上传接口,里面实现了具体的上传方法
    private static IUploadImageStrategy sStrategy;
    //主线程,保证在子线程中调用也没事
    static final Executor sMainThreadExecutor = new MainThreadExecutor(); 
    //多张图片的 url List
    private List<String> imagePaths = new ArrayList<>(); 
    //单张图片的图片 url
    private String imagePath; 
    
    //构造方法
    private ImageUploadManager() {
        //可以看到,这里通过策略模式可以实现一键切换上传方案,不影响具体业务逻辑
        if (Constant.useQiNuiUpload) {
            setGlobalImageLoader(new QiNiuUploader()); //选择七牛上传
        } else {
            setGlobalImageLoader(new Otherloader()); //选择其他方式上传
        }
    }

    //设置上传方式
    public void setGlobalImageLoader(IUploadImageStrategy strategy) {
        sStrategy = strategy;
    }
    
    //单例模式
    public static ImageUploadManager getInstance() {
        if (sInstance == null) {
            synchronized (ImageUploadManager.class) {
                if (sInstance == null) {
                    sInstance = new ImageUploadManager();
                }
            }
        }
        return sInstance;
    }
    
    //上传图片方法,单张图片
    public UploadOptions uploadImage(String imagePath) {
        this.imagePath = imagePath;
        UploadOptions options = new UploadOptions();
        options.setSingle(true); //设置标记位
        return options;
    }

    //上传图片方法,多张图片
    public UploadOptions uploadImage(List<String> imagePaths) {
        this.imagePaths = imagePaths;
        UploadOptions options = new UploadOptions();
        options.setSingle(false);  //设置标记位
        return options;
    }

    /**
     * 单张图片上传,被UploadOptions中的 go() 方法调用
     */
    void loadOptionsAtOneImage(UploadOptions options) {
        sMainThreadExecutor.execute(() -> setUploadImageAtOneImage(options));
    }

    /**
     * 多张图片上传,被UploadOptions中的 go() 方法调用
     */
    void loadOptionsAtMoreImage(UploadOptions options) {
        sMainThreadExecutor.execute(() -> setUploadImageAtMoreImage(options));
    }

    //单张图片上传具体实现
    private void setUploadImageAtOneImage(UploadOptions options) {
        checkStrategyNotNull(); //检查 sStrategy 是否为 null
        checkShowProgressDialog(options); //检查是否需要弹出上传提示框
        //上传
        sStrategy.uploadImageWithListener(0, imagePath, options, new UploadTaskListener(options));
    }

    /**
     * 具体上传回调
     */
    private static class UploadTaskListener implements IUploadImageStrategy.OnUploadTaskListener {
        UploadOptions options;

        UploadTaskListener(UploadOptions options) {
            this.options = options;
        }

        @Override
        public void onProcess(int sequence, long current, long total) {
            sMainThreadExecutor.execute(() -> {
                if (options.mUploadListener != null) {
                    options.mUploadListener.onProcess(sequence, current, total);
                    //当上传一张图片的时候,也把 onTotalProcess 设置一下
                    if (options.isSingle()) {
                        options.mUploadListener.onTotalProcess(current, total);
                    }
                }
            });
        }

        @Override
        public void onFailure(int sequence, int errorCode, String errorMsg) {
            sMainThreadExecutor.execute(() -> {
                //先取消掉提示框
                if (options.isSingle() && options.isShowProgress) {
                    options.dismissProgressDialog();
                }
                if (options.mUploadListener != null) {
                    options.mUploadListener.onFailure(sequence, errorCode, errorMsg);
                    //当上传一张图片的时候,回调一下上传完成方法,但是成功数量为 0
                    if (options.isSingle()) {
                        options.mUploadListener.onTotalSuccess(0, 1, 1);
                    }
                }
            });
        }

        @Override
        public void onSuccess(int sequence, String imageUrl) {
            sMainThreadExecutor.execute(() -> {
                //先取消掉提示框
                if (options.isSingle() && options.isShowProgress) {
                    options.dismissProgressDialog();
                }
                if (options.mUploadListener != null) {
                    options.mUploadListener.onSuccess(sequence, imageUrl);
                    //当上传一张图片的时候,回调一下上传完成方法,成功数量为 1
                    if (options.isSingle()) {
                        options.mUploadListener.onTotalSuccess(1, 0, 1);
                    }
                }
            });
        }
    }

    //多张图片时的:
    private int successNum; //上传成功数量
    private int failNum;    //上传失败数量
    private int totalNum;   //上传总数
    private int currentIndex; //当前上传到第几张(从0开始)

    //利用PERT图结构(总分总)上传,图片上传耗时 约等于 所有图片中耗时最长的那张图片的时间
    private void setUploadImageAtMoreImage(UploadOptions options) {
        IUploadImageStrategy strategy;
        //检查 sStrategy
        checkStrategyNotNull();
        strategy = sStrategy;
        //初始化变量
        successNum = 0;
        failNum = 0;
        currentIndex = 0;
        totalNum = imagePaths.size();
        //检查是否需要弹出提示框
        checkShowProgressDialog(options);
        //创建一个空的PERT头
        EmptyTask firstTask = new EmptyTask();
        Project.Builder builder = new Project.Builder();
        builder.add(firstTask); //添加一个耗时基本为0的紧前
        //循环添加任务到alpha中,任务名是 url 的 md5 值,任务序号是 i
        for (int i = 0; i < imagePaths.size(); i++) {
            //添加上传任务 Task
            UploadImageTask task = new UploadImageTask(MD5.hexdigest(imagePaths.get(i)),
                    i, strategy, options, imagePaths.get(i),
                    new UploadTaskListener(options));
            //每个 task 添加执行完成回调,里面做数量的计算
            task.addOnTaskFinishListener((taskName, currTaskSequence, taskStatus) -> {
                LogUtil.i(taskName + " OnTaskFinish  taskStatus = " + taskStatus);
                if ("success".equals(taskStatus)) {
                    successNum++;
                } else {
                    failNum++;
                }
                currentIndex++;
                //这里回调总的下载进度
                if (options.mUploadListener != null) {
                    options.mUploadListener.onTotalProcess((currentIndex / totalNum) * 100, 100);
                }
            });
            builder.add(task).after(firstTask); //其他任务全部为紧后,同步执行
        }
        Project project = builder.create();
        //添加全部 task 上传完时的回调
        project.addOnTaskFinishListener((taskName, currTaskSequence, taskStatus) -> {
            if (options.isShowProgress) {
                options.dismissProgressDialog();
            }
            if (options.mUploadListener != null) {
                options.mUploadListener.onTotalSuccess(successNum, failNum, totalNum);
            }
        });
        AlphaManager.getInstance(options.mContext).addProject(project);
        //开始上传
        AlphaManager.getInstance(options.mContext).start();
    }

    private static class EmptyTask extends Task {

        EmptyTask() {
            super("EmptyTask");
        }

        @Override
        public void run() {

        }

        @Override
        public void runAsynchronous(OnTaskAnsyListener listener) {

        }
    }
    
    //检查一下是否需要弹出上传提提示框
    private void checkShowProgressDialog(UploadOptions options) {
        if (options.isShowProgress) {
            if (!TextUtils.isEmpty(options.progressTip)) {
                options.showProgressDialog(options.progressTip);
            } else {
                options.showProgressDialog();
            }
        }
    }

    //检查一下 sStrategy 是否为 null
    private void checkStrategyNotNull() {
        if (sStrategy == null) {
            throw new NullPointerException("you must be set your IUploadImageStrategy at first!");
        }
    }

    //主线程,如果当前为主线程,则直接执行,否则切到主线程执行
    private static class MainThreadExecutor implements Executor {
        final Handler mHandler = new Handler(Looper.getMainLooper());

        MainThreadExecutor() {
        }

        public void execute(@NonNull Runnable command) {
            if (checkIsMainThread()) {
                command.run();
            } else {
                this.mHandler.post(command);
            }
        }
    }

    private static boolean checkIsMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }
}
  1. 上面代码的上传任务 UploadImageTask 代码如下:
public class UploadImageTask extends Task {
    private IUploadImageStrategy mStrategy;
    private UploadOptions mOptions;
    private String imagePath;
    private IUploadImageStrategy.OnUploadTaskListener mOnUploadTaskListener;

    UploadImageTask(String name, int sequence,
                    IUploadImageStrategy strategy,
                    UploadOptions options, String imagePath,
                    IUploadImageStrategy.OnUploadTaskListener taskListener) {
        super(name, true, true, sequence);
        this.mStrategy = strategy;
        this.mOptions = options;
        this.imagePath = imagePath;
        mOnUploadTaskListener = taskListener;
    }
    
    //一部执行的方法
    @Override
    public void runAsynchronous(OnTaskAnsyListener listener) {
        //上传方法
        mStrategy.uploadImageWithListener(mCurrTaskSequence, imagePath, mOptions,
                new IUploadImageStrategy.OnUploadTaskListener() {
                    @Override
                    public void onProcess(int sequence, long current, long total) {
                        mOnUploadTaskListener.onProcess(mCurrTaskSequence, current, total);
                    }

                    @Override
                    public void onFailure(int sequence, int errorCode, String errorMsg) {
                        listener.onTaskFinish(mName, "fail"); 
                        mOnUploadTaskListener.onFailure(mCurrTaskSequence, errorCode, errorMsg);
                    }

                    @Override
                    public void onSuccess(int sequence, String imageUrl, String imageName, String bucket) {
                        listener.onTaskFinish(mName, "success");
                        mOnUploadTaskListener.onSuccess(mCurrTaskSequence, imageUrl, imageName, bucket);
                    }
                });
    }
}

好,大概代码就如上所示。在上传多张图片那里可能有点懵逼,这里解释一下:

  1. 紧前的意思是 是前道工序
  2. 紧后 的意思是 是后道工序
  3. 代码中的PERT图结构是这样的:

开头的 EmptyTask 执行时间基本为 0,其他上传 Task 全部都在它的后面同步执行,最后再汇总。所以整个上传时间基本等于 N 张图片中单张上传用时最久的那个时间。而且由于的PERT图的特点,你还可以知道每个任务的用时,全部任务的用时,还有每个任务的状态以及进度,每个任务还可以随你选择在主线程还是子线程去完成。



经过一顿操作之后,可以看到经过封装后还是是有下面这些好处的:

  1. 每个上传任务都能获取到状态,进度,用时等。
  2. 采用了策略模式,将具体上传与上传参数还有上传管理分离,解耦合,而且维护和使用都方便。
  3. 满足了一开始提出来的几点要求。


最后看看折腾过后的使用方式(简单例子):

UploadOptions options = imageList.size() == 1
        ? UploadImageManager.getInstance().uploadImage(imageList.get(0))
        : UploadImageManager.getInstance().uploadImage(imageList);
options.uploadListener(new IUploadListener.SimpleUploadListener() {

        @Override
        public void onSuccess(int sequence, String imageUrl) {
            LogUtil.i("第 " + sequence + " 张图上传成功,url = " + imageUrl);
        }

        @Override
        public void onFailure(int sequence, int errorCode, String errorMsg) {
            super.onFailure(sequence, errorCode, errorMsg);
            LogUtil.i("第 " + sequence + " 张图上传失败,errorMsg = " + errorMsg);
        }

        @Override
        public void onTotalSuccess(int successNum, int failNum, int totalNum) {
            LogUtil.i("全部上传完成,成功数量 = " + successNum + " 失败数量 = " + failNum + " 总数 = " + totalNum);
        }
    }).go();

是不是感觉还行。虽然实现和原理都是平时很常见和用得比较多的东西,但是效果还可以把,你值得拥有。

代码地址: ImageUploadManager