图片上传作为一个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);}@Overridepublic 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 Listprivate List<String> imagePaths = new ArrayList<>(); //单张图片的图片 urlprivate 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 是否为 nullcheckShowProgressDialog(options); //检查是否需要弹出上传提示框//上传sStrategy.uploadImageWithListener(0, imagePath, options, new UploadTaskListener(options));}/*** 具体上传回调*/private static class UploadTaskListener implements IUploadImageStrategy.OnUploadTaskListener {UploadOptions options;UploadTaskListener(UploadOptions options) {this.options = options;}@Overridepublic 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);}}});}@Overridepublic 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);//当上传一张图片的时候,回调一下上传完成方法,但是成功数量为 0if (options.isSingle()) {options.mUploadListener.onTotalSuccess(0, 1, 1);}}});}@Overridepublic void onSuccess(int sequence, String imageUrl) {sMainThreadExecutor.execute(() -> {//先取消掉提示框if (options.isSingle() && options.isShowProgress) {options.dismissProgressDialog();}if (options.mUploadListener != null) {options.mUploadListener.onSuccess(sequence, imageUrl);//当上传一张图片的时候,回调一下上传完成方法,成功数量为 1if (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;//检查 sStrategycheckStrategyNotNull();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 值,任务序号是 ifor (int i = 0; i < imagePaths.size(); i++) {//添加上传任务 TaskUploadImageTask 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");}@Overridepublic void run() {}@Overridepublic 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 是否为 nullprivate 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;}//一部执行的方法@Overridepublic void runAsynchronous(OnTaskAnsyListener listener) {//上传方法mStrategy.uploadImageWithListener(mCurrTaskSequence, imagePath, mOptions,new IUploadImageStrategy.OnUploadTaskListener() {@Overridepublic void onProcess(int sequence, long current, long total) {mOnUploadTaskListener.onProcess(mCurrTaskSequence, current, total);}@Overridepublic void onFailure(int sequence, int errorCode, String errorMsg) {listener.onTaskFinish(mName, "fail"); mOnUploadTaskListener.onFailure(mCurrTaskSequence, errorCode, errorMsg);}@Overridepublic 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() {@Overridepublic void onSuccess(int sequence, String imageUrl) {LogUtil.i("第 " + sequence + " 张图上传成功,url = " + imageUrl);}@Overridepublic void onFailure(int sequence, int errorCode, String errorMsg) {super.onFailure(sequence, errorCode, errorMsg);LogUtil.i("第 " + sequence + " 张图上传失败,errorMsg = " + errorMsg);}@Overridepublic void onTotalSuccess(int successNum, int failNum, int totalNum) {LogUtil.i("全部上传完成,成功数量 = " + successNum + " 失败数量 = " + failNum + " 总数 = " + totalNum);}}).go();
复制代码

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

代码地址: ImageUploadManager

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

  1. ssm框架结合Ajax实现图片上传功能

    先上代码 一.html代码 <div><img src="img/头像.png" alt="选择并上传头像" id="avatar_ ...

  2. SSM框架-添加信息及图片上传到本地MultipartResolver-foreknow_cms

    MultipartResolver 用于处理文件上传,当收到请求时 DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isM ...

  3. java struts2 上传图片_Java框架Struts2实现图片上传功能

    Struts 2 框架为处理文件上传提供了内置支持,它使用"在 HTML 中基于表单的文件上传".当上传一个文件时,它通常会被存储在一个临时目录中,而且它们应该由 Action 类 ...

  4. 图片上传功能(EasyUI前台框架+SSM框架)

    文件上传步骤: * 1.采用文件正确的接收方式(修改3处配置文件/接口类型等) * 2.判断是否为一个图片,0表示无异常,1代表异常(jpg|gif|png) * 3.判断是不是一个"正经& ...

  5. ssm上传文件获取路径_ssm框架实现图片上传显示并保存地址到数据库(示例代码)...

    本案例是通过springmvc+spring+mybatis框架以商品上传为例,实现的图片上传功能,并把图片的地址保存到数据库并在前台显示上传的图片. 本项目是使用maven搭建的项目,首先看下项目结 ...

  6. python多个if怎么优化_利用策略模式优化过多 if else 代码

    前言 利用利用策略模式实际开发中 if else 条件判断过多的问题 比如平时大家是否都会写类似这样的代码: if(a) {//dosomething }else if(b) {//doshometh ...

  7. java策略模式 if else_Java如何利用策略模式替代if/else语句

    平时在开发中避免不了使用大量的if else语句,但过多层的if else对于性能有很大的开销,类似如下代码 public class MainStart { public static void m ...

  8. 商城项目笔记一:搭建Maven工程,利用Dubbo实现SOA面向服务框架,部署zookeeper注册中心,FastDFS框架实现图片上传,部署nginx服务器。

    文章目录 1. 商城项目总结笔记: 1.1. 第一天工作记录:搭建Maven工程 1.2. 第二天工作记录:创建SOA面向服务架构,通过工具类实现分页技术 1.3. 第三天工作记录:部署nginx服务 ...

  9. 优化篇-“移动端”图片上传架构的变迁

    做互联网应用少不了图片的支撑,图片的上传.浏览速度很大程度上决定着用户的体验,甚至用户去留,就因为其重要,所以,在任何时候,图片的架构和优化都在进行,不敢丝毫放松. 在以后几个章节,会从后端图片存储. ...

最新文章

  1. [转] 微软SQL Server 2008故障转移集群概述(Windows Server Failover Clustering (WSFC))
  2. linux 卸载 flash,使用率下降到8%,Chrome 87将完全移除Flash
  3. DeepFakes天敌来了!伯克利紧急研发“火眼金睛”防伪克星
  4. BZOJ1439 : YY的问题
  5. xenserver 安装新硬盘_给Xenserver添加新硬盘
  6. 数据结构(Java)-哈希表
  7. Linux操作系统基础解析之(四)——Linux基本命令剖析(2)
  8. Date对象在Android和IOS上的兼容
  9. 机器学习 - [源码实现决策树小专题]决策树学习中如何进行分类预测
  10. 角度标注-Visio制图总结(四)
  11. java手机牧场物语,Minecraft Java版 19w09a 发布
  12. 直播企业掀“冲击IPO”浪潮,为何老玩家成受益者?
  13. 钻井液中PHP是指什么,关于钻井液,你必须知道这些…
  14. C#nameof用法
  15. Some things about RESUME
  16. python 山脊图_纯Python绘制满满艺术感的山脊地图
  17. MYSQL的字符串支持保存表情,比如微信表情
  18. FOne HCI超融合系统
  19. 区块链+能源:乌托邦愿景还是未来蓝图?
  20. 营改增后计算机维修费税率,2020国税维修费税率是多少?

热门文章

  1. 实操指南:用谷歌AutoML构建图像分类模型
  2. 减免租金?也要体谅业主难处哟
  3. BERT新转变:面向视觉基础进行预训练
  4. 解析卷积的高速计算中的细节,一步步代码带你飞
  5. 印度交通部或禁止无人驾驶汽车进入本土市场
  6. SAP MM 没有录入盘点结果的盘点凭证不能执行MI07
  7. 统计学习的三个招式:模型、策略和算法
  8. 距离产生美?k近邻算法python实现
  9. 意见征集,世界AI智商评测量标准2018年新版讨论方案
  10. 福利丨网友授课视频分享:机器学习实战-KNN-第一部分