For Android Developer, many open source libraries are essential knowledge points for development, from the use of ways to implementation principles to source code parsing, which require us to have a certain degree of understanding and application ability. So I’m going to write a series of articles about source code analysis and practice of open source libraries, the initial target is EventBus, ARouter, LeakCanary, Retrofit, Glide, OkHttp, Coil and other seven well-known open source libraries, hope to help you 😇😇

Official account: byte array

Article Series Navigation:

  • Tripartite library source notes (1) -EventBus source detailed explanation
  • Tripartite library source notes (2) -EventBus itself to implement one
  • Three party library source notes (3) -ARouter source detailed explanation
  • Third party library source notes (4) -ARouter own implementation
  • Three database source notes (5) -LeakCanary source detailed explanation
  • Tripartite Library source note (6) -LeakCanary Read on
  • Tripartite library source notes (7) -Retrofit source detailed explanation
  • Tripartite library source notes (8) -Retrofit in combination with LiveData
  • Three party library source notes (9) -Glide source detailed explanation
  • Tripartite library source notes (10) -Glide you may not know the knowledge point
  • Three party library source notes (11) -OkHttp source details
  • Tripartite library source notes (12) -OkHttp/Retrofit development debugger
  • Third party library source notes (13) – may be the first network Coil source analysis article

The last article on EventBus for a comprehensive source code analysis, the principle of understanding, then also need to carry out a real combat. In addition to learning how to use a good third-party library, the more difficult part is knowing how to implement it, how to modify it or even implement it yourself. In this article, we will implement an EventBus by ourselves. We don’t need many functions, but we can realize simple registration, unregistration, sending and receiving messages 😇😇

Let’s take a look at the final implementation

For the two listeners: EasyEventBusMainActivity and EasyEventBusTest, decorate the listener method by annotating the @Event annotation, and then use the custom class EasyEventBus to register, unregister, and send messages

/ * * *@Author: leavesC
 * @Date: 2021/1/15 just *@Desc:
 * @Github: https://github.com/leavesC * /
class EasyEventBusMainActivity : BaseActivity() {

    override val bind by getBind<ActivityEasyEventBusMainBinding>()

    private val eventTest = EasyEventBusTest()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        EasyEventBus.register(this)
        eventTest.register()
        bind.btnPostString.setOnClickListener {
            EasyEventBus.post("Hello")
        }
        bind.btnPostBean.setOnClickListener {
            EasyEventBus.post(HelloBean("hi"))}}@Event
    fun stringFun(msg: String) {
        showToast("$msg ${this.javaClass.simpleName}")}@Event
    fun benFun(msg: HelloBean) {
        showToast("${msg.data} ${this.javaClass.simpleName}")}override fun onDestroy(a) {
        super.onDestroy()
        EasyEventBus.unregister(this)
        eventTest.unregister()
    }

}

class EasyEventBusTest {

    @Event
    fun stringFun(msg: String) {
        showToast("$msg ${this.javaClass.simpleName}")}@Event
    fun benFun(msg: HelloBean) {
        showToast("${msg.data} ${this.javaClass.simpleName}")}fun register(a) {
        EasyEventBus.register(this)}fun unregister(a) {
        EasyEventBus.unregister(this)}}data class HelloBean(val data: String)
Copy the code

It is similar to the real EvnetBus 😁😁, although it is actually much rougher ~~

The final custom EasyEventBus is only about fifty lines of code, with only three external methods for registering, unregistering, and sending messages

/ * * *@Author: leavesC
 * @Date: 2020/10/3 11:44
 * @Desc:
 * @Github: https://github.com/leavesC * /
object EasyEventBus {

    private val subscriptions = mutableSetOf<Any>()

    private const val PACKAGE_NAME = "github.leavesc.easyeventbus"

    private const val CLASS_NAME = "EventBusInject"

    private const val CLASS_PATH = "$PACKAGE_NAME.$CLASS_NAME"

    private val clazz = Class.forName(CLASS_PATH)

    // Generate EventBusInject by reflection
    private val instance = clazz.newInstance()

    @Synchronized
    fun register(subscriber: Any) {
        subscriptions.add(subscriber)
    }

    @Synchronized
    fun unregister(subscriber: Any) {
        subscriptions.remove(subscriber)
    }

    @Synchronized
    fun post(event: Any) {
        subscriptions.forEach { subscriber ->
            val subscriberInfo =
                getSubscriberInfo(subscriber.javaClass)
            if(subscriberInfo ! =null) {
                val methodList = subscriberInfo.methodList
                methodList.forEach { method ->
                    if (method.eventType == event.javaClass) {
                        val declaredMethod = subscriber.javaClass.getDeclaredMethod(
                            method.methodName,
                            method.eventType
                        )
                        declaredMethod.invoke(subscriber, event)
                    }
                }
            }
        }
    }

    // Call the getSubscriberInfo method of EventBusInject through reflection
    private fun getSubscriberInfo(subscriberClass: Class< * >): SubscriberInfo? {
        val method = clazz.getMethod("getSubscriberInfo", Class::class.java)
        return method.invoke(instance, subscriberClass) as? SubscriberInfo
    }

}
Copy the code

First, how to achieve

Let’s first think about what this custom EasyEventBus should do, and how

The core focus of EasyEventBus is the process of generating auxiliary files through the annotation processor, which is not perceived by the user, and this logic is triggered only during the compile phase. We want to be able to get all @Event declared methods at compile time so that they don’t have to be reflected at run time, so we want to be able to generate the following auxiliary files at compile time:

/** * This is automatically generated by leavesC */
public class EventBusInject {

    private static finalMap<Class<? >, SubscriberInfo> subscriberIndex =newHashMap<Class<? >, SubscriberInfo>(); { List<EventMethodInfo> eventMethodInfoList =new ArrayList<EventMethodInfo>();
        eventMethodInfoList.add(new EventMethodInfo("stringFun", String.class));
        eventMethodInfoList.add(new EventMethodInfo("benFun", HelloBean.class));
        SubscriberInfo subscriberInfo = new SubscriberInfo(EasyEventBusMainActivity.class, eventMethodInfoList);
        putIndex(subscriberInfo);
    }

    {
        List<EventMethodInfo> eventMethodInfoList = new ArrayList<EventMethodInfo>();
        eventMethodInfoList.add(new EventMethodInfo("stringFun", String.class));
        eventMethodInfoList.add(new EventMethodInfo("benFun", HelloBean.class));
        SubscriberInfo subscriberInfo = new SubscriberInfo(EasyEventBusTest.class, eventMethodInfoList);
        putIndex(subscriberInfo);
    }

    private static final void putIndex(SubscriberInfo info) {
        subscriberIndex.put(info.getSubscriberClass(), info);
    }

    public final SubscriberInfo getSubscriberInfo(Class
        subscriberClass) {
        returnsubscriberIndex.get(subscriberClass); }}Copy the code

As you can see, the subscriberIndex stores the signature information of all listening methods. When the application is running, we can get all listening methods of subscriberClass by using the getSubscriberInfo method

Finally, you need to provide an API call entry point. The custom EasyEventBus class, posted above, is provided for the user to call at runtime. All listening methods are picked up from EventBusInject by an externally passed subscriberClass for a reflection callback when there is a message to be sent

So, EasyEventBus logically splits into two moudle:

  • Easyeventbus_api. Expose EasyEventBus and @Event externally
  • Easyeventbus_processor. It is not exposed and only takes effect at compile time

2. Annotation processor

First, we need to provide an annotation to mark the listening method

@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION)
annotation class Event
Copy the code

Then, we need to pre-abstract all the listening methods at compile time, so we need to define two Javabeans to serve as carriers

/ * * *@Author: leavesC
 * @Date: 2020/10/3 *@Desc:
 * @Github: https://github.com/leavesC * /
data class EventMethodInfo(val methodName: String, val eventType: Class<*>)

data class SubscriberInfo(
    val subscriberClass: Class<*>,
    val methodList: List<EventMethodInfo>
)
Copy the code

We then declare an EasyEventBusProcessor class that inherits from AbstractProcessor, and the compiler passes in the code elements we care about during compilation

/ * * *@Author: leavesC
 * @Date: 2020/10/3 15:55
 * @Desc:
 * @Github: https://github.com/leavesC * /
class EasyEventBusProcessor : AbstractProcessor() {

    companion object {

        private const val PACKAGE_NAME = "github.leavesc.easyeventbus"

        private const val CLASS_NAME = "EventBusInject"

        private const val DOC = "This is automatically generated code by leavesC"

    }

    private lateinit var elementUtils: Elements

    private val methodsByClass = LinkedHashMap<TypeElement, MutableList<ExecutableElement>>()

    override fun init(processingEnvironment: ProcessingEnvironment) {
        super.init(processingEnvironment)
        elementUtils = processingEnv.elementUtils
    }

    override fun getSupportedAnnotationTypes(a): MutableSet<String> {
        // Only the Event annotation needs to be processed
        return mutableSetOf(Event::class.java.canonicalName)
    }

    override fun getSupportedSourceVersion(a): SourceVersion {
        returnSourceVersion. RELEASE_8}...}Copy the code

All listener methods are obtained by collectSubscribers method and stored in methodsByClass. The method signatures need to be verified: they must be instance methods, they must be public, and they must contain at least one incoming parameter

override fun process(
        set: Set<TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        val messager = processingEnv.messager
        collectSubscribers(set, roundEnvironment, messager)
        if (methodsByClass.isEmpty()) {
            messager.printMessage(Diagnostic.Kind.WARNING, "No @Event annotations found")}else{...}return true
    }

    private fun collectSubscribers(
        annotations: Set<TypeElement>,
        env: RoundEnvironment,
        messager: Messager
    ) {
        for (annotation in annotations) {
            val elements = env.getElementsAnnotatedWith(annotation)
            for (element in elements) {
                if (element is ExecutableElement) {
                    if (checkHasNoErrors(element, messager)) {
                        val classElement = element.enclosingElement as TypeElement
                        var list = methodsByClass[classElement]
                        if (list == null) {
                            list = mutableListOf()
                            methodsByClass[classElement] = list
                        }
                        list.add(element)
                    }
                } else {
                    // @event can only be used to modify methods
                    messager.printMessage(
                        Diagnostic.Kind.ERROR,
                        "@Event is only valid for methods",
                        element
                    )
                }
            }
        }
    }

    /** * Check whether the signature is valid */
    private fun checkHasNoErrors(element: ExecutableElement, messager: Messager): Boolean {
        // Can't be static methods
        if (element.modifiers.contains(Modifier.STATIC)) {
            messager.printMessage(Diagnostic.Kind.ERROR, "Event method must not be static", element)
            return false
        }
        // Must be a public method
        if(! element.modifiers.contains(Modifier.PUBLIC)) { messager.printMessage(Diagnostic.Kind.ERROR,"Event method must be public", element)
            return false
        }
        // The method can contain at most one parameter
        val parameters = element.parameters
        if(parameters.size ! =1) {
            messager.printMessage(
                Diagnostic.Kind.ERROR,
                "Event method must have exactly 1 parameter",
                element
            )
            return false
        }
        return true
    }
Copy the code

Then, the subscriberIndex static constant, the corresponding static method block, and the putIndex method are generated

	// Generate the subscriberIndex static constant
    private fun generateSubscriberField(a): FieldSpec {
        val subscriberIndex = ParameterizedTypeName.get(
            ClassName.get(Map::class.java),
            getClassAny(),
            ClassName.get(SubscriberInfo::class.java)
        )
        return FieldSpec.builder(subscriberIndex, "subscriberIndex")
            .addModifiers(
                Modifier.PRIVATE,
                Modifier.STATIC,
                Modifier.FINAL
            )
            .initializer(
                "new The ${"$"}T<Class<? >,The ${"$"}T>()",
                HashMap::class.java,
                SubscriberInfo::class.java
            )
            .build()
    }

    // Generate static method blocks
    private fun generateInitializerBlock(builder: TypeSpec.Builder) {
        for (item in methodsByClass) {
            val methods = item.value
            if (methods.isEmpty()) {
                break
            }
            val codeBuilder = CodeBlock.builder()
            codeBuilder.add(
                "The ${"$"}T<The ${"$"}T> eventMethodInfoList = new The ${"$"}T<The ${"$"}T>();",
                List::class.java,
                EventMethodInfo::class.java,
                ArrayList::class.java,
                EventMethodInfo::class.java
            )
            methods.forEach {
                val methodName = it.simpleName.toString()
                val eventType = it.parameters[0].asType()
                codeBuilder.add(
                    "eventMethodInfoList.add(new EventMethodInfo(The ${"$"}S, The ${"$"}T.class));",
                    methodName,
                    eventType
                )
            }
            codeBuilder.add(
                "SubscriberInfo subscriberInfo = new SubscriberInfo(The ${"$"}T.class, eventMethodInfoList); putIndex(subscriberInfo);",
                item.key.asType()
            )
            builder.addInitializerBlock(
                codeBuilder.build()
            )
        }
    }

	// Generate the putIndex method
	private fun generateMethodPutIndex(a): MethodSpec {
        return MethodSpec.methodBuilder("putIndex")
            .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
            .returns(Void.TYPE)
            .addParameter(SubscriberInfo::class.java, "info")
            .addCode(
                CodeBlock.builder().add("subscriberIndex.put(info.getSubscriberClass() , info);")
                    .build()
            )
            .build()
    }
Copy the code

The getSubscriberInfo public method is then generated for runtime invocation

    // Generate the getSubscriberInfo method
    private fun generateMethodGetSubscriberInfo(a): MethodSpec {
        return MethodSpec.methodBuilder("getSubscriberInfo")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .returns(SubscriberInfo::class.java)
            .addParameter(getClassAny(), "subscriberClass")
            .addCode(
                CodeBlock.builder().add("return subscriberIndex.get(subscriberClass);")
                    .build()
            )
            .build()
    }
Copy the code

With the above method defined, you can complete the build of the entire EventBusInject class file in the Process method

	override fun process(
        set: Set<TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        val messager = processingEnv.messager
        collectSubscribers(set, roundEnvironment, messager)
        if (methodsByClass.isEmpty()) {
            messager.printMessage(Diagnostic.Kind.WARNING, "No @Event annotations found")}else {
            val typeSpec = TypeSpec.classBuilder(CLASS_NAME)
                .addModifiers(Modifier.PUBLIC)
                .addJavadoc(DOC)
                .addField(generateSubscriberField())
                .addMethod(generateMethodPutIndex())
                .addMethod(generateMethodGetSubscriberInfo())
            generateInitializerBlock(typeSpec)
            val javaFile = JavaFile.builder(PACKAGE_NAME, typeSpec.build())
                .build()
            try {
                javaFile.writeTo(processingEnv.filer)
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
        return true
    }
Copy the code

Third, EasyEventBus

The EasyEventBus logic is simple, generating EventBusInject through reflection, getting the SubscriberInfo associated with the subscriber, and traversing the call when a message is posted

/ * * *@Author: leavesC
 * @Date: 2020/10/3 11:44
 * @Desc:
 * @Github: https://github.com/leavesC * /
object EasyEventBus {

    private val subscriptions = mutableSetOf<Any>()

    private const val PACKAGE_NAME = "github.leavesc.easyeventbus"

    private const val CLASS_NAME = "EventBusInject"

    private const val CLASS_PATH = "$PACKAGE_NAME.$CLASS_NAME"

    private val clazz = Class.forName(CLASS_PATH)

    // Generate EventBusInject by reflection
    private val instance = clazz.newInstance()

    @Synchronized
    fun register(subscriber: Any) {
        subscriptions.add(subscriber)
    }

    @Synchronized
    fun unregister(subscriber: Any) {
        subscriptions.remove(subscriber)
    }

    @Synchronized
    fun post(event: Any) {
        subscriptions.forEach { subscriber ->
            val subscriberInfo =
                getSubscriberInfo(subscriber.javaClass)
            if(subscriberInfo ! =null) {
                val methodList = subscriberInfo.methodList
                methodList.forEach { method ->
                    if (method.eventType == event.javaClass) {
                        val declaredMethod = subscriber.javaClass.getDeclaredMethod(
                            method.methodName,
                            method.eventType
                        )
                        declaredMethod.invoke(subscriber, event)
                    }
                }
            }
        }
    }

    // Call the getSubscriberInfo method of EventBusInject through reflection
    private fun getSubscriberInfo(subscriberClass: Class< * >): SubscriberInfo? {
        val method = clazz.getMethod("getSubscriberInfo", Class::class.java)
        return method.invoke(instance, subscriberClass) as? SubscriberInfo
    }

}
Copy the code

Fourth, making

The text implementation of EasyEventBus is pretty rudimentary 😂😂 because MY idea is to get a better understanding of EventBus by doing it myself. Here’s a GitHub link to the above code: AndroidOpenSourceDemo