本篇总结使用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更新。
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。
给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在设备开机后自启动成功了。
也可以在系统设置中查看正在运行的服务,设置--关于手机--多次点击版本号进入开发者模式--系统和更新--开发人员选项--正在运行的服务。