【24】应用开发——Android多线程编程与Service的运用

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。

一、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

  1. Message
    Message是在线程之间进行传递的消息,它可以携带少量信息在不同的线程之间进行数据传递。Message中有几个常用字段:①what是一个int类型的字段,可以标识消息的种类。②arg1和arg2是两个int类型的字段,用于传递简单的参数值。③obj是一个Object类型的字段,用于存储任意类型的对象

  2. Handler
    Handler用于发送和处理消息。发送消息有两种方式:Handler.sendMessage()方法post()方法。通过sendMessage()方法发送的消息会被添加到消息队列中等待线程处理。这意味着消息何时能被执行,取决于线程的调度和消息队列中的其他消息。而post()方法则会立即执行消息,不受其他消息的影响。发出的消息最终会被传递到Handler的handleMessage()方法中

  3. MessageQueue(消息队列)
    MessageQueue用于存放所有通过Handler发送的消息。当Handler将一个Message对象发送到消息队列中时,消息会被添加到队列的末尾,等待线程处理。每个线程只会有一个消息队列,线程会按照队列中的顺序处理其中的消息。

  4. 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中的几个方法才能完成任务的定制:

  1. onPreExecute( ) :这个方法会在后台任务开始执行之前调用,用于界面的初始化,例如显示进度条等。
  2. doInBackground( )这个方法中的所有代码都会在子线程中运行(不可更新UI),我们应该在这里处理所有的耗时任务。任务结束后若AsyncTask需要返回结果,可通过return语句返回结果。如果要更新UI(反馈任务进度),可以调用publishProgress()通知UI进行更新
  3. onProgressUpdate( )在doInBackground()中调用publishProgress()方法后会被立即调用,可以在此方法内更新UI。
  4. 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确实是开启了一个子线程去处理任务,并在任务处理结束后自动停止。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值