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