利用策略模式結合alibaba/alpha框架優化你的圖片上傳功能

圖片上傳做爲一個App常常用到的功能,項目中可使用各類成熟的框架去完成,但每每實際的狀況比想象的複雜。假設咱們的上傳功能須要知足下面的狀況:git

  1. 支持上傳一張圖
  2. 支持上傳多張圖
  3. 上傳多張圖時能獲取到每張圖的上傳進度,成功或失敗的狀態還有上傳時間,失敗緣由等
  4. 上傳多圖時獲取到總的上傳進度
  5. 上傳多圖時儘量縮短總時間
  6. 在項目的迭代中圖片上傳框架可能會被替換,因此要支持方便地切換上傳框架,但上傳的業務邏輯不受影響。

上面幾點要求實際上是比較廣泛的,讓咱們一點一點來看吧。 上傳一張圖好像沒什麼好說的。最後一點支持框架一鍵替換這裏選擇使用策略模式去封裝恰好比較符合要求。 主要是中間那幾點,多圖狀況下的上傳封裝優化。github

在我看過的項目中通常上傳多張圖片大概有三種寫法:bash

  1. for 循環上傳
  2. 遞歸上傳
  3. 放到一個隊列中上傳

顯然第一種很容易出錯且不可取,第二三種是上傳完一張再上傳下一張,這種方式的缺點就是時間太長了,並且一樣也容易出錯,咱們須要併發上傳。數據結構

有沒有一種數據結構能知足上面的要求呢,答案是 PERT圖 結構,若是不知道的你們能夠百度一下這個東西就知道了,可是這個結構該如何用代碼實現,阿里的 alpha 框架已經幫咱們實現好了,雖然它的介紹是用於 app 初始化的,但我以爲它也很適合這種狀況。併發

下面開始擼代碼(alpha 的源碼請本身去了解,這裏不是重點,下面整個思路很簡單,就是策略模式加框架的使用):app

第一步

由於 alpha 的任務是同步運行的,因爲上傳是一個異步操做,直接使用會致使有時序問題,因此要修改一下源碼,本身添加一個異步方法:框架

alpha:Task#start() 方法:

修改成: 異步

就是在 Task 的構造方法裏面添加多一個參數 isRunAsynchronous 判斷是否須要異步,異步的話無非就是加多個回調監聽,等回調回來的時候再往下執行。 (主要修改的地方是這裏,詳細代碼能夠本身看看 alpha 框架,其餘代碼見文末地址)ide

第二步,接下來開始寫策略模式了:

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 是封裝了一些其餘的上傳參數的一個類,這裏的監聽是內部用的,暴露在外面給咱們使用的回調是另外一個,下面會講到,之因此分開兩個監聽是由於不想太耦合。oop

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

相關文章
相關標籤/搜索