前言
功能實現思優化,程序員的奮鬥路漫漫。
最近項目接入了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<PutObjectResult> asyncPutObject(
PutObjectRequest request, final OSSCompletedCallback<PutObjectRequest, PutObjectResult> completedCallback) {
return internalRequestOperation.putObject(request, completedCallback);
}
public OSSAsyncTask<PutObjectResult> putObject(
PutObjectRequest request, final OSSCompletedCallback<PutObjectRequest, PutObjectResult> completedCallback) {
··· ···
return OSSAsyncTask.wrapRequestTask(executorService.submit(callable), executionContext);
}
which means,oss內部維護了一個線程池,每次觸發上傳將從中獲得相應線程執行任務。
既然這樣,那不如··· ···創建一個遠端Service,處於新的進程,用於上傳的線程全都依附於這個新的進程,將上傳任務真正隔離,既減少了對主進程的影響,邏輯上也更清晰。
<!-- 遠端服務 -->
<service
android:name="com.dasheng.b2s.service.RemoteService"
android:process=":remoteservice" >
</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接口,支持重試,但是如何設置等待時間,對因網絡情況,文件過大或服務器原因等造成的失敗如何區別處理,分配不同的優先級?多想一步總會多條思路。