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
In the last article, the source of ARouter for a comprehensive analysis, the principle of understanding, then you also need to carry out a 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 and even implement it yourself. In this paper, to achieve a routing framework, their own purpose is not to do and the same function as ARouter, but just a training project, the purpose is to deepen the understanding of the principle of ARouter, so their own custom implementation is called EasyRouter 😂😂
EasyRouter supports the same module and cross-module Activity jump, only need to specify a string path can:
EasyRouter.navigation(EasyRouterPath.PATH_HOME)
Copy the code
The result achieved:
The implementation and use of EasyRouter involves the following modules:
- The app. This is the main module of the project, from which to jump to submodules
- The base. Used to share path across multiple modules
- Easyrouter – the annotation. Defines annotations and Bean objects associated with the EasyRouter implementation
- Easyrouter – API. To define the API entry associated with the EasyRouter implementation
- Easyrouter – processor. To define an annotation processor related to the EasyRouter implementation, which is used in the compile phase
- Easyrouter_demo. Submodule, used to test whether the app module can jump to the submodule normally
EasyRouter is implemented in a slightly different way than ARouter. EasyArouter stores and initializes all routing information in the same module through a static method block, resulting in the following auxiliary files:
package github.leavesc.easyrouter;
import java.util.HashMap;
import java.util.Map;
import github.leavesc.ctrlcv.easyrouter.EasyRouterHomeActivity;
import github.leavesc.ctrlcv.easyrouter.EasyRouterSubPageActivity;
import github.leavesc.easyrouterannotation.RouterBean;
/** * This is automatically generated by leavesC */
public class EasyRouterappLoader {
public static final Map<String, RouterBean> routerMap = new HashMap<>();
{
routerMap.put("app/home".new RouterBean(EasyRouterHomeActivity.class, "app/home"."app"));
routerMap.put("app/subPage".new RouterBean(EasyRouterSubPageActivity.class, "app/subPage"."app")); }}Copy the code
Since static variables and static method blocks are not initialized before the class is loaded, they can also be loaded on demand. That is, only when the external initiates a request to jump to the app module, EasyRouter will go to instantiate EasyRouterappLoader class, and then will load all routing table information of app module, so as to avoid memory waste
Here is a brief introduction to the implementation process of EasyRouter
First, pre-preparation
Since the routing framework is based on modules, the routing information in the same module can be stored in the same auxiliary file. In order to avoid the occurrence of the same name of auxiliary files generated among multiple modules, it is necessary to proactively configure a specific unique identifier for each module externally. This unique identifier is then picked up by the AbstractProcessor during compilation
For example, the only identifier I set for the easyRouter-Test module is RouterTest
kapt {
arguments {
arg("EASYROUTER_MODULE_NAME"."RouterTest")}}Copy the code
The resulting auxiliary file will have a fixed package name, but the class name will contain this unique identifier. Since the generation rules of package name and class name are regular, it is convenient to get this class at run time. Meanwhile, it is necessary to find that the routing path under the same module must belong to the same group
package github.leavesc.easyrouter;
import java.util.HashMap;
import java.util.Map;
import github.leavesc.easyrouter_test.EasyRouterTestAActivity;
import github.leavesc.easyrouterannotation.RouterBean;
/** * This is automatically generated by leavesC */
public class EasyRouterRouterTestLoader {
public static final Map<String, RouterBean> routerMap = new HashMap<>();
{
routerMap.put("RouterTest/testA".new RouterBean(EasyRouterTestAActivity.class, "RouterTest/testA"."RouterTest")); }}Copy the code
The @router is used to annotate activities. You only need to set a parameter, path. The first word in path is group
/ * * *@Author: leavesC
* @Date: 2020/10/6 1:08
* @Desc:
* @Github: https://github.com/leavesC * /
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class Router(val path: String)
data class RouterBean(val targetClass: Class<*>, val path: String, val group: String)
Copy the code
2. Annotation processor
Declare an EasyRouterProcessor class that inherits from AbstractProcessor and scans code elements during the compile phase to get the @Router annotation information
/ * * *@Author: leavesC
* @Date: 2020/10/5 * are intent@Desc:
* @Github: https://github.com/leavesC * /
class EasyRouterProcessor : AbstractProcessor() {
companion object {
private const val KEY_MODULE_NAME = "EASYROUTER_MODULE_NAME"
private const val PACKAGE_NAME = "github.leavesc.easyrouter"
private const val DOC = "This is automatically generated code by leavesC"
}
private lateinit var elementUtils: Elements
private lateinit var messager: Messager
private lateinit var moduleName: String
override fun init(processingEnvironment: ProcessingEnvironment) {
super.init(processingEnvironment)
elementUtils = processingEnv.elementUtils
messager = processingEnv.messager
valoptions = processingEnv.options moduleName = options[KEY_MODULE_NAME] ? :""
if (moduleName.isBlank()) {
messager.printMessage(Diagnostic.Kind.ERROR, "$KEY_MODULE_NAME must not be null")...}}override fun getSupportedAnnotationTypes(a): MutableSet<String> {
return mutableSetOf(Router::class.java.canonicalName)
}
override fun getSupportedSourceVersion(a): SourceVersion {
return SourceVersion.RELEASE_8
}
override fun getSupportedOptions(a): Set<String> {
return hashSetOf(KEY_MODULE_NAME)
}
}
Copy the code
First, you need to generate the routerMap field to store routing table information. The key value of the Map field is path, and the value value is the page information corresponding to the path
// Generate the Static constant routerMap
private fun generateSubscriberField(a): FieldSpec {
val subscriberIndex = ParameterizedTypeName.get(
ClassName.get(Map::class.java),
ClassName.get(String::class.java),
ClassName.get(RouterBean::class.java)
)
return FieldSpec.builder(subscriberIndex, "routerMap")
.addModifiers(
Modifier.PUBLIC,
Modifier.STATIC,
Modifier.FINAL
)
.initializer("new The ${"$"}T<>()", HashMap::class.java)
.build()
}
Copy the code
Then you need to generate the static method block. Take the path attribute contained in the @Router annotation and the Class object corresponding to the annotated Class to build a RouterBean object and store it in the routerMap
// Generate static method blocks
private fun generateInitializerBlock(
elements: MutableSet<out Element>,
builder: TypeSpec.Builder
) {
val codeBuilder = CodeBlock.builder()
elements.forEach {
val router = it.getAnnotation(Router::class.java)
val path = router.path
val group = path.substring(0, path.indexOf("/"))
codeBuilder.add(
"routerMap.put(The ${"$"}S, new The ${"$"}T(The ${"$"}T.class, The ${"$"}S, The ${"$"}S));",
path,
RouterBean::class.java,
it.asType(),
path,
group
)
}
builder.addInitializerBlock(
codeBuilder.build()
)
}
Copy the code
The auxiliary files are then generated in the Process method
override fun process(
mutableSet: MutableSet<out TypeElement>,
roundEnvironment: RoundEnvironment
): Boolean {
val elements: MutableSet<out Element> =
roundEnvironment.getElementsAnnotatedWith(Router::class.java)
if (elements.isNullOrEmpty()) {
return true
}
val typeSpec = TypeSpec.classBuilder("EasyRouter" + moduleName + "Loader")
.addModifiers(Modifier.PUBLIC)
.addField(generateSubscriberField())
.addJavadoc(DOC)
generateInitializerBlock(elements, 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, EasyRouter
EasyRouter this singleton object is finally provided to the external call entry, the total number of lines of code is less than 50 lines. The external user can jump by calling the navigation method and passing in the target page path. The user can judge the group it belongs to by the path and try to load the auxiliary file generated by the module where the user belongs. If the loading succeeds, the user can jump successfully, otherwise, the user will Toast
/ * * *@Author: leavesC
* @Date: 2020/10/5 23:45
* @Desc:
* @Github: https://github.com/leavesC * /
object EasyRouter {
private const val PACKAGE_NAME = "github.leavesc.easyrouter"
private lateinit var context: Application
private val routerByGroupMap = hashMapOf<String, Map<String, RouterBean>>()
fun init(application: Application) {
this.context = application
}
fun navigation(path: String) {
val routerBean = getRouterLoader(path)
if (routerBean == null) {
Toast.makeText(context, "No matching path found:$path", Toast.LENGTH_SHORT).show()
return
}
val intent = Intent(context, routerBean.targetClass)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
private fun getRouterLoader(path: String): RouterBean? {
val group = path.substring(0, path.indexOf("/"))
val map = routerByGroupMap[group]
if (map == null) {
var routerMap: Map<String, RouterBean>? = null
try {
val classPath = PACKAGE_NAME + "." + "EasyRouter" + group + "Loader"
val clazz = Class.forName(classPath)
val instance = clazz.newInstance()
val routerMapField = clazz.getDeclaredField("routerMap")
routerMap =
(routerMapField.get(instance) as? Map<String, RouterBean>) ? : hashMapOf() routerByGroupMap[group] = routerMap }catch (e: Throwable) {
e.printStackTrace()
} finally {
if (routerMap == null) {
routerByGroupMap[group] = hashMapOf()
}
}
}
returnrouterByGroupMap[group]? .get(path)
}
}
Copy the code
Fourth, making
I’m trying to implement EasyRouter just to get a better understanding of how ARouter works, and I’m not going to make it fully functional, but for some readers I think it’s worth mentioning 😂😂. Here’s a GitHub link to the above code: AndroidOpenSourceDemo