本文代码以及相关注释均来自于《第一行代码——Android》郭霖著,对书中365-378页的【完整版的下载实例】代码进行逻辑梳理以及对初学者的相关概念拓展。
希望在我自我巩固的同时,也能对你有所帮助,加深对Service的理解。
相信你能通过本文掌握Service的基本实现。
目录
Android入门——后台下载的Service实例
1 准备步骤
1.1 添加依赖:
1.2 声明权限:
1.3修改布局文件
2 新建回调接口:DownloadListener
2.1 作用
2.2 代码解析
2.3 用法
2.4 代码实例
3 新建AsyncTask:DownloadTask
3.1 作用
3.2 代码解析
3.2.1 参数
3.2.2 构造方法
3.2.3 重写
3.3 用法
3.4 代码实例
4 新建服务:DownloadService。
4.1 作用
4.2 代码解析
4.2.1 参数
4.2.2 类
4.2.3 方法
4.3 用法
4.4代码实例
5 修改MainActivity
5.1 作用
5.2 代码解析
5.2.1 参数
5.2.2 方法
5.3 用法
5.4 代码实例
'com.squareup.okhttp3:okhttp:3.4.1'
用于实现AsyncTask和Service之间的通信
定义五个方法,分别对应:正在加载、成功、失败、暂停、取消、和错误。
其中,正在加载方法要求传入一个int类型的表示当前进度的参数。
在DownloadTask中调用接口中的方法,返回当前状态,
在DownloadService中实现相应的方法,进行UI操作。
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
void onError();
}
用于耗时网络操作的子线程。
a.定义五个int类型常量,用于在doInBackground方法中,返回当前状态,传递给onPostExecute方法。
b.定义一个成员变量DownloadListener,用于在doInBackground方法中回调当前状态。
c.定义两个boolean类型参数,和int型参数,分别用于表示当前状态,和当前下载进度。
DownTask类的构造方法,要求传入一个Download类型参数,将DownloadTask中的下载状态通过这个参数进行回调。
a.泛型参数
DownloadTask的父类AsyncTask
params:参数,本例中将params的类型设为String,用于表示资源下载地址。
通过Download.execute()方法传入,在doInBackground方法中作为参数调用。
progress:进度,本例中将progress的类型设为Integer,用于表示当前异步任务完成的进度。
通常由doInBackground方法中的publishProgress(progress)方法传递出,由onProgressUpdate方法作为参数调用。
result:结果,本例中将result的类型设为Integer,用于表示当前异步任务的状态。
通常为doInBackground方法的返回值,由onPostExecute方法作为参数调用。
b.重写方法
DownloadTask的父类AsyncTask通常有几个需要重写的方法
在doInBackground()方法中,编写后台异步任务具体执行的操作。
在onProgressUpdate()方法中,编写进度更新的操作。
在onPostExecute()方法中,编写异步任务结束后的操作。(结束包含成功结束以及暂停取消失败等。总之通过判断doInBackground()方法的返回值,执行相应的操作)
c.定义方法
定义了一些新的方法包括pauseDownload(),cancelDownload(),getContentLength().分别是暂停下载,取消下载,和获取下载目标文件大小。具体原理和方法参见代码。
作为DownloadService的成员变量。
public class DownloadTask extends AsyncTask {
public static final int TYPE_SUCCESS = 0;
public static final int TYPE_FAILED = 1;
public static final int TYPE_PAUSED = 2;
public static final int TYPE_CANCELED = 3;
public static final int TYPE_ERROR = 4;
private DownloadListener listener;
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;
public DownloadTask(DownloadListener listener){
this.listener = listener;
}
@Override
protected Integer doInBackground(String... params) {
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try { long downloadedLength = 0;//记录已下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
file= new File(directory +fileName);
if (file.exists()){
downloadedLength = file.length();
}
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0){
return TYPE_ERROR;
}else if (contentLength ==downloadedLength){
//已下载字节和文件总字节相等,说明已经下载完成了
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//断点下载,指定从哪个字节开始下载
.addHeader("RANGE","bytes="+downloadedLength+"-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response !=null){
is = response.body().byteStream();
savedFile = new RandomAccessFile(file,"rw");
savedFile.seek(downloadedLength);//跳过已下载的字节
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = is.read(b)) != -1){
if (isCanceled){
return TYPE_CANCELED;
}else if (isPaused){
return TYPE_PAUSED;
}else {
total += len;
savedFile.write(b,0,len);
//计算已下载的百分比
int progress = (int)((total+downloadedLength)*100/contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
if (is!=null){
is.close();
}
if (savedFile!=null){
savedFile.close();
}
if (isCanceled&& file !=null){
file.delete();
}
}catch (Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress>lastProgress){
listener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer status) {
switch (status){
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
case TYPE_ERROR:
listener.onError();
default:
break;
}
}
public void pauseDownload(){
isPaused = true;
}
public void cancelDownload(){
isCanceled = true;
}
private long getContentLength(String downloadUrl) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response !=null && response.isSuccessful()){
long contentLength = response.body().contentLength();
response.body().close();
return contentLength;
}
return 0;
}
}
作为程序后台执行的解决方案,执行那些不需要和用户交互而且还要求长期运行的服务。
需要注意的几点:
1.服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
2.不要被服务的后台概念所迷惑,实际上服务并不会自动开启进程,所有的代码都是默认运行在主线程中的。也就是说我们需要在服务的内部手动创建子线程(本例使用的是AsyncTask模式)
a.定义一个DownloadTask,在需要下载时实例化,在下载结束或手动取消时清空。
b.定义并实例化一个DownloadListener接口,并实现接口中的方法。(主要是执行一些UI操作和将downloadTask清空)
c.定义一个DownloadBinder类的实例mBinder,并初始化。
创建一个DownloadBinder类继承自Binder。作为Activity和Service之间的通讯工具。在该类中定义开始下载、暂停下载和取消下载的方法。用于在Activity中执行相应操作。
重写onBind方法,返回mBinder。
定义一个getNoftificationManager()方法,用于获取系统通知管理类
定义一个getNotification(String title,int progress)方法,用于设置通知属性以及进度。
1.在Activity中启动并绑定服务,在Activity被销毁时进行解绑。
2.在Activity中监听到下载相关的按钮操作,执行downloadBinder的对应方法(需要在Activity中定义一个DownloadBinder类)。
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener = new DownloadListener() {
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1,getNotification("Downloading...",progress));
}
@Override
public void onSuccess() {
downloadTask = null;
//下载成功时将前台服务通知关闭,并创建一个下载成功的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Success",-1));
Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
downloadTask = null;
//下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this,"Download Paused",Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Download Canceled",Toast.LENGTH_SHORT).show();
}
@Override
public void onError() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Download Error",Toast.LENGTH_SHORT).show();
}
};
public DownloadService() {
}
private DownloadBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private NotificationManager getNotificationManager(){
return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
private Notification getNotification(String title,int progress){
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if (progress>=0){
//当progress大于或者等于0时才需显示下载进度
builder.setContentText(progress+"%");
builder.setProgress(100,progress,false);
}
return builder.build();
}
class DownloadBinder extends Binder{
public void startDownload(String url){
if(downloadTask == null){}
downloadUrl =url;
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Download ...",Toast.LENGTH_SHORT).show();
}
public void pauseDownload(){
if(downloadTask != null){
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if (downloadTask !=null){
downloadTask.cancelDownload();
}
if (downloadUrl!=null){
//取消下载时需将文件删除,并将通知关闭
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory+fileName);
if (file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
}
}
}
}
程序运行的主界面,在这里执行UI操作,以及Service的开启和绑定。
a.定义一个DownloadBinder实例,用于实现Service和Activity之间的通信。
b.定义一个ServiceConnection实例,用于实现Service和Activity之间的连接。并在onServiceConnected方法中,进行downloadBinder的实例化操作。
a.重写onCreate()方法,并在其中进行
1).UI控件的初始化操作以及按钮监听绑定;
2).DownloadService的启动及绑定;
3).对写外部存储器操作的运行时权限申请。
b.重写onClick()方法,实现按钮监听操作,调用downloadBinder的相关方法。
c.重写onRequestPermissionResult()方法,当用户拒绝权限申请时弹出Toast。
d.重写onDestroy()方法,在该方法中进行服务解绑,否则会造成内存泄露。
在Manifest.xml中注册,并编写对应的布局文件
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button startDownload = findViewById(R.id.start_download);
Button pauseDownload = findViewById(R.id.pause_download);
Button cancelDownload = findViewById(R.id.cancel_download);
startDownload.setOnClickListener(this);
pauseDownload.setOnClickListener(this);
cancelDownload.setOnClickListener(this);
Intent intent = new Intent(this, DownloadService.class);
startService(intent);//启动服务
bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
@Override
public void onClick(View v) {
if (downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start_download:
String url="https://www.imooc.com/mobile/mukewang.apk";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]!=PackageManager.PERMISSION_GRANTED){
Toast.makeText(this,"拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}
致谢:@guolin https://blog.csdn.net/guolin_blog
如对本文有任何疑问,可以在评论区提问。或致信[email protected] 我会倾尽解答。