第一行代码学习笔记第十章——探究服务
知识点目录
知识点回顾
10.1 服务是什么
服务(Service)是Android中实现程序后台运行的解决方案。它非常适合去执行一些不需要和用户交互而且还要求长期运行的任务。
-
服务并不是运行在独立的进程中,而是依赖创建服务时所在的应用程序进程
-
服务并不会自动开启线程,所有的代码都是默认运行在主线程中。
正常情况下,我我们都需要手动创建子线程,并在子线程中执行具体的任务,否则就有可能出现主线程被阻塞住的情况。
10.2 Android多线程编程
当我们需要执行一些耗时操作(例如:网络请求)时,都会将这些操作放在子线程中去运行,否则容易被阻塞住,从而影响用户对软件的正常使用。
10.2.1 线程的基本用法
创建线程的方式一般有如下两种:
1. 继承Thread
新建一个类继承Thread,然后重写父类的run()方法,在run()方法里面写耗时逻辑。
public class MyThread extends Thread {
@Override
public void run() {
// 处理具体的逻辑
}
}
然后new出一个MyThread实例,调用它的start()方法,这样run()方法中的代码就会在子线程中运行。
new MyThread().start();
2. 实现Runnable接口
继承的耦合性太高,我们可以通过实现Runnable接口的方式来定义一个线程。
public class MyThread implements Runnable {
@Override
public void run() {
// 处理具体的逻辑
}
}
启动线程的方式如下:
MyThread myThread = new MyThread();
new Thread(myThread).start();
3. 匿名类
如果不想去专门再定义一个类去实现Runnable接口,那么也可以使用匿名类的形式,这种方式更加常见。
new Thread(new Runnable() {
@Override
public void run() {
// 处理具体的逻辑
}
}).start();
10.2.2 在子线程中更新UI
下面我们写一个Demo在子线程中更新UI看看效果。
1.布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/change_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="Change text"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textAllCaps="false"
android:text="Hello world"
android:textSize="20sp"/>
</RelativeLayout>
2.在子线程中更新UI
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.text);
Button changeText = (Button) findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
//开一个子线程更新UI
new Thread(new Runnable() {
@Override
public void run() {
mText.setText("Nice to meet you");
}
}).start();
break;
}
}
}
运行程序,并点击"Change text"按钮。程序会直接崩溃:
观察logcat中的错误日志如下:
为了解决这种情况,Android提供了一套异步消息处理机制,很好地解决了子线程中进行UI操作的问题。
异步消息机制的基本用法:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final int UPDATE_TEXT = 1; // 定义个整型常量,用于表示Handler中某个动作
private TextView mText;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
//在这里进行UI操作
mText.setText("Nice to meet you");
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.text);
Button changeText = (Button) findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;
mHandler.sendMessage(message); //将Message对象发出去
}
}).start();
break;
default:
break;
}
}
}
10.2.3 解析异步消息处理机制
在了解了Android异步消息处理的基本用法后,我们来深入学习下异步消息机制的原理。
Android异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。
Message
在线程之间传递消息,可以在内部携带少量的信息,主要用于在不同线程之间交换数据。
Handler
主要用于发送和处理消息。sendMessage()方法用于发送消息,handleMessage()方法用于处理消息。
MessageQueue
MessageQueue是消息队列,主要用于存放所有通过Handler发送的消息,这部分消息会一直存在于消息队列中,等待被处理。每一个线程中只会有一个MessageQueue对象。
Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环中,每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每一个线程中只会有一个Looper对象。
综上所述:异步消息处理的整个流程如下:
-
主线程中创建一个Handler对象,并重写handleMessage()方法
-
子线程中需要进行UI操作时,创建一个Message对象,并通过Handler的sendMessage()方法将这条消息发送出去
-
发送出去的消息被添加到MessageQueue中等待被处理
-
Looper会一直尝试从MessageQueue中取出待处理的消息
-
分发到Handler的handleMessage()方法中进行处理
整个异步消息处理机制的流程示意图如下所示:
前面使用的runOnUiThread()方法就是一个异步消息处理机制的接口封装。
10.2.4 使用AsyncTask
AsyncTask是Android帮我们封装好的对异步消息处理的工具,背后实现的原理也是异步消息处理机制。
AsyncTask是一个抽象类,需要创建一个类去继承它。在继承时可以为AsyncTask类指定3个泛型参数:
-
第一个参数Params。可用于在后台任务中使用
-
第二个参数Progress。后台任务执行时,如果需要在界面上显示当前进度,则使用这里指定的泛型作为进度单位
-
第三个参数Result。当任务执行完毕,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型
基本使用方法:
class DownloadTask extends AsyncTask <Void,Integer,Boolean>{
/**
* 在后台任务开始之前调用
* 用于一些界面的初始化操作,例如显示一个进度条对话框
* 主线程中运行
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
//显示进度对话框
progressDialog.show();
}
/**
*在这里处理所有的耗时操作
* 如果需要更新UI元素,例如反馈当前任务的执行进度,可以调用publishProgress(Progress...)
* 如果任务完成可以通过return语句来将任务执行的结果返回,返回类型是AsyncTask中的第三参数
* 如果AsyncTask的第三个参数是Void,那么就可以不返回任务执行的结果
* 子线程中运行
* @param voids
* @return
*/
@Override
protected Boolean doInBackground(Void... voids) {
try {
while (true) {
int downloadPercent = doDownload(); //这是一个虚构的方法
publishProgress(downloadPercent);
if (downloadPercent >= 100) {
break;
}
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 当doInBackground()中调用了publishProgress(Progress...),则此方法会很快被调用
* 该方法中携带的参数就是在后台任务中传递多来的
* 可以对UI进行操作,利用参数中的值可以对界面上的元素进行更新
* 主线程中运行
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
//在这里更新下载进度
progressDialog.setMessage();
}
/**
* 当doInBackground()中调用了return语句时,这个方法会很快被调用
* 返回的数据作为参数传递到此方法中
* 可以利用返回的数据来进行一些UI操作,比如提醒任务执行的结果或关闭掉进度条对话框等
* 主线程中运行
* @param aBoolean
*/
@Override
protected void onPostExecute(Boolean aBoolean) {
//关闭对话框
progressDialog.dismiss();
if (aBoolean) {
Toast.makeText(MainActivity.this, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "Download failed", Toast.LENGTH_SHORT).show();
}
}
}
AsyncTask的用法就是:
-
在onPreExecute()中做一些初始化工作
-
在doInBackground()中执行具体的耗时操作
-
在onProgressUpdate()中进行UI操作
-
在onPostExecute()中执行一些任务的收尾工作
10.3 服务的基本用法
10.3.1 定义一个服务
可以通过Android Studio的快捷键来创建:
和
通过这样创建的Service,会自动在AndroidManifest.xml中注册。
public class MyService extends Service {
public MyService() {
}
/**
* 在服务创建的时候调用
*/
@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 IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* 在服务销毁的时候调用,一般用于回收那些不再使用的资源
*/
@Override
public void onDestroy() {
super.onDestroy();
}
}
10.3.2 启动和停止服务
启动和停止Service的方法都是借助Intent来实现的。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 启动服务
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服务
break;
}
}
服务启动后,可以在Settings—>Developer options—>Running servcies中找到它,如下图所示:
onCreate()与onStartCommand()的区别:
-
onCreate()只在服务第一次创建的时候使用
-
onStartCommand()是在服务每次启动的时候都会调用
10.3.3 活动和服务进行通信
如果在活动中想决定何时开始下载和随时查看下载进度,我们可以创建一个专门的Binder对象来对下载功能进行管理。同时借助onBind()方法。
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
private DownloadBinder mDownloadBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload() {
Log.d(TAG, "startDownload executed");
}
public int getProgress() {
Log.d(TAG, "getProgress executed");
return 0;
}
}
/**
* 在服务创建的时候调用
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate executed ");
}
@Override
public IBinder onBind(Intent intent) {
return mDownloadBinder;
}
/**
* 在服务销毁的时候调用,一般用于回收那些不再使用的资源
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy executed");
}
}
在活动中绑定和解绑服务:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MyService.DownloadBinder mDownloadBinder;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mDownloadBinder = (MyService.DownloadBinder) service;
mDownloadBinder.startDownload();
mDownloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button bindService = (Button) findViewById(R.id.bind_service);
Button unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
//BIND_AUTO_CREATE 表示在活动和服务进行绑定后自动创建服务
bindService(bindIntent, mConnection, BIND_AUTO_CREATE); // 绑定服务
break;
case R.id.unbind_service:
Intent unbindIntent = new Intent(this, MyService.class);
unbindService(mConnection); // 解绑服务
break;
default:
break;
}
}
}
备注:服务在整个应用程序范围内都是通用的,即可以和不同的活动进行绑定,而且在绑定完成后它们都可以获取到相同的DownloadBinder实例。
10.4 服务的生命周期
服务的生命周期主要有以下两种路径:
-
启动服务
该服务在其他组件调用startService()时创建,并回调onStartCommand()方法。如果这个服务之前没有创建过,onCreate()方法会先于onStartCommand()方法执行。直到调用stopService()或stopSelf()方法服务才会停止。注意:虽然每调用一次startService()方法onStartCommand()都会执行一次,但实际上每个服务都只会存在一个实例。
-
绑定服务
该服务在其他组件调用bindService()时创建,并回调服务中的onBind()方法。如果服务之前没有创建过,onCreate()方法会优于onBind()方法执行。客户端获取到onBind()方法里面返回的IBinder对象的实例,就能与服务端进行通信了。
当我们对服务同时进行了startService()和bindService()时,这时候我们需要同时调用stopService()和unbindService()方法,服务才会被销毁,onDestory()方法才会执行。
Service两种情况下生命周期图如下:
10.5 服务的更多技巧
10.5.1 使用前台服务
服务一直都在后台运行,优先级比较低,当系统出现内存不足的情况时,有可能被系统回收掉。如果想服务一直保持运行,可以考虑使用前台服务。
前台服务与后台服务的区别:
-
不会被系统回收掉
-
状态栏会有一个正在运行的图标,下拉状态栏后可以看到更加详细的信息。
让服务变成前台服务的方法如下:
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);
}
......
}
具体做法就是创建一个Notification,然后调用startForeground()方法即可。
startForeground()有两个参数:
参数一:通知的id
参数二:构建的Notification对象。
点击StartService或BindService按钮后,MyService就会以前台服务的模式启动,并且在系统状态栏会显示一个通知图标,下拉状态栏后可以看到该通知的详细内容,如下图所示:
10.5.2 使用IntentService
IntentService是一种异步的,会自动停止的服务。
基本用法如下:
public class MyIntentService extends IntentService {
private static final String TAG = "MyIntentService";
public MyIntentService() {
super("MyIntentService"); //调用父类的有参构造函数
}
/**
* 处理一些耗时操作
* 运行在子线程中,不会出现ANR问题
* @param intent
*/
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "Thread id is " + Thread.currentThread().getId());
}
/**
* 服务运行结束后,会自动停止
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy executed");
}
}
点击Start IntentServcie按钮后,打印log如下:
可以看出IntentServcie集开启线程和自动停止于一身。
10.6 服务的最佳实践——完整版的下载示例
完整版的功能需求:结合AsyncTask和Service实现开始下载、暂停下载和取消下载的功能。
代码已上传到我的github上,需要的朋友可以点击如下链接查看:
10.7 小结与点评
本章主要学习了Android多线程编程、服务的基本用法、服务的生命周期、前台服务和IntentService等。最后通过完整版下载示例对前面学习的知识进行了综合运用。