Recently, Small has been used to implement the plug-in of the original project, and the effect is not bad. As long as the project is a componentized structure, it is easy to reconstruct. When you use ARouter, however, you cannot find the ARouter automatically generated by the Route annotation because the queried apK path is only base.apk when initialized. Group? XXX file. In order to adapt to the plug-in version, you need to manually build a simplified version of the ARouter framework.
APT
The Activity class marked with annotations is processed through APT to generate the corresponding mapping file. Here we create two Modules of type Java Library. A library (ARouter handles the logic), a Compiler (annotations, source code generation)
Gradle introduces dependencies
The library of the build. Gradle
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs'.include: ['*.jar'])
compileOnly 'com. Google. Android: android: 4.1.1.4'
}
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
Copy the code
CompilerOnly is the Android related library
The build of the compiler. Gradle
apply plugin: 'java'
dependencies {
compile 'com. Squareup: javapoet: 1.9.0'
compile 'com. Google. Auto. Services: auto - service: 1.0 -rc3'
compile project(':library')}targetCompatibility = '1.7'
sourceCompatibility = '1.7'
Copy the code
Auto-service automatically generates a Processor configuration file in the META-INF folder so that the processing class corresponding to the annotation can be found during compilation. Javapoet is an open source library from Square that gracefully generates Java source files.
Create an annotation @route
Next, we create an annotation CLASS in the Library. Target indicates the TYPE of the modification (CLASS or interface, method, attribute, TYPE indicates CLASS or interface), Retention indicates the level of visibility (compile-time, run-time, etc., CLASS indicates visible at compile time)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path(a);
}
Copy the code
We then introduce dependencies in app Gradle
dependencies {
annotationProcessor project(':compiler')
compile project(':library')}Copy the code
Note: Gradle2.2 requires the annotationProcessor to be apt and introduced in the project root directory
classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'
Copy the code
Add annotations to MainActivity
.import io.github.iamyours.aarouter.annotation.Route;
@Route(path = "/app/main")
public class MainActivity extends AppCompatActivity {... }Copy the code
Create the annotation processing class RouteProcessor
package io.github.iamyours.compiler;
import com.google.auto.service.AutoService;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import io.github.iamyours.aarouter.annotation.Route;
/** * Created by yanxx on 2017/7/28. */
@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
System.out.println("= = = = = = = = = = = = ="+roundEnvironment);
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes(a) {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Route.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion(a) {
returnSourceVersion.latestSupported(); }}Copy the code
Then we make project below, get the following log information, it indicates that apt configuration is successful.
:app:javaPreCompileDebug
:compiler:compileJava UP-TO-DATE
:compiler:processResources NO-SOURCE
:compiler:classes UP-TO-DATE
:compiler:jar UP-TO-DATE
:app:compileDebugJavaWithJavac
=============[errorRaised=false, rootElements=[io.github.iamyours.aarouter.MainActivity, ...] =============[errorRaised=false, rootElements=[], processingOver=true]
:app:compileDebugNdk NO-SOURCE
:app:compileDebugSources
Copy the code
Generate source files using Javapoet
The use of Javapoet can be found here github.com/square/java… To hold the class name marked by the Route annotation, a mapping class is saved as a method call. The generated classes are as follows
public final class AARouterMap_app implements IRoute {
@Override
public void loadInto(Map<String, String> routes) {
routes.put("/app/main"."io.github.iamyours.aarouter.MainActivity"); }}Copy the code
In order to find mapped classes from DexFile in Android APK later, we need to put these mapped Java classes in the same package as follows: Add IRoute interface to library
public interface IRoute {
void loadInto(Map<String, String> routes);
}
Copy the code
In the compiler
@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {
private Filer filer;
private Map<String, String> routes = new HashMap<>();
private String moduleName;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element e : roundEnvironment.getElementsAnnotatedWith(Route.class)) {
addRoute(e);
}
createRouteFile();
return true;
}
private void createRouteFile(a) {
TypeSpec.Builder builder = TypeSpec.classBuilder("AARouterMap_" + moduleName).addModifiers(Modifier.PUBLIC);
TypeName superInterface = ClassName.bestGuess("io.github.iamyours.aarouter.IRoute");
builder.addSuperinterface(superInterface);
TypeName stringType = ClassName.get(String.class);
TypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), stringType, stringType);
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("loadInto")
.addAnnotation(Override.class)
.returns(void.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(mapType, "routes");
for (String key : routes.keySet()) {
methodBuilder.addStatement("routes.put($S,$S)", key, routes.get(key));
}
builder.addMethod(methodBuilder.build());
JavaFile javaFile = JavaFile.builder(ARouter.ROUTES_PACKAGE_NAME, builder.build()).build();// Output the source code to arouter.routes_package_name,
try {
javaFile.writeTo(filer);
} catch (IOException e) {
// e.printStackTrace();}}/* moduleName (); /* moduleName (); /* moduleName (); Ali's access method is to build in each module file by annotationProcessorOptions incoming, this simplified directly from the path (such as "/ app/login" app, "/ news/newsinfo" news) * /
private void addRoute(Element e) {
Route route = e.getAnnotation(Route.class);
String path = route.path();
String name = e.toString();
moduleName = path.substring(1,path.lastIndexOf("/"));
routes.put(path, name);
}
@Override
public Set<String> getSupportedAnnotationTypes(a) {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Route.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion(a) {
returnSourceVersion.latestSupported(); }}Copy the code
ARouter initialization
To get all the routes with the @route annotation mark, find the AARouterMap_xxx class file in arouter.routes_package_name from DexFile and load the Route with the loadInto call via reflection initialization.
public class ARouter {
private Map<String, String> routes = new HashMap<>();
private static final ARouter instance = new ARouter();
public static final String ROUTES_PACKAGE_NAME = "io.github.iamyours.aarouter.routes";
public void init(Context context){
try {// Find the mapping class file in the ROUTES_PACKAGE_NAME directory
Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
initRoutes(names);
} catch(Exception e) { e.printStackTrace(); }}// Initialize the route through reflection
private void initRoutes(Set<String> names) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
for(String name:names){
Class clazz = Class.forName(name);
Object obj = clazz.newInstance();
if(obj instanceofIRoute){ IRoute route = (IRoute) obj; route.loadInto(routes); }}}private ARouter(a) {}public static ARouter getInstance(a) {
return instance;
}
public Postcard build(String path) {
String component = routes.get(path);
if (component == null) throw new RuntimeException("could not find route with " + path);
return newPostcard(component); }}Copy the code
Get the route mapping class file
We put the mapped classes under the ROUTES_PACKAGE_NAME using the RouterProcessor, and we just need to walk through the dex file to find them. While The ARouter of Alibaba takes the dex file sought by the current app application directory base.apk, and then loads the DexFile by DexClassLoader. But if the project is made up of plug-ins, the dexFile is not just base.apk, so you need to get it in another way. Through breakpoint debugging, it is found that the pathList in the Context’s classloader contains all apK paths. We can get the dexFile simply by reflecting the context’s classloader, and we don’t need to reload the dexFile. LoadDex in the field ourselves.
public class ClassUtils {
// Obtain all dexfiles of app through BaseDexClassLoader reflection
private static List<DexFile> getDexFiles(Context context) throws IOException {
List<DexFile> dexFiles = new ArrayList<>();
BaseDexClassLoader loader = (BaseDexClassLoader) context.getClassLoader();
try {
Field pathListField = field("dalvik.system.BaseDexClassLoader"."pathList");
Object list = pathListField.get(loader);
Field dexElementsField = field("dalvik.system.DexPathList"."dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(list);
Field dexFilefield = field("dalvik.system.DexPathList$Element"."dexFile");
for(Object dex:dexElements){ DexFile dexFile = (DexFile) dexFilefield.get(dex); dexFiles.add(dexFile); }}catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return dexFiles;
}
private static Field field(String clazz,String fieldName) throws ClassNotFoundException, NoSuchFieldException {
Class cls = Class.forName(clazz);
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
/** * Scan all classnames ** contained under the package by specifying the package name@param context U know
* @paramPackageName package name *@returnThe set of all classes */
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws IOException {
final Set<String> classNames = new HashSet<>();
List<DexFile> dexFiles = getDexFiles(context);
for (final DexFile dexfile : dexFiles) {
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if(className.startsWith(packageName)) { classNames.add(className); }}}returnclassNames; }}Copy the code
With the above implementation, we can get the mapped route files by passing in the context’s classloader during initialization, and then reflect them and call loadInto to get all the routes. The next route jump is as simple as wrapping it as ComponentName
public class ARouter {...public Postcard build(String path) {
String component = routes.get(path);
if (component == null) throw new RuntimeException("could not find route with " + path);
return newPostcard(component); }}Copy the code
public class Postcard {
private String activityName;
private Bundle mBundle;
public Postcard(String activityName) {
this.activityName = activityName;
mBundle = new Bundle();
}
public Postcard withString(String key, String value) {
mBundle.putString(key, value);
return this;
}
public Postcard withInt(String key, int value) {
mBundle.putInt(key, value);
return this;
}
public Postcard with(Bundle bundle) {
if (null! = bundle) { mBundle = bundle; }return this;
}
public void navigation(Activity context, int requestCode) {
Intent intent = new Intent();
intent.setComponent(newComponentName(context.getPackageName(), activityName)); intent.putExtras(mBundle); context.startActivityForResult(intent, requestCode); }}Copy the code
The project address
Github.com/iamyours/AA…
added
The current version also works with Small, but finding mapped classes by reflecting private apis is still a bit of a catch. Then I came up with another solution: each module build passes in the package name of the module, and the generated file is named AARouterMap. During initialization, small can get the package name of each plug-in through small.getBundleversions ().keys
ARouter.getInstance().init(Small.getBundleVersions().keys)
Copy the code
To get the package name for each plug-in and then ARouter initializes it using the package name list
public void init(Set<String> appIds) {
try {
initRoutes(appIds);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch(InstantiationException e) { e.printStackTrace(); }}private void initRoutes(Set<String> appIds) throws IllegalAccessException, InstantiationException {
for (String appId : appIds) {
Class clazz = null;
try {
clazz = Class.forName(appId + ".AARouterMap");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if(clazz==null)continue;
Object obj = clazz.newInstance();
if (obj instanceofIRoute) { IRoute route = (IRoute) obj; route.loadInto(routes); }}}Copy the code
You don’t have to traverse the dex to get the map, and the performance and security are better. Non-plug-in projects can also be adapted by manually passing the package name list.