Android四大组件之Service

news2024/11/25 13:33:41

文章目录

  • Android四大组件之默默劳动的Service
    • 什么是Service
    • Android多线程编程
      • 线程的基本用法
      • 在子线程中更新UI
      • 解析异步消息处理机制
        • Message
        • Handler
        • MessageQueue
        • Looper
        • 异步消息的整个流程
      • 使用AsyncTask
    • Service的基本用法
      • 定义一个Service
      • 启动和停止Service
      • Activity和Service进行通信
    • Service的生命周期
    • Service的更多的技巧
      • 使用前台Service
      • 使用IntentService

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集开启线程和自动停止于一身,深受程序员喜爱.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114418.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【4】axi协议学习

1、axi背景介绍: Advanced extensible Interface(AXI)是为了满足高性能系统设计而定义的一套独立通道协议,首次是在2003年发布的AMBA3标准中出现,经历AMBA4,目前已经到达AMBA5版本。 2、axi 特性: AXI满足如下的特性: 适合于高带宽,低延迟的设计 不需要通过复杂的桥…

去耦电容和旁路的概念说明与应用说明

回想当初第一眼看觉得&#xff0c;这啥玩意自己又菜了&#xff01; 电容&#xff1a;本质就是充放电&#xff0c;实际应该我们围着这个来转 一、解释 旁路电容从英文角度看&#xff0c;就是抄小路的意思&#xff0c;意思当有一个干扰来临时候&#xff0c;可以通过这个电容抄…

LeetCode 1739. 放置盒子:数学 思维

【LetMeFly】1739.放置盒子 力扣题目链接&#xff1a;https://leetcode.cn/problems/building-boxes/ 有一个立方体房间&#xff0c;其长度、宽度和高度都等于 n 个单位。请你在房间里放置 n 个盒子&#xff0c;每个盒子都是一个单位边长的立方体。放置规则如下&#xff1a; …

世界杯竞猜项目Dapp-第五章(合约升级)

目前主流有三种合约升级方法 transparent 方式&#xff1b;(通用&#xff0c;业务逻辑和代理逻辑解耦合&#xff0c;比较贵&#xff09;uups 方式&#xff1b;&#xff08;代理逻辑集成到了业务逻辑&#xff0c;通过继承来实现&#xff0c;便宜&#xff09;beacon 方式&#x…

408 考研《操作系统》第三章第二节:基本分页存储管理、两级页表、基本分段存储管理方式、段页式管理方式

文章目录教程1. 基本分页存储管理的基本概念1.1 连续分配方式的缺点1.2 把“固定分区分配”改造为“非连续分配版本”1.3 什么是分页存储1.4 如何实现地址的转换&#xff1f;1.5 逻辑地址结构1.6 重要的数据结构——页表1.7 知识回顾与重要考点2. 基本地址变换机构2.1 例题2.2 …

node.js+uni计算机毕设项目购物小程序(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

不写一行代码(一):实现安卓基于GPIO的LED设备驱动

文章目录系列文章一、前言二、准备工作2.1 内核版本2.2 内核文档&#xff1a;bindings->leds2.3 文档解析&#xff1a; leds-gpio.txt三、编写DTS3.1 查原理图&#xff0c;挑选GPIO3.2 编写DTS文件四、编译测试4.1 编译dt.img4.2 烧录dt.img五、基于fs的测试5.1 测试命令5.2…

第二十五章 数论——约数

第二十五章 数论——约数一、什么是约数二、约数的求解——试除法1、问题2、思路分析3、代码实现三、约数个数1、问题描述2、算法思路3、代码实现四、约数之和1、问题描述2、算法思路3、代码实现五、最大公约数——欧几里德算法1、问题描述2、算法思路&#xff08;1&#xff09…

前端实现文件上传(点击+拖拽)

一、简介 之前在Vue项目中使用过element的上传组件&#xff0c;实现了点击上传拖拽上传的两种上传功能。然后我就在想是否可以通过原生的htmljs来实现文件的点击上传和拖拽上传&#xff0c;说干就干。 首先是点击获取上传文件自然没的说&#xff0c;只需要借助input标签即可&a…

Java面试题(六)RabbitMQ常见面试题

RabbitMQ常见面试题RabbitMQ架构设计RabbitMQ有哪些优点RabbitMQ事物机制RabbitMQ持久化机制持久化和非持久化什么时候需要持久化&#xff1f;落盘过程文件删除RabbitMQ如何保证消息不丢失RabbitMQ如何保证消息不被重复消费RabbitMQ死信队列&#xff0c;延时队列死信队列延时队…

如何使用 ChatGPT 进行教学,教师可以利用 ChatGPT 的 5 种方式

我们听说过很多关于学生如何使用 ChatGPT 撰写论文和布置家庭作业的信息。 我们一直在讨论围绕这个问题的担忧,并争先恐后地为 ChatGPT 寻找 AI 检测工具,据传 OpenAI 也在致力于此。 但是关于教师如何将 ChatGPT 用于他们自己的工作的讨论并不多。 在从教师的角度对 Chat…

20221222 Coppeliasim的视频导出功能

Video exporter CoppeliaSim的视频记录器可以对the page, scene hierarchy and model browser areas进行录屏&#xff0c;保存成视频文件。Dialogs, menu bar, toolbars and popup menus &#xff08;对话框、菜单栏、工具栏和弹出菜单&#xff09;不会被录进去. 默认情况下&am…

【C++11】包装器

目录 1.function包装器 1.1什么是函数包装器(function)&#xff1f; 1.2为啥使用函数包装器(function)&#xff1f; 2.bind包装器 2.1绑定普通函数和调整传参顺序 2.2绑定类成员函数 1.function包装器 头文件#include<functional> 1.1什么是函数包装器(function)&a…

2022年终总结

不知不觉就到了年末&#xff0c;感叹时间过的真快。 我自己坚持写了七年多的博客&#xff0c;但这其实是我第一次去写年终总结。也不知道怎么写&#xff0c;就简单聊聊。 写博客的初衷就是个人收获&#xff0c;学习的记录&#xff0c;分享出来如果能帮到别人那就更好了。毕竟…

(一)LTspice简介2

文章目录前言一、LTspice的仿真过程二、spice的模型三、LTspice的工具栏和快捷键四、LTspice中的数量级前言 上一节我们学习了LTspice的安装&#xff0c;很简单&#xff0c;无脑安装 &#xff08;一&#xff09;LTspice安装 这一节我们继续学习LTspice的简介&#xff0c;主要包…

题:A-B 数对(二分)

A-B 数对 - 洛谷 题目背景 出题是一件痛苦的事情&#xff01; 相同的题目看多了也会有审美疲劳&#xff0c;于是我舍弃了大家所熟悉的 AB Problem&#xff0c;改用 A-B 了哈哈&#xff01; 题目描述 给出一串正整数数列以及一个正整数 CC&#xff0c;要求计算出所有满足 A…

通过Wireshark分析Apache服务器的SSL协议工作过程

文章目录一、实验环境二、为Apache服务器启用SSL1.获取SSL证书2.修改httpd.conf配置文件3.修改httpd-ssl.conf配置文件4.启动Apache服务三、SSL/TLS工作过程分析一、实验环境 操作系统&#xff1a;macOS Ventura 13.0.1 Apache&#xff1a;Apache/2.4.54 (Unix)&#xff0c;此…

喜提JDK的BUG一枚、多线程的情况下请谨慎使用这个类的stream遍历。

前段时间在 RocketMQ 的 ISSUE 里面冲浪的时候&#xff0c;看到一个 pr&#xff0c;虽说是在 RocketMQ 的地盘上发现的&#xff0c;但是这个玩意吧&#xff0c;其实和 RocketMQ 没有任何关系。 纯纯的就是 JDK 的一个 BUG。 我先问你一个问题&#xff1a;LinkedBlockingQueue…

vue3 antd table表格的样式修改(二)利用rowClassName给table添加行样式

vue3 antd项目实战——修改ant design vue组件中table表格的默认样式&#xff08;二&#xff09;知识调用场景复现修改table表格的行样式一、rowClassName添加行样式二、表格的不可控操作写在最后知识调用 文章中可能会用到的知识链接vue3ant design vuets实战【ant-design-vu…

Autosar MCAL-SPI配置及使用

文章目录前言SPI协议基础Autosar SPI专有名词SpiDriverSpiChannelSpiChannelIdSpiChannelTypeSpiDataWidthSpiDefaultDataSpiEbMaxLengthSpiIbNBuffersSpiTransferStartSpiExternalDeviceSpiBaudrateSpiAutoCalcBaudParamsSpiCsIdentifierSpiCsPolaritySpiCsSelectionSpiDataSh…