Android 路由实践(二)

前言

          继上一篇Android 路由实践(一)之后,断更已经差不多一个月,毕竟是年前的最后一个月,各种事情扎堆,直到近几天才稍微闲下来,于是有了此文。简单回顾下,上一篇文章中简单介绍了三种实现路由的方式,分别是:隐式的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;/*** 解析的目标注解集合*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);mElementUtils = processingEnv.getElementUtils();mMessager = processingEnv.getMessager();mFiler = processingEnv.getFiler();}@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> types = new LinkedHashSet<>();types.add(RouterTarget.class.getCanonicalName());return types;}@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {mMessager.printMessage(Diagnostic.Kind.WARNING, "processprocessprocessprocess");Set<? extends Element> 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";@Overridepublic void injectRouter() {com.example.commonlib.RouterByAnnotationManager.getInstance().addRouter(routerKey,fullClassName);}
}
复制代码

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

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

通过Application对Router进行初始化:

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

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

public static void init(Application application) {try {Set<String> 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<String> getFileNameByPackageName(Context context, final String suffix) throws PackageManager.NameNotFoundException, IOException, InterruptedException {final Set<String> classNames = new HashSet<>();List<String> paths = getSourcePaths(context);final CountDownLatch parserCtl = new CountDownLatch(paths.size());for (final String path : paths) {RouterPoolExecutor.getInstance().execute(new Runnable() {@Overridepublic 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.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<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.classesString extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;//        如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
//        通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的if (!isVMMultidexCapable()) {//the total dex numbersint 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<String> tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) {List<String> 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原生AndroidvmName = "'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<String> getFileNameByPackageName(Context context, final String suffix) throws PackageManager.NameNotFoundException, IOException, InterruptedException {final Set<String> classNames = new HashSet<>();List<String> paths = getSourcePaths(context);final CountDownLatch parserCtl = new CountDownLatch(paths.size());for (final String path : paths) {RouterPoolExecutor.getInstance().execute(new Runnable() {@Overridepublic 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.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<String, RouterInjector> 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();复制代码

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452690.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php中newself(),在php代码中新建对象用到的new self与new static有什么不同

我们在使用php代码新建对象的时候&#xff0c;一般会用到new self与new static,那么它们在使用的时候区别在哪里&#xff1f;先说说new static(),new static与new self一样&#xff0c;在php代码中&#xff0c;它是用来新建一个对象的.那么他们之间的不同之处在哪里呢&#xff…

ORACLE rac集群概念和原理

参考文献&#xff1a; 文献一 文献二 文献三 谢谢作者分享&#xff01;

Python基础-变量作用域

1.函数作用域介绍 函数作用域 Python中函数作用域分为4种情况&#xff1a; L&#xff1a;local&#xff0c;局部作用域&#xff0c;即函数中定义的变量&#xff1a;E&#xff1a;enclosing&#xff0c;嵌套的父级函数的局部作用域&#xff0c;即包含此函数的上级函数的局部作用…

视频源常见接口介绍

在录制和播放中&#xff0c;要通过接口实现文件的传输&#xff0c;下面介绍常用接口。 [AV IN/OUT/PHONES]&#xff1a;这是一个多功能的插孔&#xff0c;在菜单里可以设置其功能。当设置为[IN/OUT]时&#xff0c;此插孔可以输入和输出音频以及视频信号&#xff0c;用于连接电视…

php爱奇艺筛选标签,三种排序 快速筛选好视频_软件资讯技巧应用-中关村在线

使用在线视频播放器来观看视频&#xff0c;通常有可以分为有目标和无目标两种。即是用户如果定点找一部视频和随意查看喜欢的视频&#xff0c;这两种模式往往查找视频的方法也是不一样的。我们这里要和大家讲解的是&#xff0c;用户在没有目标视频的情况下&#xff0c;怎样更快…

TSAP传输服务访问点

TSAP(Transport Service Access Point) 传输服务访问点在计算机网络当中&#xff0c;传输层要在用户之间提供可靠和有效的端-端&#xff08;如TSAP源端->[1]TSAP目的端的传输选择&#xff09;服务&#xff0c;必须把一个用户进程和其他的用户进程区分开&#xff0c;主要由传…

Flask项目--发送短信验证码

1.后端代码 具体代码如下&#xff1a; # GET /api/v1.0/sms_codes/<mobile>?image_codexxxx&image_code_idxxxx api.route("/sms_codes/<re(r1[34578]\d{9}):mobile>") def get_sms_code(mobile):"""获取短信验证码""&q…

Jenkins系列之五——通过Publish over SSH插件实现远程部署

Jenkins通过Publish over SSH插件实现远程部署 步凑一、配置ssh免秘钥登录 部署详情地址&#xff1a;http://www.cnblogs.com/Dev0ps/p/8259099.html 步凑二、安装Publish over SSH插件 插…

Java高级应用开发之Servlet

学习路径&#xff1a; 1.Servlet简介 2.Servlet基础 3.表单处理 4.请求头信息 5.响应头信息 6.cookie 7.session 8.scope: Servlet Context 全局变量 Http Session 会话变量 Http Servlet Request 请求变量 9.Filter Filter是一种特殊的Servlet&#xff0c;其核心函数doFilter(…

typedef 数组使用详解

typedef到处都是&#xff0c;但是能够真正懂得typedef使用的不算太多。对于初学者而言&#xff0c;看别人的源码时对到处充斥的typedef往往不知所错&#xff0c;而参考书又很少&#xff0c;所以在此给出一个源码&#xff0c;供大家参考。 懂得这些&#xff0c;基本上是 对typed…

php柱状图实现年龄分布,考官雅思写作小作文满分范文 柱状图:年龄分布

考官雅思写作小作文满分范文 柱状图:年龄分布2017年06月12日14:48 来源&#xff1a;小站教育作者&#xff1a;小站雅思编辑参与(40)阅读(63981)摘要&#xff1a;为大家分享前考官simon演示的9分雅思小作文。考官亲笔&#xff0c;用最正统的4段式写作&#xff0c;本文主题-柱状图…

Flask项目--注册

0.效果展示 1.后端代码 # coding:utf-8from . import api from flask import request, jsonify, current_app, session from ihome.utils.response_code import RET from ihome import redis_store, db, constants from ihome.models import User from sqlalchemy.exc import I…

图片处理

//图片处理public function img(){//读取图片$imageImage::open(./img/02.jpg);//dump($image);//获取图片的信息// 返回图片的宽度$width $image->width();// 返回图片的高度$height $image->height();// 返回图片的类型$type $image->type();// 返回图片的mime类…

react 项目总结

前言 最近在写一个项目,在写react的过程中遇到过很多坑,现在总结一下,为以后的项目作参考.另外借此项目来比较一下 vue.js 和 react.js 之间的一些异同. 先说说组件 刚刚开始写组件的时候&#xff0c;感觉难度不大&#xff08;跟vue差不多&#xff09;。最有意思的应该是jsx语法…

现代数字影视 电影使用标准

1、国际数字电影标准1&#xff09;DCI&#xff08;Digital Cinema Initiatives数字影院系统规范&#xff09;美国好莱坞七大制片公司——Disney、MGM、Fox、Paramount Pictures、Sony Pictures Entertainment、Universal Studios和Warner Bros于2002年联合成立了DCI机构&#x…

数据流图 系统流程图 程序流程图 系统结构图联系与区别

1.数据流图&#xff08;Data Flow Diagram&#xff09;&#xff0c;简称DFD&#xff0c;它从数据传递和加工角度&#xff0c;以图形方式来表达系统的逻辑功能、数据在系统内部的逻辑流向和逻辑变换过程&#xff0c;是结构化系统分析方法的主要表达工具及用于表示软件模型的一种…

Linux--安装yum源

linux配置yum源 一、修改yum的配置文件 /etc/yum.repos.d/xxx.repo 1、进入yum配置文件目录 # cd /etc/yum.repos.d 2、删除全部原有的文件 # rm -rf * 3、新建一个yum的配置文件 # vi my.repo [myrepo] 标识配置文件名称&#xff08;名字随意&#xff09; namemyrepo 标识yum …

在 Confluence 6 中禁用 workbox 应用通知

如果你选择 不提供应用通知&#xff08;does not provide in-app notifications&#xff09;&#xff1a; Confluence workbox 图标将不会可见同时用户也不能在这个服务器上访问 workbox。这个 Confluence 服务器将不会发送消息到 workbox 中&#xff0c;同时也不会发送消息到其…

迄今为止最快的 JSON 序列化工具 Jil

2019独角兽企业重金招聘Python工程师标准>>> 迄今为止最快的 JSON 序列化工具 Jil https://github.com/kevin-montrose/Jil 转载于:https://my.oschina.net/xainghu/blog/1621365