博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 路由实践(二)
阅读量:6717 次
发布时间:2019-06-25

本文共 16710 字,大约阅读时间需要 55 分钟。

前言

          继上一篇之后,断更已经差不多一个月,毕竟是年前的最后一个月,各种事情扎堆,直到近几天才稍微闲下来,于是有了此文。简单回顾下,上一篇文章中简单介绍了三种实现路由的方式,分别是:隐式的Intent、通过初始化路由表的方式实现、通过注解。最后总结了下优缺点,建议使用第二种,今天我们讲下第四种,为啥单开一篇文章呢?因为第四种涉及到知识点有点多,并且参考ButterKbife以及部分阿里巴巴ARouter的实现。

大体思路

         通过 annotationProcessor处理编译期注解,在编译的时候给路由表注入数据,这样在运行时通过annotationProcessor生成java代码并编译class文件。以下代码部分参考了Butterknife的实现:

/** * 自定义的编译期Processor,用于生成xxx$$Router.java文件 */@AutoService(Processor.class)public class RouterProcessor extends AbstractProcessor {    /**     * 文件相关的辅助类     */    private Filer mFiler;    /**     * 元素相关的辅助类     */    private Elements mElementUtils;    /**     * 日志相关的辅助类     */    private Messager mMessager;    /**     * 解析的目标注解集合     */    @Override    public synchronized void init(ProcessingEnvironment processingEnv) {        super.init(processingEnv);        mElementUtils = processingEnv.getElementUtils();        mMessager = processingEnv.getMessager();        mFiler = processingEnv.getFiler();    }    @Override    public Set
getSupportedAnnotationTypes() { Set
types = new LinkedHashSet<>(); types.add(RouterTarget.class.getCanonicalName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set
annotations, RoundEnvironment roundEnv) { mMessager.printMessage(Diagnostic.Kind.WARNING, "processprocessprocessprocess"); Set
routeElements = roundEnv.getElementsAnnotatedWith(RouterTarget.class); for (Element element : routeElements) { String packageName = element.getEnclosingElement().toString(); String fullClassName = element.toString(); String className = fullClassName.substring(fullClassName.indexOf(packageName) + packageName.length() + 1, fullClassName.length()); /** // * 构建类 // */ try { RouterTarget annotation = element.getAnnotation(RouterTarget.class); RouterByAnnotationManager.getInstance().addRouter(annotation.value(), element.toString()); mMessager.printMessage(Diagnostic.Kind.WARNING, RouterByAnnotationManager.getInstance().getRouter(annotation.value()) + RouterByAnnotationManager.getInstance()); FieldSpec routerKey = FieldSpec.builder(String.class, "routerKey", Modifier.FINAL, Modifier.PRIVATE).initializer("$S", annotation.value()).build(); FieldSpec clazz = FieldSpec.builder(String.class, "fullClassName", Modifier.FINAL, Modifier.PRIVATE).initializer("$S", fullClassName).build(); /** * 构建方法 */ MethodSpec methodSpec = MethodSpec.methodBuilder("injectRouter") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addCode("com.example.commonlib.RouterByAnnotationManager.getInstance().addRouter($L,$L);", "routerKey", "fullClassName") .build(); TypeSpec finderClass = TypeSpec.classBuilder(className + "$$Router") .addModifiers(Modifier.PUBLIC) .addMethod(methodSpec) .addField(routerKey) .addField(clazz) .addSuperinterface(RouterInjector.class) .build(); JavaFile.builder(packageName, finderClass).build().writeTo(mFiler); } catch (Exception e) { error("processBindView", e.getMessage()); } } return true; } public String getPackageName(TypeElement type) { return mElementUtils.getPackageOf(type).getQualifiedName().toString(); } private void error(String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args)); } private void info(String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(msg, args)); }}复制代码

注意getSupportedAnnotationTypes(),如果你要对那些类进行处理,就要把Class的类名加入到Set中并且返回。然后看下process()方法,里面利用javaPoet生成java文件,文件形如UserInfoActivity$$Router,内容如下:

import java.lang.Override;import java.lang.String;public class UserInfoActivity$$Router implements RouterInjector {  private final String routerKey = "android.intent.action.USERINFO";  private final String fullClassName = "com.example.userlib.UserInfoActivity";  @Override  public void injectRouter() {    com.example.commonlib.RouterByAnnotationManager.getInstance().addRouter(routerKey,fullClassName);}}复制代码

那么相信重点来了,怎么去调用injectRouter()方法,将数据注入到路由表中,到这里的时候

差点因为这个问题前功尽弃,最后祭出了阿里大法,参考了ARouter的实现。具体如下:

通过Application对Router进行初始化:

public class RouterApplication extends Application {    @Override    public void onCreate() {        super.onCreate();        //初始化路由        Router.init(this);    }}复制代码

Router初始化的时候通过反射将数据注入到路由表

public static void init(Application application) {    try {        Set
classNames = ClassUtils.getFileNameByPackageName(application, ActionConstant.SUFFIX); for (String className : classNames) { RouterFinder.bind(className); } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); }}复制代码

来看下阿里ARouter的反射帮助类:

** 这个类是从alibaba的ARouter复制过来的用来扫描所有的类等 */public class ClassUtils {    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 suffix 包名     * @return 所有class的集合     */    public static Set
getFileNameByPackageName(Context context, final String suffix) throws PackageManager.NameNotFoundException, IOException, InterruptedException { final Set
classNames = new HashSet<>(); List
paths = getSourcePaths(context); final CountDownLatch parserCtl = new CountDownLatch(paths.size()); for (final String path : paths) { RouterPoolExecutor.getInstance().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.endsWith(suffix)) { 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.e("getFileNameByPackage", "Filter " + classNames.size() + " classes by packageName <" + suffix + ">"); 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() + "'"); } } } sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo)); return sourcePaths; } /** * Get instant run dex path, used to catch the branch usingApkSplits=false. */ private static List
tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) { List
instantRunSourcePaths = new ArrayList<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs) { // add the split apk, normally for InstantRun, and newest version. instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs)); Log.d("tryLoadInstantRunDex", "Found InstantRun support"); } else { try { // This man is reflection from Google instant run sdk, he will tell me where the dex files go. Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths"); Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class); String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName); File instantRunFilePath = new File(instantRunDexPath); if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) { File[] dexFile = instantRunFilePath.listFiles(); for (File file : dexFile) { if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex")) { instantRunSourcePaths.add(file.getAbsolutePath()); } } Log.d("tryLoadInstantRunDex", "Found InstantRun support"); } } catch (Exception e) { Log.d("tryLoadInstantRunDex", "InstantRun support error, " + e.getMessage()); } } return instantRunSourcePaths; } /** * 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("isVMMultidexCapable", "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; } }}复制代码

注意看tryLoadInstantRunDexFile()这个方法,记得在上一篇文章中说到资源路径获得DexFile,注意5.0以上版本要求关掉instant run 方法否则会自动拆包遍历不到所有activity类,导致有些加了RouterTarget注解的Activity扫描不到,Arouter在tryLoadInstantRunDexFile()解决了这个问题,如果不调用这个方法的话,,只有如下图的apk:

base.apk一般是不包括我们自己写的代码,这个方法调用之后结果如下:

可以扫描到所有的apk,之后接下来我们就可以解压出项目里面所有的类,通过找出类名后缀为$$Router的类进行发射,代码如下:

/** * 通过指定包名,扫描包下面包含的所有的ClassName * * @param context     U know * @param suffix 包名 * @return 所有class的集合 */public static Set
getFileNameByPackageName(Context context, final String suffix) throws PackageManager.NameNotFoundException, IOException, InterruptedException { final Set
classNames = new HashSet<>(); List
paths = getSourcePaths(context); final CountDownLatch parserCtl = new CountDownLatch(paths.size()); for (final String path : paths) { RouterPoolExecutor.getInstance().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.endsWith(suffix)) { 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.e("getFileNameByPackage", "Filter " + classNames.size() + " classes by packageName <" + suffix + ">"); return classNames;}复制代码

注意这里:

dexfile = new DexFile(path);复制代码

我们上一篇文章中建议不要使用,因为安卓8.0已经打上了废弃标志

但是既然阿里爸爸这么用了,相信以后也会有相应的解决办法,我们及时跟进,如果读者有好的方法,欢迎提出,大家一起研究研究,接下来就是反射调用injectRouter() ,

public class RouterFinder {    public RouterFinder() {        throw new AssertionError("No .instances");    }    private static Map
FINDER_MAP = new HashMap<>(); /** * 获取目标类 * * @param className */ public static void inject(String className) { try { Log.e("inject",className); RouterInjector injector = FINDER_MAP.get(className); if (injector == null) { Class
finderClass = Class.forName(className); injector = (RouterInjector) finderClass.newInstance(); FINDER_MAP.put(className, injector); } injector.injectRouter(); } catch (Exception e) { } }}复制代码

到此完成了路由表的数据填充,具体使用如下:

new Router.Builder(this, RouterByAnnotationManager.getInstance().getRouter(ActionConstant.ACTION_USER_INFO)).        addParams(ActionConstant.KEY_USER_NAME, etUserName.getText().toString())        .addParams(ActionConstant.KEY_PASS_WORD, etPassWord.getText().toString()).go();复制代码

到此完成了编译期路由的实现,牵扯的东西还是挺多,历经千辛万苦。,喜欢的给个星吧!

转载地址:http://jnumo.baihongyu.com/

你可能感兴趣的文章
.NET Framework 源码
查看>>
ArrayList源码分析
查看>>
JS Object的静态方法汇总( 上 )
查看>>
优朋普乐:OTT正重构电视版图
查看>>
Ubuntu 14.04 LTC 有线网络——网线不识别,灯不亮问题
查看>>
21_css布局2_浮动布局.html
查看>>
DateUtils 单元下的公用函数目录
查看>>
jQuery 练习[二]: 获取对象(1) - 基本选择与层级
查看>>
Sublime Text 2 快捷键用法大全
查看>>
用U盘安装debian系统
查看>>
SequoiaDB 笔记
查看>>
lduan HyPer-V 网络存储(三)
查看>>
SSH 命令行参数详解【英】
查看>>
前端技术学习之选择器(四)
查看>>
2016年4月4日中项作业
查看>>
条件+努力=?
查看>>
hadoop常用服务管理命令
查看>>
洛谷P4169 天使玩偶 (算竞进阶习题)
查看>>
Order By操作
查看>>
(三)mybatis之对Hibernate初了解
查看>>