Android--线程间通信 + Service用法

本篇总结使用kotlin语言实现的几种线程通信方法(包括异步消息处理机制、Thead使用方法、AsyncTask工具使用方法)和Service的用法,包括Service生命周期、独立运行、与Activity通信、前台Service、无页面自启动Service。

线程间通信

通常进程的主线程用来处理页面更新等UI相关的操作,如网络请求等耗时操作会开子线程去执行,防止主线程阻塞导致页面卡住,给用户带来不好的体验,子线程执行获得的数据需要显示在UI上,所以需要线程间进行通信。

1. 异步消息处理机制

Android 中的异步消息处理主要由4部分组成:Message、Handler、MessageQueue 和Looper 。

  • Message
    Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。

  • Handler
    Handler主要是用于发送和处理消息,发送消息一般使用Handler 的sendMessage()方法、post()方法等,发出的消息经过一系列辗转处理,最终会传递到Handler 的handleMessage()方法中。

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

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

异步消息处理流程:
① 在主线程创建Handler对象,重写handleMessage()方法,该方法做更新UI操作;
② 定义子线程执行耗时操作获取到结果数据,创建Message对象,将结果数据携带在Message对象中,通过主线程Handler对象的sendMessage方法将携带子线程数据的Message对象发给主线程;
③ Message传递到主线程中,在handleMessage中接收并处理数据,实现UI更新。

1.png

2. Thread的使用方法

  • 常规用法

定义一个线程需要新建一个实现Runnable接口的类,重写父类Thread中的run()方法来实现耗时操作即可。

class MyThread: Runnable{
    override fun run() {
        // 耗时操作(此处用以100个数字相加为例)
        var num = 0
        while (num < 100){
            num += 1
        }
        Log.d("cyclicAdd", "num = $num ")
    }
}

启动线程是在需要执行耗时操作的地方创建MyThread实例,调用start()方法启动线程,线程中的耗时操作就会开始执行

        val myThread = MyThread()
        Thread(myThread).start()
  • 简单用法

可以不用专门创建类定义线程,可以直接使用Lambda方式定义一个子线程

        Thread{
            // 耗时操作(此处用以100个数字相加为例)
            var num = 100
            while (num < 200){
                num += 1
            }
            Log.d("cyclicAdd", "num = $num ")
        }.start()

or

        thread{
            // 耗时操作(此处用以100个数字相加为例)
            var num = 100
            while (num < 200){
                num += 1
            }
            Log.d("cyclicAdd", "num = $num ")
        }
    }
  • 子线程获取数据,主线程更新UI

做一个逻辑简单的练习,效果是点击按钮更新TextView的text内容,子线程获取到text内容,send至主线程更新UI。
注意:页面销毁时要释放handler资源。

class MainActivity : AppCompatActivity() {

    var handler : Handler? = null
    val update = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initView()
        initThread()
    }

    private fun initView(){

        btn.setOnClickListener{
            thread {
                // 执行耗时操作将获取的数据通过arg或obj携带给其他线程
                val msg = Message()
                msg.what = update
                msg.obj = "Nice to meet you!"
                handler?.sendMessage(msg)
            }
        }

    }

    private fun initThread(){

        handler = object : Handler(Looper.getMainLooper()){
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                when(msg.what){
                    update ->
                        textview.text = msg.obj.toString()
                }
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        // 释放handler资源
        handler?.removeCallbacksAndMessages(null)
        handler = null
    }

}

3. AsyncTask工具实现异步消息处理

AsyncTask是一个抽象类,必须创建子类继承并重写AsyncTask的4个方法执行task:

  • onPreExecute()
    这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作。

  • doInBackground(Params...)
    这个方法中的所有代码都会在子线程中运行,在这里去处理所有的耗时任务,可以通过return语句将任务的执行结果返回。

  • onProgressUpdate(Progress...)
    当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate (Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行更新。

  • onPostExecute (Result)
    当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行UI操作。

class DownloadTask(context: Context) : AsyncTask<Unit, Int, Boolean>() {

    private var num = 0

    @SuppressLint("StaticFieldLeak")
    val mContext = context

    override fun onPreExecute() {
        super.onPreExecute()
        Toast.makeText(mContext, "UI初始化操作", Toast.LENGTH_SHORT).show()
    }

    override fun doInBackground(vararg params: Unit?) = try{
        // 子线程执行耗时操作
        while(true){
            val downloadPercent = doDownload()
            publishProgress(downloadPercent)
            if (downloadPercent >= 10){
                break
            }
        }
        true
    }catch (e: Exception){
        false
    }

    override fun onProgressUpdate(vararg values: Int?) {
        super.onProgressUpdate(*values)
        Toast.makeText(mContext, "下载进度 = ${values[0].toString()}", Toast.LENGTH_SHORT).show()

    }

    override fun onPostExecute(result: Boolean?) {
        super.onPostExecute(result)
        Toast.makeText(mContext, "下载结果:${result.toString()}", Toast.LENGTH_SHORT).show()
        num = 0
    }

    private fun doDownload() : Int{
        return ++num
    }

}

MainActivity.kt使用:

    private fun initView(){
        btn.setOnClickListener{
            downloadTask = DownloadTask(this)
            downloadTask.execute()
        }
    }

Service基本用法

1. Service生命周期

  • onCreate():Service创建时调用,每个Service 只会存在
    一个实例,当开启Service的应用进入后台时,Service有可能会被系统回收掉。

  • onStartCommand():Service每次启动时调用,只有当Context通过startService()方法开启服务时才会回调该方法,Activity与Service绑定时不会调用该方法。

  • onBind():当调用Context的bindService()来获取一个Service 的持久连接, 这时就会回调Service的onBind()方法,Activity通过该方法获取到IBinder实例,可以选择执行实例中的方法逻辑。

  • onDestroy():用来销毁Service,当Context单独调用stopService或unbindService时都会执行该方法,但当Context同时调用startService和bindService时,只有同时调用stopService和unbindService才会执行该方法销毁Service。

2. Service独立运行

如果单独开启或停止活动的话,所有逻辑内容都在Service的生命周期中执行,不需要Activity参与。

  • 创建服务
    AS项目目录,在需要创建服务的包名文件夹右击—>New—>Service—>Service,Exported是否将这个Service暴露给外部程序访问,Enabled是否启用这个Service。
4.png

给Service中的回调方法加日志<MyService.kt>

class MyService : Service() {

    override fun onCreate() {
        super.onCreate()
        Log.d("=======", "Service onCreate =======")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("=======", "Service onStartCommand =======")
        // 类似 onResume
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent): IBinder {
        TODO("Not yet implemented")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("=======", "Service onDestroy =======")
    }

}

在Activity中增加开启和关闭Service的按钮<MainActivity.kt>

   private fun initView(){

       start_service.setOnClickListener{
           startService(Intent(this, MyService::class.java))
       }

       stop_service.setOnClickListener{
           stopService(Intent(this, MyService::class.java))
       }

   }

当第一次点击开启Service时,首先会回调onCreate()创建Service,当Service已经存在实例时,再次点击开启Service只会回调onStartCommand(),类似于Activity的onResume();当点击关闭Service时,会回调onDestroy()销毁Service。此方法开启的Service非前台Service,随时有可能被回收掉。

3. Service与Activity通信

Activity可以指定Service执行哪些逻辑,流程大致为:① Service中新建一个继承自Binder的类,提供实现功能的方法,在onBind()回调中返回该类的实例;② Activity中创建一个SereviceConnection匿名类的实现,重写绑定和解绑的方法,在重写的onServiceConnected绑定方法中可以获取onBind()返回的实例,通过该实例执行内部的方法;③ Activity与Service的绑定主要是通过bindService方法,方法主要通过传intent和SereviceConnection匿名类的实现来完成Activity和Service的绑定。

<MyService.kt>

class MyService : Service() {

    private val mBinder = DownloadBinder()

    class DownloadBinder : Binder(){
        fun startDownload(){
            Log.d("=======", "Service startDownload =======")
        }

        fun getProgress(): Int{
            Log.d("=======", "Service getProgress =======")
            return 0
        }
    }

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

    override fun onCreate() {
        super.onCreate()
        Log.d("=======", "Service onCreate =======")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("=======", "Service onStartCommand =======")
        // 类似 onResume
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("=======", "Service onDestroy =======")
    }

}

<MainActivity.kt>

    private fun initView(){

        start_service.setOnClickListener{
            bindService(Intent(this, MyService::class.java), connection, Context.BIND_AUTO_CREATE)
        }

        stop_service.setOnClickListener{
            unbindService(connection)
        }

    }

4. 前台Service

Android 8.0系统开始,Service所在的应用进入后台,该Service就有可能被系统回收,想要Service一直处于运行状态的话可以将其改为前台Service,即使退出应用程序Service仍会处于运行状态。

修改Myservice.kt中的onCreate,创建前台Service:

    override fun onCreate() {
        super.onCreate()
        Log.d("=======", "Service onCreate =======")

        // 前台Service
        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("Service title")
            .setContentText("Service content")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background))
            .setContentIntent(pi)
            .build()
        startForeground(1, notification)
        
    }

从Android 9.0 系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明。

    <!-- 前台Service权限 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

5. 无页面自启动Service

有时需要设备重启后自启动一些服务,比如手机微信接收消息,比如带物理按键的设备,设备启动后点击物理按键可以执行对应操作,这时就需要写一个无页面的自启动服务,监听物理按键对应的按键事件,然后根据键值映射执行指定操作。

实现无页面自启动Service:
① 创建一个Service,在Service中实现该服务的功能;
② 静态注册系统开机广播,在接收到开机广播时开启服务;
③ 隐藏该应用程序的应用图标;

  • 创建Service

由于是无页面的Service,开启Service之后要一直处于运行状态,所有需要使用前台Service。

<MyService.kt>

class MyService : Service() {

    private val TAG = "noUI-MyService"

    override fun onBind(intent: Intent): IBinder {
        TODO()
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Service onCreate =======")

        // 前台Service
        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("Service title")
            .setContentText("Service content")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background))
            .setContentIntent(pi)
            .build()
        startForeground(1, notification)

        // 添加按键事件监听
        // addEventListener()
    }

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

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "Service onDestroy =======")

        // 移除按键事件监听
        // removeEventListener()
    }

    // 按键事件执行的逻辑
    // handleEvent()

}
  • 静态注册开机广播

apk通过注册系统开机广播实现Service自启动功能,当监听到设备开机广播时开启要使用的Service。Android 8.0 之后为了保护用户安全和隐私,需要在AndroidManifest.xml中配置权限,开机广播和前台服务都需要配置。

<BootReceiver.kt>

class BootReceiver : BroadcastReceiver(){

    private val TAG = "noUI-BootReceiver"

    override fun onReceive(context: Context?, intent: Intent?) {
        // 开启服务
        context?.startService(Intent(context, MyService::class.java))
        Log.d(TAG, "开机服务自启动.....")
    }

}
  • 隐藏应用图标

隐藏应用图标只需在MainActivity中增加一个data属性即可。

<AndroidManifest.xml>

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

    <!-- 前台Service -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <!-- 开机广播 -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <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/Theme.NoUIServiceDemo">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <!-- 隐藏应用图标 -->
                <data android:scheme="nouiapp" android:host=".app" android:pathPrefix="/openwith"/>
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- 注册广播接收器 -->
        <receiver android:name=".BootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

        <!-- 注册Service -->
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"/>

    </application>

</manifest>
  • 查看正在运行的服务

完成以上3步之后运行程序,设备上会显示MainActivity中的内容,且Service未启动,重启设备后就看不到该Service的界面了,设备完全启动后Service中重写的方法会被执行,说明无页面Service在设备开机后自启动成功了。

2.png

也可以在系统设置中查看正在运行的服务,设置--关于手机--多次点击版本号进入开发者模式--系统和更新--开发人员选项--正在运行的服务。

3.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容