Android 数据通信:从基本的Activity、Fragment、Service数据传递到EventBus的使用

in 垃圾文章

本文主要内容:首先介绍 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

标签:, , , , ,
© 2018 一隅 - Powered by Wordpress / Theme: Tabinikki