本文主要内容:首先介绍 Android 自带的 Activity 互相传递数据、Activity 与 Fragment 互相传递数据、Activity 与 Service 互相传递数据。随后分析它们的使用场景与优缺点,引出 EventBus,最后介绍 EventBus 的基本使用。
注:本文 Demo 皆为 kotlin 编写,有 Java 基础的读者应该能大致理解,请放心阅读。
注2:本文属于垃圾文章补完计划
Android 自带的数据通信机制
Activity 与 Activity 互相传递数据
我们将从 AActivity 启动 BActivity,并通过 intent 传送数据。
AActivity 的 onCreate 方法内:
into_b.setOnClickListener {
val intent = Intent(baseContext, BActivity::class.java)
//传递基本类型
intent.putExtra("att1", "普通的字符串")
//传递序列化对象
val person = Person("xloger", 18)
intent.putExtra("att2", person)
startActivity(intent)
}
BActivity 的 onCreate 方法内:
val sendMsg = intent.getStringExtra("att1")
val person = intent.getSerializableExtra("att2") as? Person
Log.d("XExample", "收到的消息:$sendMsg,收到的对象:$person")
输出结果:
D/XExample: 收到的消息:普通的字符串,收到的对象:Person(name=xloger, age=18)
注意,虽然 intent 看似有传 extra 和传 bundle 两种方式,但是 extra 只是 bundle 的封装。需要传递对象时需该类实现 Serializable 接口。
而当需要接收 BActivity 的返回数据时,需将原先的startActivity(intent)
改为startActivityForResult(intent,233)
,其中整型233为 requestCode,作用是区分不同请求。
而 BActivity 在销毁前通过以下代码即可传递数据给 AActivity:
val resultIntent = Intent()
resultIntent.putExtra("resultMsg", "返回的内容")
setResult(124, resultIntent)
然后在 AActivity 类中实现以下方法:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 233 && resultCode == 124) {
Log.d("XExample", "返回的内容为:${data?.getStringExtra("resultMsg")}")
}
}
当进入 BActivity 后返回 AActivity,输出结果:
D/XExample: 返回的内容为:返回的内容
注意,requsetCode 与 resultCode 目的是为了区分不同的请求。而实际开发中,这些数应该统一放入常量池中,防止数据过多时不好区分。
Activity 与 Fragment 传递数据
他俩交互就很简单了,Activity 内使用 Fragment 时,基本都会创建 Fragment 的实例,既然实例都有了想向 Fragment 传递实例轻而易举。而 Fragment 向 Activity 传递数据的方式,其实你在创建 Fragment 时勾选“Include interface callbacks?”即可。或者,因为本身 Fragment 就有 getActivity() 方法,可以直接获取 Activity 然后通信,但是这样耦合度比较高,还是推荐采用接口实现。
Activity:
class CActivity : AppCompatActivity(), AFragment.OnFragmentInteractionListener {
override fun onFragmentInteraction(uri: Uri) {
Log.d("XExample", "接收到Fragment发送的内容:$uri")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_c)
val transaction = supportFragmentManager.beginTransaction()
val aFragment = AFragment()
transaction.add(R.id.frame, aFragment)
transaction.commit()
act_btn.setOnClickListener {
aFragment.changeText("从 activity 向 fragment 传递数据")
}
}
}
Fragment:
class AFragment : Fragment() {
private var mListener: OnFragmentInteractionListener? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val inflate = inflater!!.inflate(R.layout.fragment_a, container, false)
inflate.findViewById<Button>(R.id.fragment_btn).setOnClickListener {
onButtonPressed(Uri.parse("http://www.baidu.com"))
}
return inflate
}
fun changeText(content: String) {
fragment_text.text = content
}
fun onButtonPressed(uri: Uri) {
if (mListener != null) {
mListener!!.onFragmentInteraction(uri)
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
mListener = context
} else {
throw RuntimeException(context!!.toString() + " must implement OnFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
mListener = null
}
interface OnFragmentInteractionListener {
// TODO: Update argument type and name
fun onFragmentInteraction(uri: Uri)
}
}
Activity 与 Service 交互
Service 有两种启动方式, bindService 与 startService。bindService 的话其实跟 Activity 与 Fragment 交互类似,能获取到 service 的实例一切很简单,我拿一个我以前的下载 Service 举例,Service 提供了对应的下载状态回调:
class DownloadService : Service() {
private var callBack: DownloadCallBack? = null
private var binder: DownloadBinder by Delegates.notNull()
override fun onCreate() {
binder = DownloadBinder()
}
override fun onBind(intent: Intent): IBinder? {
// val url = intent.getStringExtra("downloadUrl")
// localPath = intent.getStringExtra("localPath")
// type = intent.getStringExtra("type")
// if (url.isEmpty()) {
// Xlog.toast(this, getString(R.string.not_have_download))
// return binder
// }
// download(url)
return binder
}
fun setDownloadCallBack(callBack: DownloadCallBack) {
this.callBack = callBack
}
inner class DownloadBinder : Binder() {
/**
* 返回当前服务的实例
*
* @return
*/
val service: DownloadService
get() = this@DownloadService
}
interface DownloadCallBack {
/**
* 返回实时下载进度,1秒调用一次。
* 需要调整调用评论修改 @see com.xloger.civitas.DownloadService.DownloadChangeObserver.onChange 方法内 scheduleAtFixedRate 方法的最后一个参数
*/
fun onProgress(nowBytes: Int, allBytes: Int)
/**
* 下载成功后回调
* @param type 调用时传入的文件类型
* @param path 文件的路径
*/
fun onSuccess(type: String, path: String)
/**
* 下载失败后回调
*/
fun onError(errorCode: Int, errorMsg: String = "")
}
}
然后调用方:
val serviceIntent = Intent(context, DownloadService::class.java)
serviceIntent.putExtra("downloadUrl", downloadUrl)
with(Build.VERSION_CODES.M) {
val permissions = XPermission.findDeniedPermissions(context, XPermission.Write_SD)
if (permissions.size == 0) {
Xlog.log("有SD卡权限,保存在公开目录")
serviceIntent.putExtra("localPath", localPath)
}
}
serviceIntent.putExtra("type", fileType)
context.bindService(serviceIntent, object : ServiceConnection {
override fun onServiceDisconnected(p0: ComponentName?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
val binder = p1 as DownloadService.DownloadBinder
if (downloadCallBack != null) {
binder.service.setDownloadCallBack(downloadCallBack)
}
}
}, Service.BIND_AUTO_CREATE)
至于 startService,没法获取实例,那就有一种更通用的做法了,就是广播。下面展示一个简单的例子:Activity 有两个按钮一个是启动 Service,一个是停止 Service,还有一个文本框是用来显示状态。然后 UpdateTextReceiver 的构造方法接收一个回调,当收到了广播时执行该回调。而 UpdateTextService 则执行一个异步任务,一秒钟发送一次广播更新 UI。
UpdateTextReceiver:
class UpdateTextReceiver(val callback: (text: String) -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
callback(intent.getStringExtra("text"))
}
}
UpdateTextService:
class UpdateTextService : Service() {
private var isRun = true
override fun onBind(intent: Intent): IBinder? {
throw UnsupportedOperationException("Not yet implemented")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val intent = Intent("updateText")
var time = 0
Thread {
run {
while (isRun) {
intent.putExtra("text", time.toString())
sendBroadcast(intent)
time++
Thread.sleep(1000)
}
}
}.start()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
isRun = false
}
}
Activity:
class DActivity : AppCompatActivity() {
private var mReceiver by Delegates.notNull<UpdateTextReceiver>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_d)
mReceiver = UpdateTextReceiver {
d_text.text = it
}
registerReceiver(mReceiver, IntentFilter("updateText"))
start_service_btn.setOnClickListener {
startService(Intent(baseContext, UpdateTextService::class.java))
}
stop_service_btn.setOnClickListener {
stopService(Intent(baseContext, UpdateTextService::class.java))
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(mReceiver)
}
}
分析总结 Android 自带的通信机制
可以看到,你需要任意两个组件通信时是都能做到的,但是问题是方式不统一,而且有的复杂有点麻烦,比如 Activity 之间直接通过 Intent 传递参数就行,而能持有实例的时候通直接调用或者用回调实现,再复杂一点就是用广播实现。
而且当数据量比较大的时候,比如用 Intent 传容易发送 TransactionTooLargeException,所以项目中通常使用 EventBus 来完成数据通信功能。
EventBus 使用
Github 地址
导入 Lib 后,我们先用一个之前出现的例子(Activity 根据 Service 的进度更新 UI)展示一下 EventBus 的使用。
Activity:
class EBMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ebmain)
EventBus.getDefault().register(this)
eb_start_service.setOnClickListener {
startService(Intent(baseContext, EBService::class.java))
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun updateText(text: String) {
eb_text.text = text
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
stopService(Intent(baseContext, EBService::class.java))
}
}
Service:
class EBService : Service() {
private var isRun = true
override fun onBind(intent: Intent): IBinder? {
throw UnsupportedOperationException("Not yet implemented")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
var time = 0
Thread {
run {
while (isRun) {
EventBus.getDefault().post(time.toString())
time++
Thread.sleep(1000)
}
}
}.start()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
isRun = false
}
}
可以看到,EventBus 的使用效果跟广播有些类似,发送方调用 EventBus 的 post 方法就能发送对应内容了,接收方需要在生命周期内调用 register 和 unregister 方法,然后实现 @Subscribe 注解的方法将收到对应消息。
如何区分多个 EventBus 事件
当发送方 post 时,与 post 参数相同的接收方都会收到回调,而如果我们如果经常用 String 或者 Int 这样的基本类型当参数,很可能会混乱,所以通常一个事件我们定义一个类,避免了冲突问题,例如这样:
class Event {
data class DownLoadProgressEvent(val name: String, val progress: Long, val size: Long)
data class DownLoadReqEvent(val name: String, val url: String, val path: File)
}
这里的 data 是 kotlin 的一个特性,默认每个参数都实现了 get set 方法之类的。然后我们改写之前的代码:
Service:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
var downloadSize = 0L
val maxSize = 100L
Thread {
run {
while (isRun) {
EventBus.getDefault().post(Event.DownLoadProgressEvent("xxx.jpg", downloadSize, maxSize))
downloadSize++
Thread.sleep(1000)
if (downloadSize >= maxSize){
stopSelf()
}
}
}
}.start()
return super.onStartCommand(intent, flags, startId)
}
Activity 则加入这个方法:
@Subscribe(threadMode = ThreadMode.MAIN)
fun updateProgress(ev: Event.DownLoadProgressEvent) {
eb_text.text = "${ev.name}已下载:${ev.progress} / ${ev.size}"
}
Subscribe 参数的介绍
Subscribe 一共有 threadMode、sticky、priority 三个参数。
threadMode 代表该方法将在哪个线程被调用,有五个值可选:
– POSTING:发送方所在的线程。如果你不指定 threadMode 那么默认就是这个。
– MAIN:如果发送方线程为主线程,则直接执行该方法;如果发送方不是主线程,则将该方法加入队列等待自动执行。
– MAIN_ORDERED:无论发送方线程是啥,都加入队列等待自动执行。
– BACKGROUND:如果发送方是主线程,则新建一个线程执行方法;如果发送方不是主线程,则在该线程执行方法。
– ASYNC:直接将该方法假如到线程池执行。
需要注意下 MAIN_ORDERED 与 MAIN 的区别,MAIN_ORDERED 是新版本加入的,因此网上很多哪怕是 EventBus3.0 的教程也没有,比如现在有一个事件正在等待被执行,此时第二个事件假如用的是 MAIN 的话,则会立即执行(因为是主线程直接调用该方法),而如果第二个事件是 MAIN_ORDERED 的话,则会入队,等第一个事件执行完毕再执行。因此 MAIN_ORDERED 更可控更一致,官方也是更加推荐这种做法。
而 BACKGROUND,假如有多个 BACKGROUND 的事件等待执行,它们会按顺序执行,视觉效果上跟只有一个线程一样(虽然实现上并不是同一个线程)。至于 ASYNC,当多个事件同时等待执行时,它们会一同执行。
sticky:默认值 false,类似粘性广播,它的作用是比如你希望发送某个事件,而接收者此时还没注册,这时候就需要粘性广播了。正常的事件提交就是 post
方法,而粘性事件提交就是 postSticky
方法。同时,接收者的 sticky 必须得是 true 才能收到。我们模拟一个 Intent 传递消息的机制:
对上面的 Activity 改动:
eb_start_service.setOnClickListener {
EventBus.getDefault().postSticky(Event.DownLoadReqEvent("download.jpg", "www.baidu.com", File("SDPath/download.jpg")))
startService(Intent(baseContext, EBService::class.java))
}
然后在 Service 也注册 EventBus,并实现该方法:
@Subscribe(sticky = true)
fun parseDownloadInfo(ev: Event.DownLoadReqEvent) {
Log.d("XExample", ev.toString())
}
请注意,粘性事件的订阅者会自动获取匹配的最后一次粘性事件,如果你不希望这样做那请消费该事件后删除它。比如我写了一个鉴权页面,当鉴权成功后会发送一个粘性事件,调用页订阅了这个粘性事件,假如我调用页打开鉴权页,鉴权失败,回到调用页,一切正常,订阅方法并不会调用。如果鉴权成功回到调用页,订阅方法被调用,也是一切正常。但是,当你鉴权成功一次后,再次鉴权,哪怕失败了回到调用页,依旧会调用订阅方法。
因此合适的处理方案就是每次消费完该粘性事件后,删除它,代码为 EventBus.getDefault().removeStickyEvent(event)
。
priority:优先级,默认值0。当有多个接收者都将接收某事件时,priority 大的会获得优先处理权,还能通过以下代码取消该事件向更低优先级的接收者传递:
EventBus.getDefault().cancelEventDelivery(event)
EventBus 的性能优化
在 EventBus3.0后,因为改用注解实现功能,而它可以通过在编译器生成索引提高效率,因此官方开发了 EventBusAnnotationProcessor(注解分析生成索引)技术。而我并不怎么了解它,为了不误导读者,推荐https://zhuanlan.zhihu.com/p/20871335该文。如果你只是为了使用,可以跳过原理分析部分,使用并不复杂。
所以说,互联网上已经有很多比我厉害写的更好的文章了,我这篇文章本身价值不大,不过既然决定进行垃圾文章补完计划
了,那我就献丑了。
总结
当需要解决数据通信这个基本而重要的功能时,市面上有这几种解决方案:Android 自带的交互机制与手写回调、通过观察者模式自己封装一个通信框架、EventBus、RxJava。
Android 自带的学习成本低,简单的数据时较为方便。但是各种组件交互之间比较混乱,而且有些复杂度比较高(比如 Fragment 之间交互)。因此商业项目推荐采用后面几种方式解决数据通信问题。
通过观察者模式自己写一个类似 EventBus 的机制,因为我在查阅 EventBus 时阅读到了这篇文章https://zhuanlan.zhihu.com/p/26160377,大概是我的水平不够,对作者描述的问题并不太能理解,因此目前持观望态度。
EventBus 如上文介绍,是我觉得目前最适合应用,学习成本最低,解决方案也简单的方式。
RxJava 我并不熟悉,但是也有很多人拿 RxBus 与 EventBus 比较。因为我看 RxJava 宣传的一些优势,很多 Kotlin 与 Anko 已经解决了。所以你尽可以根据自己需求,假如你近期打算学习 RxJava 那就去试试 RxBus 吧。
参考资料
EventBus 3.0 事件总线介绍
EventBus 3.0的用法详解
老司机教你 “飙” EventBus 3
https://www.jianshu.com/p/aa52bc01c317