神刀安全网

Android N DownloadManager源码分析

DownloadManager是Android用系统服务的方式提供的用来优化处理长时间下载任务的工具。
本文将基于Android N的源码进行分析。

DownloadManager的使用方式
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); Uri uri = Uri.parse("downloadUrl"); DownloadManager.Request request = new Request(uri); long reference = downloadManager.enqueue(request);

调用enqueue方法之后,只要数据连接可用并且Download Manager可用,下载就会开始。
要在下载完成的时候获得一个系统通知(notification),注册一个广播接受者来接收ACTION_DOWNLOAD_COMPLETE广播,这个广播会包含一个EXTRA_DOWNLOAD_ID信息在intent中包含了已经完成的这个下载的ID。

其他更详细API使用方法请参考Android DownloadManager的使用一文,此处不再详述。

DownloadManager的调用处理

DownloadManager的执行入口方法enqueue的源码如下所示:

ContentValues values = request.toContentValues(mPackageName); Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values); long id = Long.parseLong(downloadUri.getLastPathSegment()); return id;

其中,request为请求初始化传入的DownloadManager.Rquest对象,传入请求后
toContentValues()方法会以传入包名将待插入的数据生成ContentValues,方法中会有一个断言检查,代码如下所示:

ContentValues toContentValues(String packageName) {     ContentValues values = new ContentValues();     assert mUri != null;     //....... }

其实看到这处断言检查有点疑惑,在构造Uri对象的时候已经进行了空判断,为什么此处还要进行一次断言检查呢,不是会有冗余吗?

在插入ContentValues时,mResolver.insert()实际调用的是系统DownloadProvider中的insert方法,插入返回的downloadUri会在原有Uri基础上调用ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID)添加一个rowId返回一个形如content://downloads/my_downloads/33的Uri,经过Uri截取之后,实际操作的reference其实是数据库中的rowId(数据库行号)。

DownloadProvider的调用处理

在之前版本中,DownloadProvider在插入数据后,会直接以context.startService的方式
来启动DownloadService。进行异步任务下载。而在Android N版本中引入了JobSchedule组件来进行异步下载任务的处理。
在Android L版本中引入的JobScheduler可以控制耗电,具体使用可以参考:Android JobSchedule工作调度,
其中DownlaodProvider中的insert方法中的关键操作如下所示:

final long token = Binder.clearCallingIdentity(); try {   Helpers.scheduleJob(getContext(), rowID); } finally {   Binder.restoreCallingIdentity(token); }

其中Helpers.scheduleJob()方法中使用rowId将那条下载信息查询出来,然后调用绑定的DownloadJobService进行下载任务。如果线程调度失败,会返回false。

public static void scheduleJob(Context context, long downloadId) {     final boolean scheduled = scheduleJob(context, DownloadInfo.queryDownloadInfo(context, downloadId));     if (!scheduled) {         // If we didn't schedule a future job, kick off a notification         // update pass immediately         getDownloadNotifier(context).update();     } }

此时getDownloadNotifier(context).update()会将遍历出所有未删除的

DownloadJobService调度执行

DownloadService中调度的线程开始下载,在onStartJob中用rowId查出来后,直接开线程开始下载,具体代码如下所示:

public boolean onStartJob(JobParameters params) {   final int id = params.getJobId();   // Spin up thread to handle this download   final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);   if (info == null) {       Log.w(TAG, "Odd, no details found for download " + id);       return false;     }     final DownloadThread thread;     synchronized (mActiveThreads) {       thread = new DownloadThread(this, params, info);       mActiveThreads.put(id, thread);     }     thread.start();     return true; }
DownloadJobService中的暂停、取消与完成

DownloadJobService中在线程开启后,会刷新展示相应的通知栏,通过通知栏UI中的相应控制,可以实现对于下载任务的控制。

  • 在开始下载后,当点击取消后,会发送广播到DownlaodReceiver,当接受到这个广播后,会调用DownloadManager.remove(downloadIds),而DownloadManager.remove()方法则会调用DownloadProvider.delete去删除记录任务。同时会依据rowId移除该线程调度。

  • 任务完成时,会发送一个广播,通知下载完成,但是这里比较意外的是,下载完成的广播发送是放在DownloadInfo中调用DownloadInfo.sendIntentIfRequested()发送的, 而不是在DownloadThread中。

  • 暂停,比较奇怪的是,DownloadManager的异步下载线程提供了断点下载的功能,写入文件也会检查任务的下载状态是不是暂停,但是,却并未提供暂停下载任务的API方法,同时它的下载状态查询的方法也是私有类型的。如果需要暂停任务就需要自定义自己的下载任务了。

DownloadThread中的断点下载的实现方法

其实在DownloadThread中,主要的下载方法就是就是线程中的excuteDownload()方法。部分关键代码如下:

private void executeDownload() throws StopRequestException {   final boolean resuming = mInfoDelta.mCurrentBytes != 0;   ...   int redirectionCount = 0;   while (redirectionCount++ < Constants.MAX_REDIRECTS) {       ......       conn = (HttpURLConnection) mNetwork.openConnection(url);       addRequestHeaders(conn, resuming);       final int responseCode = conn.getResponseCode();       switch (responseCode) {           case HTTP_OK:               if (resuming) {                   throw new StopRequestException(                           STATUS_CANNOT_RESUME, "Expected partial, but received OK");               }               parseOkHeaders(conn);               transferData(conn);               return;           case HTTP_PARTIAL:               if (!resuming) {                   throw new StopRequestException(                           STATUS_CANNOT_RESUME, "Expected OK, but received partial");               }               transferData(conn);               return;           ......       }       ......   } }

在addRequestHeaders()方法中,如果从数据库中查出的数据已读取写入文件的字节数不为0,则会在请求头前添加一个rangeconn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");,当添加上此请求头后,当求求成功后,服务器会返回HTTP_PARTIAL,将接收到的数据通过transferData()方法写入到文件中。在写入文件中时,DownloadThread引入了android.drm.DrmManagerClient与android.drm.DrmOutputStream,这两个包位于framework/base/core/drm包下,部分引用代码如下所示:

if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {   drmClient = new DrmManagerClient(mContext);   out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType); } else {   out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd); }

对于这两个类的引入,目前还不是特别熟悉,后续研究后会进一步进行分析

最后丧心病狂的自己画个图,简单总结下DownloadManager的工作流程:整体外源应用层通过FrameWork层DownloadManager API调用到DownloadProvider,通过操作数据库,最后通过DownloadService中的线程调度完成工作。整体上都是由DownloadProvider进行过渡调用。而数据库与Service都通过DownloadProvider进行隔离。

Android N DownloadManager源码分析
简单结构图

DownloadManager中的分析目前就先告一段落,文中如有分析错误或描述不清楚之处,请大家留言指出~:)

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Android N DownloadManager源码分析

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址