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

L_Xian發表於2019-04-19

圖片上傳作為一個App經常用到的功能,專案中可以使用各種成熟的框架去完成,但往往實際的情況比想象的複雜。假設我們的上傳功能需要滿足下面的情況:

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

上面幾點要求其實是比較普遍的,讓我們一點一點來看吧。 上傳一張圖好像沒什麼好說的。最後一點支援框架一鍵替換這裡選擇使用策略模式去封裝剛好比較符合要求。 主要是中間那幾點,多圖情況下的上傳封裝優化。

在我看過的專案中一般上傳多張圖片大概有三種寫法:

  1. for 迴圈上傳
  2. 遞迴上傳
  3. 放到一個佇列中上傳

顯然第一種很容易出錯且不可取,第二三種是上傳完一張再上傳下一張,這種方式的缺點就是時間太長了,而且同樣也容易出錯,我們需要併發上傳。

有沒有一種資料結構能滿足上面的要求呢,答案是 PERT圖 結構,如果不知道的大家可以百度一下這個東西就知道了,但是這個結構該如何用程式碼實現,阿里的 alpha 框架已經幫我們實現好了,雖然它的介紹是用於 app 初始化的,但我覺得它也很適合這種情況。

下面開始擼程式碼(alpha 的原始碼請自己去了解,這裡不是重點,下面整個思路很簡單,就是策略模式加框架的使用):

第一步

因為 alpha 的任務是同步執行的,由於上傳是一個非同步操作,直接使用會導致有時序問題,所以要修改一下原始碼,自己新增一個非同步方法:

alpha:Task#start() 方法:

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

修改為:

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

就是在 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圖結構是這樣的:
    利用策略模式結合alibaba/alpha框架優化你的圖片上傳功能

開頭的 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

相關文章