background
Why repeat the wheel?
- I think only from the perspective of the author can we understand the design idea of the frame more thoroughly
- Step in the pit where the gods have stepped.
- To gain a deeper understanding of the functionality provided by the framework
- Learn from excellent works to improve yourself
Before we begin let me ask you a few questions about ARouter
- Why add the following configuration to the build.gradle file of the Module? What does it do? What does it have to do with the grouping in the URL that we defined?
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
Copy the code
- In a business scenario, create a new business component, user, which has the page UserActivity, and configure the URL
/user/main
; There is a service interface, whose implementation class is in app, configured with a URL of/user/info
; The code is as follows:
//module:user
@Route(path = "/user/main")
public class UserActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.user_activity);
}
}
public interface IUserService extends IProvider {
void test(String s); } //module:app //user service @route (path ="/user/info")
public class UserServiceImpl implements IUserService {
public void test(String test) {
Log.d("xxxx->".test); }}Copy the code
Now that the development is complete, let’s compile the project and see what it looks like:
According to???
Let’s start our RouterManager tour with these two questions.
Step 1 Architecture design idea (handle page jump)
Our goal is to open a specified page based on a URL. How do we do that? Very simple, we make a corresponding relationship between THE URL and the corresponding page, for example, put the URL as the key in the map, and the corresponding page activity as the value. So when we want to start the activity, we need to find the corresponding activity in the map based on the URL that was sent to us, and then call startActivity.
So how do we maintain this map, you might ask? So how do we store this mapping in our map? You can’t manually put it. Let’s manually initialize my mapping when the app starts, so that when the page is opened, we can get it directly from the URL. So the question is, are you tired, big brother? So the first thing to think about for a lazy person is can I generate this mapping table automatically? The answer is yes.
Thinking summary
We can take advantage of the compile annotation feature to add a new annotation to each activity that needs to be opened via a URL. Get all the annotated classes in the annotation handler, dynamically generate the mapping table, and load the generated mapping into memory when the app starts.
The second section of the code
0x01
First we need to create three modules, as shown below:
Why three projects? Here’s why:
-
AbstractProcessor, the annotation processor we need, is in the Javax package, but there is no such package in the Android project, so we need to build a Java library, namely router-compiler, which helps us dynamically generate code. Exists only at compile time
-
Since router-Compiler only exists at compile time, our annotations need to be used in the project. Where should the class be placed? This creates a second Java library, router-Annotation, which is dedicated to storing the annotations we define and the code we want to call into our app.
-
Since both libraries are Java projects, and we will eventually use android projects, we will definitely use android project classes such as Context when providing external apis. So there is a third Module router-API for handling the generated artifacts. For example, load the generated mapping table into memory, and provide a unified call entry.
0x02
Let’s start by defining our own annotations:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path();
String group() default "";
String name() default "";
int extras() default Integer.MIN_VALUE;
int priority() default -1;
}
Copy the code
Define your own Route processor, RouterProcessor
@autoService (processor.class) // Automatically register annotation Processor @supportedOptions ({consts.key_module_name}) // parameter SupportedSourceVersion(SourceVersion.release_7) // Java version SupportedAnnotationTypes({ANNOTATION_ROUTER_NAME}) Public class RouterProcessor extends AbstractProcessor{private Map<String,Set<RouteMeta>> groupMap = new HashMap<>(); Private Map<String,String> rootMap = new TreeMap<>(); private Filer mFiler; private Logger logger; private Types types; private TypeUtilstypeUtils;
private Elements elements;
private String moduleName = "app"; // Default app private TypeMirror iProvider = null; / / / / IProvider type...Copy the code
SupportedAnnotationTypes specifies the annotation Route defined above
The next step is to collect all annotated classes and generate the mapping as follows:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if(CollectionUtils.isNotEmpty(set) {// get all the annotated classes Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Route.class); try { logger.info(">>> Found routers,start... < < <");
parseRoutes(elementsAnnotatedWith);
} catch (IOException e) {
logger.error(e);
}
return true;
}
return false;
}
Copy the code
We pass it to the parseRoutes method:
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if(CollectionUtils.isNotEmpty(routeElements)) {
logger.info(">>> Found routes, size is " + routeElements.size() + "< < <"); rootMap.clear(); / /... TypeMirror type_activity = elements.getTypeElement(ACTIVITY).asType();for (Element element : routeElements) {
TypeMirror tm = element.asType();
Route route = element.getAnnotation(Route.class);
RouteMeta routeMeta;
if(types.isSubtype(tm,type_activity)) { //activity
logger.info(">>> Found activity route: "+ tm.toString() + "< < <");
routeMeta = new RouteMeta(route,element,RouteType.ACTIVITY,null);
} else if(types.isSubtype(tm,iProvider)) { //IProvider
logger.info(">>> Found provider route: " + tm.toString() + "< < <");
routeMeta = new RouteMeta(route,element,RouteType.PROVIDER,null);
} else if(types.isSubtype(tm,type_fragment) || types.isSubtype(tm,type_v4_fragment)) { //Fragment
logger.info(">>> Found fragment route: " + tm.toString() + " <<< ");
routeMeta = new RouteMeta(route,element,RouteType.parse(FRAGMENT),null);
} else {
throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "]."); } categories(routeMeta); } / /...Copy the code
This is a long method, so let’s look at the main processing. We’ll iterate through routeElements to determine the type of class being annotated, activity, IProvider,Fragment. And fragments (note that fragments include fragments from both native and V4 packages) then construct a routeMeta object based on its type, passing it to the Categories method:
private void categories(RouteMeta routeMete) {
if (routeVerify(routeMete)) {
logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + "< < <"); RouteMeta < routeMeta > routeMetas = groupmap.get (routemet.getGroup ()); routemet.getGroup (); routemet.getGroup ();if (CollectionUtils.isEmpty(routeMetas)) {
Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
@Override
public int compare(RouteMeta r1, RouteMeta r2) {
try {
return r1.getPath().compareTo(r2.getPath());
} catch (NullPointerException npe) {
logger.error(npe.getMessage());
return0; }}}); routeMetaSet.add(routeMete); groupMap.put(routeMete.getGroup(), routeMetaSet); }else{ routeMetas.add(routeMete); }}else {
logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + "< < <"); }}Copy the code
In this method, we first look up the groupMap based on the current url group, that is, to see if there is a group. If there is a corresponding RouterMeta set, we put the generated routeMeta in the groupMap. There is no new collection.
So far we have captured all the annotation classes and grouped them. The next step is to generate a Java class to hold this information:
Here’s just the code that handles the activity mapping:
/ / (1)for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
String groupName = entry.getKey();
// (2)
MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(groupParamSpec);
Set<RouteMeta> groupData = entry.getValue();
for(RouteMeta meta : groupData) { ClassName className = ClassName.get((TypeElement) meta.getRawType()); / /... (3) loadIntoMethodOfGroupBuilder.addStatement("atlas.put($S," +
"$T.build($T." + meta.getType() + ",$T.class,$S.$S," + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + "," + meta.getPriority() + "," + meta.getExtra() + ")",
meta.getPath(),
routeMetaCn,
routeTypeCn,
className,
meta.getPath().toLowerCase(),
meta.getGroup().toLowerCase());
}
//Generate groups (4)
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
logger.info(">>> Generated group: " + groupName + "< < <"); rootMap.put(groupName, groupFileName); } / / (5)if(MapUtils.isNotEmpty(rootMap)) {
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
loadIntoMethodOfRootBuilder.addStatement("routes.put($S.$T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); }} / /... // Write root meta into disk. (6) String rootFileName = NAME_OF_ROOT + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(rootFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(elements.getTypeElement(IROUTE_ROOT))) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfRootBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated root, name is " + rootFileName + "< < <");
}
Copy the code
The above code is explained as follows:
- Iterate over the groupMap we stored before and extract the corresponding set, such as comment (1)
- Generates a method body and puts all the mappings in the collection into the parameter map. (2) (3)
- Generate a Java class named RouterManager? Group? + moduleName, moduleName is here in the build. Gradle file configuration, such as configuration, live access to null if (4)
- Make a mapping between each group and the generated class, which is used to realize the function of loading by group, such as (5) and (6).
Now let’s go to the next product
/**
DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", RouterManager$$Group$$service.class); }}Copy the code
Mapping between storage groups
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Group$$service implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/service/test/main",RouteMeta.build(RouteType.ACTIVITY,OtherActivity.class,"/service/test/main"."service",null, -1,-2147483648)); }}Copy the code
Now that the mapping is automatically generated, how do YOU use it? Now let me introduce our Api
0x03
Since our mapping table exists globally, we must initialize it in the Application to load the mapping into memory. Let’s look at the implementation code
First we need a container to store our mappings, hence the Warehouse class
class Warehouse { // Cache route and metas static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); static Map<String, RouteMeta> routes = new HashMap<>(); / /... static voidclear() { providers.clear(); providersIndex.clear(); }}Copy the code
We instantiate two maps in this class to store our grouping information and the mapping information in each grouping
GroupIndex: stores group information. This item takes precedence over data load. Routes: stores relationship data
Next we call the following code to initialize the App:
RouterManager.init(this);
Copy the code
So let’s go inside the init method and see what we’re doing, okay?
public static synchronized void init(Application application){
if(! hasInit) { hasInit =true; mContext = application; mHandler = new Handler(Looper.getMainLooper()); logger = new DefaultLogger(); LogisticsCenter.init(mContext,logger); }}Copy the code
You can see that the key line here is logisticScenter.init (mContext, Logger)
So let’s go ahead and logisticScenter. init(mContext, Logger); Look at the methods:
public synchronized static void init(Context context, ILogger log) {
logger = log;
Set<String> routeMap;
try {
if(RouterManager debuggable () | | PackageUtils. IsNewVersion (context)) {/ / development mode or version upgrade when scanning a logger. The local info (TAG,"The current environment is in DEBUG mode or a new version, need to regenerate the mapping table");
//these class was generated by router-compiler
routeMap = ClassUtils.getFileNameByPackageName(context, Consts.ROUTE_ROOT_PAKCAGE);
if(! routeMap.isEmpty()) { PackageUtils.put(context,Consts.ROUTER_SP_KEY_MAP,routeMap); } PackageUtils.updateVersion(context); }else{// Read cache logger.info(TAG,"Read router mapping table in cache");
routeMap = PackageUtils.get(context,Consts.ROUTER_SP_KEY_MAP);
}
logger.info(TAG,"Router Map scan completed"); // Load grouped data into memoryfor (String className : routeMap) {
//Root
if(className.startsWith(Consts.ROUTE_ROOT_PAKCAGE + Consts.DOT + Consts.SDK_NAME + Consts.SEPARATOR + Consts.SUFFIX_ROOT)) { ((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); } / /... } logger.info(TAG,"Read the mapping into the cache");
if(Warehouse.groupsIndex.size() == 0) {
logger.error(TAG,"No mapping files,check your configuration please!");
}
if (RouterManager.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.providersIndex.size()));
}
} catch (Exception e) {
e.printStackTrace();
logger.error(TAG,"RouterManager init logistics center exception! [" + e.getMessage() + "]"); }}Copy the code
Specific explanations are as follows:
1) First, scan all generated class files by package name and place them in routeMap. 2), iterate over the scanned array, cache all group information to Warehouse. GroupIndex
You can see that the initialization only does these two things, scanning the class file, reading the grouping information; If you think about it, you’ll notice that our URL and activity mapping information is not being read, which is called loading on demand.
So all of our work is done here, so how do we use it?
Let’s see how it’s used
0x04
Let’s start with a piece of code:
RouterManager.getInstance().build("/user/main").navigation(MainActivity.this);
Copy the code
The code above is how we open the UserActivty page and you can see that only one URL is passed. So let’s look inside and see how it works, okay?
First let’s look at the code in the build method:
public Postcard build(String path) {
if(TextUtils.isEmpty(path)) {
throw new HandlerException("Parameter is invalid!");
} else {
return build(path,extractGroup(path));
}
}
public Postcard build(String path,String group) {
if(TextUtils.isEmpty(path)) {
throw new HandlerException("Parameter is invalid!");
} else {
returnnew Postcard(path,group); }}Copy the code
The Postcard object is returned as an overloaded method, and the Postcard’s navigation method is called. You can see that the Postcard is really just an entity that carries data. Let’s look at the navigation method:
public Object navigation(Context context) {
return RouterManager.getInstance().navigation(context,this,-1);
}
Copy the code
RouterManager’s navigation method is called:
Object navigation(final Context context,final Postcard postcard,final int requestCode) {
try {
LogisticsCenter.completion(postcard);
} catch (HandlerException e) {
e.printStackTrace();
return null;
}
final Context currentContext = context == null ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
final Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if(flags ! = -1) { intent.setFlags(flags); }else if(! (currentContext instanceof Activity) {// if the currentContext is not an Activity, Intent.setflags (intent.flag_activity_new_task); intent.setflags (intent.flag_activity_new_task); } runInMainThread(newRunnable() {
@Override
public void run() { startActivity(requestCode,currentContext,intent,postcard); }});break; / /... }return null;
}
Copy the code
By the above code can be seen that the first call is LogisticsCenter.com pletion hand into the object of it () method, then let’s go to see how this method:
/** * Fill in data * @param postcard */ public synchronized static void completion(postcard) {RouteMeta RouteMeta = Warehouse.routes.get(postcard.getPath());if(routeMeta ! = null) { postcard.setDestination(routeMeta.getDestination()); postcard.setType(routeMeta.getType()); postcard.setPriority(routeMeta.getPriority()); postcard.setExtra(routeMeta.getExtra()); / /... }else {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
if(groupMeta == null) {
throw new NoRouteFoundException("There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} elseIRouteGroup = groupMeta.getconstructor ().newinstance (); iRouteGroup.loadInto(Warehouse.routes); Warehouse.groupsIndex.remove(postcard.getGroup()); } catch (Exception e) { throw new HandlerException("Fatal exception when loading group meta. [" + e.getMessage() + "]"); } } completion(postcard); }}Copy the code
RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta Then reflect the example object and call the loadInfo() method to read all the mappings in this group into Warehouse.routes, and continue to call the current method to fill in the relevant information.
After filling in the information continue back to the navigation method:
switch (postcard.getType()) {
case ACTIVITY:
final Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if(flags ! = -1) { intent.setFlags(flags); }else if(! (currentContext instanceof Activity) {// if the currentContext is not an Activity, Intent.setflags (intent.flag_activity_new_task); intent.setflags (intent.flag_activity_new_task); } runInMainThread(newRunnable() {
@Override
public void run() { startActivity(requestCode,currentContext,intent,postcard); }});break; / /... }Copy the code
You can see that the normal startup method startActivity is used to start a new activity.
Ok, so far, the whole process is finished. As for passing parameters, obtaining fragments, and serving IProvider, the routine is the same, which will not be repeated here.
conclusion
ARouter’s idea is very simple: generate a list of urls that map to the page using compile-time annotations, load the list into memory when the application is launched, and then go directly to memory to find it and perform the normal page launch method.
So let’s answer the two questions we raised earlier
First: Why configure a moduleName in every build.gradle file?
This is because compile-time annotations generate code in modules, which means that we need to configure the dependency on the annotation generator for each Module project, to ensure that the name of the generated Java file is not repeatedly suffixed with module. This configuration has nothing to do with grouping. Just to avoid the duplication of generated grouping classes.
Second: why would you report a number of similar names?
We know that the Router mapping table has two tables. The first table stores groups and their corresponding classes, and the second table stores specific URL mappings in each group. In the first problem, we use moduleName to avoid storing group class names. Is it possible for each group class itself to have the same name? The answer is yes. For example, if we set the url /user/main to user, we will automatically generate a class named RouterManager when compiling the User component. Group? User class, used to store all page mappings grouped by user. So when we configure a group named User in the app, we will generate a class named RouterManager in the app when we compile the app. Group? The user class. Our app project relies on the User component, which results in two files with the same class name. An error will occur at compile time.
A few thoughts on RouterManager
- RouterManager can be used for quadrate process calls:
I think so. The relational mapping table of The RouterManager is stored in a global static variable and we only need to provide an interface to get the mapping when other processes need to access it.
- Whether RouterManager can be used in RePlugin:
The answer is yes. Since RePlugin uses multiple ClassLoaders, the objects we retrieve from the main classloader and the plug-in classloader are separate objects. If you want to use RouterManager in a plug-in to open a host page, there is no mapping because the RouterManager object obtained in the plug-in is not a host singleton object, but a new object is created. So what to do? The answer is simple: we use reflection in the plug-in to get the host RouterManager instance.
Note: The RouterManager framework is derived from the ARouter framework, which only implements page hopping, fragment obtaining, and service Provider obtaining functions. As for the other degradation strategies, dependency injection is not implemented
Project source please move to my Github warehouse view: github.com/qiangzier/R…