admin管理员组

文章数量:1657207

Download模块 (十一)


DownloadService<C>,该service是为了实现APP切换到后台仍可以下载而实现的, 这也是Android所提倡的一种后台处理方式。
该Service因为会和MainActivity这一端频繁交互,因此设计成为了bind的service<start的也不是不可以,但是显然bind更加合适>.
而同时在系统的Notification栏中也会有复数个<正在进行的Download>不可撤销的Notification与此Service绑定发生交互,交互的方式通过intent传递到
Service内部定义的BroardCastReceiver来实现的<Intent在构造时会指定要传递给BroardcastReceiver>。


<1> Service内部扩展了一个BroadcastReceiver来实现两个主要交互:
(1)监听ConnectivityManager.CONNECTIVITY_ACTION这个action<Intent.getAction()>, 在网络变化并且可以连通的情况下尝试恢复所有暂定的Download.
(2)监听来自Notification的相关,Intent,获取用户当前交互的Notification对应的DownloadTask的Id,然后根据其当前的状态对其进行操作。


<2>因为要和MainActivity端的DownloadProxy通过binder的transact进行通信,因此需要为所有可定的操作<注意这操作可以是service触发proxy,也可是proxy触发service的>
制定相应的code<transact和onTransact中的code参数>.
项目中一般都是直接将这些code值声明为public static final int<必须是public static,否则对端的DownloadProxy>, 从INIT = Binder.FIRST_CALL_TRANSACTION开始,不断 +1.
MSG_BASE = Binder.FIRST_CALL_TRANSACTION;
INIT = MSG_BASE + 0; <Proxy对Service进行初始化>
NEW_TASK = MSG_BASE + 1; <Proxy请求Service为一个新的Download创建创建相应的DownloadTask,并看情况执行>
PAUSE = MSG_BASE + 2; <Proxy请求Service暂停某个Download的DownloadTask>
RESUME = MSG_BASE + 3; <Proxy请求Service恢复某个Download的DownloadTask>
REMOVE = MSG_BASE + 4; <Proxy请求Service移除某个Download的DownloadTask>
DELETE = MSG_BASE + 5; <Proxy请求Service移除某个Download的DownloadTask和文件>
BROWSER_EXIT = MSG_BASE + 6; <通知Service MainActivity已经退出了,这种情况和APP切到后台是不一样的,Service也应该退出>
LOG_EVENT = MSG_BASE + 7; <辅助log>
PROGRESS_CHANGED = MSG_BASE + 8; <Service告知Proxy某个Download的progress变化了> M影响V,以下部分的变量定义其实放在DownloadProxy更好,不过没关系。
STATUS_CHANGED = MSG_BASE + 9; <Service告知Proxy某个Download的status变化了>
FILE_CHANGED = MSG_BASE + 10; <Service告知Proxy某个Download的对应的File对象变化了,一般是因为下载完毕以后,文件重命名>
DISPLAY_NAME_CHANGED = MSG_BASE + 11; <Service告知Proxy某个Download的在UI上显示的Name变化了>
SIZE_CHANGED = MSG_BASE + 12; <Service告知Proxy某个Download的已下载量变化了>
TOTAL_SIZE_DETERMINED = MSG_BASE + 13; <Service告知Proxy某个Download的总大小终于得到了,一般是对于http transfer chunk这种一开始不能得到大小的case>
INTEGRITY_CHECK_FINISHED = MSG_BASE + 14;
SPEED_REPORTED = MSG_BASE + 15; <Service告知Proxy某个Download的当前下载速度,用于UI显示>
上面的所有binder通信基本都是针对某个Download来说,这么设计的原因是目前的交互设计中,用户一个时间点只能和一个Download任务发生交互<没有添加暂停所有Download这种比较冷门的功能,
并且,即使有这种需求,也可以通过遍历Download单个处理来实现,只不过繁琐浪费>,而service 到 proxy这一端,目前看也不要什么对Download集体性的操作,因此,现在
的binder所定义的行为,都是针对某个Download来说的。


一个Download对象在是一个下载任务在Proxy端的代表, 而在Service端,则对应一个DownloadTask对象。


<3>DownloadService内部维护一个sparseArray来保存DownloadTask<M>.


<4>为实现与proxy端的binder通信,必然需要自己定制一个binder提供给对端进行交互,一般需要定制的也就是onTransact函数,根据对端传来的code和
parcel, flag进行相应的操作。


<5>binder实现的INIT操作的内容,设计时将恢复上次下载任务的持久化信息的工作交给了Service,因此,在INIT的时候,就由Service从持久化在磁盘文件中的
下载任务的信息恢复出来,并且以字符串的形式填入到reply中<Proxy会将这些信息更新到UI上>,然后,通过data的readStrongBinder()获取和proxy的binder,这样才能向proxy发送信息。
注意在这一步虽然读取出了持久化的Download任务信息,但是Service这时候本身并没有保存这些信息,而是完全交由Proxy选择性的将Download再发送回来进行操作。
虽然感觉有点浪费,不过这样做职责明确,在Download任务的指派上,基本是Proxy来分配任务给Service,单方向的简单体系。


Service在被创建以后,会有一次被调用onCreate<多次不同进程bind也只会触发一次,除非Service被干掉重启>,在这一步,会初始化两个handler,一个handler是Service的主进程,
直接context.getManLooper构造即可,另外一个用于IO<Android中,比较费时的操作都建议起一个新的thread执行>,需要创建一个新的handlerThread,
然后getLooper来构造<注意之前handlerThread必须已经start了>,同时还会创建一个File对象
<只是在内存中的一个对象,不代表File才磁盘存在>代表DownloadInfo持久化位置。以及register自定义的BroadCastReceiver, 通过new 一个 intentFilter并设置其Action.


onBind不需要干任何事情,只需返回定制的binder供Proxy调用。


onStartCommand在bind的时候也不会被调到<项目中对该Service都是bindService,没有startService>,但是在第一次创建或者系统恢复Service的时候<也是创建>会被调用<可以通过在传来的intent中
设立自定义flag检测或者intent是否是null是哪一种case来>,如果是因为系统回收导致的恢复重启,那么需要将所有的DownloadTask自动resume。
注意返回START_STICKY标示需要系统的kill后恢复<START_STICKY tells the OS to recreate the service after it has enough memory and call onStartCommand() again with a null intent. 
><bindService也有这个作用,双保险>,出于鲁棒性,Service也维护了一个started flag标示 startCommnad时候已经被运行过,防止误用startService导致的问题<startService
每次都能startCommand,即使是一个已经start或bind的Service>。


采用startService + bindService的原因是 即要能够与Service方便交互,也要Service能够在没有bind的情况下继续运行<后台下载>。


<6>NEW_TASK, 从送过来的Parcel中读取要创建的DownloadTask的info<这里的info是一个自己定义的类,因此要实现parcable的相关要求>,一同送来的还有是否立刻开始此Download或者是
delay一段时间,考虑到proxy与service的交互性能,项目在onTransact<单独的线程>线程中基本不会做过多的操作,而都是提取了数据并final 引用保存以后<为了Runnable匿名类中使用>,
直接post相关的处理任务到Service的mainHandler中。
出于鲁棒性,对 传过来的new task info,要检查是否Service当前实际维护的Downloads已经有了该task,没有的话,就根据info create一个DownloadTask<不考虑Proxy直接创建DownloadTask然后
传过来直接使用,因为这破坏了封闭性, Proxy不应该影响Service的DownloadTask如何定义, Proxy只负责传递info,如何使用组装,是Service自己决定的>
Service负责根据info创建一个DownloadTask, 并将DownloadTask放入自己维护的DownloadTask list中<M>,这一步操作其实可以抽离出来放在一个专门的DownloadTaskManager<名字可能不恰当>类中,
转发start的请求到DownloadTask的start<Task start的细节Service不操心,只负责告诉Task start即可>


<7>对于PAUSE/RESUME/REMOVE/DELETE, Proxy传递的信息很少,只需要一个要进行操作的Task 的Id即可。获得相应的Task对象<用id可以直接在spareArray中得到>以后,直接转发方法到Task对象。


<8>BROWSER_EXIT, Proxy会告知在APP退出的情况下是否下载还要在后台继续,如果不需要,那么将当前进行的task持久化,注意,不能直接调用stopSelf来停止Service,因为这时候可能还有一部分
下载的内容在内存中没有同步到磁盘文件中<或者其他应该在进程结束时要做的一些事情>,因此,这一步能做的只是将一个stopped flag打开,在后面的行为中,检测到,才回真正的stopself,
 这就是所谓的 gracefulShutdown.


<9>DownloadService同时还监听了所有的DownloadTask,通过实现DownloadTask开发的一个Observer接口,每个DownloadTask都会将Service设置为Observer,在DownloadTask本身有
什么变化的时候,可以被DownService感知到,每个Task在通知DownloadService的时候会告知自己的Id号以作为区分。


<10>当某个DownloadTask的progress发生了变化,就会通知DownloadService<通知一词表明了处理职责已经在DownloadService里了,松耦合>,出于鲁棒性,要考虑到调用时的taskId是无效id,这种
情况直接返回即可。同时还要更新持久化的profile文件,最后则是告知Proxy端,obtain一个Parcel然后将id和progress填入,通过binder 打上相应的code send出去<这一部分逻辑也是可以抽离
出来移到前面说的manager里>.


<11>在用binder向Proxy send parcel时<一般出于方便,都会这一部分直接封装为一个函数或者对象>,
 要考虑到APP已经退出的情况<Proxy对应的binder为null或者 isBinderAlive = false>,直接返回,不必多余的尝试,
 如果中间出现了RemoteException,那么可以认为和对端的连接已经中断,将Proxy对应的binder
应用直接设为null,认定此binder已经失效。记得回收parcel,只有成功送出去或者发送失败但是没有异常,才返回true<这里在发送失败没有异常也返回true是有些不恰当,不过该函数的返回项
想表达的是binder通信信道本身的有效性,并且在后面使用中,其实没有使用该返回值>。


<12>持久化的Download信息被称为profile,目前项目中数据配置持久化与恢复都采用了Json方式,是一种比较方便的手段,JsonObject可以和String直接做转换。
Json的一个缺点是会进行大量的字符串拼接,对于非常多的数据量,性能比较低。Json擅长的是Key-value这种方式<不过其实任何数据都可以表示为这种形式>,
Json在这里提供了一种比较方便的从数据结构到字符串的转化。
本模块中的DownloadInfo持久化采用了Json,DownloadTask负责具体实现如何将自己转化为JsonObject,然后将所有的JsonObject都放到一个JsonArray中,
在每次定期/或者是被触发 提取并持久化DownloadInfo时,都会将所有的DownloadTask的info<遍历SparseArray,valueAt>转化为JsobObject然后放入到JsonArray中,
<只持久化info的信息已经足够恢复所有的信息>,然后再通过put (Key, Array)的方式
放到一个JsonObject中<当时这样设计是出于还存储其他的信息到该JsonObject的目的,不过后来没用上>。
在信息全部收集完以后,会讲新生成的JsonObject的toString与上一次的结果<这一部分也是一个成员变量>做对比,如果发现没有变化,那么就不需要更新磁盘上的持久化文件。
如果确实变化了,那么出于性能的考虑,延迟一个真正执行持久化的Runnable<IO操作>到IOhandler<这也是一个需要gracefulShutdown的原因,如果此IOhandler正在执行,Service stopSelf,就会
出现Profile持久化不完整的问题>.


<13>真正持久化的操作也很简单,因为Json已经将DownloadInfo从内存中的数据结构转化为了字符串,因此这一步只需要将字符串写入到持久化的磁盘文件即可。
文件写入过程采用了 通用的 写入暂时文件完成后覆盖原文件的 回退模式,如果创建的临时文件已经存在,删除即可,不会有什么安全性的影响。
因为会有中文字符,因此采用了FileWriter和BufferWriter,在写入到文件并且覆盖原文件成功以后,才会将维护的当前DownloadInfo的字符串信息为最新的值,
这是出于内存硬盘数据一致性的考虑. 
在写入以后会检查stop flag,看是否Service已经被要求停止了<gracefulShutdown>,stopSelf不必一定在主线程执行。


<14>在Service onDestory的时候,也要做一些检查和持久化,看当前是否还有没有完成的DownloadTask,并将其持久化到Setting中<sharedPreference>,注销Receiver<Android规范行为>,
以及将所有的Download Notification撤销<Service都没了>, 最后调用 Process.killProcess(Process.myPid())来将自己的进程干掉<算是内存优化,这样Andorid就不能缓存Service进程了占内存了,
个人感觉其实和Android理念相违背,不过.....>


<15>从持久化的DownloadInfo恢复DownloadTask,得益于Json,也很简单,将文件的内容读取完成String以后,直接以此构造一个JsonObject,然后根据Key取得存储DownloadInfo的JsonArray,
再提取出每个JsonObject存储在ArrayList中<中间如果出了JSONException,仍然继续后面的Task创建,力所能及的恢复能恢复的>,之所以不直接利用JsonArray而是又存在ArrayList中,
就是为了防止JsonArray操作中出现的JSONExcpetion打断恢复操作<当然了真直接来其实也用该没啥的>。 在将DownloadInfo提取出来<具体实现也由DownloadTask来实现>以后,就可以根据info 
构建Task了<并放在DownloadTask List中,保存在内存中>,然后转发其start。


<16>在要start一个DownlodTask前,可以加一些检测来决定是不是真的要开始<这个检测其实可以集成在DownloadTask内,但是似乎也有些不太好,会混淆语义,start了却其实没有start>


<17>DownloadTask通过Observer触发Service的操作主要是3种,更新Notiication, 同Proxy通信, 以及更新并持久化DownloadInfo。
很多可以抽离在一个单独的manager类实现,并且为了鲁棒性,所有这类回调都要检测id的有效性。

本文标签: 模块Download