Service

Service

2021.2.25

Gary哥哥的哥哥的哥哥

后台默默的劳动者–Service

实现后台功能的Service属于四大组件之一,其重要性不言而喻

Service是什么

Service是Android中实现程序后台运行的解决方案

它非常适合执行哪些不需要和用户交互而且还要求长期运行的任务。

Service的运行不依赖任何见面,即使程序被奇幻到后台,Service仍能够保持正常运行

不过需要注意的是,Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。

当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行

实际上,Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。

也就是说,我们需要在Service的内部的手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。

Android多线程编程

如果你熟悉Java,多线程想必不会陌生,一些比较耗时间的操作都会放在子线程当中运行,避免主线程被阻塞

线程的基本用法

Android的多线程和Java有着很相似的语法

  • 一个线程继承Thread,然后重写父类的run()方法
class MyThread : Thread(){
    override fun run(){
        //逻辑代码
    }
}

当然,启动这个线程只需要

MyThread().start()
  • 上面这种方式耦合性有点高,我们可能更多的使用实现Runnable接口的方式来定义一个线程:
class MyThread : Runnable{
    override fun run(){
        //逻辑代码
    }
}

//启动线程:
val myThread = MyThread()
Thread(myThread).start()

可以看到Thread的构造函数接收了一个Runnable参数,接着调用Thread的start方法即可

当然如果你不想实现一个Runnable接口,那用Lambda表达式的写法也行

Thread{
    //编写逻辑代码
}.start()

上面这些方法在java中都可以看到,下面来个Kotlin特别的地方

  • 一个简单的开启线程的方式:
thread {
    //编写逻辑代码
}

这里的thread是一个在Kotlin内置的鼎城函数,这里连start方法都不用调用了

在子线程中更新UI

Android的UI也是也是线程不安全的,也就是说,如果想要更新应用程序里的UI元素,必须在主线程中进行,否则就会出现异常

  • 下面我们用一个例子AndroidThreadTest来验证一下
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Change Text"
        android:id="@+id/changeTextBtn"/>
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello world"
        android:textSize="20sp"
        android:id="@+id/textView"/>

</RelativeLayout>

需要注意的是,Android是不允许在子线程更新相应的UI控件的,但我们有的耗时操作如何处理好呢?

对于这种情况Android提供了一套异步消息的处理机制,完美的解决在子线程中进行UI操作的问题,我们在下一小节去分析

MainActivity:

class MainActivity : AppCompatActivity() {
    val updateText = 1
    val handler = object : Handler(){
        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对象发送出去
            }
        }
    }
}

这次我们没有在子线程当中进行UI操作

而是将它的what字段变成updateText

然后调用Handler的sendMessage()方法将这条Message发送出去,很快Handler就会收到这条message

重写hanleMessage方法

注意:此时的handleMessage()方法就是在主线程当中运行的了,可以放心使用

  • 这就是Android异步消息处理的基本用法,出色的解决了在子线程中更新UI的问题
    • 下面我们对异步消息处理机制进行进一步的分析

异步消息处理机制

Android中的异步消息处理主要由四个部分组成: Message ,Handle , MessageQueue 和 Looper

  • 这里其中Message和Handler已经在上一小节接触过了

下面我们仍对这四个部分分开来讲解一下

Message

Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。通常使用 Message 的 what 字段携带命令,除此之外还可以使用 arg1 和arg2 字段来携带一些整形数据,使用 obj 字段携带一个 Object 对象。

Handler

Handler 顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用 Handler 的 sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到 Handler 的 handlerMessage()方法中。

MessageQueue

MessageQueue 是消息队列的意思,它主要用于存放所有通过 Handler 发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个 MessageQueue 对象。

Looper

Looper 是每个线程中的 MessageQueue 的管家,调用 Looper 的 loop() 方法后,就会进入到一个无限循环当中,然后每当发现 MessageQueue 中存在一条消息,就会将它取出,并传递到 Handler 的 handleMessage() 方法中。每个线程中也只会有一个 Looper 对象。

核心步骤
  • 首先需要在主线程当中创建一个 Handler 对象,并重写 handleMessage() 方法。

  • 然后当子线程中需要进行UI操作时,就创建一个 Message 对象,并通过 Handler 将这条消息发送出去。

  • 之后这条消息会被添加到 MessageQueue 的队列中等待被处理,而 Looper 则会一直尝试从 MessageQueue 中取出待处理消息

  • 最后分发回 Handler 的 handleMessage() 方法中。

  • 由于 Handler 是在主线程中创建的,所以此时 handleMessage() 方法中的代码也会在主线程中运行,于是就可以安心地进行UI操作了。

img

一条 Message 经过这样一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新 UI 变成了可更新 UI,整个异步消息处理的核心思想也就如此。

使用AsyncTask

为了更加方便我们在子线程中对 UI 进行操作,**Android 还提供了另外一些好用的工具,AsyncTask 就是其中之一。**借助 AsyncTask,即使你对异步消息处理机制完全不了解,**也可以十分简单地从子线程切换到主线程。**当然,AsyncTask 背后的实现原理也是基于异步消息处理机制的,只是 Android 做了很好的封装而已。

于 AsyncTask 是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为 AsyncTask 类指定三个泛型参数,这三个参数的用途如下:

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

ex:

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

接着我们还需要重写 AsyncTask 中的几个方法才能完成对任务的定制。

  • onPreExecute(): 这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  • doInBackground(Params…):**这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务**。注意,在这**个方法中是不可以进行 UI 操作的。
  • **onProgressUpdate(Progress…):**当后台任务中调用了 publishProgress(Progress…)方法后,这个方法就会很快被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应地更新。
  • **onPostExecute(Result):当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,**比如提醒任务执行的结果,以及关闭掉进度条对话框等。
  • 需要注意的是,Android其实确实已经把AsyncTask弃用了,具体原因可以自己上网查阅资料,但这里为了讲解知识点,我们还是强行搞一下

ex:

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){
        e.printStackTrace()
        false
    }

    override fun onProgressUpdate(vararg values: Int?) {
        //在这里更新下载进度
        progressDialog.setMessage("Downloaded ${values[0]}%")
    }

    override fun onPostExecute(result: Boolean) {
        progressDialog.dismiss()//关闭进度条
        //这里提示下载结果
        if (result){
            Toast.makeText(context,"Succeeded",Toast.LENGTH_SHORT).show()
        }else{
            Toast.makeText(context,"Failed",Toast.LENGTH_SHORT).show()
        }
    }
}

诀窍在于:

doInBackground子线程中执行耗时任务,但不能改UI

onProgressUpdate改UI

onPostExecute收尾

想启动这个程序只需要:

DownloadTask().execute()
//execute()方法可以传入任意数量的参数,这些参数传递到doInBackground()方法当中

Service的基本用法

新建一个ServiceTest项目来进行测试,由于此部分流程的图片较多,所以具体内容自己建书本p399页,这里只展示重点代码

定义一个Service

class MyService : Service() {
    //onBind方法我们在后面的小节再做讲解
    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():创建Service时候调用
  • onStartCommand()方法在每次Service启动的时候调用
  • onDestroy()方法在Service销毁时候调用

通常情况下,如果我们希望Service一旦启动就立即去执行某些动作,就可以将逻辑卸载onStartCommand()方法里面。而当Service销毁时候,我们又要在OnDestroy()方法当中回收那些不再使用的资源

每个Service都需要在注册文件中注册才能生效

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.workaholiclab.servicetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

启动和停止Service

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

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

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Service"
        android:id="@+id/stopServiceBtn"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            startService(intent)//启动Service
        }
        stopServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            stopService(intent)//停止Service
        }
    }
}
class MyService : Service() {
    //onBind方法我们在后面的小节再做讲解
    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")
    }
}

Activity和Service的通信

在上一小节可能你也发现了,Service是在Activity中启动的。但是启动之后,Activity并不知道Service干了什么,只能叫他stop。

如果让Activity知道Service干了什么,指挥Service干什么,就要用到onBind()方法了,这力我们希望MyService实现一个下载的功能,Activity决定什么时候开始,随时查看下载进度

class MyService : Service() {

    private val mBinder = DownloadBinder()

    class DownloadBinder : 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 mBinder
    }

    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")
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

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

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Service"
        android:id="@+id/stopServiceBtn"/>
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Bind Service"
        android:id="@+id/bindServiceBtn"/>
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Unbind Service"
        android:id="@+id/unbindServiceBtn"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
    lateinit var  downloadBinder : MyService.DownloadBinder

    //ServiceConnection的匿名类
    private val connection = object : ServiceConnection{
        override fun onServiceDisconnected(name: ComponentName?) {
        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            downloadBinder = service as MyService.DownloadBinder
            downloadBinder.startDownload()
            downloadBinder.getProgress()
        }

    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            startService(intent)//启动Service
        }
        stopServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            stopService(intent)//停止Service
        }

        bindServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            bindService(intent,connection, Context.BIND_AUTO_CREATE)//绑定Service
        }

        unbindServiceBtn.setOnClickListener {
            unbindService(connection)//解绑Service
        }
    }
}

在成功绑定的时候调用

onServiceConnected()

Service的生命周期

实际上每次调用一个startService()方法,就有多少次onStartCommand()执行,但Service只会存在一个实例

还可以调用Context的bindService()来获取一个Service的持久链接,这是就会毁掉Service的onBind()方法,若还没创建,则会在此之前先调用onCreate()方法,之后调用onBind返回Ibinder对象实例,这样子就能和Service进行通信了

stopService和unbindService同时调用才会执行onDestroy方法

更多Service的小技巧

Service还存在众多的小技巧

前台Service

如果你想Service一直保持运行状态,没有回收的风险,那就要使用前台Service了,它最大的区别就是下拉状态栏的通知会常在

下面我们来实现一下:

修改MyService的onCreate()方法:

class MyService : Service() {

    private val mBinder = DownloadBinder()

    class DownloadBinder : Binder() {
        fun startDownload(){
            Log.d("MyService", "startDownload executed")
        }
        fun getProgress():Int{
            Log.d("MyService","getProgress executed")
            return 0
        }
    }

    override fun onBind(intent: Intent): IBinder {
        Log.d("MyService","onBind executed")
       return mBinder
    }

    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("my_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,"my_service")
            .setContentTitle("This is the content title")
            .setContentText("This is the content text")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.ic_launcher_background))
            .setContentIntent(pi)
            .build()
        startForeground(1,notification)
    }

    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")
    }
}

这里和前面的Notification很类似,只不过我们没有使用NotificationManager将通知显示出来,而是

startForeground(1,notification)

p1:通知的id,类似于notify()方法的第一个参数

p2:构建Notification对象

startForeground(1,notification)方法会让MyService变成一个前台Service并在系统状态栏显示出来

Android 9.0 后,前台Service还需要在注册文件注册

  • 对了,前台Service在Android 8.0就有了
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.workaholiclab.servicetest">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    ......

使用IntentService

如果直接在Sercice里处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)

这时候,Android的多线程技术,我们应该在Service的每个具体的方法里面开启一个子线程,然后在这里处理那些耗时的逻辑,因此一个标准的Service改成写法如下:

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("MyService","onStartCommand executed")
    thread { 
        //处理具体的耗时逻辑
    }
    return super.onStartCommand(intent, flags, startId)
    
}

但这种Service一旦启动就会一直处于运行状态,必须调用stopService()或stopSelf()方法或者被系统回收Service才会停止,想让一个Service在执行完毕之后就停止

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("MyService","onStartCommand executed")
    thread {
        //处理具体的耗时逻辑
        stopSelf()
    }
    return super.onStartCommand(intent, flags, startId)
}

为了进一步我们管理Service,Android专门提供了一个IntentService方法,很好解决了程序员忘记开启线程,或者忘记调用stopSelf()方法

新建一个MyIntentService类:

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("MyIntentService","onDestroy executed")
    }

}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

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

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

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Bind Service"
        android:id="@+id/bindServiceBtn"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Unbind Service"
        android:id="@+id/unbindServiceBtn"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start IntentService"
        android:id="@+id/startIntentServiceBtn"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
    lateinit var  downloadBinder : MyService.DownloadBinder

    //ServiceConnection的匿名类
    private val connection = object : ServiceConnection{
        override fun onServiceDisconnected(name: ComponentName?) {
        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            downloadBinder = service as MyService.DownloadBinder
            downloadBinder.startDownload()
            downloadBinder.getProgress()
        }

    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            startService(intent)//启动Service
        }
        stopServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            stopService(intent)//停止Service
        }

        bindServiceBtn.setOnClickListener {
            val intent = Intent(this,MyService::class.java)
            bindService(intent,connection, Context.BIND_AUTO_CREATE)//绑定Service
        }

        unbindServiceBtn.setOnClickListener {
            unbindService(connection)//解绑Service
        }
        
        startIntentServiceBtn.setOnClickListener { 
            //打印主线程id
            Log.d("MainActivity","Thread id is ${Thread.currentThread().name}")
            val intent = Intent(this,MyIntentService::class.java)
            startService(intent)
        }
    }
}

记得注册哦!!!

<service android:name=".MyIntentService"
            android:enabled="true"
            android:exported="true"/>
    </application>

</manifest>

但其实,MyIntentService已经被Java弃用了

Service博客完更

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值