最近用Small实现原有项目的插件化,效果还不错,只要工程是组件化的结构就很好重构。但在使用ARouter时,因为初始化时,查询的apk路径只有base.apk,因此不能找到由Route注解自动生成的ARouter$$Group$$xxx文件。为了适配插件化版本,因此须要本身手动打造简易版的ARouter框架。java
经过APT处理用注解标记的Activity类,生成对应的映射文件。这里建立两个类型为java library的module。一个library(ARouter处理逻辑),一个compiler(处理注解,生成源码)android
library的build.gradlegit
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'
复制代码
compilerOnly里的是Android的相关类库github
compiler的build.gradleapi
apply plugin: 'java'
dependencies {
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
compile project(':library')
}
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
复制代码
auto-service会自动在META-INF文件夹下生成Processor配置信息文件,使得编译时能找到annotation对应的处理类。javapoet则是由square公司出的开源库,能优雅的生成java源文件。安全
接着,咱们在library中建立一个注解类,Target代表修饰的类型(类或接口、方法、属性,TYPE表示类或接口),Retention代表可见级别(编译时,运行时期等,CLASS表示在编译时可见)bash
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path();
}
复制代码
而后在app的gradle引入依赖app
dependencies {
annotationProcessor project(':compiler')
compile project(':library')
}
复制代码
注意:gradle2.2如下须要将annotationProcessor改成apt,同时在工程根目录引入框架
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
复制代码
在MainActivity中添加注解ide
...
import io.github.iamyours.aarouter.annotation.Route;
@Route(path = "/app/main")
public class MainActivity extends AppCompatActivity {
...
}
复制代码
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() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Route.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
复制代码
而后咱们make project如下,获得以下日志信息,则代表apt配置成功。
: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
复制代码
javapoet的用法能够看这里github.com/square/java… 为了保存由Route注解标记的class类名,这里用一个映射类经过方法调用的形式保存。具体生成的类以下
public final class AARouterMap_app implements IRoute {
@Override
public void loadInto(Map<String, String> routes) {
routes.put("/app/main","io.github.iamyours.aarouter.MainActivity");
}
}
复制代码
为了以后可以从Android apk中的DexFile中找到映射类,咱们要把这些映射java类放到同一个package下,具体实现逻辑以下: 在library中添加IRoute接口
public interface IRoute {
void loadInto(Map<String, String> routes);
}
复制代码
在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() {
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();//将源码输出到ARouter.ROUTES_PACKAGE_NAME,
try {
javaFile.writeTo(filer);
} catch (IOException e) {
// e.printStackTrace();
}
}
/* 这里有一个注意的点事moduleName,因为每一个library或application模块的环境不一样, 也只能取到当前模块下的注解,所以须要生成不一样的映射文件保存到每一个模块下, 阿里的获取的方法是在每一个模块的build文件经过annotationProcessorOptions传入, 这边简化直接从path获取(如“/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() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Route.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
复制代码
为了获得全部有@Route注解标记的路由,须要从DexFile中找到ARouter.ROUTES_PACKAGE_NAME目录下的AARouterMap_xxx的class文件,经过反射初始化调用loadInto加载路由。
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 {//找到ROUTES_PACKAGE_NAME目录下的映射class文件
Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
initRoutes(names);
} catch (Exception e) {
e.printStackTrace();
}
}
//经过反射初始化路由
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 instanceof IRoute){
IRoute route = (IRoute) obj;
route.loadInto(routes);
}
}
}
private ARouter() {
}
public static ARouter getInstance() {
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 new Postcard(component);
}
}
复制代码
以前咱们经过RouterProcessor将映射class放到了ROUTES_PACKAGE_NAME下,咱们只须要在dex文件中遍历寻找到它们便可。而alibaba的ARouter取的是当前app应用目录的base.apk寻找的dex文件,而后经过DexClassLoader加载取得DexFile。但若是项目插件化构成的,dexFile就不仅是base.apk下了,所以须要经过其余方式获取了。 经过断点调试,发现context的classloader中的pathList便含有了全部apk的路径。咱们只需经过反射context的classloader就能够获取dexFile,并且也不须要本身经过现场DexFile.loadDex从新加载了。
public class ClassUtils {
//经过BaseDexClassLoader反射获取app全部的DexFile
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;
}
/** * 经过指定包名,扫描包下面包含的全部的ClassName * * @param context U know * @param packageName 包名 * @return 全部class的集合 */
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);
}
}
}
return classNames;
}
}
复制代码
有了上面的实现,咱们就能够在初始化时,经过传入context的classloader,获取到映射路由文件,而后反射初始化他们,调用loadInto,便可获得全部的路由。而接下来的路由跳转就很简单了,只需包装成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 new Postcard(component);
}
}
复制代码
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(new ComponentName(context.getPackageName(), activityName));
intent.putExtras(mBundle);
context.startActivityForResult(intent, requestCode);
}
}
复制代码
如今这个版本虽然也适配Small,可是经过反射私有api找到映射class终究仍是有些隐患。后来想到另一种方案:每一个模块build传入模块的包名,生成的文件统一命名为AARouterMap,初始化时small能够经过Small.getBundleVersions().keys获取每一个插件的包名
ARouter.getInstance().init(Small.getBundleVersions().keys)
复制代码
来获取每一个插件的包名 而后ARouter使用包名列表初始化
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 instanceof IRoute) {
IRoute route = (IRoute) obj;
route.loadInto(routes);
}
}
}
复制代码
这样就不用遍历dex获取映射,性能和安全性也会好一点。非插件化的项目也能够经过手动传包名列表适配了。