Android四大组件之Service

Android四大组件之默默劳动的Service

什么是Service

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

Android多线程编程

  • 当我们需要执行一些耗时操作的时候,比如发起一条网络请求的时候,考虑到网络的情况,服务器未必能够立刻去响应我们的请求,如果不将这类操作放在子线程当中去执行,就会导致主线程被阻塞,从而影响用户对软件的正常使用.

线程的基本用法

  • Android多线程和Java当中的多线程,使用的语法也比较相似,如果定义一个线程只需要创建一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可,如下所示:
class MyThread : Thread {
    override fun run() {
        //编写具体代码逻辑
    }
}
  • 那么如何启动这个线程呢,其实很简单,只需要创建MyThread的实例,然后调用它的start()方法,这样run()方法中的代码就会在子线程当中运行了,如下所示
val myThread = MyThread()
myThread.start()
  • 使用继承的方式使得代码的耦合度还是比较高的,我们更多的情况下会选择实现Runnable接口的方式来定义一个线程,如下所示
class MyThread : Runnable {
    override fun run() {
        //编写具体的代码逻辑
    }
}
  • 如果使用这种方法,启动线程的方法就要用下面这种方式
val myThread = MyThread()
Thread(myThread).start()
  • 可以看到Thread的构造函数接受一个Runnale参数,而我们创建的MyThread实例正是一个实现了Runnale接口的对象,所以可以直接将他传入到Thread的构造函数当中,接着调用Thread的start()方法,run()方法当中的代码逻辑就会在子线程当中运行了.
  • 当然如果不想专门在定义一个类去实现Runnale接口,也可以直接使用Lambda的方式,这种写法比较常用
Thread {
    //编写具体的代码逻辑
}.start()
  • 以上几种线程的使用方式,在Java中创建和启动线程的方式也是一样,而在Kotlin当中还给我们提供了一种更加简单的开启线程的方式,写法如下:
thread {
    //编写具体的代码逻辑
}
  • 这里的thread是Kotlin内置的一个顶层函数,我们只需要在Lambda表达式中编写具体的逻辑,连start()方法都不需要调用,thread函数在内部全部帮我们处理好了
  • 下面是就Android多线程和Java多线程不同的地方了

在子线程中更新UI

  • 和许多的GUI一样,Android的UI也不是线程安全的,也就是说,想要更新应用程序里的UI元素,必须在主线程进行,否则就会出现异常
  • 新建一个AndroidThreadTest项目来验证一下,创建完成之后修改activity_main.xml的代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/changeTextBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <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
  • 接下来修改MainActivity当中的代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        changeTextBtn.setOnClickListener {
            thread { 
                textView.text = "nice to meet you"
            }
        }
    }
}
  • 可以看到我们在按钮的点击事件里面开启了一个子线程,然后在子线程当中调用TextView的setText()方法将显示的字符改成了nice to meet you
  • 需要注意的是我们是在子线程当中更新UI
  • 运行程序之后会发现程序崩溃了,报下面的错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfJkF8ah-1671952178076)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221222223139941.png)]

  • 由此可以证实Android确实不允许在子线程中进行UI操作的,但是有些时候我们又必须在子线程里面执行一些耗时的任务,然后根据任务的执行结果来更新相应的UI控件
  • 对于这种情况,Android提供了一套异步消息处理机制,完美解决了在子线程中更新UI操作的问题,修改MainActivity当中的代码逻辑如下所示:
class MainActivity : AppCompatActivity() {
    val updateText = 1
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            //在这里可以进行UI操作
            when(msg.what) {
                updateText -> textView.text = "Nice to meet you"
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        changeTextBtn.setOnClickListener {
            thread {
                val msg = Message()
                msg.what= updateText
                handler.sendMessage(msg) //将Message对象发送出去
            }
        }
    }
}
  • 这里先定义了一个整形变量updateText,用来更新TextView这个动作,然后新增了一个Handler对象,并重写了父类的handlerMessage()方法这里对具体的Message进行处理,如果发现Message的what字段的值,等于updateText,就将TextView显示的内容改成Nice to meet you
  • 然后再按钮的点击事件当中,这次并没有直接在子线程当中更新UI的内容,而是先创建了一个Message对象的实例,然后调用该实例的setWhat()方法设置它的what字段值为updateText,然后调用handler的sendMessage()方法将这条Message发送出去,很快Handler就能够收到这条Message,并在handleMessage()方法当中对这条消息进行处理,然后我们UI就自然而然地被处理了.
  • 接下来分析Android异步消息处理机制到底是如何进行工作的

解析异步消息处理机制

  • Android中的异步消息处理主要分为4个部分进行组成:Message,Handler,MessageQueue和Looper
Message
  • Message是在线程之间传递消息的,它可以在内部携带少量地信息,用于在不同线程之间传递数据,在上个示例当中使用了Message地what字段,除此之外还可以使用arg1和arg2字段来携带一些整形数据,使用obj字段携带一个Object对象.
Handler
  • Handler顾名思义地意思就是,它主要用于发送和处理消息,发送消息一般是使用Handler的sendMessage()方法,post()方法等,而发出的消息经过一系列的辗转处理之后,最终会传递到Handler的handlerMessage()方法当中.
MessageQueue
  • MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息,这部分消息会一直存放在消息队列当中,等待被处理,每一个线程中只会有一个MessageQueue对象.
Looper
  • Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限的循环中,然后每当发现MessageQueue中存在一条消息的时候,就会将他取出,并传递到Handler的handlerMessage()方法当中,每个线程只会有一个Looper对象
异步消息的整个流程
  • 首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法,然后当子线程中需要进行UI操作的时候,就会创建一个Message对象,并通过Handler将这条消息发送出去,之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper会一直尝试从MessageQueue中取出待处理的消息,最后分发回Handler的handleMessage()方法中.
  • 由于Handler的构造函数中我们传入了Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程当中运行,于是我们就可以安心的进行UI操作了,一整个异步消息的执行流程图如下所示

在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GhpgkCuk-1671952178078)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221222232358115.png)]

  • 一条Message经过上述流程的辗转之后,也就从子线程进入了主线程,从不能更新UI变成了可以更新UI,整个异步消息处理的核心思想就是如此.

使用AsyncTask

  • 为了能够更加方便我们在子线程中对UI进行操作,Android还提供了另外一些好用的工具,比如AsyncTask,借助AsyncTask,即使你对异步消息机制完全不了解,也可以十分简单的从子线程切换到主线程
  • AsyncTask背后原理也是基于异步消息处理机制的,只是Android帮我们做了很好的封装而已.
  • AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它,在继承的时候我们可以为AsyncTask类指定三个泛型参数,这三个参数的用途如下.
    • Params.在执行AsyncTask时需要传入的参数,可用于在后台任务当中使用
    • Progress.在后台任务执行的时候,如果需要在界面显示当前进度,则使用这里指定的泛型作为进度单位.
    • Result.当任务执行完毕之后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型.
  • 因此定义一个最简单的AsyncTask就可以写成如下这种形式
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
    ...
}
  • 这里我们将AsyncTask的第一个泛型参数指定为Unit,表示在执行AsyncTask的时候不需要传入参数给后台任务
  • 第二个泛型参数指定为Int,表示使用整形数据来作为进度显示的单位
  • 第三个泛型参数指定为Boolean,则表示用布尔类型来反馈执行的结果
  • 我们在继承了AsyncTask抽象类之后,还需要重写AsyncTask中的几个方法来完成对任务的定制,经常需要重写的方法有以下4个:
  1. onPreExecute() : 这个方法在后台任务执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等
  2. doInBackground(Params…) 这个方法中所有的代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务,任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务的执行结果,需要注意的是在这个方法当中是不能进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgres(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("Download ${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()函数用于计算当前的下载进度,假设这个方法已经存在了,在得到下载的进度之后就是考虑如何把它显示在界面上了,由于doInBackgroun()方法时在子线程当中运行的,在这里肯定不能进行UI操作,所以我们可以调用publishProgress()方法并传入当前的下载进度,这样onProgressUpdate()方法就会很快被调用,在这里就可以进行UI操作了.
  • 当下载完成之后,doInBackground()方法会返回一个布尔变量,这样onPostExecute()方法很快就会被调用,这个方法也是在主线程中进行的,然后在这里我们会根据下载的结果弹出相应的Toast提示,从而完成整个DownloadTask任务.
  • 简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行某个具体耗时的任务,在onProgressUpdate()方法中进行UI操作,在doPostExecute()方法中执行一些任务的收尾工作.
  • 如果想要启动这个任务,只需要编写一下代码即可:
DownloadTask().execute()
  • AsyncTask相比于之前异步消息处理机制来说,变得简单了很多,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法,就可以轻松地从子线程切换到主线程了.

Service的基本用法

  • 作为Android的四大组件之一,Service由于多非常重要的知识

定义一个Service

  • 新建一个ServiceTest项目,点击com.zb.servicetest->New->Service->Service,然后就可以定一个Service

  • 可以看到Service中的初始代码如下所示
class MyService : Service() {

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
}
  • 可以看到MyService类继承系统的Service类,在该类中有一个onBind()方法,这个方法时Service中唯一的抽象方法,所以必须在子类里实现
  • 想要在Service中处理一些事情,那么就需要重写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()和onDestory()这三个方法,他们是每一个Service最常用的三个方法
  • 其中onCreate()方法会在Service创建的时候调用
  • onStartCommand()方法会在每次Service启动的时候调用
  • onDestroy()方法会在Service销毁的时候进行调用
  • 通常情况下,如果我们希望Service一旦启动就立即去执行某个动作,那么我么就将这个代码逻辑写在onStartCommand()方法当中,而当当Service被销毁的时候,我们可以在onDestory()方法当中回收那些不再使用的资源.
  • 另外需要注意的是,每一个Service都需要在AndroidManifest.xml文件中进行注册才能生效,这也是四大组件共有的特点,但是通过刚才的创建Service的方法,AS已经很智能的帮我们进行了注册.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g89CFd30-1671952178079)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223153955331.png)]

启动和停止Service

  • 定义好一个Service之后,需要考虑的就是如何来启动和停止这个Service了,启动和停止的方法,是借助Intent来进行实现的,下面在ServiceTest项目当中尝试启动以及停止MyService
  • 首先修改activity_main.xml中的代码,如下所示
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/startServiceBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/stopServiceBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
  • 在布局文件当中添加了两个按钮,分别用于启动和停止Service,然后修改MainActivity当中的代码,如下所示
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceBtn.setOnClickListener {
            //启动Service的方式
            val intent = Intent(this, MyService::class.java)
            startService(intent)
        }
        
        stopServiceBtn.setOnClickListener {
            //停止Service的方法
            val intent = Intent(this, MyService::class.java)
            stopService(intent)
        }
    }
}
  • 上述就是启动和停止Service的方法了,那么如何证实Service已经启动或者停止了呢?
  • 最简单的方法就是在Service当中打印日志,如下所示
class MyService : Service() {
    private val tag = "MyService"

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

    override fun onCreate() {
        super.onCreate()
        Log.d(tag, "onCreate executed")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(tag, "onStartCommand executed")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy executed")
    }
}
  • 启动程序点击按钮分别会打印下面的日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KcTUUOF6-1671952178080)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223162353578.png)]

  • 以上就是Service启动和停止的基本用法,但是从Android8.0开始,应用后台的功能被大幅度的削减,现在只有当应用程序在前台保持可见的时候,Service才能平稳的运行,一旦进入后台,Service随时都有被系统回收的可能.
  • 之所以做这样的改动,是因为要防止恶意的应用程序长期在后台占用手机资源
  • 再回到onCreate()和onStartCommand()方法,onCreate()方法是在Service第一次创建的时候调用的,而onStartCommand()方法则在每次启动Service的时候都会调用
  • 刚才在点击Start Service按钮的时候,Service此时还没有创建过,所以两个方法都会执行,所以两个方法当中的日志都被打印出来了,之后如果连续多点击几次Start Service按钮,就只有onStartCommand()方法会进行执行了.

Activity和Service进行通信

  • 通过上面的代码示例可以发现,虽然Service是在Activity当中启动的,但是在Service启动之后,Activity和Service就没有什么关系了.
  • 在Activity里面调用了startSetvice()方法来启动MyService,然后MyService中的相关方法执行完毕之后,Service一直就处在了运行状态当中,但之后具体是什么逻辑,Activity就控制不了了.
  • 这就类似于Activity通知了Service一下,你可以启动了,然后Service就去忙自己的事情了,但是Activity并不知道Service到底做了什么事情,以及完成的如何.
  • 如果我们想要让Activity和Service的关系更加紧密一些,比如在Activity当中去指挥Service干什么,Service就去干什么,这就要用到onBind()方法了
  • 比如说,我们想要在Service中提供一个下载方法,我们在Activity当中控制什么时候开始下载,以及随时查看下载的进度
  • 实现这个方法的思路是创建一个专门的Binder对象来对下载进行管理,修改MyService当中的代码如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxFASdxU-1671952178080)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223165715324.png)]

  • 新建了一个DownloadBinder类,并让他继承自Binder,然后在它的内部提供了开始下载和下载进度两个方法(模拟方法)
  • 下面看看如何在Activity中调用Service里面的这些方法
  • 首先需要在布局文件当中新增两个按钮,修改该activity_main.xml中的代码如下所示
    <Button
        android:id="@+id/binServiceBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/unBindServiceBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="unBindServiceBtn" />
  • 这两个按钮分别是用于绑定和取消绑定Service
  • 所以我们现在需要让Activity进行绑定,然后Activity就可以调用Service里的Binder提供的方法了,修改MainActivity当中的代码,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwWqssG2-1671952178081)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223171025100.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLER5tgF-1671952178082)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223171038494.png)]

  • 在代码当中首先创建了一个ServiceConnection的匿名类实现,并在里面重写了onServiceConnected()方法和onServiceDisconnected()方法.
  • onServiceConnected()方法会在Activity和Service成功绑定的时候进行调用,而onServiceDisconnected()方法只有在Service的创建进程崩溃或者被杀掉的时候,才会调用,这个方法不太常用
  • 而在onServiceConnected()方法当中,又通过上下转型,得到了DownloadBinder的实例,有了这个实例,Activity和Service的关系就变得十分的紧密了
  • 我们就可以在Activity当中根据场景调用DownloadBinder中的任何public方法,这样就实现了指挥Service干什么,Service就干什么
  • 当然此时Activity和Service其实还没有进行绑定呢,绑定的操作是在Bind Service按钮当中进行完成的
  • 我们在该按钮的点击事件当中,先构建了一个Intent对象,然后调用bindService()方法,该方法接收三个参数
  • 第一个参数是刚刚构建出来的Intent对象,第二个参数是前面创建出来的ServiceConnection实例,第三个参数是一个标志位,这里传入Context.BIND_AUTO_CREATE,表示Activity和Service进行绑定之后自动创建Service,这就会使得MyService得onCreate()方法得到执行.
  • 如果我们想要解除绑定调用unbindService()方法就可以了,这也就是Unbind Service按钮点击事件里面实现的功能
  • 这样运行程序之后,点击binServiceBtn按钮,就会看到日志中打印出以下得内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBrUSyyd-1671952178083)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223201103162.png)]

  • 可以看到,首先MyService得onCreate()方法得到了执行,然后startDownload()和getProgress()都得到了执行,说明我们确实在Activity当中调用了Service里面提供的方法.
  • 另外需要注意的是,Service在整个应用程序范围内都是通用的,也就是说MyService不仅可以和MainActivity进行绑定,还可以和任何一个其他的Activity进行绑定,而且在绑定之后,它们都能够获得相同的DownloadBinder实例.

Service的生命周期

  • 和Activity一样,Service也拥有自己得生命周期,在上面得代码示例当中,我们使用到的onCreate(),onStartCommand(),onBind()和onDestory()等方法都是在Service的生命周期内可能回调的方法.
  • 在项目的任何位置调用了Context的startService()方法,相应的Service就会立马启动,并且回调onStartCommand()方法,如果这个Service在之前还没有创建过,还会在onStartCommand()方法回调之前,调用一次onCreate()方法,用来创建Service.
  • Service启动之后就会一直保持运行状态,知道stopService()方法或者stopSelf()方法被调用,或者系统回收之后,就会停止运行.
  • 虽然没当调用一次startService()方法,onStartCommand()方法就会调用一次,但是Service实例只会存在一个,所以只需要调用一次停止方法,Service就能停止运行.
  • 另外还可以调用Context的binService()来获取一个Service持久连接,这时候会回调Service当中的onBind()方法,类似的,如果这个Service之前还没有创建过,那么会优先回调onCreate()方法进行Service的创建,之后调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样就可以自由的和Service进行通信了,只要调用方和Service之间的连接还没有断开,Service就会一直保持运行状态直到被系统回收.
  • 当调用了startService()方法后,再去调用stopService()方法,这时Service的onDestory()方法就会执行,表示Service已经被销毁了,类似的当调用了bindService()方法后,再去调用unbinService()方法,onDestory()方法也会得到执行
  • 但是如果对一个Service即调用了startService()方法,又调用了bindService()方法,这两种方法的调用都会使得Service处在运行状态,但是Android系统提供了一种机制就是,当一个Service必须要让以上两种条件同时不满的时候,Service才能被销毁,所以在这种情况下需要同时调用stopService()和unbindService()方法,onDestory()方法才会进行调用.
  • 以上就是Service整个的生命周期,相比较于Activity的生命周期来讲还是比较简单的.

Service的更多的技巧

使用前台Service

  • 从Android8.0之后开始,只有当应用保持在前台可见状态的情况下,Service才能够保证稳定的运行,一旦应用进入后台,Service随时都有可能被系统回收
  • 如果希望Service能够一直保持运行状态,就可以考虑前台Service,前台Service和普通Service最大的区别就是,它一直会有一个正在运行的图标在系统的状态栏进行显示,下拉状态栏之后就可以看到更多的相信的信息,非常类似于通知的效果
  • 由于状态栏中一直有一个正在运行的图标,相当于我们的应用以另外一种形式保持在前台可以见的状态,所以系统不会倾向于回收前台Service.
  • 另外用户也可以通过下拉状态栏清楚的知道当前什么应用正在运行,因此不会存在某一些恶意的应用长期在后台偷偷他占用手机资源的情况.
  • 下面来修改MyService当中的代码,创建一个前台Service

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lDrnNE2Y-1671952178084)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221223212051423.png)]

  • 可以看到只是修改了onCreate()当中的代码,在里面创建了一个通知,但是这次在构建Notification对象后并没有使用NotificationManager将通知显示出来,而是调用了startForeground()方法
  • 这个方法接收两个参数,第一个参数是通知的id,类似于notify()方法当中的第一个参数,第二个参数则是构建Notification对象.
  • 调用startForeground()方法后就会让MyService变成一个前台Service,并在系统状态栏中显示出来.
  • 另外从Android9.0开始,使用前台Service必须要在AndroidManifest.xml文件中进行权限声明
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

注意注意!!!->PendingIntent是Android框架的重要组成部分。Android 12创建的每个PendingIntent对象必须使用PendingIntent.FLAG_MUTABLE或PendingIntent.FLAG_IMMUTABLE标志指定可变性,以提高应用的安全性。

  • 所以创建PendingIntent对象部分的代码需要改正为:
val pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
  • 现在启动应用,即使用户退出应用,MyService也会一直处在运行状态,而且不用担心会被系统回收,当然MyService所对应的通知也会一直显示在状态栏上面
  • 如果用户不希望我们的程序一直运行,也可以选择手动杀掉应用,这样MyService就会一直跟着停止运行了.

使用IntentService

  • Service中的代码都是默认运行在主线程中的,如果直接在Service当中处理一些耗时的逻辑,就很容易会出现ANR(应用程序未响应)的情况.
  • 所以在这个时候就需要用到Android多线程技术了,我们应该在Service的每一个具体方法里面开一个子线程,然后在这里去处理那些耗时的操作,因此一个比较标准的Service可以写成如下的形式
class MyService : Service() {
    ...
    override fun onStartCommand(intent: Intent, flages: Int, startId: Int) : Int {
        thread {
            //处理具体耗时的逻辑
        }
        return super.onStartcommand(intent, flages, startId)
    }
}
  • 这种Service一旦启动,就会一直处在运行状态,必须调用stopService()或者stopSelf()方法,或者被系统回收,Service才会停止,所以想要实现让一个Service在执行完毕后自动停止的功能,就可以这样写,就是在具体耗时的逻辑处理完毕之后,就调用相关方法停止Service
class MyService : Service() {
    ...
    override fun onStartCommand(intent: Intent, flages: Int, startId: Int) : Int {
        thread {
            //处理具体耗时的逻辑
            stopSelf()
        }
        return super.onStartcommand(intent, flages, startId)
    }
}
  • 但是为了防止一些程序员,忘记开线程,或者忘记调用stopSelf()方法,为了能够简单的创建一个异步,会自动停止的Service,Android专门提供了一个IntentService类,这个类就很好的解决了前面提到的两个问题
  • 新建一个MyIntentService类继承自InentService
class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        //打印当前线程id
        Log.d("MyIntentService", "Thread id is ${Thread.currentThread().name}")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("MyIntendService", "onDestroy executed")
    }

}
  • 继承IntentService,首先要求调用父类的构造函数,然后传入一个字符串,这个字符串是可以随意进行指定的,只有在后续的调试当中才有用
  • 然后在子类中要实现onHandleIntent()方法,在这个方法中可以处理一些耗时的逻辑操作,而且不用担心ANR问题,因为这个方法已经是在子线程中运行的了.
  • 然后根据IntentService的特性来说,这个Service在运行之后,是会停止运行的,所以在onDestory()方法当中打印了一行日志,用来测试是否会停止.
  • 下面修改activity_main.xml文件中的代码,加入一个用于启动MyIntentService的按钮
 <Button
     android:id="@+id/startIntentServiceBtn"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:text="Start IntentService" />
  • 然后在MainActivity编写该按钮的点击事件,如下所示
  //启动IntentService
  startIntentServiceBtn.setOnClickListener {
      //打印主线程id
      Log.d("MainActivity", "Thread id is ${Thread.currentThread().name}")
      val intent = Intent(this, MyIntentService::class.java)
      startService(intent)
  }
  • 运行程序点击Start IntentService,观察Logcat当中的日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQr3aCP3-1671952178086)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221224151942259.png)]

  • 通过上面打印的日志可以发现,MyIntentService在执行完毕之后确实自动停止了
  • 所以IntentService集开启线程和自动停止于一身,深受程序员喜爱.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值