本章要点
Android沿用了诺基亚系统的Symbian操作系统的老习惯,从一开始就支持后台功能,这使得应用程序即使在关闭的情况下仍然可以在后台继续运行。后台功能属于四大组件之一,重要程度言不可寓。
10.1 服务是什么
服务(Service)是Android中是实现程序后台运行的解决方案,它非常适合执行那些不需要与用户进行交互还需要长期运行的任务。服务的界面不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了一个应用程序,服务仍然能保持正常运行。
服务并不是运行在一个独立的进程中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所以依赖于该进程的服务也会停止运行。
默认在主线程,可能会造成主线程阻塞的问题。
10.2 Android多线程编程
当我们执行耗时操作,就必须放在子线程中运行,避免线程阻塞的问题,增强用户的体验度。
10.2.1 线程的基本用法
Android多线程与Java多线程使用语法相同。比如说,定义一个线程只需要定义一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可,如下:
class MyThread extends Thread {
@Override
public void run(){
//处理具体的逻辑
}
}
启动这个线程,new出MyThread的实例,然后调用start()方法,这样run()方法就运行在子线程中了,如下:
new MyThread().start();
当然,使用继承的方式高耦合,更多的时候我们选择实现Runable接口的方式来定义一个线程,如下:
class MyThread implements Runable {
@Override
public void run() {
//处理具体的逻辑
}
}
那么启动方式如下:
MyThread myThread = new MyThread();
new Thread(myThread).start();
常见的实现方式如下:
new Thread(new Runable() {
@Override
public void run() {
//处理具体的逻辑
}
}).start();
以上就是线程的基本用法。在Java中创建和启动线程的方式是一样的。
10.2.2 在子线程中更新UI
更新应用程序中的UI元素,必须放在主线程。否则就会出现异常。
那我们就来验证下吧。新建AndroidThreadTest项目,修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_change_tv"
android:text="Change Text"
android:textAllCaps="false"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv"
android:layout_centerInParent="true"
android:text="Hello World"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
接下来我们对Hello World 进行修改,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_change_tv;
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_change_tv= (Button) findViewById(R.id.btn_change_tv);
tv= (TextView) findViewById(R.id.tv);
btn_change_tv.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_change_tv:
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("Nice to meet you");
}
}).start();
break;
default:
break;
}
}
}
我们在子线程中更新UI,运行程序,点击Change Text按钮,我们会发现程序崩溃。观察logcat日志,可以看到原因是由于在子线程中更新UI操作,如下:
由此我们证明了Android不能在子线程中更新UI操作。有时候我们会在耗时操作返回的结果需要进行相应的UI更新。那么我们通过Android提供的异步消息处理机制,完美解决在子线程中更新UI操作。
使用异步处理消息的方法。
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final int UPDATE_TEXT=1; //表示更新TextView的动作
private Button btn_change_tv;
private TextView tv;
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) { //4.主线程
switch (msg.what){
case UPDATE_TEXT: //5.判断是否相等,进行操作
//在这里进行UI操作
tv.setText("Nice to meet you");
break;
default:
break;
}
}
};
...
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_change_tv:
new Thread(new Runnable() {
@Override
public void run() {
Message message=new Message(); //1.创建Message对象
message.what=UPDATE_TEXT; //2.指定what值
handler.sendMessage(message); //3.将Message对象发送过去
}
}).start();
break;
default:
break;
}
}
}
重新运行程序,点击Change Text按钮,Hello World就会变成Nice to meet you。如图:
使用Handler机制顺利的解决了在子线程中更新UI的问题。
10.2.3 解析异步消息处理机制
Android中的异步消息处理主要有4部分组成:Message,Handler,MessageQueue,Looper。
- Message
Message是在线程之间传递的消息,它可以携带少量的信息,用于在不同线程之间交换数据。Message的what字段,arg1和arg2字段携带整型数据,obj字段携带一个Object对象。 - Handler
Handler处理者,主要用于发送和处理消息。发送消息Handler的sendMessage()方法,发送的消息最终会传递到Handler的handleMessage()方法中。 - MessageQueue
MessageQueue消息列队,主要用于存放所有通过Handler发送的消息。这部分消息一直会存在于消息列队中,等待被处理。每个线程中只会有一个MessageQueue对象。 - Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的lop方法后,就会进入到无线循环中,然后每当发送MessageQueue中存在的一条消息,就会将它取出,并传递到Handler中的handleMessage()方法中。每个线程中也只会有一个Looper对象。
异步消息处理的整个流程:首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后在子线程中进行需要UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息被添加在MessageQueue消息列队中等待被处理,而Looper会一直尝试者从MessageQueue中取出待处理的消息,最后发送给Handel的handleMessage方法。由于Handler是在主线程中创建的,所以此时handleMessage()方法也会在主线程中执行,就可以进行UI操作了。整个异步消息处理机制流程图:
整个异步消息处理的核心思想:一条Message经过一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新UI操作,变成了可以更新UI操作。
之前我们使用的runOnUiThread()方法就是一个异步消息处理机制的接口封装。原理跟上图描述的一样。
10.2.4 使用AsyncTask
Android为了更加方便在子线程中更新UI操作,使用AsyncTask。它的实现原理也是基于异步消息处理机制的封装。
AsyncTask的基本用法,AsyncTask是一个抽象类,我们必须创建一个类继承它。继承时可以为AsyncTask指定3个泛型参数。
- Params。在执行AsyncTask时传入的参数,可用于在后台的使用。
- Progress。 后台执行任务时,如果需要在界面显示当前的进度条,则使用这里指定的泛型作为进度单位。
- Result。 任务执行完毕,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
一个完整的自定义AsyncTask如下:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
/**
* 开始执行任务之前,用于初始化见界面。例如:显示一个进度条
*/
@Override
protected void onPreExecute() {
progressDialog.show();//显示进度对话框
}
/**
* 在子线程中执行,处理耗时操作。
* 如果需要更新UI元素,则调用publishProgress()
*
* @param params
* @return
*/
@Override
protected Boolean doInBackground(Void... params) {
try {
while (true) {
int downloadPercent = doDownlod(); //这是一个虚方法
publishProgress(downloadPercent);
if (downloadPercent >= 100) {
break;
}
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 当在后台中调用了publishProgress()方法后,onProgressUpdate()很快就会调用,
* 该方法携带的参数就是从后台传过来的。在这里执行对UI操作。
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
//在这里执行下载进度
progressDialog.setMessage("Download" + values[0] + "%");
}
/**
* 当后台执行完毕返回进行返回时。利用返回数据更新UI操作。
* 比如:提醒任务执行的结果,以及关闭对话框等。
*
* @param result
*/
@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss(); //关闭进度对话框
//在这里提醒下载结果
if (result) {
Toast.makeText(content, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(content, "Download failed", Toast.LENGTH_SHORT).show();
}
}
}
第一个泛型参数Void,表示不需要传入参数给后台任务。
第二个泛型参数指定为Integer,表示使用整型数据类型作为进度显示单位。
第三个泛型参数指定为Boolean,表示返回回馈的结果。
在这个DownloadTask中,在doInBackground()方法执行具体的下载任务(子线程)。doDownload()方法是一个虚方法,计算当前的下载进度并返回,并让它显示到界面中。我们通过调用 publishProgress()方法并将下载进度传进来。这样onProgressUpdate()就会调用,进行UI操作。下载完成后,doInBackground()返回的是布尔值,然后通过onPostExecute()弹出相应的Toast提示。完成异步加载任务。
简单的来说,在AsyncTask中,doInBackground()进行耗时操作;onProgressUpdate()更新UI操作,onPostExecute()执行收尾工作。
启动这个任务,代码如下:
new DownloadTask().execute();
本章最佳实践,完善下载这个功能。
10.3 服务的基本用法
Android四大组件之一 —— 服务。
10.3.1 定义一个服务
新建ServiceTest项目,点击包—>New—>Service—>Service。弹出如下窗口:
Enabled:表示是否启动这个服务。 Exported:是否允许除了当前程序之外的程序进行访问这个服务。全部勾选,点击Finish完成创建。
MyService的代码如下:
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
MyService类继承自Service。onBind()方法是Service中唯一的一个抽象方法。
处理事情,定义Service其他方法,代码如下:
public class MyService extends Service {
...
/**
* 创建服务的时候调用
*/
@Override
public void onCreate() {
super.onCreate();
}
/**
* 每次启动的服务的时候调用
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
/**
* 销毁服务的时候调用
*/
@Override
public void onDestroy() {
super.onDestroy();
}
}
每个服务都需要在AndroidManifest.xml注册是才能够使用,这是Android四大组件的共同点。创建服务的时候Android Studio已经帮我们智能的创建完成了。打开AndroidManifest.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>
这样,我们就定义好了一个服务。
10.3.2 启动和停止服务
借助Intent来启动和停止这个服务,在ServiceTest项目中来实现吧。
修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Service"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_stop_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Service"
android:textAllCaps="false" />
</LinearLayout>
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_start_service = (Button) findViewById(R.id.btn_start_service);
btn_stop_service = (Button) findViewById(R.id.btn_stop_service);
btn_start_service.setOnClickListener(this);
btn_stop_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); //启动服务
break;
case R.id.btn_stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); //停止服务
break;
default:
break;
}
}
}
测试服务启动或停止,我们在MyService中的方法加入日志,如下所示:
public class MyService extends Service {
private static final String TAG = "MyService";
...
/**
* 创建服务的时候调用
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: executed");
}
/**
* 每次启动的服务的时候调用
*
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand: executed");
return super.onStartCommand(intent, flags, startId);
}
/**
* 销毁服务的时候调用
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: executed");
}
}
接下来我们运行程序,如下:
点击Start Service按钮,观察logcat日志如下:
这时这个服务已经启动了,我们打开设置—>开发者选项—>正在运行的服务,如图:
然后我们点击Stop Service按钮,观察日志如下:
可以看出MyService的确停止服务了。
onCreate()和onStartCommand()区别,onCreate()会在第一次创建的时候调用,而onStartCommand()则在每次创建服务的时候调用。当我们点击多次Start Service按钮,第一次会执行两个方法,而以后只会执行onStartCommand()方法。
10.3.3 活动和服务进行通信
借助onBind()方法,使我们的服务与活动关联起来。
我们希望实现MyService实现一个下载功能,在活动中决定合适开始下载,以及查看进度。那我们创建一个Binder对象对下载功能进行管理。修改MyService中的代码如下:
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
private DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
class DownloadBinder extends Binder{
/**
* 模拟方法
*/
public void startDownload(){
Log.d(TAG, "startDownload: executed");
}
public int getProgress(){
Log.d(TAG, "getProgress: executed");
return 0;
}
}
...
}
新建DownloadBinder类,并继承Binder,再里面实现下载和查看进度的模拟方法(这里我们分别打印一行日志)。
在MyService中创建DownloadBinder的实例,然后在onBind()返回实例。做完MyService工作了。
在活动中调用服务里的方法。新增两个按钮(绑定服务,解绑服务),修改activity_main中的文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<Button
android:id="@+id/btn_bind_service"
android:textAllCaps="false"
android:text="Bind Service"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_unbind_service"
android:textAllCaps="false"
android:text="unBind Service"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
当一个活动和服务绑定之后,就可以调用服务里的Binder提供的方法,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//指定服务去干什么
downloadBinder= (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
btn_bind_service= (Button) findViewById(R.id.btn_bind_service);
btn_unbind_service= (Button) findViewById(R.id.btn_unbind_service);
...
btn_bind_service.setOnClickListener(this);
btn_unbind_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.btn_bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent,connection,BIND_AUTO_CREATE); //绑定服务
break;
case R.id.btn_unbind_service:
unbindService(connection); //解绑服务
break;
default:
break;
}
}
}
首先创建一个ServiceConnection匿名类,重写了onServiceConnected()和onServiceDisconnected()方法,与服务成功绑定和解绑的时候调用。通过向下转型得到了DownloadBinder的实例,接下来在onServiceConnected()中调用DownloadBinder的startDownload()和getProgress()方法。
绑定服务:binderService()接受三个参数,第一个参数Intent,第二个参数ServiceConnection,第三个参数BIND_AUTO_CREATE(表示在活动和服务进行绑定后自动创建服务)。这样MyService中的onCreate()会得到执行,但onStartCommand()方法不会执行。
解除绑定:unBinderService()方法。
运行程序,点击Bind Service中的按钮,观察logcat日志如下:
这样就完成了我们的绑定服务,任何一个服务在应用范围内都是通用的(一个服务可以和任意一个活动绑定)。
10.4 服务的生命周期
服务也有自己的生命周期,官方给出两种服务的生命周期,一目了然,如图:
10.5 服务的更多技巧
接下来我们学习服务高级使用技巧。
10.5.1 使用前台服务
希望服务一直保持运行,而不希望系统内存不足而被回收,我们可以使用前台服务。前台服务和普通服务的最大的区别就在与,它会一直有一个正在运行的图标在系统的状态栏显示,下拉菜单显示详情内容,类似于通知。
创建前台服务,修改MyService中的代码如下:
public class MyService extends Service {
...
/**
* 创建服务的时候调用
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: executed");
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
Notification notification=new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1,notification);
}
...
}
调用startForeground()方法来显示通知,就会让MyService变成一个前台服务,显示在系统的状态栏。
运行程序,并点击Start Service或Bind Service按钮,就会启动前台服务。如图:
10.5.2 使用IntentService
服务的代码默认在主线程,如果在服务里进行一些耗时操作,很容易出现ANR(Application Not Responding)的情况。
为了解决ANR,我们必须在每个服务的每个具体方法里开启一个子线程,去处理一些耗时的操作。一个标准的服务的代码如下:
public class MyService extends Service {
...
/**
* 每次启动的服务的时候调用
*
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
//处理具体的逻辑
stopSelf();
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
...
}
为了解决我们忘记开启子线程,或忘记调用stopSelf()方法。Android提供了一个异步的,会自动停止服务的IntentService类。
新建MyIntentService类继承自IntentService,代码如下:
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService"); //调用父类的有参构造函数
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
//打印当前线程的id
Log.d("MyIntentService", "Thread id is "+ Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestroy: executed ");
}
}
首先我们提供一个无参的构造函数,并且必须调用内部父类的有参构造函数。然后在子类中去实现onHandleIntent()抽象方法,在这个方法中处理具体的逻辑,这个方法在子线程中运行(解决了ARN问题)。
接下来修改activity_main.xml中的代码,加入一个启动MyiIntentStart按钮,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<Button
android:id="@+id/btn_start_intent_service"
android:textAllCaps="false"
android:text="Start IntentService"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service,btn_start_intent_service;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
btn_start_intent_service= (Button) findViewById(R.id.btn_start_intent_service);
...
btn_start_intent_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.btn_start_intent_service:
Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());//打印主线程的id
Intent intentService=new Intent(this,MyIntentService.class);
startService(intentService);
break;
default:
break;
}
}
}
在AndroidManifest.xml中注册服务,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service android:name=".MyIntentService"/>
</application>
</manifest>
运行程序,点击Start IntentService按钮,观察logcat日志,如下:
我们可以看到MyIntentService和MainActivity中的所在线程的id不一样,而且onDestory()也得到了执行。集开启线程和自动停止与一身,IntentService是我们的最爱。
10.6 服务的最佳实践 — 完整版的下载
实现服务中经常使用到的功能个——下载功能。
创建一个ServiceBestPractice项目。
添加OkHttp依赖库,如下:
compile 'com.squareup.okhttp3:okhttp:3.8.0'
接下来定义一个DownloadListener接口回调,用于对下载过程中的各种状态进行监听。如下:
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
}
编写下载功能,新建DownloadTask继承自AysncTask,代码如下:
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
private static final int TYPE_SUCCESS = 0;
private static final int TYPE_FAILED = 1;
private static final int TYPE_PAUSED = 2;
private static final int TYPE_CANCELED = 3;
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 downloadLength = 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()) {
downloadLength = file.length();
}
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0) {
return TYPE_FAILED;
} else if (contentLength == downloadLength) {
//已下载字节和文件总字节相等,就等于已经下载完了
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//断点下载,指定从哪个子节点开始下载
.addHeader("RANGE", "bytes=" + downloadLength + "-")
.url(downloadUrl).build();
Response response = client.newCall(request).execute();
if (request != null) {
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadLength); //跳过已下载的字节
byte[] bytes = new byte[1024];
int total = 0;
int len;
while ((len = is.read(bytes)) != -1) {
if (isCanceled) {
return TYPE_CANCELED;
} else if (isPaused) {
return TYPE_PAUSED;
} else {
total += len;
savedFile.write(bytes, 0, len);
//计算已下载的百分比
int progress = (int) ((total + downloadLength) * 100 / contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCanceled && file != null) {
file.delete();
}
} catch (IOException 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;
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.close();
return contentLength;
}
return 0;
}
}
分析以上代码,首先AsyncTask中的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask时需要传入字符串参数给后台;第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位;第三个泛型参数指定为Integer,表示使用整型数据来反馈执行结果。
接下类我们定义了4个整型常量表示下载的状态。然后在DownloadTask的构造函数中要求传入刚刚定义的DownloadListener参数,通过这个参数将下载的状态进行回调。
接下来我们重写doInBackground(),onProgressUpdate()和onPostExecute()这3个方法。
doInBackground()方法用于在后台执行具体下载的逻辑,首先我们从参数中获取了下载路径的URL地址,并根据URL地址解析出来的下载名,然后将文件下载到SD卡的Download路径下(Environment.DIRECTORY_DOWNLOADS)。判断目录下是否存在要下载的文件,存在则读取下载的字节数,这样就可以在后面启动断电下载传的功能。接下来先是调用了getContentLength()方法来获取待下载文件的长度,如果长度等于0,则说明文件有问题,返回TPE_FAILD,如果文件长度等于已经下载文件的长度,则说明下载完成,返回TYPE_SUCCESS。紧接着我们使用OKHttp发送网络请求,在请求中添加了header,指用于告诉服务器我们想从哪个节点开始下载,因为下载过的就不需要下载了(断点下载)。接下来读取服务器响应的数据,并使用Java的文件流的方式,不断从网络上读取数据,不断写入本地,直到文件全部下载完。如果用户在下载过程中没有执行暂停或取消的操作,则实时计算当前下载的进度,然后调用publishProgress()方法进行通知。暂停或取消都是用布尔值进行控制的,调用pauseDownload()或cancelDownload()方法即可更改变量的值。
onProgressUpdate()用于在界面中更新当前的下载进度,首先获取当前的下载进度,与上次下载的进行对比,如果有变化则调用DownloadListener的onProgress()方法来通知下载进度更新。
最后onPostExecute()用于通知最终下载的结果,根据参数中传入的下载状态的参数进行回调。
这样我们就实现了下载的功能,接下来我们为了保证DownloadTask一直运行在后台,则需要创建DownloadService(服务),修改代码如下:
public class DownloadService extends Service {
public DownloadService() {
}
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, "Pause", Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
};
private DownloadBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
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, "Downloading...", Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload() {
if (downloadTask != null) {
downloadTask.pauseDownload();
}
}
public void cancelDownload() {
if (downloadTask != null) {
downloadTask.cancelDownload();
} else {
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();
}
}
}
}
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();
}
}
首先我们创建了DownloadListener的匿名实例,实现下载状态的5个方法。在onProgress方法在,调用getNotification()构建了一个用于显示下载进度的通知,然后调用NotificationManager的notify()方法出发通知,就可在下载状态中查看下载进度。再其他几个方法中将正在下载的前台服务通知关闭,创建一个新的通知告诉用于返回的结果。
为了让DownloadService和活动进行通信,我们创建了一个DownloadBinder。它提供了startDownload(),pauseDownload()和cancelDownload()方法分别是开始,暂停和取消下载的。
DownloadService类中的所有使用的通知都调用getNotification。
实现了下载的服务。编写activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Download"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_pause_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause Download"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_cancel_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cancel Download"
android:textAllCaps="false" />
</LinearLayout>
接下来修改MainActivity中的代码,如下:
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) {
}
};
private Button btn_start_download, btn_pause_download, btn_cancel_download;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_start_download = (Button) findViewById(R.id.btn_start_download);
btn_pause_download = (Button) findViewById(R.id.btn_pause_download);
btn_cancel_download = (Button) findViewById(R.id.btn_cancel_download);
btn_start_download.setOnClickListener(this);
btn_pause_download.setOnClickListener(this);
btn_cancel_download.setOnClickListener(this);
Intent intent = new Intent(this, DownloadService.class);
startService(intent); //启动服务
bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(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.btn_start_download:
String downloadUrl = "http://eclipse.stu.edu.tw/oomph/epp/neon/R3/eclipse-inst-win64.exe";
downloadBinder.startDownload(downloadUrl);
break;
case R.id.btn_pause_download:
downloadBinder.pauseDownload();
break;
case R.id.btn_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:
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}
首先创建了一个ServiceConnection的匿名类,然后在onServiceConnected()获取DownloadBinder的实例。然后在活动中调用服务提供过的各种方法。
在onCreate()方法中进行初始化操作,调用了startService()和bindService()来启动和绑定服务。这点相当重要,因为启动服务就可以保证DownloadService运行在后台,绑定服务可以让与MainActivity进行通信(两个方法必不可少)。我们还进行了申请权限的操作。
在onClick()方法中对点击事件进行判断,点击开始就调用DownloadBinder中的startDowmload()方法;暂停调用pauseDownload()。取消调用cancelDownload()。
销毁活动,一定要对服务进行解绑,否则造成内存泄露。我们在onDestroy()完成解绑。
在AndroidManifest.xml中声明权限,以及DownloadService,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.servicebestpractice">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DownloadService"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>
运行程序,会弹出授权对话框,如下:
点击允许,然后点击Start Download按钮就开始下载了,下载过程下拉系统状态栏进行查看,如下:
我们还可以点击Pause Download或Cancel Download,甚至断网操作来测试这个程序的健壮性。下载完会弹出Download Success,提示用于下载完成。我们打开SD卡的Download查看,如下:
我们可以看到文件已经下载完成了。你可以任意的测试,这个下载实例的健壮性,综合性都是很强的。
10.7 小结与点评
学习了Android多线程,服务的基本用法,服务的生命周期,前台服务和IntentServiced等。