0 x1, introduction

EventBus, just like Handler, is a cliche, a tutorial that’s been around for a while, and an interviewer will occasionally ask you:

Looking at the Commit record, which dates back to July 2012, the author hasn’t had a major code update for quite a few years, probably because the library has stabilized + there are better alternatives (LiveData, Rx).

write Thin | read the book “the beauty of design patterns, design patterns and paradigm (behavior – the observer pattern), passing through the its source, understanding the initialization, subscribe, unsubscribe, send, ordinary, send the cohesive events about call process and principle. However, I didn’t fully understand it. I wanted to explore it more carefully, so I wrote this article to record the process of realizing EventBus. Before we do that, let’s take a look at how data delivery was handled without EventBus.


0x2, nesting doll → the most original page data transfer

The original page data transfer

Android page can be generally divided into two types: Activity and Fragment, data transfer also includes data return, the three common transfer is as follows:

(1) the Activity < = > Activity

Simple scenario:

/* ====== one-way transmission ====== */

// OriginActivity passes data to TargetActivity
val intent = Intent(this, TargetActivity::class.java)
intent.putExtra("info"."Some Data")
startActivity(intent)

// TargetActivity Parses the data passed by the OriginActivity
val data = getIntent().getStringExtra("info")

/* ====== Send data to ====== */

// Pass the Intent instance along with the request code
startActivityForResult(intent, 0x123)

// OriginActivity overwrites this method and calls back when the incoming data is parsed and the TargetActivity is destroyed
override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 0x123) {
        valbackData = intent? .getStringExtra("back_info")}}// TargetActivity Calls this method before finish() to set the data to be returned:
setResult(0x123, Intent().putExtra("back_info"."Some Back Data"))
finish()
Copy the code

To A more complex scene, such as this page open order: A → B → C → D, and then have the following requirements:

  • (1) D is notified to refresh the page after A time-consuming operation is performed in A:

A: startActivity(intent); b: onNewIntent(Intent); C: startActivity(intent);

  • ② D sends data back to A

(intentActivityForResult) (intentActivityForResult) (intentActivityForResult) (intentActivityForResult) (intentActivityForResult) (intentActivityForResult) (IntentActivityForResult) (IntentActivityForResult) (IntentActivityForResult) (IntentActivityForResult)

If there is only one scene in D, it will be awkward… Let’s talk about another common scenario: the registration process

  • (3) Suppose A is the login page and BCD is the filling information page. After D fills in, click Finish to close DCB and pass the registration information to A

StartActivityForResult (Intent) : D passes information to C, C to B, and B to A

  • D: B: finish(); D: B: FINISH (); D: B: FINISH ()

Idle and boring children’s shoes can downward how to do? True story, don’t doubt the product manager’s imagination! This is followed by data transfers to activities and fragments.

(2) the Activity < = > fragments

// Fragments were not added to the FragmentManger
TargetFragment targetFragment = TargetFragment()
targetFragment.setArguments(Intent())

supportFragmentManager.beginTransaction().add(targetFragment, "TargetFragment").commit()

// Added to the FragmentManager
// (1) Interface callback, Fragment implementation interface, Activity call corresponding callback incoming data;
// ② The Fragment defines a public method. The Activity is passed directly through the Fragment instance calling method.

// Data return Activity
// (1) Interface callback, Activity to implement the interface, Fragment call corresponding callback back;
GetActivity () getActivity() getActivity() getActivity() getActivity();
Copy the code

The above is the data transfer between Fragment and host Activity, while the data transfer between Fragment and non-host Activity is another layer on the host Activity and non-host Activity, which obviously exposes the coupling problem. Moreover, the task of hosting the Activity is too heavy, dealing with data transfer between activities and subfragments.

(3) fragments < = > fragments

With the host Activity

  • Target fragments Settings defined in the data, the method of launching fragments called getActivity () getSupportFragmentManager () findFragmentByTag turn () + strong for target Fragment instance, Then call the method that sets the data;
  • Interface callback → define the interface, the Activity implements the interface (define the method of updating the target Fragment), the incoming initiates the Fragment, and calls this method when the incoming Fragment wants to pass data;

Different host activities

Initiate Fragment → host Activity → target host Activity → target Fragment

It is easy to see that the data transfer mechanism mentioned above is too much coupling between the pages, and if there are multiple sub-fragments, or a bunch of sub-fragments nested, the Activity will become oversized, so you need to write a way to decouple.


0x3, temporary cope → data temporary storage + life cycle callback

That is, use memory or hard disk to temporarily store the data to be passed, and then read in the Activity, Fragment corresponding lifecycle callback. A simple example is as follows:

// Data staging class
object DataTempUtils {
    private val tempDataSet = hashMapOf<String, Any>()

    fun getTempDataByKey(key: String) = tempDataSet[key]


    fun updateTempData(key: String, any: Any) {
        this.tempDataSet[key] = any
    }
}

// Parse the page that sent back data
override fun onResume(a) {
    super.onResume()
    val backData = DataTempUtils.getTempDataByKey("${this.javaClass.name}")
    Log.e("Test", backData.toString())
}

// Pass back the data page
DataTempUtils.updateTempData("com.example.test.IndexActivity"."Some Back Data")
Copy the code

Compared to the original method of data transfer, it is a little simpler. When the data is passed, it is written, and when the data is read, it is read in the callback of the lifecycle function, but there are the following problems:

  • How to generate a unique identifier? Who is in charge? The page itself, or write another utility class?
  • (2) May have done some invalid operations: each time in the life cycle callback active pull, regardless of whether the data is updated;
  • (3) Additional processing logic is introduced, such as judging whether it is the first time to enter onResume and judging the validity of data;
  • (4) Some strange bugs may be introduced: for example, someone has updated the data in other places, but you do not know, resulting in the data has been wrong;

0x4. Lessons learned → local broadcast

The data cache method is not robust, try another solution → Broadcast, one of the four components of Android, can be used for intra-process communication, also can be used for certain components of the process information/data transfer. You can use it, but it’s too heavy to use directly! How do you say? Look at the internal process of launching a broadcast:

  • ① sendBroadcast initiates a broadcast
  • ② Inform system_server of the broadcast information
  • ③ System_server finds the corresponding receivers
  • ④ The broadcast is queued for distribution
  • ⑤ Call the onReceiver() callback of App process Receiver

Two Binder calls are required for use within our APP, and broadcasts can be received by others (at risk of hijacking), and can even be faked to deceive our receivers. Of course, you can circumvent this by configuring permissions:

  • When sending: specify the permission the recipient must have, or intent.setpackage () sets that are valid only for one program;
  • Android :exported=”false”;

To address the above issues, Android v4 package introduced lightweight LocalBroadcast → LocalBroadcast, the use is very simple: Dynamically register the broadcasts to be monitored in the Activity and Fragment and bind them. When a broadcast is sent, register the receiver of this type of broadcast and call back the corresponding onReceiver() method. The following is a code example:

// ============ Dynamic registration for broadcast ============

// Instantiate the broadcast receiver instance (anonymous inner class is lazy here, otherwise you would have to define your own broadcast receiver class)
private var mReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context? , intent:Intent?). {
        Log.e("Test"."Return data received")}}// Register for broadcast
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, IntentFilter("index_action"))

// Unregister the broadcast to prevent memory leaks
override fun onDestroy(a) {
    super.onDestroy()
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver)
}

// ============ send the broadcast ============
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent("index_action"))

// In AndroidX, you need to add the following dependencies to use local broadcast, otherwise you will be notified that the class cannot be found:
/ / implementation 'androidx. Localbroadcastmanager: localbroadcastmanager: 1.0.0'
Copy the code

= LocalBroadcastManager = LocalBroadcastManager = LocalBroadcastManager = LocalBroadcastManager = LocalBroadcastManager = LocalBroadcastManager

We define two classes: ReceiverRecord and BroadcastRecord. Look at the constructor and getInstance() method:

Follow the method registerReceiver() to register a broadcast:

Mentally? The definition of mActions and mReceivers is:

Take a look at unregisterReceiver() to unregister a broadcast:

Take a look at sendBroadcast() :

A broadcast record list (Action, receiver list) is generated based on the Action information, and then the handler is used to initiate an empty message of the broadcast type. Back to the constructor:

To verify the broadcasts, go executePendingBroadcasts() :

There is also a method that executes immediately: sendBroadcastSync()

This is the implementation logic for local broadcasts, decoupled in this way:

  • Page to transmit data: broadcast directly, don’t care who receive;
  • To receive data page, register for broadcast, receive the broadcast automatically perform the corresponding callback;

This gameplay is fine as observer mode, although it is not a regular implementation. The core is: self-maintenance broadcast (observed) and receiver (observer) set, with Handler to complete event distribution, can ~

But!!! It’s still not lightweight enough. The BroadcastReceiver, which is a dependent system for data transfer, is a combination of a lot of things unrelated to our business and violates The Demeter principle.

So, based on the idea of observer mode, using LocalBroadcast for reference, to explore the implementation of a more lightweight broadcast ~


See Move open move → write a more lightweight broadcast

Observer mode of conventional writing → write a prototype first

Did not understand the observer pattern can see: read the book thin | “” the beauty of design patterns” design patterns and paradigm (behavior type – the observer pattern), direct drive type:

// Pass the data class
data class Entity(
    val key: String,
    var value: Any
)

// Update the callback interface
interface IUpdate {
    fun updateData(entity: Entity)
}

// The observer abstract class
abstract class Observer: IUpdate

// The observed
object Subject {
    private val observerList = arrayListOf<Observer>()

    fun register(observer: Observer) {
        this.observerList.add(observer)
    }

    fun unregister(observer: Observer) {
        this.observerList.remove(observer)
    }

    fun postMessage(entity: Entity) {
        observerList.forEach { it.updateData(entity) }
    }
}
Copy the code

A → B → C → D, D sends A message, and ABC receives A message (page ~) :

class ATestActivity : AppCompatActivity() {
    // The observer callback
    val mObserver: Observer = object : Observer() {
        override fun updateData(entity: Entity) {
            tv_content.text = entity.value.toString()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = A "page"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        // Register the event
        Subject.register(mObserver)
    }

    override fun onDestroy(a) {
        super.onDestroy()
        // Cancel event registration
        Subject.unregister(mObserver)
    }
}

class DTestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        bt_test.setOnClickListener {
            // Send the event
            Subject.postMessage(Entity("back_data"."Data returned from page D ~"))
            finish()
        }

    }
}
Copy the code

The running effect is as follows:

D sent the broadcast, and then ABC received it, and the page was updated. Ok, the prototype was developed, and we began to optimize it.


② Only focus on the broadcast you want to focus on → data structure optimization

The postMessage() of the observed Subject directly iterates through all the observers in the list, which is a bit too rude, since the observers don’t have to pay attention to all the behaviors of the observed. Let’s use this as a starting point to introduce the key and optimize the stored data structure:

object Subject {
    private val observerMap = hashMapOf<String, ArrayList<Observer>>()

    fun register(key: String, observer: Observer) {
        val observerList = observerMap[key]
        if(observerList.isNullOrEmpty()) { observerMap[key] = arrayListOf() } observerMap[key]!! .add(observer) }fun unregister(key: String, observer: Observer) {
        if (observerMap[key].isNullOrEmpty()) returnobserverMap[key]!! .remove(observer) }fun postMessage(key: String, entity: Entity) {
        if (observerMap[key].isNullOrEmpty()) returnobserverMap[key]!! .forEach { it.updateData(entity) } } }Copy the code

Differentiating subscribers by different keys reduces invalid traversal, but it also creates the problem of sending one more key for registration, deregistration, and broadcast.

Subject.register("back_data", mObserver)
Subject.unregister("back_data", mObserver)
Subject.postMessage("back_data", Entity("back_data"."Data returned from page D ~"))
Copy the code

Well, passing two parameters doesn’t look very elegant, sending a broadcast can put the Key into an Entity, registering and deregistering can be done in an Observer class, and the code changes are as follows:

// Abstract observer
abstract class Observer: IUpdate {
    abstract val key: String
}

// The observer callback
val mObserver: Observer = object : Observer() {
    override val key = "back_data"
    override fun updateData(entity: Entity) {
        tv_content.text = entity.value.toString()
    }
}

// The observed
object Subject {
    private val observerMap = hashMapOf<String, ArrayList<Observer>>()

    fun register(observer: Observer) {
        val observerList = observerMap[observer.key]
        if(observerList.isNullOrEmpty()) { observerMap[observer.key] = arrayListOf() } observerMap[observer.key]!! .add(observer) }fun unregister(observer: Observer) {
        if (observerMap[observer.key].isNullOrEmpty()) returnobserverMap[observer.key]!! .remove(observer) }fun postMessage(entity: Entity) {
        if (observerMap[entity.key].isNullOrEmpty()) returnobserverMap[entity.key]!! .forEach { it.updateData(entity) } } }Copy the code

③ FBI WARNING: Be alert to the risk of memory leak

The code above, it looks fine, doesn’t it? But… Really?

The anonymous inner class we used for lazy purposes has this problem:

The anonymous inner class holds a reference to the outer class, in this case the Activity. Forgetting to unbind (remove the collection) causes the collection in the Subject to still hold the Activity reference after onDestory(). When the Subject traversal executes this callback, BOOM! This is where the memory leak comes in

To verify this, build. Gradle relies on LeakCanary:

debugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 2.7'
Copy the code

Then the page intentionally misses the unbinding of an Observer, and after the page finishes () is dropped, it initiates an event on another page. After several attempts, it will find:

So, remember to unbind!! A more mindless way to unbind (stupid, but robust ~) :

Define a collection in the page base class, add each Observer instance to it, and iterate over the unregistration in onDestory()

The following is an example:

protected val mObserverList = arrayListOf<Observer>()

// Add observers directly to the list
mObserverList.add(object : Observer() {
    override val key = "other_data"
    override fun updateData(entity: Entity) {
        tv_content.text = entity.value.toString()
    }
})
// Iterate over the list registration
mObserverList.forEach { Subject.register(it) }

override fun onDestroy(a) {
    super.onDestroy()
    // Iterate over cancel event registration
    mObserverList.forEach { Subject.unregister(it) }
}
Copy the code

④ Who is the undercover → who is the real observer

Knowing that you want to avoid the risk of memory leaks, continue to optimize, in the use of the process is not difficult to find such problems:

An Observer may observe a variety of behaviors of observers, and as many behaviors as there are, so many observers must be instantiated

em… The observer is supposed to wrap the callback to the behavior. Obviously the page is the observer. Simply, the page directly implements the IUpdate interface and overwrites the method to update the data.

class ATestActivity : AppCompatActivity(), IUpdate {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = A "page"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        Subject.register(this)}override fun updateData(entity: Entity) {
        when(entity.key) {
            "back_data" -> tv_content.text = entity.value.toString()
        }
    }

    override fun onDestroy(a) {
        super.onDestroy()
        Subject.unregister(this)}}// Return to the original observed
object Subject {
    private val observerList = arrayListOf<IUpdate>()

    fun register(observer: IUpdate) {
        observerList.add(observer)
    }

    fun unregister(observer: IUpdate) {
        observerList.remove(observer)
    }

    fun postMessage(entity: Entity) {
        observerList.forEach { it.updateData(entity) }
    }
}
Copy the code

It could be, but postMessage() goes back to its previous thoughtless traversal state because it’s a bit cumbersome for pages to pass this Key, given the IUpdate interface. If you define an additional key attribute in the page, you also have to type a key to get an observer in the Subject.

Simply put: the observer also needs to know the type of observer, which is coupled…

Let’s put that on the back burner, we’ll get to that in a second, but here’s another question:

Is it necessary to broadcast the Key of an Entity since there is no mental traversal for the moment?

No, we can define the different broadcast types, and the observer can determine the type and perform the corresponding operation.

interface IUpdate {
    fun updateData(any: Any)
}

object Subject {
    private val observerList = arrayListOf<IUpdate>()

    fun register(observer: IUpdate) {
        observerList.add(observer)
    }

    fun unregister(observer: IUpdate) {
        observerList.remove(observer)
    }

    fun postMessage(entity: Any) {
        observerList.forEach { it.updateData(entity) }
    }
}

// Pass the data
data class DataEntity(var data: String)

// Refresh the page
object RefreshEntity

// Test the page
class ATestActivity : AppCompatActivity(), IUpdate {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = A "page"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        Subject.register(this)}// Perform different processing for different broadcasts
    override fun updateData(any: Any) {
        when (any) {
            is DataEntity -> tv_content.text = any.data
            is RefreshEntity -> Toast.makeText(this."Update broadcast received", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy(a) {
        super.onDestroy()
        Subject.unregister(this)}}class BTestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = "Page B"
        bt_test.setOnClickListener {
            Subject.postMessage(DataEntity("Return data"))
            Subject.postMessage(RefreshEntity)
            finish()
        }
    }
}
Copy the code

See here, have used EventBus children’s shoes, surely will beep: this TM is not EventBus?

Yes, but not yet. Do you need to implement interfaces with EventBus, activities and fragments? So the next step is to figure out a way that the observer doesn’t have to implement the interface.


⑤ Clever reflection → write an interface less

Assuming we have a convention, as long as the updateData(any: any) method is defined in the class, we treat it as an observer callback method.

So all you have to do is get all the methods of the observer class, iterate over the matching method name and the number of parameters, and the matching method is the callback method, using reflection to implement a wave. Modified observer:

object Subject {
    private valobserverMap = hashMapOf<Any, Method? > ()fun register(any: Any) {
        var method: Method? = null
        try {
            // Reflection gets this method
            method = any.javaClass.getDeclaredMethod("updateData", Any::class.java)
            observerMap[any] = method
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
        }
    }

    fun unregister(any: Any) {
        observerMap[any] = null
        observerMap.remove(any)
    }

    fun postMessage(entity: Any){ observerMap.forEach { (key, value) -> value? .invoke(key, entity) } } }Copy the code

Class: the observer implementation IUpdate interface and the updateData (any: any) before the override kill, run validation of a wave, consistent results.


⑥ Skillfully use runtime annotations to avoid the method name written wrong

The above method does not need to implement an interface, but it also brings a problem, the method can not be automatically generated, only hand typing or copy and paste, there may be a misspelling of the function name, after all, people are easy to make mistakes.

Can you explicitly tell the compiler that this function is an observer callback, and you don’t care what it’s called?

Still true, use annotation can be achieved, some friends may be unfamiliar with annotations (have not used), it doesn’t matter, simple wave ~

If you Override a method, use this annotation. Otherwise, the compiler will explode and it won’t compile. In this case, @override is an annotation that tells the compiler that a method is being overridden, so that the compiler prompts an error message when the superclass method is deleted or modified.

Annotations can be used to modify classes, methods, parameters, and so on in the following three scenarios:

  • Compiler prompts: used by the compiler to detect errors, or to clear unnecessary warnings;
  • Compile-time code generation: Using tools outside the compiler (such as KAPT) to automatically generate code from comments;
  • Runtime processing: At run time according to the annotation, through reflection to obtain specific information, and then do some operations;

Limited to space did not explain annotate relevant position, can search information study, or see the Kotlin practical tutorial | 0 x9 – annotations and reflection, Java annotations and Kotlin rules ha ~ is a little different

This is the third scenario where annotations are applied. First define an annotation class:

// Unlike Java, which declares annotations with @interface, Kotlin uses annotation classes to declare annotations
@Target(AnnotationTarget.FUNCTION)  // Modify the function
@Retention(AnnotationRetention.RUNTIME) // The keepalive time of the function
annotation class Subscribe
Copy the code

It then retrives all the methods of the subscriber class, determines the modifier and whether it contains a Subscribe annotation, and adds them to the collection:

// The modified observed
object Subject {
    private const val BRIDGE = 0x40
    private const val SYNTHETIC = 0x1000
    private const val MODIFIERS_IGNORE = Modifier.ABSTRACT or Modifier.STATIC or BRIDGE or SYNTHETIC

    private valobserverMap = hashMapOf<Any, Method? > ()fun register(any: Any) {
        try {
            val methods = any.javaClass.declaredMethods
            methods.forEach {
                val modifiers = it.modifiers
                // Check whether the method modifier is public, not Static, ABSTRACT, etc
                if(modifiers and Modifier.PUBLIC ! =0 && modifiers and MODIFIERS_IGNORE == 0) {
                    // Get the parameter list
                    val parameterTypes = it.parameterTypes
                    // Determine if there is only one parameter
                    if (parameterTypes.size == 1) {
                        // Get the SubScribe annotation
                        valsubscribeAnnotation = it? .getAnnotation(Subscribe::class.java)
                        // The Subscribe annotation is not empty, adding the callback method to the collectionsubscribeAnnotation? .let { _ -> observerMap[any] = it } } } } }catch (e: NoSuchMethodException) {
            e.printStackTrace()
        }
    }

    fun unregister(any: Any) {
        observerMap[any] = null
        observerMap.remove(any)
    }

    fun postMessage(any: Any){ observerMap.forEach { (key, value) -> value? .invoke(key, any) } } }// Add @subscribe annotation to the observer callback method and change it to a different function name
@Subscribe
fun onMainEvent(any: Any) {
    when (any) {
        is DataEntity -> tv_content.text = any.data
        is RefreshEntity -> Toast.makeText(this."Update broadcast received", Toast.LENGTH_SHORT).show()
    }
}
Copy the code

The results are consistent, and the interesting thing comes to mind is that the observer does not have to pay attention to all the behaviors of the observed. Using the annotation, we can distinguish between different broadcast types, and the observer should pay attention to the corresponding broadcast as needed


Use runtime annotations to focus only on the broadcasts you want to focus on

Three elements: broadcast, observer, callback method, can be defined in a data class, but it is tedious to iterate over them, directly space for time, plus multiple map< broadcast type, observer list >. The same broadcast can be sent multiple times, with the Class type, and the observer directly with the normal type.

Given that the Java reflection Method is called by passing in an object instance + a broadcast type corresponding to the Method, create two data classes:

class SubscriberMethod(varmethod: Method? .var eventType: Class<*>?)

class Subscription(varsubscriber: Any? .var subscriberMethod: SubscriberMethod?)
Copy the code

Then there is some judgment, traversal logic, and null-setting when the registration is cancelled. It is relatively simple. The modified code is as follows:

object Subject {
    private const val BRIDGE = 0x40
    private const val SYNTHETIC = 0x1000
    private const val MODIFIERS_IGNORE = Modifier.ABSTRACT or Modifier.STATIC or BRIDGE or SYNTHETIC

    // Key: broadcast type, value: observer list
    private val typeMap = hashMapOf<Class<*>, ArrayList<Any>>()
    // key: observer, value: observer data set
    private val observerMap = hashMapOf<Any, ArrayList<Subscription>>()

    fun register(any: Any) {
        try {
            val methods = any.javaClass.declaredMethods
            methods.forEach {
                val modifiers = it.modifiers
                if(modifiers and Modifier.PUBLIC ! =0 && modifiers and MODIFIERS_IGNORE == 0) {
                    val parameterTypes = it.parameterTypes
                    if (parameterTypes.size == 1) {
                        valsubscribeAnnotation = it? .getAnnotation(Subscribe::class.java) subscribeAnnotation? .let { _ ->// Check whether the list of observers is empty, and add Subscription
                            if (observerMap[any] == null) observerMap[any] = arrayListOf() observerMap[any]!! .add(Subscription(any, SubscriberMethod(it, parameterTypes[0)))// If the list of events is empty, create an observer instance and add it
                            if (typeMap[parameterTypes[0= =]]null) typeMap[parameterTypes[0]] = arrayListOf()
                            typeMap[parameterTypes[0]]!!!!! .add(any) } } } } }catch (e: NoSuchMethodException) {
            e.printStackTrace()
        }
    }

    fun unregister(any: Any) {
        // A strong reference is set to null when untyingany.javaClass.let { cls -> observerMap[cls]? .forEach { it.subscriber =nullit.subscriberMethod? .method =nullit.subscriberMethod? .eventType =null
            }
            observerMap.remove(cls)
            typeMap.values.forEach {
                if (cls in it) {
                    it.clear()
                    it.remove(any.javaClass)
                }
            }
        }
    }

    fun postMessage(any: Any) {
        any.javaClass.let { cls ->
            if (cls intypeMap.keys) { typeMap[cls]!! .forEach { observerMap[it]? .forEach { subscription ->// Determine if it is an Event to be processed
                        if(subscription.subscriberMethod!! .eventType!! .isInstance(any)) { subscription.subscriberMethod!! .method!! .invoke(it, any) } } } } } } }Copy the code

The performance is consistent. Here we use reflection to scan the whole observer. It can be ok, but when there are many observers, it takes more time, which will cause performance impact. Is there a way to get the subscription function information that needs to be reflected before run time (compile time), instead of waiting until run time to iterate to get it? Yes, they are compile-time annotations


⑧ Clever use compile time annotation → save compile time scan entire class

Customize an annotation handler → parse @subscribe annotations → generate a Java file containing the subscribed broadcast information → the broadcast library parses the Java class to retrieve the subscribed broadcast information.

1) Custom annotation handler

Create a New Module → Java or Kotlin Library → project name and class name are LBProcessor

Annotation handlers run only at compile time and do not need to be bundled together

Then LBProcessor inherits from AbstractProcessor, defines a Messager instance for information printing, and overrides init() and process() methods:

@SupportedAnnotationTypes("com.example.test.temp.Subscribe")  // Support to explain which annotation classes
@SupportedSourceVersion(SourceVersion.RELEASE_8) // JDK version to support
class LBProcessor: AbstractProcessor() {
    private var messager: Messager? = null

    override fun init(processingEnv: ProcessingEnvironment?). {
        super.init(processingEnv) messager = processingEnv? .messager messager? .printMessage(Diagnostic.Kind.WARNING,"LBProcessor init")}override fun process(annotations: MutableSet<out TypeElement>? , roundEnv:RoundEnvironment?).: Boolean{ messager!! .printMessage(Diagnostic.Kind.WARNING,"LBProcessor process")
        return true}}Copy the code

Next, specify the annotation handler to create the folders in the following path:

Create a new file in the main directory: \resources\ meta-inf \services\

New file:

javax.annotation.processing.Processor

The contents of the file are the full class name of the annotation processing class:

com.coderpig.lbprocessor.LBProcessor

You can also use AutoService to generate this file automatically and then add the following dependencies to build.gradle in the app hierarchy:

kapt project(":LBProcessor")
implementation project(':LBProcessor')
Copy the code

After sync, type GradLew Build in the terminal to see what happens:

Processer, a parent class of AbstractProcessor, is initialized during compilation, scans the code in the current module for annotations, and then calls the process method to do subsequent operations based on these annotations. This occurs in source -> complier:


2) Get class and function information through annotations

Some annotations related to the use of the API, directly give the code:

@SupportedAnnotationTypes("com.example.test.temp.Subscribe")  // Support to explain which annotation classes
@SupportedSourceVersion(SourceVersion.RELEASE_8) // JDK version to support
class LBProcessor : AbstractProcessor(a){
    private var messager: Messager? = null
    private var methodsByClass = hashMapOf<TypeElement, ArrayList<ExecutableElement>>()

    override fun init(processingEnv: ProcessingEnvironment?) {
        super.init(processingEnv) messager = processingEnv? .messager messager? .printMessage(Diagnostic.Kind.WARNING,"LBProcessor init")}override fun process( annotations: MutableSet
       
        , roundEnv: RoundEnvironment )
       : Boolean {
        annotations.forEach {
            val elements = roundEnv.getElementsAnnotatedWith(it)
            elements.forEach { element ->
                if (element is ExecutableElement) {
                    // Get the class instance
                    val classElement = element.enclosingElement as TypeElement
                    if(methodsByClass[classElement] == null) { methodsByClass[classElement] = arrayListOf() } methodsByClass[classElement]!! .add(element) } } }return true}}Copy the code

I want to debug build.gradle. I want to debug build.gradle. I want to debug build.gradle.

Well, there you go. Design your Java classes


3) Automatically generate Java class designs

From above we get and can transfer information: the Class of the subscriber Class, the method name, and the Class of the broadcast type

Need to design the data structure to pass these three, package two layers (Java implementation, KT external tone may have problems) :

// Method name + broadcast type
public class SubscriberMethodGen {
    private String methodName;
    privateClass<? > eventType;public SubscriberMethodGen(String methodName, Class
        eventType) {
        this.methodName = methodName;
        this.eventType = eventType;
    }

    public String getMethodName(a) {
        return methodName;
    }

    publicClass<? > getEventType() {returneventType; }}// An array of subscriber types + subscription methods
public class SubscriberInfoGen {
    privateClass<? > subscriberClass;private SubscriberMethodGen[] subscriberMethodGens;

    public SubscriberInfoGen(Class
        subscriberClass, SubscriberMethodGen[] subscriberMethodGens) {
        this.subscriberClass = subscriberClass;
        this.subscriberMethodGens = subscriberMethodGens;
    }

    publicClass<? > getSubscriberClass() {return subscriberClass;
    }

    public SubscriberMethodGen[] getSubscriberMethodGens() {
        returnsubscriberMethodGens; }}Copy the code

Next, we design our generated class structure:

public class SubscriberGen {
    private static finalMap<Class<? >, SubscriberInfoGen> SUBSCRIBE_INFO_MAP;static {
        SUBSCRIBE_INFO_MAP = new HashMap<>();
        putSubscribe(new SubscriberInfoGen(com.example.test.ATestActivity.class, new SubscriberMethodGen[]{
                new SubscriberMethodGen("onXXXEvent", DataEntity.class),
                new SubscriberMethodGen("onYYYEvent", RefreshEntity.class)
        }));
    }

    private static void putSubscribe(SubscriberInfoGen info) { SUBSCRIBE_INFO_MAP.put(info.getSubscriberClass(), info); }}Copy the code

The class is initialized when it is loaded. All that is left to do when generating Java is to plug a few subscribe () methods with a few subscribers.

Then we define a Method that gets the SubscriberMethod, and since the Method parameter here is just the Method name, we need the Method object to iterate through and generate the SubscriberMethod array:

publicSubscriberMethod[] getSubscriberMethod(Class<? > subscriberClass) { SubscriberInfoGen gen = SUBSCRIBE_INFO_MAP.get(subscriberClass);if(gen ! =null) {
        SubscriberMethodGen[] methodGens = SUBSCRIBE_INFO_MAP.get(subscriberClass).getSubscriberMethodGens();
        SubscriberMethod[] methods = new SubscriberMethod[methodGens.length];
        for (int i = 0; i < methodGens.length; i++) {
            try {
                Method method = subscriberClass.getDeclaredMethod(methodGens[i].getMethodName(), methodGens[i].getEventType());
                methods[i] = new SubscriberMethod(method, methodGens[i].getEventType());
            } catch(NoSuchMethodException e) { e.printStackTrace(); }}return methods;
    }
    return null;
}
Copy the code

The next step was to test whether the parse was successful in our subscriber class, and the changes were as follows:

    private var byGen = true    // Read the tag as a Java file
    private var subscriberGen: SubscriberGen? = null
    init {
        subscriberGen = SubscriberGen() // Init block to complete initialization, avoid repeated creation
    }

    fun register(any: Any) {
        if (byGen) {
            valsubscriberInfo = subscriberGen!! .getSubscriberMethod((any::class.java)) subscriberInfo? .forEach {if (observerMap[any] == null) observerMap[any] = arrayListOf() observerMap[any]!! .add(Subscription(any, SubscriberMethod(it.method, it.eventType)))val eventTypeClass = it.eventType
                if(eventTypeClass ! =null) {
                    if(typeMap[eventTypeClass] == null) typeMap[eventTypeClass] = arrayListOf() typeMap[eventTypeClass]!! .add(any) } } }else{... The original code for scanning class methodsCopy the code

After running a test wave, the effect is consistent, can, continue to go down ~


4) Automatically generate Java classes based on annotations

The usual solution is to add lines of code to BufferedWriter and output a file, but here the generated file is only one, you can use square/ Javapoet to get lazy. Derivative dependence:

implementation 'com. Squareup: javapoet: 1.13.0'
Copy the code

Refer to the API documentation and write the generation method for the above generation class. If you are not familiar with the API and it is difficult to understand, just give the complete processing code:

// Generate Java files dynamically
private fun createLBFile(className: String?). {
    val subscriberInfoGen = ClassName.get("com.example.test.temp"."SubscriberInfoGen")
    val subscriberMethod = ClassName.get("com.example.test.temp"."SubscriberMethod")
    val subscriberMethodGen = ClassName.get("com.example.test.temp"."SubscriberMethodGen")
    val classClass = ClassName.bestGuess("Class
      ")
    val subscriberMethodArrayClass = ClassName.bestGuess("SubscriberMethod[]")
    val mapClass = ClassName.bestGuess("Map
      
       , SubscriberInfoGen>"
      >)
    / / collection
    val subscribeInfoMap = FieldSpec.builder(mapClass, "SUBSCRIBE_INFO_MAP")
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC).build()
    // Introduced to guide Map and HashMap packages, has no effect
    val tempMap = FieldSpec.builder(Map::class.java, "tempMap")
        .initializer("new \$T<String, String>()", HashMap::class.java)
        .build()
    // Static code block section
    val staticCode =
        CodeBlock.builder().addStatement("SUBSCRIBE_INFO_MAP = new HashMap<>()").apply {
            methodsByClass.forEach { (typeElement, arrayList) ->
                add(
                    "putSubscribe(new SubscriberInfoGen(\$L.class, new SubscriberMethodGen[] {\n",
                    typeElement.qualifiedName
                )
                arrayList.forEachIndexed { index, it ->
                    add("new \$T(\"\$L\"", subscriberMethodGen, it.simpleName)
                    it.parameters.forEach { param -> add(",\$L.class", param.asType()) }
                    add(")")
                    if(index ! = arrayList.size -1) add(",")
                }
                add("\n})); \n")
            }
        }.build()
    / / putSubscribe () method
    val putSubscribe = MethodSpec.methodBuilder("putSubscribe")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(Void.TYPE)
        .addParameter(subscriberInfoGen, "info")
        .addCode("SUBSCRIBE_INFO_MAP.put(info.getSubscriberClass(), info);")
        .build()
    / / getSubscriberMethod () method
    val getSubscriberMethod = MethodSpec.methodBuilder("getSubscriberMethod")
        .addModifiers(Modifier.PUBLIC)
        .returns(subscriberMethodArrayClass)
        .addParameter(classClass, "subscriberClass")
        .addStatement("SubscriberInfoGen gen = SUBSCRIBE_INFO_MAP.get(subscriberClass)")
        .addCode("if (gen ! = null) { \n")
        .addStatement("SubscriberMethodGen[] methodGens = SUBSCRIBE_INFO_MAP.get(subscriberClass).getSubscriberMethodGens()")
        .addStatement(
            "\$T[] methods = new SubscriberMethod[methodGens.length]",
            subscriberMethod
        )
        .addCode("for (int i = 0; i < methodGens.length; i++) {\n")
        .beginControlFlow("try")
        .addStatement(
            "\$T method = subscriberClass.getDeclaredMethod(methodGens[i].getMethodName(), methodGens[i].getEventType())",
            Method::class.java
        )
        .addStatement("methods[i] = new SubscriberMethod(method, methodGens[i].getEventType())")
        .nextControlFlow("catch (\$T e)", NoSuchMethodException::class.java)
        .addStatement("e.printStackTrace()")
        .endControlFlow()
        .addCode("}\n return methods; \n}\n")
        .addStatement("return null")
        .build()
    // Concatenation generates the final class
    val subscriberGen = TypeSpec.classBuilder("className")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addField(tempMap)
        .addField(subscribeInfoMap)
        .addStaticBlock(staticCode)
        .addMethod(putSubscribe)
        .addMethod(getSubscriberMethod)
        .build()
    try {
        val javaFile = JavaFile.builder("com.example.test", subscriberGen).build()
        javaFile.writeTo(filer)
    } catch (e: Exception) {
        print(e.message)
    }
}

Processingenv. options["lbClass"] : processIngenv. options["lbClass"] :
if (methodsByClass.size > 0) createLBFile("SubscriberGen")
Copy the code

Really wrote to spit blood, this piece of stuff together I 2 hours… Then change the code on page B, register the event as well, and build:

Yes, although there is no typography, but no syntax errors, directly run, try kang effect:

Here should be applause, so show things, hurry to show to friends ~


⑨ Use Handler → to solve problems caused by broadcast in child threads

At first, I thought you were 6, but after playing for a while, I said, “I’m dead.”

Look at the code:

Dude, how dare you, send a broadcast directly in the child thread? em… But it seems to work. The open thread performs some time-consuming operation, and when it’s done, it broadcasts.

So postMessage() also needs to determine the thread, the main thread directly callback, child thread needs to use the Handler message mechanism.

Determining whether the main thread is simple and straightforward:

if(Looper.getMainLooper() == Looper.myLooper())
Copy the code

If you don’t understand the principle, you can dig the source code yourself, or you can read what I wrote: “Change posture, Look at Handler with problems.”

(Chao) : (xi) : (xi) : (chao) : (xi) : (xi)

private var mHandler: Handler? = null
private const val LIGHT_BROADCASTS = Awesome!  // Broadcast flags
// Provide a temporary...
private var mTempEntity: Any? = null
private var mTempSubscription: Subscription? = null

init {
    subscriberGenDefault = SubscriberGen() // Init block to complete initialization, avoid repeated creation
    mHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when (msg.what) {
                LIGHT_BROADCASTS -> {
                    if(mTempEntity ! =null&& mTempSubscription ! =null) { mTempSubscription!! .subscriberMethod!! .method!! .invoke( mTempSubscription!! .subscriber, mTempEntity )// Reset when used up
                        mTempEntity = null
                        mTempSubscription = null}}else -> super.handleMessage(msg)
            }
        }
    }
}

// Broadcast the distribution part of the code
if (Looper.getMainLooper() == Looper.myLooper()) {
    // Direct callbacksubscription.subscriberMethod!! .method!! .invoke(it, any) }else {
    // The non-main thread, which stores the variables used, initiates a messagemHandler!! .sendMessage(mHandler!! .obtainMessage().apply { mTempEntity = any mTempSubscription = subscription what = LIGHT_BROADCASTS }) }Copy the code

Although not elegant, but the problem is solved, careful friends can see that I defined two temporary variables, but also helpless:

Handler passes objects to a class that implements a serialized interface and has a size limit…

A better solution here would be to maintain a queue of broadcast messages, but there are many more ways to detail it


Attending EventBus gleanings

Ctrl+R to replace the whole text, replace “broadcast” with “EventBus”, you will find that the reading is quite smooth, yes, this article has the “implement lightweight broadcast” head, the “basic EventBus” is given out, 23333.

The key technical details have been covered. What has not been done is:

  • Design principles + object-oriented features → refine, split, redesign, and extend the class as far as possible;
  • Synchronized + concurrent container (such as CopyOnWriteArrayList and ConcurrentHashMap) → concurrent processing to ensure thread safety;
  • Design a broadcast queue → Distinguish between different types of broadcasts and determine which processing to perform (postToSubscription() in the source code)
  • Some other details → logging, error handling, etc.

These above belong to the category of experience accumulation of things, see more library source code practice, so much for it, thank you 😁~


References:

  • Talk about AbstractProcessor and the Java compilation process