提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。
一、Android多线程编程
主线程也叫UI线程。Android系统规定,所有的UI操作都必须在主线程(UI线程)中执行;所有的网络请求操作都必须在子线程中执行。
1.1 什么是Service服务
Service是Android中实现程序后台运行的解决方案,它适合执行那些不需要和用户进行交互而且还要求长期运行的任务。不过,Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当创建Service的应用程序进程被Kill时,所有依赖于该进程的Service也会停止运行。
另外,Service并不会自动开启一个线程,所有的代码都是默认运行在主线程中的。所以,如果需要在Service中执行一些耗时任务,最好在Service内部手动创建子线程并在子线程中执行耗时任务,否则可能会阻塞主线程。
1.2 定义并启动一个线程
多线程可以用来执行耗时任务、提高程序的执行效率。例如,在网络请求时,如果网速慢,服务器可能无法立即响应我们的请求。如果将这些请求放在主线程中执行,会导致主线程阻塞,进而产生ANR(应用程序无响应)的风险。下面我们就来学习一下Android多线程的基本用法。
1.2.1 以继承Thread的方式定义一个线程
在Android中定义一个线程只需要新建一个类并让其继承自Thread,然后重写父类的run()方法,并在run()方法中编写耗时逻辑即可。下面是通过继承Thread方式来定义一个线程的示例代码:
Kotlin Code:
//以继承Thread的方式定义一个线程
class MyThread : Thread() {
override fun run() {
//编写耗时逻辑
}
}
Java Code:
public class MyThread extends Thread {
@Override
public void run() {
//编写耗时逻辑
}
}
以继承Thread的方式定义一个线程,若想启动这个线程,只需要创建一个MyThread的对象,然后调用它的start()方法即可。这样,run()方法中的代码就会在子线程中执行了。如下所示:
Kotlin Code:
//创建MyThread对象
val myThread = MyThread()
//启用MyThread线程
myThread.start()
Java Code:
MyThread myThread = new MyThread();
myThread.start();
需要注意的是,线程一旦启动就不能停止。如果需要控制线程的执行,可以在run()方法中加入相应的控制逻辑。
1.2.2 通过实现Runnable接口的方式定义一个线程
通过继承Thread类的方式定义一个线程会导致耦合性较高,我们可以选择通过实现Runnable接口的方式来定义一个线程。下面是一个通过实现Runnable接口的方式定义一个线程的示例代码:
Kotlin Code:
//通过实现Runnable接口的方式定义一个线程
class MyThread : Runnable {
override fun run() {
//编写耗时逻辑
}
}
Java Code:
public class MyThread implements Runnable {
@Override
public void run() {
//编写耗时逻辑
}
}
通过实现Runnable接口定义线程这种方式,启动线程的方法也发生了变化:
Kotlin Code:
//创建MyThread对象
val myThread = MyThread()
//创建一个新的Thread对象(Thread的构造函数需要传入一个实现了Runnable接口的对象作为参数)
//将myThread对象作为参数传递给Thread的构造函数
Thread(myThread).start()
Java Code:
MyThread myRunnable = new MyThread();
Thread thread = new Thread(myRunnable);
thread.start();// 启动线程
我们先创建了一个MyThread对象,然后将其作为参数传入一个新的Thread对象的构造函数中。我们通过调用这个新创建的Thread对象的start()方法来启动线程,这样,run()方法中的代码就会在子线程中执行了。
Thread的构造函数需要传入一个实现了Runnable接口的对象作为参数,而我们的MyThread类刚好实现了Runnable接口,所以将MyThread对象作为参数传给Thread构造函数是完全可以的。
1.2.3 通过Lambda定义一个线程
如果你也不想专门再定义一个类去实现Runnable接口,你也可以使用Lambda定义一个线程,这种写法更为常见也更简单。下面是一个通过Lambda的方式定义一个线程的示例代码:
Kotlin Code:
//通过Lambda定义一个线程
Thread{
//编写耗时逻辑
}.start()
Java Code:
//通过Lambda定义一个线程
new Thread(() -> {
//编写耗时逻辑
}).start();
1.2.4 通过Kotlin内置函数定义一个线程
当然,Kotlin还提供给我们一个更简单的写法用来开启线程:
//Kotlin特有的方法
thread{
//编写耗时逻辑
}
这里的thread{ }结构是一个Kotlin内置的顶层函数,我们只需要在Lambda表达式中编写具体的逻辑就可以了,甚至都不需要调用start()方法。
1.3 在子线程中更新UI
Android的UI操作不是线程安全的,当多个线程同时操作按钮、文本框等UI组件时可能会导致数据不一致。因此,为了确保UI操作的正确性和流畅性,Android规定所有的UI操作都必须在主线程中进行,否则会出现异常。下面我们将演示在子线程中更新UI导致程序异常的简单案例:
新建一个AndroidThreadTest项目,下面是它的主界面布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/changeTextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="改变文本" />
<TextView
android:id="@+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello World!"
android:textSize="20sp" />
</RelativeLayout>
在主界面中有一个Button和一个TextView,我们希望在点击按钮后在子线程中更改TextView显示的文本内容。下面是MainActivity.kt中的代码:
Kotlin Code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//绑定View控件
val myTextView: TextView = findViewById(R.id.myTextView)
val changeTextButton:Button = findViewById(R.id.changeTextButton)
//点击按钮并在子线程中改变文本内容
changeTextButton.setOnClickListener {
thread {
myTextView.setText("文本已经被改变!")
}
}
}
}
Java Code:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
mainBinding.changeTextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(() -> {
mainBinding.myTextView.setText("文本已经被改变!");
}).start();
}
});
}
}
可以看到我们的程序出现异常了:
报错日志也提示我们不允许在子线程中进行UI操作。有时候,我们必须在子线程中执行一些耗时任务,然后根据任务的执行结果来更新相应的UI控件。针对这种情况,Android提供了异步消息处理机制来解决在子线程中进行UI操作的问题。
修改MainActivity.kt中的代码如下:
Kotlin Code:
class MainActivity : AppCompatActivity() {
//常量
val updateText = 1
lateinit var myTextView: TextView
private lateinit var changeTextButton: Button
//通过object关键字创建一个单例对象
//Looper.getMainLooper()会获取主线程的Looper(用于处理消息队列中的消息)
//通过Handler(Looper.getMainLooper())会创建一个在主线程Looper上运行的Handler
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
updateText -> myTextView.setText("文本内容已改变!")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//绑定View控件
changeTextButton = findViewById(R.id.changeTextButton)
myTextView = findViewById(R.id.myTextView)
changeTextButton.setOnClickListener {
//点击按钮启动新线程
thread {
//创建一个Message对象 将其what属性设为updateText
val msg = Message()
msg.what = updateText
//通过Handler的sendMessage()方法发送消息
handler.sendMessage(msg)
}
}
}
}
首先,我们通过以下代码创建了一个Handler对象:
private val handler = object : Handler(Looper.getMainLooper()) {
when (msg.what) {
updateText -> myTextView.setText("文本内容已改变!")
}
}
Java Code:
public class MainActivity extends AppCompatActivity {
// 定义消息类型常量
private static final int UPDATE_TEXT = 1;
private ActivityMainBinding mainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
mainBinding.changeTextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(() -> {
Message msg = new Message();
msg.what = UPDATE_TEXT;
handler.sendMessage(msg);
}).start();
}
});
}
/*
在主线程上创建一个Handler 并重写了handleMessage方法
当有消息发送到这个Handler时 它会检查消息的类型(通过msg.what)
如果消息类型是UPDATE_TEXT 则更新TextView的文本内容 */
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == UPDATE_TEXT) {
mainBinding.myTextView.setText("文本内容已改变!");
}
}
};
}
object关键字表明我们创建的是一个单例对象,Looper.getMainLooper()方法会获取主线程的Looper(用于处理消息队列中的消息),以这种方式创建的Handler对象就会在主线程的Looper上运行。之后我们重写了handleMessage()方法用来处理Message消息,通过判断Message消息的what属性来决定要执行什么逻辑,这里我们让其修改TextView的内容。
然后我们实现按钮的点击逻辑:
Kotlin Code:
changeTextButton.setOnClickListener {
thread {
val msg = Message()
msg.what = updateText
handler.sendMessage(msg)
}
}
Java Code:
mainBinding.changeTextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(() -> {
Message msg = new Message();
msg.what = UPDATE_TEXT;
handler.sendMessage(msg);
}).start();
}
});
当我们点击按钮时,会启动一个新线程。我们在新线程内部创建了一个Message消息对象,然后将它的what属性设置为updateText。之后,我们通过调用Handler.sendMessage()方法,将刚才创建的Message消息发送出去。
到这里其实你也应该明白了,我们在使用Handler和Message机制进行异步消息处理时,实际上是还是在主线程中运行的。Handler和Message机制只是提供了一种异步处理的方式,使得我们可以在主线程中更高效地处理任务,而不会阻塞主线程的执行。下面是程序的运行效果:
总而言之:
- Thread:用于执行耗时操作,避免阻塞主线程。
- Handler:用于线程间通信,特别是将结果传递回主线程以更新UI。
这两者经常结合使用:Thread 执行耗时操作,Handler 将结果传递回主线程。
1.4 异步消息处理机制的原理
Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。
-
Message
Message是在线程之间进行传递的消息,它可以携带少量信息在不同的线程之间进行数据传递。Message中有几个常用字段:①what是一个int类型的字段,可以标识消息的种类。②arg1和arg2是两个int类型的字段,用于传递简单的参数值。③obj是一个Object类型的字段,用于存储任意类型的对象。 -
Handler
Handler用于发送和处理消息。发送消息有两种方式:Handler.sendMessage()方法和post()方法。通过sendMessage()方法发送的消息会被添加到消息队列中等待线程处理。这意味着消息何时能被执行,取决于线程的调度和消息队列中的其他消息。而post()方法则会立即执行消息,不受其他消息的影响。发出的消息最终会被传递到Handler的handleMessage()方法中。 -
MessageQueue(消息队列)
MessageQueue用于存放所有通过Handler发送的消息。当Handler将一个Message对象发送到消息队列中时,消息会被添加到队列的末尾,等待线程处理。每个线程只会有一个消息队列,线程会按照队列中的顺序处理其中的消息。 -
Looper
Looper是每个线程中的MessageQueue管家,每个线程只会有一个Looper对象。Looper会不断地从MessageQueue中取出Message对象,然后传递到Handler.handleMessage()中去处理,直到线程被终止或者调用Looper.quit()。 通过Looper.loop()方法可以进入消息循环,进而处理队列中的消息。
异步消息处理流程:在主线程中创建Handler对象 —> 重写handlerMessage()方法 —> (子线程)创建一个Message对象 —> (子线程)通过Handler.sendMessage()方法发送消息 —> 消息被添加到MessageQueue消息队列中等待处理 —> Looper一直尝试从MessageQueue中取出待处理的消息 —> 传递到Handler.handlerMessage()方法中进行处理。
由于我们给Handler的构造函数传入了Looper.getMainLooper()参数,所以handleMessage()方法中的代码也会在主线程中执行。
1.5 AsyncTask(异步任务)
AsyncTask是Android中的一种异步处理类,借助AsyncTask可以从子线程切换到主线程,方便进行UI相关操作。它允许你在后台线程上执行耗时操作,同时保持UI线程的响应和流畅度。
AsyncTask是一个抽象类(抽象类可以提供通用的模板、减少代码量、提高扩展性和复用性)。如果我们想要使用AsyncTask,就必须创建一个类并让其继承自AsyncTask类。AsyncTask类的三个泛型参数:Param表示参数、Progress表示任务进度、Result表示任务执行结果。因此一个简单的AsyncTask示例如下:
无需传参 Int进度条 Boolean执行结果
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
· · ·
}
这里我们的DownloadTask还是一个空任务,没有任何实际操作。我们还需要重写AsyncTask中的几个方法才能完成任务的定制:
- onPreExecute( ) :这个方法会在后台任务开始执行之前调用,用于界面的初始化,例如显示进度条等。
- doInBackground( ) :这个方法中的所有代码都会在子线程中运行(不可更新UI),我们应该在这里处理所有的耗时任务。任务结束后若AsyncTask需要返回结果,可通过return语句返回结果。如果要更新UI(反馈任务进度),可以调用publishProgress()通知UI进行更新。
- onProgressUpdate( ) :在doInBackground()中调用publishProgress()方法后会被立即调用,可以在此方法内更新UI。
- onPostExecute( ) : 当doInBackground()执行完毕后并通过return返回结果时会被立即调用,可以在此方法内更新UI(关闭进度条等)。
下面是一个完整的AsyncTask伪代码:
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
/**
* 在后台任务开始执行之前调用(界面初始化:显示进度条等)
*/
override fun onPreExecute() {
TODO("显示进度条")
}
/**
* 在子线程中运行(不可以更新UI) 用于处理所有耗时任务
* 任务结束可通过return语句返回结果(若AsyncTask需要返回结果)
* 可以调用publishProgress()通知UI进行更新
*/
override fun doInBackground(vararg params: Unit?): Boolean {
try {
while (true) {
val downloadPercent = doDownload() //虚构的方法
publishProgress(downloadPercent)
if (downloadPercent >= 100) {
break
}
}
return true
} catch (e: Exception) {
return false
}
}
/**
* 在doInBackground()中调用publishProgress()方法后会被立即调用
* 可以在此方法内更新UI
*/
override fun onProgressUpdate(vararg values: Int?) {
progressDialog.setMessage("下载进度:${values[0]}")
}
/**
* 当doInBackground()执行完毕后并通过return返回结果时会被立即调用
* 可以在此方法内更新UI(关闭进度条等)
*/
override fun onPostExecute(result: Boolean?) {
progressDialog.dismiss() //关闭进度条对话框
//在这里显示下载结果
if (result) {
Toast.makeText(this, "下载成功!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "下载失败!", Toast.LENGTH_SHORT).show()
}
}
}
如果想启动这个异步任务:
DownloadTask.execute()
其实AsyncTask内部的原理也是通过异步消息机制来实现的,只不过是对其进行了封装。通过AsyncTask,我们并不需要考虑异步消息机制,也不需要使用Handler发送和接受消息。只需要调用publishProgress()方法,就可以从子线程切换会主线程了。
二、Service的基本用法
2.1 定义一个Service
在Android Studio中右键包名—>New—>Service—>创建一个Service。
创建好后MyService.kt中的代码如下所示:
class MyService : Service() {
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
}
可以看到MyService是继承自系统的Service类。在MyService类中有一个onBind()方法,它是Service中唯一的抽象方法,所以必须在子类中实现。Service中还有一些常用的方法,如下所示:
class MyService : Service() {
/**
* 在Service创建时调用
*/
override fun onCreate() {
super.onCreate()
}
/**
* 在Service每次启动后立即调用 用于执行Service的初始化操作
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
/**
* 在Service被销毁时被调用 用于释放资源
*/
override fun onDestroy() {
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
}
需要注意的是:Android中四大组件都需要在AndroidManifest.xml清单文件中进行注册才能生效,Service也不例外。
下面是清单文件中关于MyService的内容,可以看到相关标签已经由Android Studio自动帮我忙生成了:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
· · ·>
//Service标签
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
· · ·
</application>
</manifest>
到这里,一个Service就已经被定义好了。
2.2 启动和停止Service
Service的启动和停止也是通过Intent来实现的。我们可以调用startService( )方法开启一个Service或调用stopService( )方法停止一个Service。首先,在主页中增加两个按钮分别用来开启、关闭一个Service:
<?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:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/startServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="启动Service" />
<Button
android:id="@+id/stopServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="停止Service" />
</LinearLayout>
然后我们在MainActivity.kt文件中为这两个按钮实现点击逻辑。需要注意,这里我们使用了ViewBinding插件来代替kotlin-android-extensions插件和findViewById()这种绑定View控件的方式,如果你是第一次使用ViewBinding插件请查看此篇文章。
class MainActivity : AppCompatActivity() {
//全局变量
private lateinit var myBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//初始化ViewBinding控件
var myBinding = ActivityMainBinding.inflate(layoutInflater)
//把根元素的对象作为参数传入到setContentView()方法中
setContentView(myBinding.root)
//startServiceBtn点击监听
myBinding.startServiceBtn.setOnClickListener {
val startIntent = Intent(this, MyService::class.java)
//启动Service
startService(startIntent)
}
//stopServiceBtn点击监听
myBinding.stopServiceBtn.setOnClickListener {
val stopIntent = Intent(this, MyService::class.java)
//停止Service
stopService(stopIntent)
}
}
}
可以看到,启动或是停止一个Service十分的简单。首先,创建一个Intent并指明要启动的Service,然后分别调用startService()方法和stopService()方法就可以了。为了观察MyService是否已经启动或者关闭,我们还需要在MyService中添加一些打印信息:
class MyService : Service() {
/**
* 在Service创建时调用
*/
override fun onCreate() {
super.onCreate()
Log.d("MyService", "Service已创建,onCreate()执行完毕!")
}
/**
* 在Service启动后立即调用 用于执行Service的初始化操作
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
//注意:需要在调用父类方法前执行,否则打印语句不会被执行
Log.d("MyService", "Service已启动,onStartCommand()执行完毕!")
return super.onStartCommand(intent, flags, startId)
}
/**
* 在Service被销毁时被调用 用于释放资源
*/
override fun onDestroy() {
super.onDestroy()
Log.d("MyService", "Service被销毁,onDestroy()执行完毕!")
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
}
接下来就可以运行程序了,点击“启动Service”按钮,查看Logcat输出:
可以看到我们的MyService服务已经成功被创建并启动了。需要注意的是,onCreate()方法只会在Service第一次创建时次才会被调用,而onStartCommand()方法则会在Service每次启动时都被调用。当我们多次点击“启动Service”按钮,查看Logcat输出:
现在,点击“停止Service”按钮,查看Logcat输出:
可以看到MyService已经停止并销毁。
2.3 Activity和Service进行通信
上一节,我们在Activity里启动了MyService,但是在启动Service之后,Activity基本就与Service没什么关系了。我们在Activity中通过startService()启动服务以后,MyService执行的具体逻辑是什么Activity都无从知晓。下面我们借助Service中的onBind()方法,将Activity和Service联系起来。
例如,我们希望在MyService中实现一个下载功能,然后在Activity中决定何时开始下载,以及随时查看下载进度。实现这个功能的思路就是创建一个专门的Binder对象,对下载功能进行管理。修改MyService中的代码:
class MyService : Service() {
//日志标签
private val tag = "MyService"
//DownloadBinder对象
private val myBinder = DownloadBinder()
//DownloadBinder类继承自Binder类
inner class DownloadBinder : Binder() {
//开始下载
fun startDownload() {
Log.d(tag, "开始下载,正在执行startDownload()···")
}
//查看下载进度
fun getProgress(): Int {
Log.d(tag, "查看下载进度,正在执行getProgress()···")
return 0
}
}
override fun onBind(intent: Intent): IBinder {
return myBinder
}
//在Service创建时调用
override fun onCreate() {
super.onCreate()
Log.d(tag, "Service已创建,onCreate()执行完毕!")
}
//在Service启动后立即调用 用于执行Service的初始化操作
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(tag, "Service已启动,onStartCommand()执行完毕!")
return super.onStartCommand(intent, flags, startId)
}
//在Service被销毁时被调用 用于释放资源
override fun onDestroy() {
super.onDestroy()
Log.d(tag, "Service被销毁,onDestroy()执行完毕!")
}
}
我们给主界面添加两个按钮,分别用于绑定Service和解绑Service:
当一个Activity和Service绑定了以后,就可以调用该Service里Binder提供的方法了。修改MainActivity.kt中的代码:
class MainActivity : AppCompatActivity() {
· · ·
//DownloadBinder对象
lateinit var downloadBinder: MyService.DownloadBinder
//单例ServiceConnection匿名类 用于建立Activity和Service之间的连接
private val connection = object : ServiceConnection {
//在Activity与Service成功绑定时调用
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
//向下转型为DownloadBinder
downloadBinder = service as MyService.DownloadBinder
//开始下载、查看下载进度
downloadBinder.startDownload()
downloadBinder.getProgress()
}
//当Service进程崩溃或被Kill时调用(不常用)
override fun onServiceDisconnected(name: ComponentName?) {
TODO("Not yet implemented")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//初始化ViewBinding控件
var myBinding = ActivityMainBinding.inflate(layoutInflater)
//把根元素的对象作为参数传入到setContentView()方法中
setContentView(myBinding.root)
· · ·
myBinding.bindServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
//绑定Service(Context.BIND_AUTO_CREATE表示在Activity和Service绑定后自动创建Service 然后会自动执行MyService的onCreate()方法)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
myBinding.unbindServiceBtn.setOnClickListener {
//解绑Service
unbindService(connection)
}
}
}
我们创建了一个ServiceConnection(用于建立Activity和Service之间的连接) 的单例匿名类,并重写了它的onServiceConnected()和onServiceDisconnected()方法,onServiceConnected()方法会在Actiity和Service成功绑定时调用。然后在onServiceConnected()方法通过向下转型将 service参数对象转型为MyService.DownloadBinder对象,并赋值给downloadBinder变量完成初始化。
通过:downloadBinder = service as MyService.DownloadBinder
将Service转型为MyService.DownloadBinder是为了让Activity能够直接调用MyService中定义的DownloadBinder类的方法。在Android中,一个Service可以与一个或多个Activity进行通信,但Activity并不能直接调用Service中的方法。因此,通过将Service转型为MyService.DownloadBinder,Activity可以间接地调用Service中的方法。
我们在bindServiceBtn按钮的点击事件中创建了一个指向MyService的Intent,然后调用bindService()方法将Activity和Service绑定起来。
myBinding.bindServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
//绑定Service
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
Context.BIND_AUTO_CREATE表示在Activity和Service绑定后会自动创建Service,这会使得MyService中的onCreate()方法会自动得到执行,但onStartCommand()方法不会执行。如果我们想解除Activity和Service之间的绑定,那么我们可以调用unbindService()方法,并传入ServiceConnection对象。
现在,我们运行程序,点击绑定Service按钮:
可以看到,我们点击绑定Service按钮后,自动执行了MyService的onCreate()方法,然后会执行DownloadBinder中的startDownload()方法和getProgress()方法。这就说明我们在Activity中成功的调用了Sevice中的方法,进而实现Activity与Service进行联系。
需要注意:任何一个Service在整个应用程序范围内都是通用的。即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的Activity进行绑定,而且在绑定完成后它们都可以获取相同的DownloadBinder实例。
2.4 Service的生命周期
- 在调用Context.startService( )方法后,若Service还没有创建则会先调用onCreate( )方法,然后启动Service的时候调用onStartCommand( )方法。
- Service一旦启动就会一直保持运行状态,直到以下情况发生:① 调用stopService( ) ② 调用stopSelf( ) ③ 被系统回收。
- 虽然每调用一次startService( )方法,onStartCommand()方法就会执行一次,但实际上每个Service只会有一个实例对象。不管你调用了多少次startService( )方法,只需要调用一次stopService( )或stopSelf( )就可以停止该Service。
- 当使用 Context.bindService( )方法启用一个Service的持久连接时,如果该Service还没有创建,系统会先调用onCreate()方法来创建Service,然后再调用onBind( )方法来建立与客户端的连接。只要调用方和Service的连接没有断开,Service就会一直保持运行状态,直到被系统系统回收。
- startService( )—> stopService( )—>onDestroy( )会执行;bindService( ) —>unBindService( )—>onDestroy( )会执行;
startService( ) + bindService( ) 的情况需要调用stopService( ) + unBindService( ) —>onDestroy( )才会执行
2.5 Service的两种启动方式
启动Service主要有两种不同的方式:startService( )和bindService( )。这两种方式的差异主要体现在Service的生命周期和如何使用该服务上:
- startService( ):当使用startService( )启动服务后,该服务会一直运行,直到应用程序明确调用方法来停止它。在这个过程中,服务的生命周期将按照以下顺序执行:首先调用onCreate(),然后是onStartCommand(),最后是onDestroy()。值得注意的是,如果多次调用startService( ),onCreate( )方法不会重复执行,但onStartCommand( )方法会每次都执行。
- bindService( ):bindService( )是一种绑定服务的方式。与startService( )不同,当应用程序使用完该服务后,必须调用unbindService( )方法来释放该服务。这种方式允许Activity等客户端与Service进行交互,因此通常用于需要双向通信的场景。
选择使用哪种方式启动Service取决于应用的具体需求。如果你只需要在后台执行某个任务,而不需要与客户端进行频繁的通信,那么使用startService( )可能更合适。反之,如果你需要与一个或多个客户端进行复杂的交互,那么bindService( )可能是更好的选择。
2.6 使用前台Service
从Android 8.0开始,只有当应用保持在前台可见的状态下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能会被系统回收。可以通过正确的使用前台Service,让你的Service一直保持运行。修改MyService.kt中的代码:
class MyService : Service() {
· · ·
//在Service创建时调用
override fun onCreate() {
super.onCreate()
Log.d(tag, "Service已创建,onCreate()执行完毕!")
//NotificationManager对象
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//定义通知渠道
val channel = NotificationChannel("my_service", "前台Service通知", NotificationManager.IMPORTANCE_DEFAULT)
//创建通知渠道
manager.createNotificationChannel(channel)
}
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
//Notification对象
val notification = NotificationCompat.Builder(this, "my_service")
.setContentTitle("通知的标题")
.setContentText("通知的正文内容")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
//将PendingIntent配置到Notification对象上
.setContentIntent(pendingIntent)
.build()
//将Service变为前台服务,并显示一个通知给用户
startForeground(1, notification)
}
· · ·
}
这里我们只修改了MyService里的onCreate()方法,添加的代码几乎都是上一节内容里发送通知时所使用的代码。唯一不同的是,我们在最后没有调用manager.notify()方法发送通知,而是调用startForeground()方法将Service变为前台服务,并显示一个通知给用户。startForeground( )方法需要两个参数:一个是通知的ID,另一个是Notification对象。
最后,我们需要在AndroidManifest清单文件中添加发送通知的权限和前台服务权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
现在运行程序,点击"启动Service"按钮,可以看到下拉通知栏中有一个前台Service:
2.7 使用IntentService
Service中的代码都是默认运行在主线程中的,如果在Service中处理一些耗时任务就有可能会导致ANR。所以我们需要通过Android多线程编程技术在Service中开启一个子线程,然后子线程中处理耗时任务。因此,一个标准的Service可以写成:
class MyService : Service() {
· · ·
//在Service启动后立即调用 用于执行Service的初始化操作
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
threat{
TODO("处理耗时任务")
}
return super.onStartCommand(intent, flags, startId)
}
· · ·
}
但是,这种Service一旦启动就会一直处于运行状态。必须要调用stopService( )或stopSelf( )方法,或着被系统回收,Service才会停止。 如果我们想要实现让一个Service在执行完毕后自动停止的功能,就需要这样写:
class MyService : Service() {
· · ·
//在Service启动后立即调用 用于执行Service的初始化操作
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
threat{
TODO("处理耗时任务")
//停止Service
stopService()
}
return super.onStartCommand(intent, flags, startId)
}
· · ·
}
除了手动调用stopService( )或stopSelf( )方法来停止Service,Android还专门提供一个IntentService类。IntentService在处理任务时,会自动创建一个工作线程来执行onHandleIntent()方法中的代码。IntentService在执行完onHandleIntent()方法后会自动停止服务,因此不需要手动停止服务。这种特性使得IntentService非常适合处理耗时操作,例如下载文件、发送数据等,因为这不会阻塞主线程,不会影响应用的响应性。
新建一个MyIntentService类继承自IntentService,代码如下:
class MyIntentService : IntentService("MyIntentService") {
//子线程中运行
override fun onHandleIntent(intent: Intent?) {
//打印当前线程的id
Log.d("MyIntentService", "Thread id is :${Thread.currentThread().name}")
}
//在Service被销毁时被调用 用于释放资源
override fun onDestroy() {
super.onDestroy()
Log.d("MyIntentService", "onDestroy() Executed!")
}
}
在主界面中添加一个"启动IntentService"按钮,然后在MainActivity.kt文件中为其添加按钮点击逻辑:
class MainActivity : AppCompatActivity() {
· · ·
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
· · ·
//startIntentServiceBtn点击监听
myBinding.startIntentServiceBtn.setOnClickListener {
//打印主线程id
Log.d("MainActivity", "Thread id is :${Thread.currentThread().name}")
val intent = Intent(this, MyIntentService::class.java)
startService(intent)
}
}
}
当我们点加按钮的时候会先打印主线程的id,然后启动IntentService。IntentService中的onHandleIntent()方法会在子线程中运行,我们打印一下子线程的id。另外,在onHandleIntent()方法执行完毕后,IntentService会自动停止并调用onDestroy()方法。
最后不要忘记,Android四大组件都需要在AndroidManifext.xml清单文件中进行注册:
<service
android:name=".MyIntentService"
android:enabled="true"
android:exported="true"></service>
运行程序,点击"启动IntentService"按钮:
可以看到MyIntentService和MainActivity的线程名是不同的,而且在MyIntentService运行完毕后会自动调用onDestroy()方法。这就说明MyIntentService确实是开启了一个子线程去处理任务,并在任务处理结束后自动停止。