主目錄見:Android高級進階知識(這是總目錄索引)
下載器Github地址:FileDownloader
?文件下載在Android的開發(fā)中應(yīng)該可以說是都會用到,所以一個完善的好的下載工具應(yīng)該是不可或缺的,第一次看這個框架還是感覺類比較多的,應(yīng)該是作者為了類的職責(zé)劃分更加明確吧,但是個人感覺適當(dāng)為好。正如介紹所說,這個框架是多任務(wù),多線程,支持斷點恢復(fù),高并發(fā),簡單易用且支持多進程的。確實是個不錯的框架,值得我們來讀讀代碼,不過我也只是看過一下午,如果有理解不到位的大家諒解一下。
一.目標(biāo)
這篇文章應(yīng)該來說也是比較實用的,而且本庫的wiki應(yīng)該也有部分的介紹,但是我們今天的主要目標(biāo)如下:
1.學(xué)習(xí)該庫中優(yōu)秀的思維;
2.懂得自己開發(fā)或者修改和改進該庫。
二.源碼解析
首先當(dāng)你拿到一個庫不知道從哪開始的時候,往往就是從使用上入手,所以我們先來看看怎么使用該庫,因為使用方式有幾種,我們挑其中一種來說明:
1.添加gradle依賴
dependencies {
compile 'com.liulishuo.filedownloader:library:1.6.8'
}
2.在Application初始化
FileDownloader.setupOnApplicationOnCreate(this)
.connectionCreator(new FileDownloadUrlConnection
.Creator(new FileDownloadUrlConnection.Configuration()
.connectTimeout(15_000) // set connection timeout.
.readTimeout(15_000) // set read timeout.
.proxy(Proxy.NO_PROXY) // set proxy
))
.commit();
3.開始下載(這里我挑一個使用任務(wù)隊列來下載的方式)
final FileDownloadQueueSet queueSet = new FileDownloadQueueSet(downloadListener);
final List<BaseDownloadTask> tasks = new ArrayList<>();
for (int i = 0; i < count; i++) {
tasks.add(FileDownloader.getImpl().create(Constant.URLS[i]).setTag(i + 1));
}
queueSet.disableCallbackProgressTimes(); // do not want each task's download progress's callback,
// we just consider which task will completed.
// auto retry 1 time if download fail
queueSet.setAutoRetryTimes(1);
if (serialRbtn.isChecked()) {
// start download in serial order
queueSet.downloadSequentially(tasks);
} else {
// start parallel download
queueSet.downloadTogether(tasks);
}
queueSet.start();
那么我們就從初始化入手,我們先來看看FileDownloader#setupOnApplicationOnCreate()
干了什么工作。
1.setupOnApplicationOnCreate
public static DownloadMgrInitialParams.InitCustomMaker setupOnApplicationOnCreate(Application application) {
final Context context = application.getApplicationContext();
//將context緩存在FileDownloadHelper中
FileDownloadHelper.holdContext(context);
//實例化InitCustomMaker
DownloadMgrInitialParams.InitCustomMaker customMaker = new DownloadMgrInitialParams.InitCustomMaker();
//賦值給DownloadMgrInitialParams然后保存在CustomComponentHolder中
CustomComponentHolder.getImpl().setInitCustomMaker(customMaker);
return customMaker;
}
我們看到這個方法沒有做什么工作,這也是合理的,因為在Application#onCreate
避免做過多的工作導(dǎo)致啟動速度變慢。這個方法會返回一個InitCustomMaker
類的實例,所以程序會調(diào)用這個類connectionCreator()
方法:
public InitCustomMaker connectionCreator(FileDownloadHelper.ConnectionCreator creator) {
this.mConnectionCreator = creator;
return this;
}
這個方法就是將一個連接創(chuàng)建器賦值給InitCustomMaker
類中的變量,返回this是為了鏈?zhǔn)讲僮餍枰驗?code>InitCustomMaker這里還有很多可以自定義的方法。這樣我們的初始化工作就完成了,我們來看我們的開始下載部分。
2.FileDownloadQueueSet的設(shè)置
這個類是用來配置下載任務(wù)和啟動下載任務(wù)的,首先我們看到構(gòu)造函數(shù)里面給他設(shè)置了一個FileDownloadListener
實例用于回調(diào)。然后會在串行下載或者并行下載的時候會給他傳一個任務(wù)集合,我們首先來看看任務(wù)集合的創(chuàng)建和添加。任務(wù)的創(chuàng)建是在FileDownloader#create
中創(chuàng)建的:
public BaseDownloadTask create(final String url) {
return new DownloadTask(url);
}
通過這個我們可以知道,一個下載任務(wù)對應(yīng)一個DownloadTask
,我們看下它的構(gòu)造函數(shù):
DownloadTask(final String url) {
this.mUrl = url;
mPauseLock = new Object();
final DownloadTaskHunter hunter = new DownloadTaskHunter(this, mPauseLock);
mHunter = hunter;
mMessageHandler = hunter;
}
這里除了給DownloadTask
的mUrl賦值之外,還實例化了一個DownloadTaskHunter
實例,我們看到這里參數(shù)將DownloadTask
實例傳給了它的構(gòu)造函數(shù),然后傳了mPauseLock
用來做線程鎖操作。我們看下DownloadTaskHunter
構(gòu)造函數(shù):
DownloadTaskHunter(ICaptureTask task, Object pauseLock) {
mPauseLock = pauseLock;
mTask = task;
final DownloadSpeedMonitor monitor = new DownloadSpeedMonitor();
mSpeedMonitor = monitor;
mSpeedLookup = monitor;
mMessenger = new FileDownloadMessenger(task.getRunningTask(), this);
}
可以看到除了傳進來的兩個參數(shù)賦值之外,還是實例化了DownloadSpeedMonitor
類,這個類主要是用來監(jiān)測下載進度的。同時還實例化了FileDownloadMessenger
類,這個類是用來給回調(diào)接口FileDownloadListener
發(fā)送消息的。這樣我們大致看了下載任務(wù)DownloadTask
的創(chuàng)建過程。
接下來就是FileDownloadQueueSet
類中一些方法的設(shè)置,例如我們這里使用到的disableCallbackProgressTimes()
方法,這個方法是設(shè)置了的話,那么我們就監(jiān)聽不到一個任務(wù)的下載進度了,只有任務(wù)下載完成的回調(diào)。與這個方法對應(yīng)的還有setCallbackProgressTimes()
設(shè)置更新進度回調(diào)次數(shù),setCallbackProgressMinInterval()
設(shè)置了更新進度的最小間隔時間。setAutoRetryTimes()
方法是設(shè)置下載失敗重試的次數(shù)。然后會把上面創(chuàng)建的任務(wù)集合賦值給FileDownloadQueueSet
類,這里調(diào)用的方法是downloadSequentially()
和downloadTogether()
,這兩個方法唯一的不同就是會設(shè)置標(biāo)志位isSerial,來說明是串行下載還是并行下載。最后最重要的當(dāng)然就是start()
方法了,這里就真正啟動開始下載了。
3.FileDownloadQueueSet start
public void start() {
//遍歷任務(wù)集合
for (BaseDownloadTask task : tasks) {
//設(shè)置任務(wù)的回調(diào)監(jiān)聽為傳進來的FileDownloadListener實例
task.setListener(target);
if (autoRetryTimes != null) {
//設(shè)置失敗重試次數(shù)
task.setAutoRetryTimes(autoRetryTimes);
}
if (syncCallback != null) {
//設(shè)置是否進行同步回調(diào)
task.setSyncCallback(syncCallback);
}
if (isForceReDownload != null) {
//是否強制重新下載
task.setForceReDownload(isForceReDownload);
}
if (callbackProgressTimes != null) {
//設(shè)置進度回調(diào)次數(shù)
task.setCallbackProgressTimes(callbackProgressTimes);
}
if (callbackProgressMinIntervalMillis != null) {
//設(shè)置進度回調(diào)最小間隔時間
task.setCallbackProgressMinInterval(callbackProgressMinIntervalMillis);
}
if (tag != null) {
task.setTag(tag);
}
if (taskFinishListenerList != null) {
//任務(wù)結(jié)束監(jiān)聽器,只有在非UI現(xiàn)場中調(diào)用
for (BaseDownloadTask.FinishListener finishListener : taskFinishListenerList) {
task.addFinishListener(finishListener);
}
}
if (this.directory != null) {
//設(shè)置下載下來的文件存放的路徑
task.setPath(this.directory, true);
}
if (this.isWifiRequired != null) {
//是否需要只在wifi情況下才下載
task.setWifiRequired(this.isWifiRequired);
}
//這個方法主要是標(biāo)識下是在隊列中的任務(wù),而且會將任務(wù)添加到FileDownloadList的任務(wù)集合中
task.asInQueueTask().enqueue();
}
//啟動下載任務(wù)
FileDownloader.getImpl().start(target, isSerial);
}
我們看到這個方法主要是給任務(wù)DownloadTask
設(shè)置一些參數(shù)值,然后將任務(wù)添加到FileDownloadList
中的集合里。然后調(diào)用FileDownloader#start()
開始下載任務(wù):
public boolean start(final FileDownloadListener listener, final boolean isSerial) {
if (listener == null) {
FileDownloadLog.w(this, "Tasks with the listener can't start, because the listener " +
"provided is null: [null, %B]", isSerial);
return false;
}
return isSerial ?
getQueuesHandler().startQueueSerial(listener) :
getQueuesHandler().startQueueParallel(listener);
}
這個方法很簡單,這里isSerial
標(biāo)志我們在前面已經(jīng)設(shè)置了,我們這里就走并行下載這條道吧,所以isSerial
為false,所以這里會調(diào)用getQueuesHandler().startQueueParallel()
方法來啟動并行下載任務(wù),我們這里跟蹤到QueuesHandler#startQueueParallel()
方法:
@Override
public boolean startQueueParallel(FileDownloadListener listener) {
final int attachKey = listener.hashCode();
//這里主要防止在添加了下載任務(wù)之后又有新的任務(wù)到達,所以這里會重新組裝任務(wù)隊列
final List<BaseDownloadTask.IRunningTask> list = FileDownloadList.getImpl().
assembleTasksToStart(attachKey, listener);
//這里會驗證是否任務(wù)隊列里面有任務(wù),然后會調(diào)用全局監(jiān)聽器的onRequestStart(),如果有打點/統(tǒng)計等需求可以考慮設(shè)置這個下載監(jiān)聽器
if (onAssembledTasksToStart(attachKey, list, listener, false)) {
return false;
}
//遍歷任務(wù)集合,然后分別啟動
for (BaseDownloadTask.IRunningTask task : list) {
task.startTaskByQueue();
}
return true;
}
我們看到前面兩步就是重新組裝一下任務(wù)隊列,然后我們會調(diào)用任務(wù)task中的startTaskByQueue()
方法進行啟動任務(wù):
@Override
public void startTaskByQueue() {
startTaskUnchecked();
}
private int startTaskUnchecked() {
if (isUsing()) {
if (isRunning()) {
throw new IllegalStateException(
FileDownloadUtils.formatString("This task is running %d, if you" +
" want to start the same task, please create a new one by" +
" FileDownloader.create", getId()));
} else {
throw new IllegalStateException("This task is dirty to restart, If you want to " +
"reuse this task, please invoke #reuse method manually and retry to " +
"restart again." + mHunter.toString());
}
}
if (!isAttached()) {
setAttachKeyDefault();
}
mHunter.intoLaunchPool();
return getId();
}
前面是判斷這個任務(wù)是否在執(zhí)行或者占用,最后會調(diào)用DownloadTaskHunter#intoLaunchPool()
方法:
@Override
public void intoLaunchPool() {
synchronized (mPauseLock) {
//如果任務(wù)是在空閑狀態(tài)則現(xiàn)在設(shè)置狀態(tài)為toLaunchPool,否則返回
if (mStatus != FileDownloadStatus.INVALID_STATUS) {
FileDownloadLog.w(this, "High concurrent cause, this task %d will not input " +
"to launch pool, because of the status isn't idle : %d",
getId(), mStatus);
return;
}
mStatus = FileDownloadStatus.toLaunchPool;
}
//獲取下載任務(wù)
final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
final BaseDownloadTask origin = runningTask.getOrigin();
//如果設(shè)置了全局監(jiān)聽就調(diào)用他的onRequestStart
if (FileDownloadMonitor.isValid()) {
FileDownloadMonitor.getMonitor().onRequestStart(origin);
}
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.v(this, "call start " +
"Url[%s], Path[%s] Listener[%s], Tag[%s]",
origin.getUrl(), origin.getPath(), origin.getListener(), origin.getTag());
}
//設(shè)置任務(wù)準(zhǔn)備就緒
boolean ready = true;
try {
//前面給任務(wù)設(shè)置了path,也就是下載文件存放路徑,這里就是創(chuàng)建這個路徑
prepare();
} catch (Throwable e) {
ready = false;
FileDownloadList.getImpl().add(runningTask);
FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
}
if (ready) {
//調(diào)用FileDownloadTaskLauncher的launch啟動任務(wù)
FileDownloadTaskLauncher.getImpl().launch(this);
}
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.v(this, "the task[%d] has been into the launch pool.", getId());
}
}
前面的步驟已經(jīng)注釋的很清楚了,最后我們看到會調(diào)用FileDownloadTaskLauncher#launch()
方法來啟動任務(wù),傳的參數(shù)就是本類DownloadTaskHunter
實例,我們看下launch()
方法:
synchronized void launch(final ITaskHunter.IStarter taskStarter) {
mLaunchTaskPool.asyncExecute(taskStarter);
}
這里的mLaunchTaskPool
是一個LaunchTaskPool
類實例,所以會調(diào)用LaunchTaskPool#asyncExecute()
方法:
public void asyncExecute(final ITaskHunter.IStarter taskStarter) {
mPool.execute(new LaunchTaskRunnable(taskStarter));
}
mPool是一個自定義的線程池ThreadPoolExecutor
實例,所以調(diào)用execute會執(zhí)行LaunchTaskRunnable#run()
方法:
LaunchTaskRunnable(final ITaskHunter.IStarter taskStarter) {
this.mTaskStarter = taskStarter;
this.mExpired = false;
}
@Override
public void run() {
if (mExpired) {
return;
}
mTaskStarter.start();
}
我們看到run方法中會調(diào)用mTaskStarter
的start()
方法,從上面我們可以知道mTaskStarter
是DownloadTaskHunter
實例,所以這里會調(diào)用DownloadTaskHunter
的start()
方法,這個方法現(xiàn)在是在子線程中執(zhí)行的:
@Override
public void start() {
//在前面我們已經(jīng)設(shè)置過標(biāo)識為toLaunchPool
if (mStatus != FileDownloadStatus.toLaunchPool) {
FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
" because the of status isn't toLaunchPool: %d",
getId(), mStatus);
return;
}
//獲取下載任務(wù)
final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
final BaseDownloadTask origin = runningTask.getOrigin();
//這個類是用來檢查服務(wù)是否連接的,因為默認是跨進程的,所以這里會有跨進程的調(diào)用檢查是否
//連接,如果沒有連接的話那么會bindService連接
final ILostServiceConnectedHandler lostConnectedHandler = FileDownloader.getImpl().
getLostConnectedHandler();
try {
//檢查服務(wù)有沒有連接,沒有連接就連接,如果是跨進程就啟動SeparateProcessService
//服務(wù),不是跨進程就啟動SharedMainProcessService服務(wù)
if (lostConnectedHandler.dispatchTaskStart(runningTask)) {
return;
}
synchronized (mPauseLock) {
if (mStatus != FileDownloadStatus.toLaunchPool) {
FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
" the status can't assign to toFileDownloadService, because the status" +
" isn't toLaunchPool: %d",
getId(), mStatus);
return;
}
//將任務(wù)狀態(tài)設(shè)置為toFileDownloadService
mStatus = FileDownloadStatus.toFileDownloadService;
}
//這里會通知FileDownloadMonitor的onTaskBegin說任務(wù)要開始下載了,同時會重新
//組裝一下任務(wù)列表
FileDownloadList.getImpl().add(runningTask);
if (FileDownloadHelper.inspectAndInflowDownloaded(
origin.getId(), origin.getTargetFilePath(), origin.isForceReDownload(), true)
) {
// Will be removed when the complete message is received in #update
return;
}
//啟動任務(wù)
final boolean succeed = FileDownloadServiceProxy.getImpl().
start(
origin.getUrl(),
origin.getPath(),
origin.isPathAsDirectory(),
origin.getCallbackProgressTimes(), origin.getCallbackProgressMinInterval(),
origin.getAutoRetryTimes(),
origin.isForceReDownload(),
mTask.getHeader(),
origin.isWifiRequired());
//如果任務(wù)是暫停的話那么這里就會執(zhí)行暫停操作
if (mStatus == FileDownloadStatus.paused) {
FileDownloadLog.w(this, "High concurrent cause, this task %d will be paused," +
"because of the status is paused, so the pause action must be applied", getId());
if (succeed) {
FileDownloadServiceProxy.getImpl().pause(getId());
}
return;
}
if (!succeed) {//如果任務(wù)啟動失敗
//noinspection StatementWithEmptyBody
//如果啟動服務(wù)失敗則進入如下代碼
if (!lostConnectedHandler.dispatchTaskStart(runningTask)) {
final MessageSnapshot snapshot = prepareErrorMessage(
new RuntimeException("Occur Unknown Error, when request to start" +
" maybe some problem in binder, maybe the process was killed in " +
"unexpected."));
//任務(wù)列表沒有當(dāng)前任務(wù)則添加
if (FileDownloadList.getImpl().isNotContains(runningTask)) {
lostConnectedHandler.taskWorkFine(runningTask);
FileDownloadList.getImpl().add(runningTask);
}
//有的話就刪除,說明執(zhí)行任務(wù)出錯了
FileDownloadList.getImpl().remove(runningTask, snapshot);
} else {
// the FileDownload Service host process was killed when request stating and it
// will be restarted by LostServiceConnectedHandler.
}
} else {
//任務(wù)執(zhí)行成功
lostConnectedHandler.taskWorkFine(runningTask);
}
} catch (Throwable e) {
e.printStackTrace();
FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
}
}
上面的代碼可以看到我已經(jīng)注釋的非常清楚了,像服務(wù)的啟動,我這里就不會展開講了,因為挺簡單的。FileDownloadHelper#inspectAndInflowDownloaded()
方法我們看到?jīng)]有注釋,有機會會拿出來講下,我們先來看看主線邏輯,我們來看看啟動任務(wù)做了什么工作FileDownloadServiceProxy#start()
:
@Override
public boolean start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
int callbackProgressMinIntervalMillis,
int autoRetryTimes, boolean forceReDownload, FileDownloadHeader header,
boolean isWifiRequired) {
return handler.start(url, path, pathAsDirectory, callbackProgressTimes,
callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
isWifiRequired);
}
這里的handler是在FileDownloadServiceProxy
的構(gòu)造函數(shù)里面初始化的,這里我們可以來看下:
private FileDownloadServiceProxy() {
handler = FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE ?
new FileDownloadServiceSharedTransmit() :
new FileDownloadServiceUIGuard();
}
我們看到這里會在FileDownloadProperties
類中去獲取PROCESS_NON_SEPARATE
標(biāo)志。如果你去看FileDownloadProperties
構(gòu)造函數(shù)就會知道,這些值是從filedownloader.properties
這個文件配置的,這里還有很多其他的標(biāo)志,這里的PROCESS_NON_SEPARATE
是為了標(biāo)識是否下載服務(wù)是在單獨的進程中的,我們這里就默認設(shè)置為在單獨進程中下載,在單獨的進程中執(zhí)行下載服務(wù)的好處是可以減少應(yīng)用進程占用的內(nèi)存,且使應(yīng)用更加穩(wěn)定。所以當(dāng)PROCESS_NON_SEPARATE
為false的情況下,我們的handler會是FileDownloadServiceUIGuard
類實例,所以這里的start()
方法就會是FileDownloadServiceUIGuard#start()
:
public boolean start(final String url, final String path, final boolean pathAsDirectory,
final int callbackProgressTimes,
final int callbackProgressMinIntervalMillis,
final int autoRetryTimes, final boolean forceReDownload,
final FileDownloadHeader header, final boolean isWifiRequired) {
if (!isConnected()) {
return DownloadServiceNotConnectedHelper.start(url, path, pathAsDirectory);
}
try {
getService().start(url, path, pathAsDirectory, callbackProgressTimes,
callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
isWifiRequired);
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
return true;
}
這里的getService()
方法返回的是遠程的服務(wù)代理,因為這里是跨進程的,所以遠程的服務(wù)是SeparateProcessService
,為什么說這個方法獲取到的是遠程的服務(wù)代理呢?因為是FileDownloadServiceUIGuard
實現(xiàn)了ServiceConnection
,所以在綁定遠程服務(wù)的時候會回調(diào)onServiceConnected()
方法,我們在這里可以看到:
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
this.service = asInterface(service);
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "onServiceConnected %s %s", name, this.service);
}
try {
registerCallback(this.service, this.callback);
} catch (RemoteException e) {
e.printStackTrace();
}
@SuppressWarnings("unchecked") final List<Runnable> runnableList =
(List<Runnable>) connectedRunnableList.clone();
connectedRunnableList.clear();
for (Runnable runnable : runnableList) {
runnable.run();
}
FileDownloadEventPool.getImpl().
asyncPublishInNewThread(new DownloadServiceConnectChangedEvent(
DownloadServiceConnectChangedEvent.ConnectStatus.connected, serviceClass));
}
可以看到第一句就是賦值service,所以我們直接看遠程服務(wù)SeparateProcessService#start()
方法,這個SeparateProcessService
是繼承FileDownloadService
類的,它本身是一個空實現(xiàn),我們來看看FileDownloadService
中的start()方法,熟悉AIDL的人都知道,這些實現(xiàn)都在Binder實體中實現(xiàn),我們來看下onBind
方法:
@Override
public IBinder onBind(Intent intent) {
return handler.onBind(intent);
}
這里又調(diào)用了handler
的onBind
方法,我們看下這里的handler
是個什么東西?
if (FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE) {
handler = new FDServiceSharedHandler(new WeakReference<>(this), manager);
} else {
handler = new FDServiceSeparateHandler(new WeakReference<>(this), manager);
}
可以看到,我們這里PROCESS_NON_SEPARATE
是為false的,所以我們的handler
是FDServiceSeparateHandler
實例。所以我們看FDServiceSeparateHandler#onBind()
方法:
@Override
public IBinder onBind(Intent intent) {
return this;
}
public class FDServiceSeparateHandler extends IFileDownloadIPCService.Stub
implements MessageSnapshotFlow.MessageReceiver, IFileDownloadServiceHandler {
}
我們看到這里onBind
方法返回this,FDServiceSeparateHandler
實現(xiàn)了IFileDownloadIPCService.Stub
,所以FDServiceSeparateHandler
就是Binder實體,我們看這個類里面的start()
方法:
@Override
public void start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
int callbackProgressMinIntervalMillis, int autoRetryTimes, boolean forceReDownload,
FileDownloadHeader header, boolean isWifiRequired) throws RemoteException {
downloadManager.start(url, path, pathAsDirectory, callbackProgressTimes,
callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
isWifiRequired);
}
這里調(diào)用了downloadManager
的start方法,這里的downloadManager
是FileDownloadManager
實例,所以程序會調(diào)用FileDownloadManager#start()
方法。
4.FileDownloadManager start
這里就是真正開始啟動下載任務(wù)了,我們完整地看下代碼:
// synchronize for safe: check downloading, check resume, update data, execute runnable
public synchronized void start(final String url, final String path, final boolean pathAsDirectory,
final int callbackProgressTimes,
final int callbackProgressMinIntervalMillis,
final int autoRetryTimes, final boolean forceReDownload,
final FileDownloadHeader header, final boolean isWifiRequired) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "request start the task with url(%s) path(%s) isDirectory(%B)",
url, path, pathAsDirectory);
}
//根據(jù)請求鏈接,文件存儲路徑,已經(jīng)一個boolean值來生成一次請求的唯一id
final int id = FileDownloadUtils.generateId(url, path, pathAsDirectory);
//從數(shù)據(jù)庫中獲取這個id對應(yīng)的請求,封裝在FileDownloadModel 中,因為有可能前面已經(jīng)有下載過了,只是由于不可抗性終止了
FileDownloadModel model = mDatabase.find(id);
List<ConnectionModel> dirConnectionModelList = null;
//如果pathAsDirectory 為false也就是說是文件,而且獲取到的model為null,那么進入如下邏輯
if (!pathAsDirectory && model == null) {
// try dir data.
//根據(jù)這個文件的上級目錄來生成一個唯一id
final int dirCaseId = FileDownloadUtils.generateId(url, FileDownloadUtils.getParent(path),
true);
//尋找這個id對應(yīng)的model
model = mDatabase.find(dirCaseId);
if (model != null && path.equals(model.getTargetFilePath())) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "task[%d] find model by dirCaseId[%d]", id, dirCaseId);
}
//獲取該id下的所有連接,即對應(yīng)的FileDownloadModel集合
dirConnectionModelList = mDatabase.findConnectionModel(dirCaseId);
}
}
if (FileDownloadHelper.inspectAndInflowDownloading(id, model, this, true)) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "has already started download %d", id);
}
return;
}
//獲取文件路徑,沒有則生成一個
final String targetFilePath = model != null ? model.getTargetFilePath() :
FileDownloadUtils.getTargetFilePath(path, pathAsDirectory, null);
if (FileDownloadHelper.inspectAndInflowDownloaded(id, targetFilePath, forceReDownload,
true)) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "has already completed downloading %d", id);
}
return;
}
//獲取文件下載了多少字節(jié),因為有可能之前下了一半斷掉了,然后獲取暫存文件路徑
final long sofar = model != null ? model.getSoFar() : 0;
final String tempFilePath = model != null ? model.getTempFilePath() :
FileDownloadUtils.getTempPath(targetFilePath);
if (FileDownloadHelper.inspectAndInflowConflictPath(id, sofar, tempFilePath, targetFilePath,
this)) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "there is an another task with the same target-file-path %d %s",
id, targetFilePath);
// because of the file is dirty for this task.
if (model != null) {
mDatabase.remove(id);
mDatabase.removeConnections(id);
}
}
return;
}
// real start
// - create model
//創(chuàng)建連接model,如果數(shù)據(jù)庫已經(jīng)存在這個model,而且狀態(tài)是下面狀態(tài)中的一種,
//那么說明真的是意外退出了,則根據(jù)下面情況看要不要更新數(shù)據(jù)庫,否則就創(chuàng)建一個新的model請求
boolean needUpdate2DB;
if (model != null &&
(model.getStatus() == FileDownloadStatus.paused ||
model.getStatus() == FileDownloadStatus.error ||
model.getStatus() == FileDownloadStatus.pending ||
model.getStatus() == FileDownloadStatus.started ||
model.getStatus() == FileDownloadStatus.connected) // FileDownloadRunnable invoke
// #isBreakpointAvailable to determine whether it is really invalid.
) {
if (model.getId() != id) {
// in try dir case.
mDatabase.remove(model.getId());
mDatabase.removeConnections(model.getId());
model.setId(id);
model.setPath(path, pathAsDirectory);
if (dirConnectionModelList != null) {
for (ConnectionModel connectionModel : dirConnectionModelList) {
connectionModel.setId(id);
mDatabase.insertConnectionModel(connectionModel);
}
}
needUpdate2DB = true;
} else {
if (!TextUtils.equals(url, model.getUrl())) {
// for cover the case of reusing the downloaded processing with the different url( using with idGenerator ).
model.setUrl(url);
needUpdate2DB = true;
} else {
needUpdate2DB = false;
}
}
} else {
if (model == null) {
model = new FileDownloadModel();
}
model.setUrl(url);
model.setPath(path, pathAsDirectory);
model.setId(id);
model.setSoFar(0);
model.setTotal(0);
model.setStatus(FileDownloadStatus.pending);
model.setConnectionCount(1);
needUpdate2DB = true;
}
// - update model to db
if (needUpdate2DB) {
mDatabase.update(model);
}
final DownloadLaunchRunnable.Builder builder = new DownloadLaunchRunnable.Builder();
//利用建造者模式創(chuàng)建DownloadLaunchRunnable 對象
final DownloadLaunchRunnable runnable =
builder.setModel(model)
.setHeader(header)
.setThreadPoolMonitor(this)
.setMinIntervalMillis(callbackProgressMinIntervalMillis)
.setCallbackProgressMaxCount(callbackProgressTimes)
.setForceReDownload(forceReDownload)
.setWifiRequired(isWifiRequired)
.setMaxRetryTimes(autoRetryTimes)
.build();
// - execute
//執(zhí)行這個Runnable
mThreadPool.execute(runnable);
}
我們看到上面的方法主要作用就是根據(jù)url,path與是否是目錄來標(biāo)識一個請求,如果符合意外退出的情況,那么會對SoFar(文件已經(jīng)下載了多少)這個值進行恢復(fù)。最后重新賦值給DownloadLaunchRunnable
對象,進行執(zhí)行,DownloadLaunchRunnable
是一個Runnable對象,所以會執(zhí)行他的run()
方法:
@Override
public void run() {
try {
//設(shè)置線程優(yōu)先級為后臺,這樣當(dāng)多個線程并發(fā)后很多無關(guān)緊要的線程分配的CPU時間將會減少,有利于主線程的處理
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// status checkout
//檢查狀態(tài)
if (model.getStatus() != FileDownloadStatus.pending) {
if (model.getStatus() == FileDownloadStatus.paused) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
"already paused %d", model.getId());
}
} else {
onError(new RuntimeException(
FileDownloadUtils.formatString("Task[%d] can't start the download" +
" runnable, because its status is %d not %d",
model.getId(), model.getStatus(), FileDownloadStatus.pending)));
}
return;
}
//更新model的狀態(tài)為啟動狀態(tài)
if (!paused) {
statusCallback.onStartThread();
}
do {
//如果是暫停狀態(tài)則返回不執(zhí)行,如日志所說,高并發(fā)可能導(dǎo)致這個結(jié)果
if (paused) {
if (FileDownloadLog.NEED_LOG) {
FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
"already paused %d", model.getId());
}
return;
}
FileDownloadConnection connection = null;
try {
// 1. connect
//檢查權(quán)限和是否需要wifi才能下載條件
checkupBeforeConnect();
// the first connection is for: 1. etag verify; 2. first connect.
final List<ConnectionModel> connectionOnDBList = database.findConnectionModel(model.getId());
//檢查是否恢復(fù)之前的連接,如果是多線程連接且不支持seek的情況是不支持
//斷點恢復(fù)的,如果支持斷點恢復(fù),在多線程連接的情況下會把每個連接下載了
//多少計算賦值給model的SoFar,
final ConnectionProfile connectionProfile = buildFirstConnectProfile(connectionOnDBList);
final ConnectTask.Builder build = new ConnectTask.Builder();
//創(chuàng)建新的連接
final ConnectTask firstConnectionTask = build.setDownloadId(model.getId())
.setUrl(model.getUrl())
.setEtag(model.getETag())
.setHeader(userRequestHeader)
.setConnectionProfile(connectionProfile)
.build();
//調(diào)用FileDownloadUrlConnection進行連接請求,獲取文件長度
connection = firstConnectionTask.connect();
//會根據(jù)ETag來判斷遠端的文件是否有改變,改變的話就是從頭開始下載,不然就獲取文件總長度
handleFirstConnected(firstConnectionTask.getRequestHeader(),
firstConnectionTask, connection);
if (paused) {
model.setStatus(FileDownloadStatus.paused);
return;
}
// 2. fetch
//檢查是否有另外一個url請求任務(wù)跟這個任務(wù)目錄存儲路徑一樣,
//一樣的話判斷這個url下載的文件是否存在,如果其他進程正在下載這個文件,
//那么這里請求就停止,如果其他進程下載文件已經(jīng)停止了,那么這里就從斷點
//進行恢復(fù)下載
checkupBeforeFetch();
final long totalLength = model.getTotal();
// pre-allocate if need.
//預(yù)先分配文件的大小
handlePreAllocate(totalLength, model.getTempFilePath());
final int connectionCount;
// start fetching
//是否支持多線程即多個連接下載
if (isMultiConnectionAvailable()) {
if (isResumeAvailableOnDB) {
connectionCount = model.getConnectionCount();
} else {
connectionCount = CustomComponentHolder.getImpl()
.determineConnectionCount(model.getId(), model.getUrl(), model.getPath(), totalLength);
}
} else {
connectionCount = 1;
}
if (connectionCount <= 0) {
throw new IllegalAccessException(FileDownloadUtils
.formatString("invalid connection count %d, the connection count" +
" must be larger than 0", connection));
}
if (paused) {
model.setStatus(FileDownloadStatus.paused);
return;
}
isSingleConnection = connectionCount == 1;
if (isSingleConnection) {
// single connection
//單個子線程下載
fetchWithSingleConnection(firstConnectionTask.getProfile(), connection);
} else {
if (connection != null) {
connection.ending();
connection = null;
}
// multiple connection
statusCallback.onMultiConnection();
if (isResumeAvailableOnDB) {
//恢復(fù)多線程下載
fetchWithMultipleConnectionFromResume(connectionCount, connectionOnDBList);
} else {
//從頭開始多線程下載
fetchWithMultipleConnectionFromBeginning(totalLength, connectionCount);
}
}
} catch (IOException | IllegalAccessException | InterruptedException | IllegalArgumentException | FileDownloadGiveUpRetryException e) {
if (isRetry(e)) {
onRetry(e, 0);
continue;
} else {
onError(e);
}
} catch (DiscardSafely discardSafely) {
return;
} catch (RetryDirectly retryDirectly) {
model.setStatus(FileDownloadStatus.retry);
continue;
} finally {
if (connection != null) connection.ending();
}
break;
} while (true);
} finally {
statusCallback.discardAllMessage();
if (paused) {
statusCallback.onPausedDirectly();
} else if (error) {
statusCallback.onErrorDirectly(errorException);
} else {
try {
statusCallback.onCompletedDirectly();
} catch (IOException e) {
statusCallback.onErrorDirectly(e);
}
}
alive.set(false);
}
}
我們看到這個方法就是下載的主要方法了,流程注釋已經(jīng)寫得很清楚了,就是里面沒有展開寫,不然本篇文章的篇幅要很長了,到這里我們已經(jīng)講完請求的大體流程了,這里明確下一個請求對應(yīng)一個FileDownloadModel
實體,每個請求下面又可以有多個ConnectionModel
實體,每個ConnectionModel
實體對應(yīng)一條請求線程。當(dāng)然這里面的FileDownloadHelper#inspectAndInflowDownloaded()
方法沒有講,如果大家看不懂可以給我留言,我會解答。
總結(jié):這個下載框架整體來說思維還是不錯的,希望里面的編程思想會有幫助到大家,在以后自己編寫框架的過程中能有所借鑒,最后祝大家源碼之路愉快哈。