上一讲学习了很多关于服务的使用技巧,但是当在真正的项目里需要用到服务的时候,可能还会有一些棘手的问题让你不知所措。接下来就来综合运用一下,尝试实现一下在服务中经常会使用到的功能——下载。

  在这一讲我们将要编写一个完整版的下载示例,其中会涉及到前面的许多内容,算是目前综合程度最高的一个例子了。

1、创建一个ServiceBestPractice项目

2、首先将项目中会使用到的依赖库添加好,编辑app/build.gradle文件,在dependencies闭包中添加如下内容,这里只需要添加一个OkHttp的依赖就行了,待会儿在编写网络相关的功能时,将使用OkHttp来进行实现。

3、接下来需要定义一个回调接口,用于对下载过程中的各种状态进行监听和回调。新建一个DownloadListener接口:

  可以看到,这里我们一共定义了5个回调方法:

  onProgress():用于通知当前的下载进度

  onSuccess():用于通知下载成功事件

  onFailed():用于通知下载失败事件

  onPaused():用于通知下载暂停事件

  onCanceled():用于通知下载取消事件

4、回调接口定义好了后,下面开始编写下载功能了,这里用学过的AsyncTask来进行实现,新建一个DownloadTask继承自AsyncTask:

代码分析:

  这段代码比较长,需要一步一步分析,首先看一下AsyncTask中的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask的时候需要传入一个字符串参数给后台任务;第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位;第三个泛型参数指定为Integer,则表示使用整型数据来反馈执行结果。

  接下来我们定义了4个整型常量用于表示下载的状态,TYPE_SUCCESS:表示下载成功;TYPE_FAILED:表示下载失败;TYPE_PAUSED:表示下载暂停;TYPE_CANCELED表示取消下载。然后在DownloadTask的构造函数中要求传入一个刚刚定义的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。

  接着就是要重写doInBackground()、onProgressUpdate()、onPostExecute()这3个方法了,我们之前学过这3个方法各自的作用,因此在这里它们各自所负责的任务也是明确的:  

  doInBackground():用于在后台执行具体的下载逻辑

  onProgressUpdate():用于在界面上更新当前的下载进度

  onPostExecute():用于通知最终的下载结果

  那么先来看一下doInBackground()方法,首先我们从参数中获取到了下载的URL地址,并根据URL地址解析出了下载的文件名,然后指定将文件下载到:Environment.DIRECTORY_DOWNLOADS目录下,也就是SD卡的Download目录。我们还要判断一下Download目录中是不是已经存在要下载的文件了,如果已经存在的话则读取已下载的字节数,这样就可以在后面启用断点续传的功能。接下来先是调用了getContentLength()方法来获取待下载的总长度,如果文件长度等于0则说明文件有问题,直接返回TYPE_FAILED,如果文件长度等于已下载文件长度,那么就说明文件已经下载完了,直接返回TYPE_SUCCESS即可。紧接着使用OkHttp来发送一条网络请求,需要注意的是,这里在请求中添加了一个header,用于告诉服务器我们想要从哪个字节开始下载,因为已下载过的部分就不需要在重新下载了。接下来读取服务器响应的数据,并使用Java的文件流方式,不断从网络上读取数据,不断写入到本地,一直到文件全部下载完成为止。在这个过程中,我们还要判断用户有没有触发暂停或者取消的操作,如果有的话则返回TYPE_PAUSED或TYPE_CANCELED来中断下载,如果没有的话则实时计算当前的下载进度,然后调用publishProgress()方法进行通知。暂停和取消操作都是使用一个布尔型的变量来进行控制的,调用pausedDownload()或cancelDownload()方法即可更改变量的值。

  接下来看一下onProgressUpdate()方法,这个方法就简单得多了,它首先从参数中获取到当前的下载进度,然后和上一次的下载进度进行对比,如果有变化的话则调用DownloadListener的onProgress()方法来通知下载进度更新。

  最后是onPostExecute()方法,也非常简单,就是根据参数中传入的下载状态来进行回调。下载成功就调用DownloadListener的onSuccess()方法,下载失败就调用onFailed()方法,暂停下载就调用onPaused()方法,取消下载就调用onCanceled()方法。

  这样我们就把具体的下载功能完成了。

5、为了保证DownloadTask可以一直在后台运行,我们还需要创建一个下载的服务。新建一个DownloadService

 代码分析:

  这段代码同样也比较长,还是来耐心慢慢看吧。首先这里创建了一个DownloadListener的匿名类实例,并在匿名类中实现了onProgress()、onSuccess()、onFailed()、onPaused()、onCanceled()这5个方法。在onProgress()方法中,我们调用getNotification()方法构建了一个用于显示下载进度的通知,然后调用NotificationManager的notify方法去触发这个通知,这样就可以在下拉状态栏中实时看到当前下载的进度了。在onSuccess()方法中,我们首先是将正在下载的前台通知关闭,然后创建了一个新的通知用于告诉用户下载成功了。其他几个方法也都是类似的,分别告诉用户下载失败、暂停和取消这几个事件。

  接下来为了要让DownloadService可以和活动进行通信,我们又创建了一个DownloadBinder,DownloadBinder中提供了startDownload()、pauseDownload()和cancelDownload()这3个方法,那么顾名思义,它们分别是用于开始下载、暂停下载、和取消下载的。在startDownload()方法中,我们创建了一个DownloadTask的实例,把刚才的DownloadListener作为参数传入,然后调用execute()方法开启下载,并将下载文件的URL地址传入到execute()方法中。同时,为了让这个下载服务成为一个前台服务,我们还调用了startForeground()方法,这样就会在系统状态栏中创建一个持续运行的通知了。接着往下看,pausedDownload()方法中的代码非常简单了,就是简单的调用了一下DownloadTask中的pauseDownload()方法。cancelDownload()方法中的逻辑也基本类似,但是要注意,取消下载的时候我们需要将正在下载的文件删除掉,这一点和暂停下载是不同的。

  另外,DownloadService类中所有使用到的通知都是调用getNotification()方法进行构建的,这个方法中的代码我们之前基本都是学过的,只有一个setProgress()方法没有见过。setProgress()方法接收3个参数,第一个参数传入通知的最大进度,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条,这里传入false。设置完setProgress()方法,通知上就会有进度条显示出来了。

  现在下载的服务也已经成功实现了,后端的工作基本都完成了。

6、接下来开始编写前端部分,修改activity_main.xml中的代码,如下所示:设置了开始下载、暂停下载、取消下载这3个按钮。

7、修改MainActivity中的代码:

代码分析:

  可以看到,这里我们首先创建了一个ServiceConnection的匿名类,然后在onServiceConnected()方法中获取到DownloadBinder的实例,有了这个实例。我们就可以在活动中调用服务提供的各种方法了。

  接下来看一下onCreate()方法,在这里我们对各个按钮都进行了初始化操作并设置了点击事件,然后分别调用了startService()和bindService()方法来启动和绑定服务。这一点至关重要,因为启动服务可以保证DownloadService一直在后台运行,绑定服务则可以让MainActivity和DownloadService进行通信,因此两个方法调用都必不可少。在onCreate()方法的最后,我们还进行了:WRITE_EXTERNAL_STORAGE的运行时权限申请,因为下载文件是要下载到SD卡的Download目录下的,如果没有这个权限的话,我们这个程序都无法正常工作。

  接下来的代码就非常简单了,在onClick()方法中我们对点击事件进行了判断,如果点击了开始按钮就调用DownloadBinder的startDownload()方法,如果点击了暂停按钮就调用pauseDownload()方法,如果点击了取消按钮就调用cancelDownload()方法。startDownload()方法中你可以传入任意的下载地址,这里我们使用了一个Eclipse的下载地址。

  另外还需要注意,如果活动被销毁了,那么一定要记得对服务进行解绑,不然就有可能会造成内存泄漏。这里我们在onDestroy()方法中完成了解绑操作。

8、现在还只有最后一步,我们还需要在AndroidManifest.xml文件中声明使用到的权限。当然除了权限外,MainActivity和DownloadService也是需要声明的,只不过Android Studio在创建的时候已经帮我们声明好了。

  其中,由于我们的程序使用到了网络和访问SD卡的功能,因此需要声明:INTERNET和WRITE_EXTERNAL_STORAGE这两个权限。

9、运行程序,程序一旦启动立刻就会有申请访问SD卡的权限,这里我们点击ALLOW,然后点击Start Download开始下载,下拉状态栏可以看到下载进度。

  

  同时,我们还可以点击Pause Download或Cancel Download,甚至断网操作来测试这个下载程序的健壮性。最终下载完成后会弹出一个Download Success的通知,然后我们可以通过任意一个文件浏览器来查看一下SD卡的Download目录,如图所示:

  

  可以看到文件下载成功了,当然我们还可以做一些更加丰富的操作,比如说再次点击Start Download按钮,你会发现程序会立刻弹出一个Download Success的提示,因为它检测到文件已经下载完成了,因而不会再重新去下载一遍,如果我们点击Cancel Download按钮先将下载文件删除掉,然后再点击Start Download按钮,你会发现程序又会开始重新下载了。

  总体来说,这个下载实例的稳定性还是挺不错的,而且综合性很强。