框架手写系列---apt方式实现ARouter框架
Posted 战国剑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架手写系列---apt方式实现ARouter框架相关的知识,希望对你有一定的参考价值。
一、ARouter
ARouter是阿里开源的组件通讯框架,在组件化开发上也是十分常用的框架之一。
它的主要作用是各个activity之间,无需直接依赖,就可以直接跳转与传参。主要用处是为组件化的解耦,添砖加瓦。
二、ARouter原理
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<String,String> 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<String> getSupportedAnnotationTypes()
Set<String> 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<String, Class<?>> 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<String> 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<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException
final Set<String> classNames = new HashSet<>();
List<String> 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<String> 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<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> 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。
它的关键词是解耦与组件化应用。
以上是关于框架手写系列---apt方式实现ARouter框架的主要内容,如果未能解决你的问题,请参考以下文章