抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

课本

书籍资源进入官网下载,PC端进入

第十章-后台默默的劳动者,探究Service

Service是什么

Service是Android中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。
不过需要注意的是,Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。
另外,也不要被Service的后台概念所迷惑,实际上Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。那么本章的第一堂课,我们就先来学习一下关于Android多线程编程的知识

Android多线程编程

如果你熟悉Java的话,对多线程编程一定不会陌生吧。当我们需要执行一些耗时操作,比如发起一条网络请求时,考虑到网速等其他原因,服务器未必能够立刻响应我们的请求,如果不将这类操作放在子线程里运行,就会导致主线程被阻塞,从而影响用户对软件的正常使用。下面就让我们从线程的基本用法开始学起吧。

线程的基本用法

Android多线程编程其实并不比Java多线程编程特殊,基本是使用相同的语法。

  1. 比如,定义一个线程只需要新建一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可,如下所示:
    class MyThread : Thread(){
     override fun run() {
         TODO("这里编写具体的逻辑代码")
     }
    }
    
  2. 要想运行它,只需要创建MyThread的示例,然后调用它的start()方法,这样它就能在子线程中运行了.
    MyThread().start()
    
  3. 当然,使用继承的方式耦合性有点高,我们会更多地选择使用实现Runnable接口的方式来定义一个线程,如下所示:
    class MyThread : Runnable{
     override fun run() {
         TODO("这里编写具体的逻辑代码")
     }
    }
    
    启动方式改为
    val myThread = MyThread()
    Thread(myThread).start()
    
    可以看到,Thread的构造函数接收一个Runnable参数,而我们创建的MyThread实例正是一个实现了Runnable接口的对象,所以可以直接将它传入Thread的构造函数里。接着调用Thread的start()方法,run()方法中的代码就会在子线程当中运行了。
  4. 如果你想专门再定义一个类去实现Runnable接口,也可以使用Lambda的方式,这种写法更为常见,如下所示:
    Thread{
     TODO("这里编写具体的逻辑代码")
    }.start()
    
  5. 以上是和Java相同创建子线程的方式,在Kotlin中提供了更简单的方式,写法如下:
    thread {
     TODO("这里编写具体的逻辑代码")
    }
    
    这个thread是Kotlin内置的顶层函数,我们只需要在代码块中编写好逻辑就可以了,他会自动帮我们调用start()方法.

在子线程中更新UI

和许多其他的GUI库一样,Android的UI也是线程不安全的。也就是说,如果想要更新应用程序里的UI元素,必须在主线程中进行,否则就会出现异常。
让我们通过一个具体的例子来验证一下吧。

  1. 新建一个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/changeTextBtn"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="改变文本" />
     <TextView
         android:id="@+id/textView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerInParent="true"
         android:text="Hello world"
         android:textSize="20sp" />
    </RelativeLayout>
    
    布局文件中定义了两个控件:TextView用于在屏幕的正中央显示一个”Hello world”字符串;Button用于改变TextView中显示的内容,我们希望在点击Button后可以把TextView中显示的字符串改成”Nice to meet you”。
  2. 接下来修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     private val updateText = 1
     private lateinit var changeTextBtn: Button
     private lateinit var textView: TextView
    
     private val handler = object : Handler(Looper.getMainLooper()){
         // handler子类必须实现这个来接收消息。
         override fun handleMessage(msg: Message) {
             // 判断传来的消息代码
             when (msg.what) {
                 updateText -> textView.text = "Nice to meet you"
             }
         }
     }
    
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         changeTextBtn = findViewById(R.id.changeTextBtn)
         textView = findViewById(R.id.textView)
    
         changeTextBtn.setOnClickListener {
             thread {
                 // 定义包含描述和可发送到处理程序的任意数据对象的消息
                 val msg = Message()
                 // 用户定义的消息代码,以便接收方能够识别此消息的内容。
                 msg.what = updateText
                 // 将消息推到消息队列的末尾,它将在附加到此处理程序的线程中的handleMessage中接收。
                 handler.sendMessage(msg)
             }
         }
     }
    }
    

    这里先定义了一个变量updateText保存消息代码,然后新增一个Handler对象,并重写父类的handleMessage()方法,在这里对具体的Message进行处理。如果发现Message的what字段的值等于updateText,就将TextView显示的内容改成“Nice to meet you”。
    改变文本按钮的点击事件中,我们没有对UI直接进行操作,而是是创建了一个Message(android.os.Message)对象,并将它的what字段的值指定为updateText,然后调用Handler的sendMessage()方法将这条Message发送出去。很快,Handler就会收到这条Message,并在handleMessage()方法中对它进行处理。在这时候handleMessage()方法中的代码就是在主线程中运行了,所以可以进行UI操作.接下来对Message携带的what字段的值进行判断,如果等于updateText,就将TextView显示的内容改成“Nice to meet you”。

  3. 现在重新运行程序,可以看到屏幕的正中央显示着“Hello world”。然后点击一下“ChangeText”按钮,显示的内容就被替换成“Nice to meet you”,如图所示。
    博客第一行代码子线程改变ui
    这样你就已经掌握了Android异步消息处理的基本用法,使用这种机制就可以出色地解决在子线程中更新UI的问题。不过恐怕你对它的工作原理还不是很清楚,下面我们就来分析一下Android异步消息处理机制到底是如何工作的。

解析异步消息处理机制

在Android中处理异步消息主要有四部分:

  • Message:
    Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
  • Handler:
    Handler顾名思义也就是处理者的意思,它主要是用于发送处理``消息的。发送消息一般是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。
  • MessageQueue:
    MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程会有一个MessageQueue对象。
  • Looper:
    Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

了解了Message、Handler、MessageQueue以及Looper的基本概念后,我们再来把异步消息处理的整个流程梳理一遍。首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler的构造函数中我们传入了Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。整个异步消息处理机制的流程如图所示。
博客第一行代码异步消息处理机制
一条Message经过以上流程的辗转调用后,也就从子线程进入了主线程,从不能更新UI变成了可以更新UI,整个异步消息处理的核心思想就是如此。

使用AsyncTask

为了更加方便的在子线程中对UI进行操作,Android还提供了AsyncTask.AsyncTask背后的实现原理也是基于异步消息处理机制的,即使对异步消息处理机制不了解,我们也只需要写自己的业务代码就行.
由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下。

  • Params: 后台任务需要的参数
  • Progress: 在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result: 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的自定义AsyncTask就可以写成如下形式:

class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
 ...
}

这里我们把AsyncTask的第一个泛型参数指定为Unit,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Int,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。
当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要重写AsyncTask中的几个方法才能完成对任务的定制。经常需要重写的方法有以下4个。

  1. onPreExecute():
    后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  2. doInBackground(Params…):
    这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress (Progress...)方法来完成。
  3. onProgressUpdate(Progress…):
    当在后台任务中调用了publishProgress(Progress…)方法后,onProgressUpdate (Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
  4. onPostExecute(Result):
    当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。

因此,一个比较完整的自定义AsyncTask就可以写成如下形式:

class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
    override fun onPreExecute() {
        // 显示进度对话框
        progressDialog.show()
    }
    override fun doInBackground(vararg params: Unit?) = try {
        while (true) {
            // 这是一个虚构的方法
            val downloadPercent = doDownload()
            publishProgress(downloadPercent)
            if (downloadPercent >= 100) {
                break
            }
        }
        true
    } catch (e: Exception) {
        false
    }
    override fun onProgressUpdate(vararg values: Int?) {
        // 在这里更新下载进度
        progressDialog.setMessage("Downloaded ${values[0]}%")
    }
    override fun onPostExecute(result: Boolean) {
        progressDialog.dismiss()// 关闭进度对话框
        // 在这里提示下载结果
        if (result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
        }
    }
}

在这个DownloadTask中,我们在doInBackground()方法里执行具体的下载任务。这个方法里的代码都是在子线程中运行的,因而不会影响主线程的运行。注意,这里虚构了一个doDownload()方法,用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了,由于doInBackground()方法是在子线程中运行的,在这里肯定不能进行UI操作,所以我们可以调用publishProgress()方法并传入当前的下载进度,这样onProgressUpdate()方法就会很快被调用,在这里就可以进行UI操作了。
当下载完成后,doInBackground()方法会返回一个布尔型变量,这样onPostExecute()方法就会很快被调用,这个方法也是在主线程中运行的。然后,在这里我们会根据下载的结果弹出相应的Toast提示,从而完成整个DownloadTask任务。
简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。
如果想要启动这个任务,只需编写以下代码即可:

DownloadTask().execute()

当然,你也可以给execute()方法传入任意数量的参数,这些参数将会传递到DownloadTask的doInBackground()方法当中。
以上就是AsyncTask的基本用法,怎么样,是不是感觉简单方便了许多?我们并不需要去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法,就可以轻松地从子线程切换到UI线程了。

Service的基本用法

了解了Android多线程编程的技术之后,下面就让我们进入本章的正题,开始对Service的相关内容进行学习。作为Android四大组件之一,Service也少不了有很多非常重要的知识点,那我们自然要从最基本的用法开始学习了。

定义一个Service

  1. 新建一个ServiceTest项目,然后右击com.example.servicetestNewServiceService,会弹出如图所示的窗口。
    博客第一行代码创建ServiceTest项目Service
    可以看到,这里我们将类名定义成MyService,Exported属性表示是否将这个Service暴露给外部其他程序访问,Enabled属性表示是否启用这个Service。将两个属性都勾中,点击“Finish”完成创建。
  2. 现在观察MyService中的代码,如下所示:

    class MyService : Service() {
    
     override fun onBind(intent: Intent): IBinder {
         TODO("Return the communication channel to the service.")
     }
    }
    

    可以看到,MyService是继承自系统的Service类的。目前MyService中可以算是空空如也,但有一个onBind()方法特别醒目。这个方法是Service中唯一的抽象方法,所以必须在子类里实现。我们会在后面的小节中使用到onBind()方法,目前可以暂时将它忽略。

  3. 既然是定义一个Service,自然应该在Service中处理一些事情了,那处理事情的逻辑应该写在哪里呢?这时就可以重写Service中的另外一些方法了,如下所示:

    class MyService : Service() {
    
     override fun onBind(intent: Intent): IBinder {
         TODO("Return the communication channel to the service.")
     }
    
     override fun onCreate() {
         super.onCreate()
     }
    
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
         return super.onStartCommand(intent, flags, startId)
     }
    
     override fun onDestroy() {
         super.onDestroy()
     }
    }
    

    可以看到,这里我们又重写了onCreate()onStartCommand()onDestroy()这3个方法,它们是每个Service中最常用到的3个方法了。其中onCreate()方法会在Service创建的时候调用,onStartCommand()方法会在每次Service启动的时候调用,onDestroy()方法会在Service销毁的时候调用。

  4. 另外需要注意,每一个Service都需要在AndroidManifest.xml文件中进行注册才能生效。不知道你有没有发现,这是Android四大组件共有的特点。不过相信你已经猜到了,智能的Android Studio早已自动帮我们完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>
    <service
        android:name=".MyService"
        android:enabled="true"
        android:exported="true"></service>
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <meta-data
            android:name="android.app.lib_name"
            android:value="" />
    </activity>
</application>

</manifest>

这样的话,就已经将一个Service完全定义好了。

### 启动和停止Service
定义好了Service之后,接下来就应该考虑如何启动以及停止这个Service。启动和停止的方法当然你也不会陌生,主要是借助Intent来实现的。下面就让我们在ServiceTest项目中尝试启动以及停止MyService。
1. 首先修改`activity_main.xml`中的代码,如下所示:
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <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>

这里我们在布局文件中加入了两个按钮,分别用于启动和停止Service。

  1. 后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
    
         val startServiceBtn:Button = findViewById(R.id.startServiceBtn)
         val stopServiceBtn:Button = findViewById(R.id.stopServiceBtn)
    
         startServiceBtn.setOnClickListener {
             val intent = Intent(this, MyService::class.java)
             startService(intent)
         }
         stopServiceBtn.setOnClickListener {
             val intent = Intent(this, MyService::class.java)
             stopService(intent)
         }
     }
    }
    

    可以看到,在开始 Service按钮的点击事件里,我们构建了一个Intent对象,并调用startService()方法来启动MyService。在停止 Service按钮的点击事件里,我们同样构建了一个Intent对象,并调用stopService()方法来停止MyService。startService()和stopService()方法都是定义在Context类中的,所以我们在Activity里可以直接调用这两个方法。另外,Service也可以自我停止运行,只需要在Service内部调用stopSelf()方法即可。

  2. 那么接下来又有一个问题需要思考了,我们如何才能证实Service已经成功启动或者停止了呢?最简单的方法就是在MyService的几个方法中加入打印日志,如下所示:

    class MyService : Service() {
    
     override fun onBind(intent: Intent): IBinder {
         TODO("Return the communication channel to the service.")
     }
    
     override fun onCreate() {
         super.onCreate()
         Log.d("MyService", "onCreate executed")
     }
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
         Log.d("MyService", "onStartCommand executed")
         return super.onStartCommand(intent, flags, startId)
     }
     override fun onDestroy() {
         super.onDestroy()
         Log.d("MyService", "onDestroy executed")
     }
    }
    
  3. 现在可以运行一下程序来进行测试了,程序的主界面如图所示。
    博客第一行代码运行service2
  4. 点击一下开始 Service按钮,观察Logcat中的打印日志,如图所示。
    博客第一行代码运行service日志打印
  5. MyService中的onCreate()和onStartCommand()方法都执行了,说明这个Service确实已经启动成功了,并且你还可以在Settings→System→Advanced→Developeroptions(开发者选项)→Running services中找到它(不同手机路径可能不同,也有可能无此选项),如图所示
    博客第一行代码查看手机运行service
  6. 然后再点击一下停止 Service按钮,观察Logcat中的打印日志,如图所示。
    博客第一行代码停止service
    由此证明,MyService确实已经成功停止下来了。
    其实onCreate()方法是在Service第一次创建的时候调用的,而onStartCommand()方法则在每次启动Service的时候都会调用。由于刚才我们是第一次点击“Start Service”按钮,Service此时还未创建过,所以两个方法都会执行,之后如果你再连续多点击几次“StartService”按钮,你就会发现只有onStartCommand()方法可以得到执行了。
    以上就是Service启动和停止的基本用法,但是从Android 8.0系统开始,应用的后台功能被大幅削减。现在只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。之所以做这样的改动,是为了防止许多恶意的应用程序长期在后台占用手机资源,从而导致手机变得越来越卡。当然,如果你真的非常需要长期在后台执行一些任务,可以使用前台Service或者WorkManager,前台Service我们待会马上就会学到,而WorkManager将会在第13章中进行学习。

Activity和Service进行通信

在上一小节中,我们学习了启动和停止Service的方法。不知道你有没有发现,虽然Service是在Activity里启动的,但是在启动了Service之后,Activity与Service基本就没有什么关系了。确实如此,我们在Activity里调用了startService()方法来启动MyService,然后MyService的onCreate()和onStartCommand()方法就会得到执行。之后Service会一直处于运行状态,但具体运行的是什么逻辑,Activity就控制不了了。这就类似于Activity通知了Service一下:“你可以启动了!”然后Service就去忙自己的事情了,但Activity并不知道Service到底做了什么事情,以及完成得如何。
那么可不可以让Activity和Service的关系更紧密一些呢?例如在Activity中指挥Service去干什么,Service就去干什么。当然可以,这就需要借助我们刚刚忽略的onBind()方法了。
比如说,目前我们希望在MyService里提供一个下载功能,然后在Activity中可以决定何时开始下载,以及随时查看下载进度。实现这个功能的思路是创建一个专门的Binder对象来对下载功能进行管理。

  1. 修改MyService中的代码,如下所示:

    class MyService : Service() {
     private val mBinding = DownloadBinding()
    
     inner class DownloadBinding : Binder() {
         fun startDownload() {
             Log.d("MyService", "startDownload executed")
         }
         fun getProgress(): Int {
             Log.d("MyService", "getProgress executed")
             return 0
         }
     }
    
     override fun onBind(intent: Intent): IBinder {
         return mBinding
     }
    }
    

    可以看到,这里我们新建了一个DownloadBinder类,并让它继承Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。当然这只是两个模拟方法,并没有实现真正的功能,我们在这两个方法中分别打印了一行日志。
    接着,在MyService中创建了DownloadBinder的实例,然后在onBind()方法里返回了这个实例,这样MyService中的工作就全部完成了。

  2. 下面就要看一看在Activity中如何调用Service里的这些方法了。首先需要在布局文件里新增两个按钮,修改activity_main.xml中的代码,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <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" />
    
     <Button
         android:id="@+id/bindServiceBtn"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Bind Service" />
     <Button
         android:id="@+id/unbindServiceBtn"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Unbind Service" />
    </LinearLayout>
    

    这两个按钮分别是用于绑定和取消绑定Service的,那到底谁需要和Service绑定呢?当然就是Activity了。当一个Activity和Service绑定了之后,就可以调用该Service里的Binder提供的方法了。

  3. 修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     // 自己定义的下载服务
     lateinit var downloadBinder:MyService.DownloadBinding
     // 监视应用程序服务状态的接口,这个类上的方法是从进程的主线程调用的。
     private val connection = object:ServiceConnection{
         // 当service与服务的连接已经建立时调用
         override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
             // 将传入的service转为MyService.DownloadBinding
             downloadBinder = service as MyService.DownloadBinding
             // 执行下载
             downloadBinder.startDownload()
             // 执行进度条
             downloadBinder.getProgress()
         }
    
         override fun onServiceDisconnected(name: ComponentName?) {
         }
     }
    
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val startServiceBtn:Button = findViewById(R.id.startServiceBtn)
         val stopServiceBtn:Button = findViewById(R.id.stopServiceBtn)
         startServiceBtn.setOnClickListener {
             val intent = Intent(this, MyService::class.java)
             startService(intent)
         }
         stopServiceBtn.setOnClickListener {
             val intent = Intent(this, MyService::class.java)
             stopService(intent)
         }
    
         val bindServiceBtn:Button = findViewById(R.id.bindServiceBtn)
         val unbindServiceBtn:Button = findViewById(R.id.unbindServiceBtn)
    
         bindServiceBtn.setOnClickListener {
             // 构建一个意图
             val intent = Intent(this,MyService::class.java)
             // 绑定Service
             bindService(intent,connection, Context.BIND_AUTO_CREATE)
         }
         unbindServiceBtn.setOnClickListener {
             // 解除Service
             unbindService(connection)
         }
     }
    }
    

    这里我们首先创建了一个ServiceConnection的匿名类实现,并在里面重写了onServiceConnected()方法和onServiceDisconnected()方法。onServiceConnected()方法方法会在Activity与Service成功绑定的时候调用,而onServiceDisconnected()方法只有在Service的创建进程崩溃或者被杀掉的时候才会调用,这个方法不太常用。那么在onServiceConnected()方法中,我们又通过向下转型得到了DownloadBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们可以在Activity中根据具体的场景来调用DownloadBinder中的任何public方法,即实现了指挥Service干什么Service就去干什么的功能。这里仍然只是做了个简单的测试,在onServiceConnected()方法中调用了DownloadBinder的startDownload()和getProgress()方法。
    当然,现在Activity和Service其实还没进行绑定呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们仍然构建了一个Intent对象,然后调用bindService()方法将MainActivity和MyService进行绑定。bindService()方法接收3个参数:

    • 参数一: 构建出的Intent对象
    • 参数二: ServiceConnection的实例
    • 参数三: 则是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service进行绑定后自动创建Service。这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。
      如果我们想解除Activity和Service之间的绑定该怎么办呢?调用一下unbindService()方法就可以了,这也是Unbind Service按钮的点击事件里实现的功能。
  4. 现在让我们重新运行一下程序吧,界面如图所示。
    博客第一行代码绑定service效果图
  5. 点击一下Bind Service按钮,观察Logcat中的打印日志,如图所示。
    博客第一行代码绑定service日志输出效果图
    可以看到,首先是MyService的onCreate()方法得到了执行,然后startDownload()和getProgress()方法都得到了执行,说明我们确实已经在Activity里成功调用了Service里提供的方法

另外需要注意,任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的Activity进行绑定,而且在绑定完成后,它们都可以获取相同的DownloadBinder实例

Service的生命周期

一旦在项目的任何位置调用了Context的startService()方法,相应的Service就会启动,并回调onStartCommand()方法。如果这个Service之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。Service启动了之后会一直保持运行状态,直到stopService()或stopSelf()方法被调用,或者被系统回收。注意,虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个Service只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopSelf()方法,Service就会停止。
博客第一行代码Server生命周期

另外,还可以调用Context的bindService()来获取一个Service的持久连接,这时就会回调Service中的onBind()方法。类似地,如果这个Service之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样就能自由地和Service进行通信了。只要调用方和Service之间的连接没有断开,Service就会一直保持运行状态,直到被系统回收。

当调用了startService()方法后,再去调用stopService()方法。这时Service中的onDestroy()方法就会执行,表示Service已经销毁了。类似地,当调用了bindService()方法后,再去调用unbindService()方法,onDestroy()方法也会执行,这两种情况都很好理解。但是需要注意,我们是完全有可能对一个Service既调用了startService()方法,又调用了bindService()方法的,在这种情况下该如何让Service销毁呢?根据Android系统的机制,一个Service只要被启动或者被绑定了之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行。

这样你就把Service的生命周期完整地走了一遍。

Service的更多技巧

以上所学的内容都是关于Service最基本的一些用法和概念,当然也是最常用的。不过,仅仅满足于此显然是不够的,关于Service的更多高级使用技巧还在等着我们呢,下面就赶快去看一看吧。

使用前台Service

前面已经说过,从Android 8.0系统开始,只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。而如果你希望Service能够一直保持运行状态,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它一直会一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果,如图所示。
博客第一行代码前台service图标显示
由于状态栏中一直有一个正在运行的图标,相当于我们的应用以另外一种形式保持在前台可见状态,所以系统不会倾向于回收前台Service。另外,用户也可以通过下拉状态栏清楚地知道当前什么应用正在运行,因此也不存在某些恶意应用长期在后台偷偷占用手机资源的情况。
那么我们就来看一下如何才能创建一个前台Service吧,其实并不复杂.

  1. 修改MyService中的代码,如下所示:

    class MyService : Service() {
     private val mBinding = DownloadBinding()
     inner class DownloadBinding : Binder() {
         fun startDownload() {
             Log.d("MyService", "startDownload executed")
         }
         fun getProgress(): Int {
             Log.d("MyService", "getProgress executed")
             return 0
         }
     }
     override fun onBind(intent: Intent): IBinder {
         return mBinding
      }
     override fun onCreate() {
         super.onCreate()
         Log.d("MyService", "onCreate executed")
         val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
             val channel = NotificationChannel("我的Service","前台Service通知",NotificationManager.IMPORTANCE_DEFAULT)
             manager.createNotificationChannel(channel )
         }
         val intent = Intent(this, MainActivity::class.java)
         val pi = PendingIntent.getActivity(this,0,intent,0)
         val notification = NotificationCompat.Builder(this,"我的Service").apply {
             setContentTitle("这是内容标题")
             setContentText("这是内容")
             setSmallIcon(R.drawable.small_icon)
             setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
             setContentIntent(pi)
         }.build()
         // 让这个服务在前台运行
         startForeground(1,notification)
     }
    }
    

    可以看到,这里只是修改了onCreate()方法中的代码,相信这部分代码你会非常眼熟。没错!这就是我们在第9章中学习的创建通知的方法,并且我还将small_icon和large_icon这两张图从NotificationTest项目中复制了过来。只不过这次在构建Notification对象后并没有使用NotificationManager将通知显示出来,而是调用了startForeground()方法。这个方法接收两个参数:

    • 参数一: 第一个参数是通知的id,类似于notify()方法的第一个参数;
    • 参数二: 则是构建的Notification对象。调用startForeground()方法后就会让MyService变成一个前台Service,并在系统状态栏显示出来。
  2. 另外,从Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行,如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.ServiceTest"
    tools:targetApi="31">
    <service
        android:name=".MyService"
        android:enabled="true"
        android:exported="true"></service>
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <meta-data
            android:name="android.app.lib_name"
            android:value="" />
    </activity>
</application>

</manifest>

3. 现在重新运行一下程序,并点击`开始 Service`按钮,MyService就会以前台Service的模式启动了,并且在系统状态栏会显示一个通知图标,下拉状态栏后可以看到该通知的详细内容
![博客第一行代码3前台服务效果图](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码3前台服务效果图.png)
4. 现在即使你退出应用程序,MyService也会一直处于运行状态,而且不用担心会被系统回收。当然,MyService所对应的通知也会一直显示在状态栏上面。如果用户不希望我们的程序一直运行,也可以选择手动杀掉应用,这样MyService就会跟着一起停止运行了。

### 使用IntentService
这个类已弃用 在 API 级别 30 中,需要的自己了解
> IntentService受到Android 8.0(API级别26)规定的[后台执行限制](https://developer.android.com/about/versions/oreo/background)。当在Android 8.0或更高版本上运行时,考虑使用[JobIntentService](https://developer.android.com/reference/androidx/core/app/JobIntentService.html)或第13章的
> WorkManager替代

### 本小节代码

## Kotlin课堂:泛型的高级特性
还记得在第8章的Kotlin课堂里我们学习的Kotlin泛型的基本用法吗?这些基本用法其实和Java中泛型的用法是大致相同的,因此也相对比较好理解。然而实际上,Kotlin在泛型方面还提供了不少特有的功能,掌握了这些功能,你将可以更好玩转Kotlin,同时还能实现一些不可思议的语法特性,那么我们自然不能错过这部分内容了。
### 对泛型进行实化
泛型实化这个功能对于绝大多数Java程序员来讲是非常陌生的,因为Java中完全没有这个概念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java的泛型擦除机制才行。
在JDK 1.5之前,Java是没有泛型功能的,那个时候诸如List之类的数据结构可以存储任意类型的数据,取出数据的时候也需要手动向下转型才行,这不仅麻烦,而且很危险。比如说我们在同一个List中存储了字符串和整型这两种数据,但是在取出数据的时候却无法区分具体的数据类型,如果手动将它们强制转成同一种类型,那么就会抛出类型转换异常。
于是在JDK 1.5中,Java终于引入了泛型功能。这不仅让诸如List之类的数据结构变得简单好用,也让我们的代码变得更加安全。
但是实际上,Java的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK 1.5之前的机制来运行,JVM是识别不出来我们在代码中指定的泛型类型的。例如,假设我们创建了一个List`<String>`集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。
所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。
然而不同的是,Kotlin提供了一个内联函数的概念,我们在第6章的Kotlin课堂中已经学过了这个知识点。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明,其工作原理如图所示。
![博客第一行代码3Kotlin内联函数替换示意图](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码3Kotlin内联函数替换示意图.png)
最终代码会被替换成如图所示的样子。
可以看到,bar()是一个带有泛型类型的内联函数,foo()函数调用了bar()函数,在代码编译之后,bar()函数中的代码将可以获得泛型的实际类型。
这就意味着,Kotlin中是可以将内联函数中的泛型进行实化的。
那么具体该怎么写才能`将泛型实化`呢?首先,该函数必须是`内联函数`才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上`reified`关键字来表示该泛型要进行实化。示例代码如下:
```kotlin
inline fun <reified T> getGenericType() {}

上述函数中的泛型T就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前提条件。那么借助泛型实化,到底可以实现什么样的效果呢?从函数名就可以看出来了,这里我们准备实现一个获取泛型实际类型的功能,代码如下所示:

inline fun <reified T> getGenericType() {}

虽然只有一行代码,但是这里却实现了一个Java中完全不可能实现的功能:getGenericType()函数直接返回了当前指定泛型的实际类型。T.class这样的语法在Java中是不合法的,而在Kotlin中,借助泛型实化功能就可以使用T::class.java这样的语法了。
现在我们可以使用如下代码对getGenericType()函数进行测试:

fun main() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result2 is $result2")
    //输出: result1 is class java.lang.String
    //输出: result2 is class java.lang.Integer
}

这里给getGenericType()函数指定了两种不同的泛型,由于getGenericType()函数会将指定泛型的具体类型返回,因此这里我们将返回的结果进行打印。
可以看到,如果将泛型指定成了String,那么就可以得到java.lang.String的类型;如果将泛型指定了Int,就可以得到java.lang.Integer的类型。

泛型实化的应用

泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is TT::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法结构,下面我们赶快来看一下吧。

到目前为止,我们已经将Android的四大组件全部学完了,除了ContentProvider之外,你会发现其余的3个组件有一个共同的特点,它们都是要结合Intent一起使用的。比如说启动一个Activity就可以这么写:

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)

有没有觉得TestActivity::class.java这样的语法很难受呢?当然,如果在没有更好选择的情况下,这种写法也是可以忍受的,但是Kotlin的泛型实化功能使得我们拥有了更好的选择。

新建一个reified.kt文件,然后在里面编写如下代码:

inline fun <reified T> startActivity(context: Context) {
 val intent = Intent(context, T::class.java)
 context.startActivity(intent)
}

这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来就是神奇的地方了,Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。

现在,如果我们想要启动TestActivity,只需要这样写就可以了:

startActivity<TestActivity>(context)

Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。怎么样,是不是觉得代码瞬间精简了好多?这就是泛型实化所带来的神奇功能。

不过,现在的startActivity()函数其实还是有问题的,因为通常在启用Activity的时候还可能会使用Intent附带一些参数,比如下面的写法:

val intent = Intent(context, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", 123)
context.startActivity(intent)

而经过刚才的封装之后,我们就无法进行传参了。
这个问题也不难解决,只需要借助之前在第6章学习的高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示:

inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
 val intent = Intent(context, T::class.java)
 intent.block()
 context.startActivity(intent)
}

可以看到,这次的startActivity()函数中增加了一个函数类型参数,并且它的函数类型是定义在Intent类当中的。在创建完Intent的实例之后,随即调用该函数类型参数,并把Intent的实例传入,这样调用startActivity()函数的时候就可以在Lambda表达式中为Intent传递参数了,如下所示:

startActivity<TestActivity>(context) {
 putExtra("param1", "data")
 putExtra("param2", 123)
}

不得不说,这种启动Activity的代码写起来实在是太舒服了,泛型实化和高阶函数使这种语法结构成为了可能,感谢Kotlin提供了如此多优秀的语言特性。

好了,泛型实化的具体应用学到这里就基本结束了。虽然我们一直在使用启动Activity的代码来举例,但是启动Service的代码也是基本类似的,相信对于你来说,通过泛型实化和高阶函数来简化它的用法已经是小菜一碟了,这个功能就当作课后习题让你练练手吧。

那么接下来我们继续学习泛型更多的高级特性。

泛型的协变

泛型的协变和逆变功能不太常用,而且我个人认为有点不容易理解。但是Kotlin的内置API中使用了很多协变和逆变的特性,因此如果想要对这个语言有更加深刻的了解,这部分内容还是有必要学习一下的。

我在学习协变和逆变的时候查阅了很多资料,这些资料大多十分晦涩难懂,因此也让我对这两个知识点产生了一些畏惧。但是真正掌握之后,发现其实也并不是那么难,所以这里我会尽量使用最简明的方式来讲解这两个知识点,希望你可以轻松掌握。

在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如图所示。

博客第一行代码3协变示意图1

有了这个约定前提,我们就可以继续学习了。首先定义如下3个类:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

这里先定义了一个Person类,类中包含name和age这两个字段。然后又定义了Student和Teacher这两个类,让它们成为Person类的子类。

现在有一个问题:如果某个方法接收一个Person类型的参数,而我们传入一个Student的实例,这样合不合法呢?很显然,因为Student是Person的子类,学生也是人呀,因此这是一定合法的。

那么我再来升级一下这个问题:如果某个方法接收一个List<Person>类型的参数,而我们传入一个List<Student>的实例,这样合不合法呢?看上去好像也挺正确的,但是Java中是不允许这么做的,因为List<Student>不能成为List<Person>的子类,否则将可能存在类型转换的安全隐患。

为什么会存在类型转换的安全隐患呢?下面我们通过一个具体的例子进行说明。这里自定义一个SimpleData类,代码如下所示:

class SimpleData<T> {
 private var data: T? = null
 fun set(t: T?) {
 data = t
 }
 fun get(): T? {
 return data
 }
}

SimpleData是一个泛型类,它的内部封装了一个泛型data字段,调用set()方法可以给data字段赋值,调用get()方法可以获取data字段的值。

接着我们假设,如果编程语言允许向某个接收SimpleData<Person>参数的方法传入SimpleData<Student>的实例,那么如下代码就会是合法的:

fun main() {
 val student = Student("Tom", 19)
 val data = SimpleData<Student>()
 data.set(student)
 handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
 val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
 val teacher = Teacher("Jack", 35)
 data.set(teacher)
}

发现这段代码有什么问题吗?在main()方法中,我们创建了一个Student的实例,并将它封装到SimpleData<Student>当中,然后将SimpleData<Student>作为参数传递给handleSimpleData()方法。但是handleSimpleData()方法接收的是一个SimpleData<Person>参数(这里假设可以编译通过),那么在handleSimpleData()方法中,我们就可以创建一个Teacher的实例,并用它来替换SimpleData<Person>参数中的原有数据。这种操作肯定是合法的,因为Teacher也是Person的子类,所以可以很安全地将Teacher的实例设置进去。

但是问题马上来了,回到main()方法当中,我们调用SimpleData<Student>的get()方法来获取它内部封装的Student数据,可现在SimpleData<Student>中实际包含的却是一个Teacher的实例,那么此时必然会产生类型转换异常。

所以,为了杜绝这种安全隐患,Java是不允许使用这种方式来传递参数的。换句话说,即使Student是Person的子类,SimpleData<Student>并不是SimpleData<Person>的子类。

不过,回顾一下刚才的代码,你会发现问题发生的主要原因是我们在handleSimpleData()方法中向SimpleData<Person>里设置了一个Teacher的实例。如果SimpleData在泛型T上是只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData<Student>可不可以成为SimpleData<Person>的子类呢?

讲到这里,我们终于要引出泛型协变的定义了。假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。

但是如何才能让MyClass<A>成为MyClass<B>的子类型呢?刚才已经讲了,如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass<T>类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上。

现在修改SimpleData类的代码,如下所示:

class SimpleData<out T>(val data: T?) {
 fun get(): T? {
 return data
 }
}

这里我们对SimpleData类进行了改造,在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。

由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data参数赋值了,所以这里改成了使用构造函数的方式来赋值。你可能会说,构造函数中的泛型T不也是在in位置上的吗?没错,但是由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。

经过了这样的修改之后,下面的代码就可以完美编译通过且没有任何安全隐患了:

fun main() {
 val student = Student("Tom", 19)
 val data = SimpleData<Student>(student)
 handleMyData(data)
 val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {
 val personData = data.get()
}

由于SimpleData类已经进行了协变声明,那么SimpleData<Student>自然就是SimpleData<Person>的子类了,所以这里可以安全地向handleMyData()方法中传递参数。

然后在handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是完全安全的,所以这段代码没有任何问题。

学到这里,关于协变的内容你就掌握得差不多了,不过最后还有个例子需要回顾一下。前面我们提到,如果某个方法接收一个List<Person>类型的参数,而传入的却是一个List<Student>的实例, 在Java中是不允许这么做的。注意这里我的用语,在Java中是不允许这么做的。

你没有猜错,在Kotlin中这么做是合法的,因为Kotlin已经默认给许多内置的API加上了协变声明,其中就包括了各种集合的类与接口。还记得我们在第2章中学过的吗?Kotlin中的List本身就是只读的,如果你想要给List添加数据,需要使用MutableList才行。既然List是只读的,也就意味着它天然就是可以协变的,我们来看一下List简化版的源码:

public interface List<out E> : Collection<E> {
 override val size: Int
 override fun isEmpty(): Boolean
 override fun contains(element: @UnsafeVariance E): Boolean
 override fun iterator(): Iterator<E>
 public operator fun get(index: Int): E
}

List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。不过这里还有一点需要说明,原则上在声明了协变之后,泛型E就只能出现在out位置上,可是你会发现,在contains()方法中,泛型E仍然出现在了in位置上。

这么写本身是不合法的,因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是contains()方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置上了。但是如果你滥用这个功能,导致运行时出现了类型转换异常,Kotlin对此是不负责的。

好了,关于协变的内容就学到这里,接下来我们开始学习逆变的内容。

泛型的逆变

理解了协变之后再来学习逆变,我觉得会相对比较容易一些,因为它们之间是有所关联的。

不过仅从定义上来看,逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图所示。
博客第一行代码3逆变和协变区别

从直观的角度上来思考,逆变的规则好像挺奇怪的,原本A是B的子类型,怎么MyClass<B>能反过来成为MyClass<A>的子类型了呢?别担心,下面我们通过一个具体的例子来学习一下,你就明白了。

这里先定义一个Transformer接口,用于执行一些转换操作,代码如下所示:

interface Transformer<T> {
 fun transform(t: T): String
}

可以看到,Transformer接口中声明了一个transform()方法,它接收一个T类型的参数,并且返回一个String类型的数据,这意味着参数T在经过transform()方法的转换之后将会变成一个字符串。至于具体的转换逻辑是什么样的,则由子类去实现,Transformer接口对此并不关心。

那么现在我们就尝试对Transformer接口进行实现,代码如下所示:

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom", 19)
    val result = trans.transform(student)
}

首先我们在main()方法中编写了一个Transformer<Person>的匿名类实现,并通过transform()方法将传入的Person对象转换成了一个“姓名+年龄”拼接的字符串。而handleTransformer()方法接收的是一个Transformer<Student>类型的参数,这里在handleTransformer()方法中创建了一个Student对象,并调用参数的transform()方法将Student对象转换成一个字符串。

这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用Transformer<Person>的匿名类实现将Student对象转换成一个字符串也是绝对安全的,并不存在类型转换的安全隐患。但是实际上,在调用handleTransformer()方法的时候却会提示语法错误,原因也很简单,Transformer<Person>并不是Transformer<Student>的子类型。

那么这个时候逆变就可以派上用场了,它就是专门用于处理这种情况的。修改Transformer接口中的代码,如下所示:

interface Transformer<in T> {
 fun transform(t: T): String
}

这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。

没错,只要做了这样一点修改,刚才的代码就可以编译通过且正常运行了,因为此时Transformer<Person>已经成为了Transformer<Student>的子类型。

逆变的用法大概就是这样了,如果你还想再深入思考一下的话,可以想一想为什么逆变的时候泛型T不能出现在out位置上?为了解释这个问题,我们先假设逆变是允许让泛型T出现在out位置上的,然后看一看可能会产生什么样的安全隐患。

修改Transformer中的代码,如下所示:

interface Transformer<in T> {
 fun transform(name: String, age: Int): @UnsafeVariance T
}

可以看到,我们将transform()方法改成了接收name和age这两个参数,并把返回值类型改成了泛型T。由于逆变是不允许泛型T出现在out位置上的,这里为了能让编译器正常编译通过,所以加上了@UnsafeVariance注解,这和List源码中使用的技巧是一样的。

那么,这个时候可能会产生什么样的安全隐患呢?我们来看一下如下代码就知道了:

fun main() {
 val trans = object : Transformer<Person> {
 override fun transform(name: String, age: Int): Person {
 return Teacher(name, age)
 }
 }
 handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
 val result = trans.transform("Tom", 19)
}

上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在Transformer<Person>的匿名类实现中,我们使用transform()方法中传入的name和age参数构建了一个Teacher对象,并把这个对象直接返回。由于transform()方法的返回值要求是一个Person对象,而Teacher是Person的子类,因此这种写法肯定是合法的。

但在handleTransformer()方法当中,我们调用了Transformer<Student>的transform()方法,并传入了name和age这两个参数,期望得到的是一个Student对象的返回,然而实际上transform()方法返回的却是一个Teacher对象,因此这里必然会造成类型转换异常。

由于这段代码是可以编译通过的,那么我们可以运行一下,打印出的异常信息如图所示。
博客第一行代码3逆变类型转换异常

可以看到,提示我们Teacher类型是无法转换成Student类型的。

也就是说,Kotlin在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进去了。只要我们严格按照其语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance注解可以打破这一语法规则,但同时也会带来额外的风险,所以你在使用@UnsafeVariance注解时,必须很清楚自己在干什么才行。

最后我们再来介绍一下逆变功能在Kotlin内置API中的应用,比较典型的例子就是Comparable的使用。Comparable是一个用于比较两个对象大小的接口,其源码定义如下:

interface Comparable<in T> {
 operator fun compareTo(other: T): Int
}

可以看到,Comparable在T这个泛型上就是逆变的,compareTo()方法则用于实现具体的比较逻辑。那么这里为什么要让Comparable接口是逆变的呢?想象如下场景,如果我们使用Comparable<Person>实现了让两个Person对象比较大小的逻辑,那么用这段逻辑去比较两个Student对象的大小也一定是成立的,因此让Comparable<Person>成为Comparable<Student>的子类合情合理,这也是逆变非常典型的应用

啊吧啊吧,Kotlin这段完全复制第一行代码内容,我还是看视频教程去吧

评论