Download模块 (十三)

134 篇文章 0 订阅
36 篇文章 0 订阅
Download模块 (十三)


DownloadTask类封装了一次下载任务的全部信息<M> 以及 真正下载的实现<C>
DownloadTask接收DownloadInfo作为构造参数,并且内部维护一个指向其的引用,作为一个组成部件。


<1>DownloadTask有状态,使用enum类实现:
NOT_START,
IN_PROGRESS,
PAUSED,
FAILED,
COMPLETED,
REMOVED,
DELETED,
FILE_BROKEN;


因为Proxy端对于Download类的status有自己的一套enum,在根据DownloadInfo构造DownloadTask时,会有一个这样的enum转换,
直接将这个转换实现在enum类中了<虽然两套enum的status是一样的,不过出于一种封装性,就分别在Proxy和Service设定了两套冗余的,用一种,意味着两者被紧密耦合了,所以
用两种是比较合适的>


<2>一个DownloadTask还对外开放了一个监听接口,在自己产生各种变化的时候都会通知注册的实现了接口的listener。
一般在通知时都会将Task的id传过去以区分其他task.


<3>DownloadTask内部有一个Downloader类,承担了真正下载的实现,而此Downloader还是一个abstract基类,因为真正Download的实现方式会有不同<比如多线程下载>,
这里采用了一种比较简单的组成方式,每个DownloadTask都一个自己的Downloader<组成>, 这个Downloader是专属于此DownloadTask的,
而对于比较大型的架构,可能不会这么设计,而是做一个单独的DownloadManager,维护一个Downloader池,每个DownloadTask将自己提交到DownloaderManager的任务序列中,
由DownloadManager来负责scheduleDownloadTask何时运行,这样做的好处就是不必频繁的创建销毁资源。 不过在当前的场景中,因为DownloadTask可以继续/暂停,这样要求能够
反向操作Downloader,如果直接占用着Downloader却只是暂停什么都不敢,会造成资源的浪费,如果暂时的将task挪出工作序列,用户点击继续时再挪入工作序列,设计上会比较复杂。
因此对于这样一个小型模块,就采用了简单的一对一组合方式。


<4>实际下载的IO操作是在一个独立的Thread中进行的,而downloader显然应该可以感知某个DownloadThread的状态和变化,因此这里Downloader还要实现DownloadThread开发的监听接口。


<5>因为Downloader是内部类,因此很多外部信息<DownloadInfo>都可以直接引用,不用自己再维护一套,这也是内部类的一个好处。
因为要控制DownloadTask的开始/暂停/停止...,Downloader自然也要有这些流程控制函数, 因为控制的实现会根据 Downloader的实现
而不同,因此,这些函数也是抽象函数。一个已经实现的逻辑是在DownloadThread的Progress发生变化时<显然,进度变化这个事件,对任何
不同下载方式的Downloader来说,应该都是一样的,起码在我们的需求范围内是一样的>,因为回调是在DownloadThread中
发生的,因此所有的处理逻辑要post到MainThread去处理,在这里,直接用DownloadManager调用了DownloadTask的函数,其实更好的做法
是Downloader也开放一个progressChange的接口,在感知到内部Thread的progressChange以后,继续将这个event广播到外部,如何处理event
的逻辑应该在DownloadTask内部<不过因为项目使用的Eventbus都是同步调用,因此没有采用这个做法>。
有一个潜在的issue是在post到MainHandler的runnable中会尝试获取“当前”的网络状况,但是因为是post的,所以并非是一个同步的操作,
这就有潜在的时机不同步。因为progressChange的时刻和runnable真正run的时刻已经不是一个时刻了。
还有一个设计上的问题是在base class的onProgressChange的输入参数已经有了thread的id<这个可以接受,单线程方式也会有一个线程>
,并且后面处理逻辑也包含了对多线程的处理,这是不合适的,baseClass不应该知道这些细节,这些都应该放在子类中,而不是在baseClass这那个做判断分支处理。不过当时没考虑这么细节,并且在设计DownloadInfo时,也在里面泄露了多线程这个底层细节。

<6>MultiDownloader extends Downloader, 顾名思义,它是一个支持多线程下载的Downloader,考虑到并行线程不能太多以及Java本身对
线程使用的意见,使用了ExecutorService这种线程池的方式来进行多线程下载,并且使用了fixedNumber的Executor:
Executors.newFixedThreadPool(fixedNumber),内部还维护了一个DownloadThread列表代表交给Executor运行的任务<Thread也是extends自
Runnable,所以可以当成任务>,一个多线程的Downloader,一开始只会有一个Runnable在运行发动一次http get<或者 http head>,因为不
这么做,是无从知晓要下载的资源是否支持Range下载<能够多线程下载的前提>,以及content-length等信息,所以,必须有一个先行,在得到了这些信息以后再通过回调<listener>启动其他的线程来下载,并且这个thread不需要放在Executor里执行,直接start即可
<MultiDownloader 的start就是先start这个探路者线程>,不过探路者Thread也会被放在Downloader的Task列表中。
MultiDownloader还实现了downloadThread开放的ConnectionListner,以感知探路者线程的变化以及其他后继的DownloadThread。

<7>MultiDownloader的pause是遍历Task转发pasue请求, stop则代表着完全停止取消本次下载,销毁探路者线程获取http resp时的回调
Runnbale,遍历Task停止,并且将Task列表清空,Executor shutdown.

<8>MultiDownloader的resume比较复杂,会分阶段,如果resume的时候,探路者线程取得了http resp,但是没有来的及运行回调,那么就将
回调的runnable取消,并重新post到mainHandler中,

<9>为了统计速度,还会收集整合每个downloadThread在单位时间的下载数据量。

<10>在某个DownloadThread因为某种原因被stop的时候,Downloader会通过listener感知,
如果传入的cause是null,说明该thread是正常的下载完以后stop,并且再检测到所有的downloadThread都已经正常stop了,那么可以认为
Downloader的任务已经完成设定flag并调用专门的收尾函数,否则也会调用收尾函数,不过会附带上cause.注意此回调不是在主线程被调用的。

<11>Downloader的onConnect是在探路者线程收到了http resp的时候被回调的,因为不是在主线程,因此处理逻辑会被封装在Runnable中,post出去,并且此Runnable是否存在也会成为判断探路者是否受到http resp的标志<一个HttpUrlConnection的final输入会封装resp的信息
>。注意因为多线程的原因,Downloader内对此Runnable的引用赋值都要同步。
在这一步,运行此runnable的时候,要检查当前DownloadTask是否是IN_Progress的,如果不是,那说明不能继续下去,因此要直接返回,等到
resume的时候,会将此runnable重新post,而如果进一步发现不是PAUSED,那么说明这次Download应该是被停止撤销了,这时候要将维护的
Runnable的引用设为null,以后不会再被run。
否则就继续下去,从代表resp的httpUrlConnection中得到Etag/Last-Modified/Content-Type等信息,并且还要检测resp的header是否有
Accept-Ranges: bytes, 这样才能支持多线程下载。
下一步是则建立File对象以持久化下载数据, 考虑到断点下载,相应的File文件可能已经存在,这种情况下,已经下载的文件长度可以通过
读取文件的长度得到。有时候DownloadInfo里会已经有下载文件的大小,如果没有,那么用resp里的content-length作为文件总大小。
然后考虑建立子task进行多线程下载,后面还有一步,就是对探路者线程的处理,因为采用的是http get,现在的探路者线程会获得整个文件,
但是其实不需要,只需要重新给探路者线程指定需要下载的数据量就可以,当然,如果只能单线程下载,那就不需要做什么了,
最后把指向此runnbale的引用设为null来防止再次被run。
如果在此回调过程中由于某种原因出现了IOException,那么视为失败。

<12>分配子task进行多线程下载,首先要从DownloadInfo中获取文件的大小,还要考虑到断点下载时已经下载存在磁盘的数据,然后会有一个检
测来判断当前是否使用多线程,如果使用,那么优先使用DownloadInfo中指定的Thread数量,否则使用默认的,线程数量为TN
如果确认要使用多线程,那么就构造一个有TN-1个线程的FixedThreadPool 的Executor<好吧,没有使用Executor少数线程通过调度运行多个
task的作用>,然后结合之前已经下载的数据和从大小为每个thread分配合适的chunk大小,创建Downthread的时候就设置好然后将其加入到
Downloader的Thread list并且用Executor来执行。
最后更新还没有完成工作的Thread的数量为当前使用的线程数量,每一个线程成功结束,该值-1,减为0代表着下载全部结束。

<13> Downloader是被封装在DownloadTask中的,Task的start会转发start到内置组成的Downloader<start的时候才会创建一个
Downloader ,lazy,将Downloader作为内部类的好处就是Downloader可以直接访问DownloadTask的所有成员,如果完全独立出来的话,
getter/setter会有一堆,这是非静态内部类的一个好处>,然后切换自己的状态为IN_PROGRESS<影响M>.

<14>一个downLoadTask被pause可能有两种原因:1 是用户主动UI操作 2是网络发生变化导致,在pause函数中应该体现这个区别,
处于鲁棒性,只有IN_PROGRESS的才可以被pause。DownloadTask内部会维护一个是哪种原因导致pause的flag。

<15>同上resume也会被这两种原因resume,

<16>为了获取当前Task的下载速度,会启动一个Timer来定时的计算刷新当前的下载速度,在某些时候<比如pasue>,不需要测速,可以将Timer
cancel并引用设为null来释放资源。在需要的时候重新new 一个 Timer<Timer cancel以后不可重用>并schedule 一个 TimerTask,
每次计算出最新的speed以后,会更新到Notification并且通知监听速度变化的Listener<V上刷新>,保险起见,在开始会尝试释放一次Timer以避免重复启用。
测速测的是从上一次测速TimerTask被schedule以后到现在的平均速度,每次测速的Timer被取消,都会将已经保存下载数据量的成员变量reset
清0<比如pause一次以后>.
测速Timer只有在DownloadTask状态重新变成IN_PROGRESS以后才会重新被schedule开始测速。

<17>一个Downloader被stop的时候会有一个回调函数调用到downloaderStopped()来由DownloadTask处理遇到的下载问题
,给一个自定义的DownloadStopException的enum Cause作为输入,如果为null,
那么视为一次成功的下载完成,更新结束时间到DownloadInfo,然后设置状态为COMPLETE,停止Downloader,最后做一次完整性检查。
否则根据不同的cause来做不同处理:
case INTERRUPTED: 下载线程被interrupt,一般是由于pause引起的,不需要做什么。
case DOWNLOAD_NEED_RETRY: 需要重试,直接pause,
case NET_IOEXCEPTION:
case NET_WIFI_IS_OFF: 有IO错误或者网络切换到非WIFI,直接pasue
case PRECONDITION_FAILED: 对应http 412, 请求头信息中的某些先决条件是错误的,或者没有满足这种条件的文件存在,这种情况下将Etag
和LastModified设为null<去掉所有前置条件>并且停止Downloader,然后重新开始Task
case RANGE_NOT_SATISFIABLE: 提取的资源不支持Range<可能resp声明支持,但是真正请求的时候收到了错误>,重新下载
case FILE_BROKEN:文件损害,停止下载,并且改变状态到 文件被损害
其他的统一视为下载失败<不可克服的>,标记状态为失败.

<18>文件完整性检查被实现在一个static工具类里,这里要做的只是在完整性检测失败以后以FILE_BROKEN调用downloaderStopped

<19>resume里面隐藏了比较多的细节,如果此次resume是因为网络变化而出发的,那么需要检查当前的网络还将是否允许下载,
而如果Task其实已经完成了,但是在UI上还没有反应出来,那么调用downloaderStopped(null)进行一系列收尾工作以后直接返回。
否则就将当前状态置为IN_PROGRESS, 而如果这时候这个DownloadTask还没有配套的Downloader,那么create一个并开始下载,否则resume Downloader。

<20>贯彻始终的问题是如何保持 M V C的一致性,在琐碎而繁多的成员和复杂的回调以后,很多corner case容易被忽略。
DownloadTask的code很杂乱,一开始设计时很多没有考虑到,后期的不断迭代导致了现在。

还有很多细节被文档所忽略,回头补上。

<21>探路者Thread在onConnect以后回调,因为是在非UI线程,因此需要post一个runnable到UI线程,这个runnable的运行也要考虑到stop resume的影响,

在该Runnable中,在运行到一定完成度会将指向自己的引用设为null来表示onConnect的回调在主线程已经运行完毕,在开发时,当pause的时候对该runnable不进行

干预,因为在下面的DownloadThread里已经实现了,runnable内部也有判断,stop时会将该引用也设为null代表着如果还没有进行onConnect回调,那么就不进行了,而在resume的时候,则要考虑到该runnable被pause的问题,如果runnable没有run完,那么会重新post。

<22>对于HTTP range请求,对端返回200,这说明对端不支持分段下载,那么将此Task的downloader stop掉并将其引用设为null,然后对应的DownloadInfo的threadNum设为-1,并且将原来下载的文件<如果有的话>删除,重新开始下载,直接将这段判断逻辑封装在了resume中,如果resume发现指向Downloader的引用为null,那么就重新开始

一次新的Download,并且将Task的FinsihedContentLength设为0,还是使用此Task对应的DownloadInfo作为Downloader初始化的信息,,其中的判断会因为threadNum为-1

而将startPos设为0<因为文件已经删除,length为0>,而后面的探路者线程再一次取到200时,因为startPos是0,因此会顺利返回到Task的onConnect回调,在分配新线程时,因为200回复周中没有"range"这个header,所以会只使用单线程<这里其实是依赖于server回复的正确,相信server在回200的时候不会带range这个header>,这就实现了对于不支持分段下载的文件的再次重新下载。

<23>一个corner case: 一个文件之前以单线程下载了一部分,然后在这一次恢复下载的时候发现可以多线程下载,那么显然之前下载的部分不必考虑了,也只有这种情况下

才能setMultiThreadStartPos,在其他情况,都认为此


<24>目前虽然在DownloadTaskInfo里保存了threadNum,并且有setter,但是目前其实这个ThreadNum只有两个有效值<1或者3>,这是为了简化逻辑,这样考虑,如果上次是X个线程下载,这次恢复的时候被设置为了Y个线程下载,这种情况下想要实现断点续穿还是很麻烦的,因此就只有两个有效值1或3,如果在可以3的时候,发现之前是1,

那么必然的之前下载的部分一定都是有效的,就在当前没有下载的部分进行分段<chunk = threadNum/unfinsihed, 如果还有余数,那么分出来的 chunk再 +1, 最后一个线程会

以file的end作为结尾>,并且建立一个数组来保存每个线程自己任务的下载量,这样下次可以断点续传了,而如果下次恢复又不幸变为了1个线程,那么只能从最后一次单线程下载的完成点重新开始,之后的所有多线程下载的数据因为不完整就全部废弃了,而如果之前就是1个线程,那么只需要从当前下载File的length开始下载即可。


<25>不管是 真的要range部分 还是 range全部, downloadThread在设置header时都强制使用range header,这是为了照顾探路者线程在初次请求时是请求全部的,也需要强制设上range 0- 来检测是否支持Range.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值