Download模块 (十四)

本文详细探讨了DownloadThread作为HTTP下载任务的核心组件,如何通过实现Runnable角色承载下载数据的任务。它提供了监听事件接口,支持并发下载,并通过特定的HTTP交互策略优化下载效率。此外,介绍了如何通过中断检测点实现对线程的控制,以及在不同场景下的异常处理机制,确保下载任务的稳定性和高效性。
Download模块 (十四)

DownloadThread extends thread<在被使用时,过多是作为runnable的角色>, 承载了真正通过http下载数据的任务。
Thread的一个问题是,其他thread<比如MainThread>对thread其实没有完全的控制权,
interrupt方法只是改变一个flag,只有在Thread 的run的interrupt定义检测点才能实现对Thread的控制,
这也注定了,基本不可能同步的控制一个thread,能做的只有改变flag,然后在thread运行到检测点的时候做出相应操作。
在哪里埋入检测点需要考虑。

<1>DownloadThread开放了两个接口,提供对三个event的监听: 1 thread因为某种原因被停止 2 Download的progress变化
3 thread收到了http resp.

<2>DownloadThread是通过Http get range来下载文件的,这就需要一些的http交互对象,并且附加很多和http有关的成员变量,
HttpAuth 以及是否pass
ProxyAuth 以及是否pass
当前网络的Proxy
当前网络的连通性
url, refer, UA, etag, lastModfiied<构造时就传入>
redirect url
retry 与 retry delay

<3>DownloadThread还对应这一个File表示要写入数据的位置,因为多线程,某个Thread可能只写入File的某一段range<不是顺序的>,因此还需要RandomAccessFile来实现这种随机读写。
每个DownloadThread都要知道自己所需要下载的部分在整个文件中的start和end,一般是在构造的时候就传入了.
在构造时,也会将要写入的file的path传入,构造相应的File对象,如果构造事没有指定start<-1>,那么就把当前File的长度作为start<断点
续传,如果文件实际不存在,那么length也是0>
还要维护一个lastUpdatePsoition来表示上次写入数据的位置,这样做是为了测速。

<4>DownloadThread还要记录所需要的contentLeght以及已经下载了多少content<都以byte为单位>

<5>为了发起HTTP request,合适的header以及connection本身需要被设置,包括了
(1)ConnectTimeout
(2)InstanceFollowRedirects = true
(3)ReadTimeout
(4)ReadTimeout
(5)Cookie: 通过url从CookieManager<webkit提供>中取得
(6)User Agent
(7)Authorization/Proxy-Authorization:在设置的时候要将字符串以byte数组形式加上参数Base64.URL_SAFE | Base64.NO_WRAP进行
Base64的encode
(8) Proxy-Connection= "Keep-Alive"
(9)如果提供了Etag那么设置If-Match,否则如果提供了LastModified,那么设置If-Unmodified-Since"
(10)Range: bytes = start-<空的话直到末尾/或者某个具体值>
(11)Accept-Encoding: "gzip, identity, deflate"<基本就这3种>

<6>Thread的run函数是逻辑运行的起点,处于鲁棒性,首先检查一些情况,比如需要的content-length为0,或者当前的start比end还要大这些
无效情况,这种情况下,也视为Download的完成,并通知监听者 这个 stop事件。
正常情况下,会通过Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);将
Thread的优先级降低,毕竟下载不是urgent的需求。
然后直接调用delay()函数来延迟指定的时间,delay到期以后reset<初始化> retry相关参数。

因为加入了重试这个设定,因此run函数中的主题部分会被一个do while包起来,while里检查是否需要重新Download。
并且考虑到在下载的过程中会遇到很多的问题,有的会以Exception形式出现,有的则不是,为了同意简单的处理逻辑,
统一将所有的问题以自定义的Exception的形式抛出,在catch里面几个Exception的内容进行处理。
当然在finally里面需要将Connection disconnect。
在这个机制下,interrupt检测点的实现就比较容易,在检测时如果发现thread被interupt,直接throw一个表示被interrupt的Exception即可.

<7>当前的interrupt有几处:
(1)初始化以后有一个
(2)在获得http resp以后会有两个
(3)在每次从网络读取数据并且写入文件前会有一个。

<8>建立http连接完全按照标准的流程,先根据url构造一个Url对象,注意catch MalformedURLException,会被进一步的封装为自定义的
Exception throw出去。
自定义的DownloadStopException throw。
根据有无Proxy来 mConnection = Url.openConnection(/Proxy),
设置http header,
最后mConnection.connect()
如果是以下几类Exception:
IllegalArgumentException/UnsupportedOperationException/ClassCastException, 会被封装为不能处理的错误 以自定义Exception throw出去。
IOException还专门处理<一般是由于网络断开>,一个细分的case是之前是WIFI,现在WIFI关闭,那么这种情况会将自定义Exception的cause
设为WIFI_OFF

<9>在connect以后就可以阻塞等待获取resp了,首先要获得status code,mConnection.getResponseCode()
这个过程也要捕获IOException, 处理的逻辑和上面一样。
成功取得status code以后,按情况进行处理:
(1)HttpStatus.SC_OK: 如果当前是CMWAP的网络的话,WAP网关<添乱的>第一次对这个http请求会送回一个wml文件,
这个wml应该被ignore,然后通过再次发起一次请求来避开这个问题,要对传回来的contentType做检查,看是否确实是
application/vnd.wap.wmlc,如果真的是,那么就throw一个自定义Exception来指定重新下载<这里throw 成了函数的脱出点>。
否则,返回200的情况只应该是没有使用range的case,如果发现实际上使用了range,但是返回了200,那么说明服务端不支持range
只能throw一个Exception来标示重新下载整个文件<RANGE_NOT_SATISFIABLE>。
(2)HttpStatus.SC_PARTIAL_CONTENT: 206,这是我们需要的<在设计中即使要下载整个文件,也会带上range header,所以206在
下载整个文件情况下也是对的>
(3)HttpStatus.SC_MOVED_PERMANENTLY 301
   HttpStatus.SC_MOVED_TEMPORARILY 302
   HttpStatus.SC_TEMPORARY_REDIRECT 307
   这几种情况都应该重新下载<重定位了>,从 Location header获取重定向以后的url,然后throw一个Exception并设置mRedirectUrl
为location,进行重试<不过cause不会设为 restart,专门有一个cause>
(4)HttpStatus.SC_UNAUTHORIZED: 401
   HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: 407
   认证失败,throw出去,每个对应一种cause。
(5)HttpStatus.SC_PRECONDITION_FAILED: 412: 根据给的条件,没有合适的资源
   HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE: 416, range给的出错了。
   这两种case还有回旋余地
(6)其他情况,直接标记为无法处理的error trhow出去,会携带status code。
做获得了status code,并且没有erro throw的情况下,就可以认为 connect是成功了,剩下的
就是开始接收http resp的data。
如果有注册的connectionListner,那么在这里要插入一个interrupt检测点,这是为了检测是否在该线程正在connect/getResp这段
等待期中<无法响应操作>,用户在主线程点击了暂停,如果点击了,那么就在这里暂停住。
设定了在connect成功以后,如果有connectListener,都会将此DownloadThread pasue<这是为了之前所说的探路者线程>,
DownloadThread的pasue函数很简单,只是一个同步的设置mPasued = false,而在通知完了connectionListener以后,
才会调用interruptCheck<一个interrupt检测点>来进行真正的暂停.
如果还可以继续,并且相应的File对象以及确实有需要的数据<鲁棒性>,那么就开始IO。
最后完成以后,调用Listener的onStopped,出入自己的Id,标示自己的任务已经完成。

<10>要让一个线程卡在某个点,两种办法,自旋忙等待<while+flag>,或者是wait<条件条件等待>,sleep不考虑,没有释放锁,不能实现
安全的交互,而自旋因为对性能的极大损耗,基本不考虑,后者的时候则要考虑wait因为某种原因被interrupt的情况<但是pause flag还没有改变>,这样最后的实现就是一个
while检测paused flag + wait<try catch 中捕获interrupt Exception> 来保证性能以及对interrupt的处理。resume的时候,
首先因为resume会由外部线程调用操作某些变量,那么resume需要是同步的,interruptCheck也需要是同步的,在将paused flag设
为false以后,再notifyAll<保险>来唤醒wait的线程,这种wait造成的闲置资源就是一个thread对象,但是不会对cpu时间片造成浪费。

pause() 与 真正实现 pause的部分是分离的两个函数,一个只是更改flag, 并一个根据flag做出是否pause<wait>的决定。
这是因为前面说过的,外部线程<pause()的主要调用者>不可能在thread的任何时候都对thread横插一脚<就算你在thread里实现了一个函数调
用wait/sleep,也只会在调用者线程进行这些操作,这个也反映了计算机的一个基本原理,程序 和 进程/线程 是截然不同的>,所能做的就只
是改变paused这个flag,至于何时真正的pause,完全由thread什么时候运行到pause/interrupt 检测点说了算,这也是一个通用的方法,基本
我看到的对线程控制都是这样实现的,java的interrupt系列函数的目的就在此<某个thread对象被interrupt,在thread自己运行到isInterrupt时才能得知,stop早就被干掉了>.

在interruptCheck中,会抛出两类Exception,

一种是说明了在wait的期间,是有过Interrupt的对应stop,这是为了与pasue区分开,pause和stop都会使线程停止,在这一点是一样的,但是为了区分是pasue还是

stop,需要一个flag, 而因为thread本身已经有了interrupt这个flag,就可以利用这个区分,对于只有 pauseFlag为true的case,认为是pause,而如果即interrupt,也pause的

case,认为是stop,并且interrupt能够打破wait,这就能够在pause的时候进行stop。当然,stop也是只有在插入的检测点才能生效.

* While the thread waits, it gives up ownership of this object's monitor.
* When it is notified (or interrupted), it re-acquires the monitor before it starts running.


另外一种则是mRestart标记的

restartException<很可能线程回复过来的时候已经变天了>,最后才是正常退出。

<11>在从不断的网络接收data,并持久化到文件的过程中,也会有interrupt检测,
一般来说 contentLength可以从header中得到了<当然有时候也没有,会返回-1>,
然后更新NeedContentLength,减去已经transferred的数据。
还要获取resp的content-Encoding<gzip/deflate这些>构造相应的GZIPInputStream/deflateInputStream来decode到文件中,
最后套上一个bufferedInputStream来写入文件,
中间的IOException也会被捕获并按照同样逻辑处理,
finally关闭inputStream.

<12>在实际正常的读取写入过程,是没有interrupt检测的,所以需要人为的制造一点检测点,
一个常规的做法是每次读取一定量的数据,然后写入文件,每次完成这个操作的时候进行一次检测<是否要pause>。
注意因为是分段下载,因此需要借助于RandomAccessFile,并通过seek来指定要写入的位置,然后用RandomAccessFile构造一个
FileOutputStream<new FileOutputStream(randomAccessFile.getFD())>, 常规的再套一个bufferedOutputStream。
然后就开始一个for循环来读取 写入<直到read返回-1>, 每次开始都会检查一次pause. 同时每次写入文件之间,还都要检测
一下File是否还存在。
在写入文件以后,这部分数据才算真正的下载了下来,这时候就要更新相关的变量,并且更新progress,
这里考虑到一个粒度问题,每次有数据来都更新progress的话,效果会很平滑,但是会带来对UI的频繁操作,
性能是个问题,因此考虑定期或这定量的来更新progress通知Listener。
在这个过程中,也会throw几类Exception:
FileNotFoundException throw出标记此错误的Exception
SocketTimeoutException/SocketException 都考虑为IOException
IOException throw Exception标记 retry

<13>主逻辑中对throw出的Exception的处理:
(1)对于HTTP_AUTHENTICATE_FAILED/PROXY_AUTHENTICATE_FAILED:
如果显示已经auth过了,那么就彻底stop此Download,
否则尝试取出auth信息存储在成员环境变量,并且标记retart进行retry。
(2)DOWNLOAD_RESTART/HTTP_NEED_REDIRECT, 标记restart
(3)UNHANDLED_SERVER_STATUS这种是server回复了某种不在处理范围的status code,检查是否retry,如果不行,彻底stop。
(4)NET_IOEXCEPTION/DOWNLOAD_NEED_RETRY
(5)NET_WIFI_IS_OFF,因为WIFI关闭,告知Listener现在Task的停止,pause,并且标记retry

<14>setFile函数可以用来改变DownloadThread的持久化File位置,以及对该File的开始写的位置 startPosition,这个函数当然不是
任何时候调用都有效,应该在开始持久化下载数据之前调用。
setNeededContentLength函数可以改变此DownloadThread需要下载的数据量,同时在相对的DownloadInfo里也会保存此Id的thread应该
下载的start和end。

<15>同DownloadTask一样,逻辑在不断的迭代中变得混乱,还有很多遗漏细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值