ARouter是阿里开源的组件通讯框架,在组件化开发上也是十分常用的框架之一。
它的主要作用是各个activity之间,无需直接依赖,就可以直接跳转与传参。主要用处是为组件化的解耦,添砖加瓦。
ARouter的核心原理,十分简单:用注解标识各个页面,注解处理器将该注解对应的页面存储到一个统一的map集合中。当需要页面跳转时,根据跳转的入参,从该map集合中取到对应的页面和传参,并跳转。
核心在于,如何构建一个合理的map集合。
根据原理分析,我们手写一个BRouter(ARouter的核心实现)。用一个单例类BRouter存储map集合,并在该类中,实现跳转与传参。
1、首先还是注解的定义
//针对的是最外层的类
@Target(ElementType.TYPE)
//编译时
@Retention(RetentionPolicy.CLASS)
public @interface Path {
String value() default "";
}
2、注解处理器的编写
//注解处理器的依赖,此处有注意点:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(path: ':brouter-annotation') //3.6+的android studio需要按以下方式依赖auto-service compileOnly'com.google.auto.service:auto-service:1.0-rc4' annotationProcessor'com.google.auto.service:auto-service:1.0-rc4' }
@AutoService(Processor.class)
public class BRouterProcessor extends AbstractProcessor {
//定义包名
private static final String CREATE_PACKAGE_NAME = "com.sunny.brouter.custom";
//定义基础类名
private static final String CREATE_CLASS_NAME = "RouterUtil";
//后续文件操作使用
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//从外部传入参数中,获取filer
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//获取注解的类
Set extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Path.class);
if(elementsAnnotatedWith.size() < 1){
return false;
}
Map collectMap = new HashMap<>();
for(Element element : elementsAnnotatedWith){
//类节点
TypeElement typeElement = (TypeElement) element;
String className = typeElement.getQualifiedName().toString();
String key = element.getAnnotation(Path.class).value();
if(collectMap.get(key)==null){
//注解内容作为key,类名作为value,存入map中--此map是单个module的map
collectMap.put(key,className+".class");
}
}
Writer writer = null;
try {
//为避免类名重复,生成的类名加上动态时间戳---此处实现与ARouter本身不一致,但更简单。
//避免了从build.gradle中传递参数的步骤
String activityName = CREATE_CLASS_NAME + System.currentTimeMillis();
JavaFileObject sourceFile = filer.createSourceFile(CREATE_PACKAGE_NAME + "." + activityName);
writer = sourceFile.openWriter();
//代码生成
StringBuilder routerBuild = new StringBuilder();
for (String key : collectMap.keySet()) {
routerBuild.append(" BRouter.getInstance().addRouter(\""+key+"\", "+collectMap.get(key)+");\n");
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("package "+CREATE_PACKAGE_NAME+";\n");
stringBuilder.append("import com.sunny.brouter.BRouter;\n" +
"import com.sunny.brouter.IRouter;\n" +
"\n" +
"public class "+activityName+" implements IRouter {\n" +
"\n" +
" @Override\n" +
" public void addRouter() {\n" +
routerBuild.toString() +
" }\n" +
"}");
writer.write(stringBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
}finally {
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
@Override
public Set getSupportedAnnotationTypes() {
Set types = new HashSet<>();
types.add(Path.class.getCanonicalName());
return types;
}
}
此处的核心有两点:(1)如何生成一个map (2)生成的类的类名处理,避免重复
3、BRouter单例类的实现
至此,各个module中,已经生成了对应的类,并把各个的map信息,添加到了BRouter这个类中。
BRouter的具体实现也很简单:
(1)收集各个module中map的key与value。
(2)加上页面跳转方法。
(3)Context参数的传递(startActivity需要用到该参数)。
public class BRouter {
private static final String TAG = "BRouter";
private static final String CREATE_PACKAGE_NAME = "com.sunny.brouter.custom";
private static volatile BRouter router;
private Map> routerMap;
private Context context;
private BRouter() {
routerMap = new HashMap<>();
}
public static BRouter getInstance() {
if (null == router) {
synchronized (BRouter.class) {
router = new BRouter();
}
}
return router;
}
//该方法用于:(1)传入context (2)调用各个module组件中的addRouter方法
public void init(Context context) {
this.context = context;
try {
//根据包名查找所有的class
Set classes = getFileNameByPackageName(context, CREATE_PACKAGE_NAME);
if (classes.size() > 0) {
for (String classStr : classes) {
Class> aClass = Class.forName(classStr);
Object o = aClass.newInstance();
if (o instanceof IRouter) {
((IRouter) o).addRouter();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//各个Module中调用该方法,把key,value存入map
public void addRouter(String key, Class> activityClass) {
if (routerMap.get(key) == null) {
routerMap.put(key, activityClass);
}
}
//页面的跳转与传参
public void jump(String key, Bundle bundle) {
Class> jumpToClass = routerMap.get(key);
if (jumpToClass == null) {
return;
}
Log.e(TAG, "jump: " + jumpToClass.getName());
Intent intent = new Intent(context, jumpToClass);
if (bundle != null) {
intent.putExtras(bundle);
}
if (context != null) {
context.startActivity(intent);
}
}
...
}
4、补充说明:BRouter中根据包名查找所有的class
可以参考ARouter中的做法,在BRouter中添加以下代码,附录如下:
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
private static final String PREFS_FILE = "multidex.version";
private static final String KEY_DEX_NUMBER = "dex.number";
private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static SharedPreferences getMultiDexPreferences(Context context) {
return context.getSharedPreferences(PREFS_FILE, Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set classNames = new HashSet<>();
List paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size());
for (final String path : paths) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
Log.d(TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public static List getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
// 通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
if (!isVMMultidexCapable()) {
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
}
}
return sourcePaths;
}
/**
* Identifies if the current VM has a native support for multidex, meaning there is no need for
* additional installation by this library.
*
* @return true if the VM handles multidex
*/
private static boolean isVMMultidexCapable() {
boolean isMultidexCapable = false;
String vmName = null;
try {
if (isYunOS()) { // YunOS需要特殊判断
vmName = "'YunOS'";
isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
} else { // 非YunOS原生Android
vmName = "'Android'";
String versionString = System.getProperty("java.vm.version");
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException ignore) {
// let isMultidexCapable be false
}
}
}
}
} catch (Exception ignore) {
}
Log.i(TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
return isMultidexCapable;
}
/**
* 判断系统是否为YunOS系统
*/
private static boolean isYunOS() {
try {
String version = System.getProperty("ro.yunos.version");
String vmName = System.getProperty("java.vm.name");
return (vmName != null && vmName.toLowerCase().contains("lemur"))
|| (version != null && version.trim().length() > 0);
} catch (Exception ignore) {
return false;
}
}
至此,手写完成了一个与ARouter功能与原理都十分类似的BRouter。
它的关键词是解耦与组件化应用。