android组件化
学习流程:
1.开源最佳实践:Android平台页面路由框架ARouter-阿里云开发者社区 (aliyun.com)
2.中文ARouter使用API:https://github.com/alibaba/ARouter/blob/master/README_CN.md
3.看当前文档后面的代码
4.这是通俗易懂的文章:https://blog.csdn.net/CodeFarmer__/article/details/102762029
5.https://developer.aliyun.com/article/71687
6.想深入研究的,可以看这篇博客(可能是最详细的ARouter源码分析):https://www.jianshu.com/p/bc4c34c6a06c
7.字节码插桩 https://juejin.cn/post/6844903709101522952 (Gradle自定义插件,热修复asm字节码插桩 )
https://www.dazhuanlan.com/2019/12/31/5e0af589e2ae9/
Github的ARouter地址:https://github.com/alibaba/ARouter
中文ARouter使用API:https://github.com/alibaba/ARouter/blob/master/README_CN.md
官方解释,最全的,同学们一定要看:https://developer.aliyun.com/article/71687
同学们,想深入研究的同学,可以看这篇博客(可能是最详细的ARouter源码分析):https://www.jianshu.com/p/bc4c34c6a06c
同学们,这是通俗易懂的文章:https://blog.csdn.net/CodeFarmer__/article/details/102762029
README.md 可以找到 英文版 要给同学们找到 给同学们看看
README_CN.md 可以找到 中文版 要给同学们找到 给同学们看看
app -- demo application
arouter-annotation -- 注解相关的声明, 其他工程都要依赖这个
arouter-api -- ARouter 框架的 API
arouter-compiler -- 编译期对注解分析的库
module-java module-kotlin -- demo 的子模块
以demo为例子,当运行项目后会在build文件下生成arouter相关的代码:
我们先不用去关心生成代码的细节,只需要知道按照用法的提示添加诸如Route等注解之后,arouter-compiler中的注解处理类会自动帮我们生成需要的代码(如上图所示)。对源码的分析也只需要知道生成的类的做了什么就够了。
arouter-api提供了给我们使用的api,以实现路由功能。
那我们就从api开始分析。
源码分析
init
按照官方说明,我们找到Arouter的入口,也就是初始化的地方:
if (isDebug()) { // 这两行必须写在init之前,否则这些配置在init过程中将无效ARouter.openLog(); // 打印日志ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化
我们直接进入ARouter.init方法:
变量hasInit用于保证初始化代码只执行一次;
logger是一个日志工具类;
注意_ARouter类的下划线,Arouter是对外暴露api的类,而_ARouter是真正的实现类。为什么这样设计那?当然是为了解耦啦。具体点说有哪些好处呢?看看这个init方法,除了对实现类的调用,还有日志的打印,有没有装饰模式的感觉?可以增加一些额外的功能。其次,对比_ARouter类与Arouter类的方法,可以看到明显Arouter类的方法少。要知道Arouter是对外暴露的,我们可以有选择暴露用户需要的方法,而把一些方法隐藏在内部。相比于用private修饰,这种方式灵活性更强。
/*** Init, it must be call before used router.*/public static void init(Application application) {if (!hasInit) { //确保只初始化一次logger = _ARouter.logger;//日志类_ARouter.logger.info(Consts.TAG, "ARouter init start.");hasInit = _ARouter.init(application);if (hasInit) {_ARouter.afterInit();}_ARouter.logger.info(Consts.TAG, "ARouter init over.");}}
接着我们进入实现类看下: 继续看 _ARouter.java 实现
protected static synchronized boolean init(Application application) {mContext = application;LogisticsCenter.init(mContext, executor); // 实际初始化的地方logger.info(Consts.TAG, "ARouter init success!");hasInit = true;return true;}
上面代码中 明显有价值的是LogisticsCenter.init(mContext, executor);,executor是一个线程池。我们接着来看下去除日志等无关代码后的LogisticsCenter.init方法:
主要实现都在 LogisticsCenter.init 方法 中, 继续查看
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {mContext = context;executor = tpe;...Set<String> routerMap;//生成类的类名集合// 如果是debug模式或者是新版本,从apt生成的包中加载类if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {// ClassUtils.getFileNameByPackageName 就是根据报名查找对应报名下的类, 就不贴代码了..routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);if (!routerMap.isEmpty()) {//加入sp缓存context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();}PackageUtils.updateVersion(context); //更新版本} else {//否则从缓存读取类名routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));}//判断类型,使用反射实例化对象,并调用方法 // 遍历获取到的 classfor (String className : routerMap) {if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);}}...
}
这里体现了debuggable模式的作用,如果没有开启debuggable,并且调试的时候肯定不会更改版本号,因此只会从缓存中读取类信息,所以新添加的路由不会加载到内存中。
ROUTE_ROOT_PAKCAGE是一个常量:
public static final String ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes";
可以看到与上面demo生成类的截图的包名相同。而ClassUtils.getFileNameByPackageName方法做的就是找到app的dex,然后遍历出其中的属于com.alibaba.android.arouter.routes包下的所有类名,打包成集合返回。可以想象遍历整个dex查找指定类名的工作量有多大,怎么办呢?就需要[arouter-gradle-plugin] ASM插桩来解决这个非常耗费性能问题
【可以看到初始化就是查找com.alibaba.android.arouter.routes包下的类, 获取实例并强制转化成IRouteRoot, IInterceptorGroup, IProviderGroup, 然后调用 loadInto 方法.
通过 demo 的代码查找能看到有com.alibaba.android.arouter.routes.ARouter$$Root$$app 这样的类
// ARouter$$Root$$app.java
/*** DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {// 以分组做为 key, 缓存到 routes. // routes 是指向 Warehouse.groupsIndex 的引用routes.put("service", ARouter$$Group$$service.class);routes.put("test", ARouter$$Group$$test.class);}
}
可以看到这是在编译期通过分析注解生成的代码. ARouter$$Group$$service.class 也是生成的.
// ARouter$$Group$$service.java
/*** DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$service implements IRouteGroup {@Overridepublic void loadInto(Map<String, RouteMeta> atlas) {atlas.put("/service/hello", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));...}
}
到这里可以看到demo代码里定义的HelloServiceImpl.java 等出现了. 其实 ARouter 就是通过分析注解在编译期自动生成了一些关联代码. 另外的 Interceptors, Providers 逻辑上类似.
Interceptors 是注册了声明 Interceptor 注解, 并实现 IInterceptor 接口的类
Providers 是注册了声明 Route 注解, 并实现了 IProvider 接口的类
到此初始化工作都做完了, 总结一下 ARouter 会在编译期根据注解声明分析自动生成一些注入代码, 初始化工作主要是把这些注解的对象和对注解的配置缓存到 Warehouse 的静态对象中.
// 跳转activity的调用
ARouter.getInstance().build("/test/activity2").navigation();
// ARouter.java
public Postcard build(String path) {return _ARouter.getInstance().build(path);
}
// _ARouter.java
// group 默认是传进来的 path 第一部分内容. 例如 path = /test/activity1, group会默认为 test
// 如果手动声明的,一定要手动传递, 不然会找不到
protected Postcard build(String path, String group) {return new Postcard(path, group);
}
这里就是直接返回了一个 Postcard 对象, 并保存了path, group. Postcard 是继承了 RouteMeta
navigation方法最后都要调用的 _ARouter.java 中, 中间过程省略.我们直接看核心代码
// _ARouter.java
// postcard 就是前面用build 方法构造的对象实例
// requestCode 是区分 startAcitivity 的方式.如果不为-1, 就用startActivityForResult的方式启动
// NavigationCallback 是对各种状态的回调.
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {try {// 验证是否能找到对应的 postcard.pathLogisticsCenter.completion(postcard);} catch (NoRouteFoundException ex) {// 如果没找到postcard的配置, 调用onLost回调方法, 或者系统配置的"降级服务"(DegradeService)回调if (null != callback) {callback.onLost(postcard);} else { DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);if (null != degradeService) {degradeService.onLost(context, postcard);}}return null;}... }
navigation调用了LogisticsCenter.completion方法做验证, 我们看下 LogisticsCenter.java 这个方法如何验证 postcard, 然后再继续看navigation方法下面的逻辑
// LogisticsCenter.java
public synchronized static void completion(Postcard postcard) {// 通过postcard 的 path 查找对应的 RouteMetaRouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());if (null == routeMeta) {// 如果没找到, 可能是还没装载过, 需要根据 group 查找对应的 groupsClass<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); if (null == groupMeta) {// 如果没找到, 抛出 NoRouteFoundException 错误方法结束throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");} else {// 返回 IRouteGroup 对象, 并调用 loadInto方法.IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();iGroupInstance.loadInto(Warehouse.routes);// 删除 group 缓存Warehouse.groupsIndex.remove(postcard.getGroup());// 重新调用 completion 方法,此时对应的 group 已经缓存完成completion(postcard); // Reload}} else {// 可以查找到 routeMeta, copy routeMeta 的原始数据到 postcard 中.postcard.setDestination(routeMeta.getDestination());postcard.setType(routeMeta.getType());postcard.setPriority(routeMeta.getPriority());postcard.setExtra(routeMeta.getExtra());switch (routeMeta.getType()) {case PROVIDER: Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();IProvider instance = Warehouse.providers.get(providerMeta);// 初始化 provider 对象, 并调用初始化方法, 缓存到Warehouse.providers中.if (null == instance) {IProvider provider;try {provider = providerMeta.getConstructor().newInstance();provider.init(mContext);Warehouse.providers.put(providerMeta, provider);instance = provider;} catch (Exception e) {throw new HandlerException("Init provider failed! " + e.getMessage());}}// 设置一个provider 引用postcard.setProvider(instance);// provider 默认设置跳过拦截器postcard.greenChannel(); break;case FRAGMENT:// fragment 默认设置跳过拦截器postcard.greenChannel(); default:break;}}
}
completion方法主要是对 group 做一次懒式加载, 我们继续查看 navigation 方法下面的内容:
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {...// 执行到这里就是找到了 postcard. 执行对应回调if (null != callback) {callback.onFound(postcard);}// 如果是"绿色通道", 则直接执行_navigation方法, 目前只有provider, fragment 默认是走绿色通道if (!postcard.isGreenChannel()) { // interceptorService 是 ARouter 配置的默认的拦截服务interceptorService.doInterceptions(postcard, new InterceptorCallback() {public void onContinue(Postcard postcard) {_navigation(context, postcard, requestCode, callback);}public void onInterrupt(Throwable exception) {if (null != callback) {callback.onInterrupt(postcard);}}});} else {// 绿色通道return _navigation(context, postcard, requestCode, callback);}return null;}
interceptorService 是 ARouter 配置的默认的拦截器com.alibaba.android.arouter.core.InterceptorServiceImpl.java
public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {// 用线程池异步执行LogisticsCenter.executor.execute(new Runnable() {public void run() {// 初始化一个同步计数类, 用拦截器的 sizeCancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());try {// 执行拦截操作, 看到这猜是递归调用.直到 index 满足条件, 退出递归._excute(0, interceptorCounter, postcard);// 线程等待计数完成, 等待300秒...interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);// 判断退出等待后的一些状态...if (interceptorCounter.getCount() > 0) { callback.onInterrupt(new HandlerException("The interceptor processing timed out."));} else if (null != postcard.getTag()) { callback.onInterrupt(new HandlerException(postcard.getTag().toString()));} else {// 没有问题, 继续执行callback.onContinue(postcard);}} catch (Exception e) {// 中断callback.onInterrupt(e);}}});
}
我们继续看看_excute方法
private static void _excute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {// 递归退出条件if (index < Warehouse.interceptors.size()) {// 获取要执行的拦截器IInterceptor iInterceptor = Warehouse.interceptors.get(index);// 执行拦截iInterceptor.process(postcard, new InterceptorCallback() {public void onContinue(Postcard postcard) {// 计数器减1counter.countDown();// 继续执行下一个拦截_excute(index + 1, counter, postcard); }public void onInterrupt(Throwable exception) {// 当被拦截后退出递归postcard.setTag(null == exception ? new HandlerException("No message.") : exception.getMessage()); counter.cancel();}});}
}
和我们猜测的一样, 一个标准的递归调用, 当所有拦截器执行后(假设都不做拦截), 最后还是要回到_navigation方法
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {final Context currentContext = null == context ? mContext : context;// 我们就先只分析 activity 的逻辑switch (postcard.getType()) {case ACTIVITY:// 初始化 intent, 把参数也添加上final Intent intent = new Intent(currentContext, postcard.getDestination());intent.putExtras(postcard.getExtras());// Set flags.int flags = postcard.getFlags();if (-1 != flags) {intent.setFlags(flags);} else if (!(currentContext instanceof Activity)) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}// 在 UI 线程进行 start activitynew Handler(Looper.getMainLooper()).post(new Runnable() {public void run() {if (requestCode > 0) { // Need start for resultActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());} else {ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());}// 动画设置if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());}if (null != callback) { // Navigation over.callback.onArrival(postcard);}}});break;}return null;
}
对 IProvider 进行 navigation
主要实现是在LogisticsCenter.completion方法中对IProvider进行了一些分支处理
switch (routeMeta.getType()) {case PROVIDER: // 返回实现IProvider接口的类Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();// 在缓存中查找IProvider instance = Warehouse.providers.get(providerMeta);// 初始化 provider 对象, 并调用初始化方法, 缓存到Warehouse.providers中.if (null == instance) {IProvider provider;try {provider = providerMeta.getConstructor().newInstance();provider.init(mContext);Warehouse.providers.put(providerMeta, provider);instance = provider;} catch (Exception e) {throw new HandlerException("Init provider failed! " + e.getMessage());}}// 设置一个provider 引用postcard.setProvider(instance);// provider 默认设置跳过拦截器postcard.greenChannel(); break;
// 可以看 _navigation 方法就是直接返回在 completion 方法中是设置的引用private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {final Context currentContext = null == context ? mContext : context;switch (postcard.getType()) {case PROVIDER:return postcard.getProvider();}return null;}
同学们.到这里我们已经知道了 ARouter 大概的已经工作流程了, 总结一下.
初始化的时候把注解标示的内容注入到缓存中, 然后启动跳转的时候根据缓存查找对应的类的实现. 看上去也是挺简单的, 主要的黑科技其实也就是在编译期生成代码. 下一步我们继续会看下编译期到底做了哪些内容.
同学们注意:下面进入注解处理器环节了:
我们来看下在编译期 arouter-compiler 都做了哪些事情
大概梳理一下 arouter-compiler, 先查看 build.gradle
可以看到 arouter-compiler 通过 自动配置的 Processor, 查找声明了 arouter-annotation 里定义的 annotation, 然后根据 annotation 的配置使用 javapoet 生成辅助注入的代码, 达到在开发中可以不用实际依赖一些类, 达到模块或者组件之间的解耦.
// build.gradle// 这里要注意, 不能用 com.android.library, 因为对 annotation 的处理需要用的javax 包下的类, android sdk是没有的.
apply plugin: 'java'...dependencies {// 这里依赖的不是源码的modulecompile 'com.alibaba:arouter-annotation:1.0.3'// 这个是自动生成 META-INF 配置 Processor 内容的compile 'com.google.auto.service:auto-service:1.0-rc2'// 用来生成代码的, 比拼字符串生成源码要高大上一些compile 'com.squareup:javapoet:1.7.0'...
arouter-compiler 源码分析
arouter-compiler 有三个 Processor
AutowiredProcessor -- 处理 @Autowired
InterceptorProcessor -- 处理 @Interceptor
RouteProcessor -- 处理 @Autowired, Route
先分析 RouteProcessor 的工作原理, 先看看 init 方法里初始化做了什么
// RouteProcessor.javapublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);// 用来生成代码的 Filer 对象mFiler = processingEnv.getFiler(); // 数据类型和类的描述types = processingEnv.getTypeUtils(); // Get type utils.elements = processingEnv.getElementUtils(); // Get class meta.typeUtils = new TypeUtils(types, elements);...// module name, 使用 arouter-compiler 的module 都要配置在build.gradle, 否则抛出 exceptionmoduleName = options.get(KEY_MODULE_NAME);// 获取 com.alibaba.android.arouter.facade.template.IProvider 的 TypeMirror 对象iProvider = elements.getTypeElement(Consts.IPROVIDER).asType();}
接下来我们进入核心的 process 方法, init, process 这两个方法都是 Processor 自动调用的
// RouteProcessor.javapublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {...Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);this.parseRoutes(routeElements);...}
主要逻辑都在 parseRoutes 这个方法中, 继续查看下去, 这个方法写的有点长...
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {// 创建参数类型 Map<String, Class<? extends IRouteGroup>>ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(ClassName.get(Map.class),ClassName.get(String.class),ParameterizedTypeName.get(ClassName.get(Class.class),WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))));// 创建参数类型 Map<String, RouteMeta>ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(ClassName.get(Map.class),ClassName.get(String.class),ClassName.get(RouteMeta.class));// 创建方法参数 routes, atlas, providersParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build();// 声明 public void loadInto(Map<String, Class<? extends IRouteGroup>> routes)MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(rootParamSpec);// 遍历所有申明的 @Route for (Element element : routeElements) {// 取得 class 的类型描述TypeMirror tm = element.asType();// 取得@Route注解的实例Route route = element.getAnnotation(Route.class);RouteMeta routeMete = null;// 判断class 是否继承自 Activityif (types.isSubtype(tm, type_Activity)) {Map<String, Integer> paramsType = new HashMap<>();// 遍历所有声明 @Autowired 的属性for (Element field : element.getEnclosedElements()) {// 满足条件是属性, 声明了 @Autowired, 并且没有实现 iProviderif (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {Autowired paramConfig = field.getAnnotation(Autowired.class);// 根据 @Autowired 声明的名字做key, 缓存数据类型的 index 值paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), typeUtils.typeExchange(field));}}// 创建 RouteMeta 对象routeMete = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);} else if (types.isSubtype(tm, iProvider)) { // IProviderrouteMete = new RouteMeta(route, element, RouteType.PROVIDER, null);} else if (types.isSubtype(tm, type_Service)) { // ServicerouteMete = new RouteMeta(route, element, RouteType.parse(SERVICE), null);} else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {routeMete = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);}// 这个方法就是把 route 按照 group 分组保存到 groupMap( Map<String, Set<RouteMeta>> )categories(routeMete);}// 声明这个方法 public void loadInto(Map<String, RouteMeta> providers)MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(providerParamSpec);// 开始生成代码, 遍历刚才生成的 groupMapfor (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {String groupName = entry.getKey();// 声明方法 public void loadInto(Map<String, RouteMeta> atlas)MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(groupParamSpec);// 遍历 group 内保存的 routesSet<RouteMeta> groupData = entry.getValue();for (RouteMeta routeMeta : groupData) {switch (routeMeta.getType()) {case PROVIDER: List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces();// 生成方法内代码, 例如下面// providers.put("com.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));for (TypeMirror tm : interfaces) {if (types.isSameType(tm, iProvider)) {loadIntoMethodOfProviderBuilder.addStatement("providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",(routeMeta.getRawType()).toString(),routeMetaCn,routeTypeCn,ClassName.get((TypeElement) routeMeta.getRawType()),routeMeta.getPath(),routeMeta.getGroup());} else if (types.isSubtype(tm, iProvider)) {loadIntoMethodOfProviderBuilder.addStatement("providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",tm.toString(), routeMetaCn,routeTypeCn,ClassName.get((TypeElement) routeMeta.getRawType()),routeMeta.getPath(),routeMeta.getGroup());}}break;default:break;}// 生成配置的参数类型 map// 例如 : put("pac", 9); put("obj", 10); put("name", 8); put("boy", 0);StringBuilder mapBodyBuilder = new StringBuilder();Map<String, Integer> paramsType = routeMeta.getParamsType();if (MapUtils.isNotEmpty(paramsType)) {for (Map.Entry<String, Integer> types : paramsType.entrySet()) {mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); ");}}String mapBody = mapBodyBuilder.toString();// 生成方法内代码// atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));loadIntoMethodOfGroupBuilder.addStatement("atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",routeMeta.getPath(),routeMetaCn,routeTypeCn,ClassName.get((TypeElement) routeMeta.getRawType()),routeMeta.getPath().toLowerCase(),routeMeta.getGroup().toLowerCase());}// 生成 java 文件, 例如 ARouter$$Group$$test.java, ARouter$$Group$$service.javaString groupFileName = NAME_OF_GROUP + groupName;JavaFile.builder(PACKAGE_OF_GENERATE_FILE,TypeSpec.classBuilder(groupFileName).addJavadoc(WARNING_TIPS).addSuperinterface(ClassName.get(type_IRouteGroup)).addModifiers(PUBLIC).addMethod(loadIntoMethodOfGroupBuilder.build()).build()).build().writeTo(mFiler);// 缓存文件名rootMap.put(groupName, groupFileName);}// 生成 ARouter$$Root$$app.java 里的代码if (MapUtils.isNotEmpty(rootMap)) {for (Map.Entry<String, String> entry : rootMap.entrySet()) {// 生成代码 : routes.put("service", ARouter$$Group$$service.class);loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));}}// 生成 providers java 文件. 例如 ARouter$$Providers$$app.javaString providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;JavaFile.builder(PACKAGE_OF_GENERATE_FILE,TypeSpec.classBuilder(providerMapFileName).addJavadoc(WARNING_TIPS).addSuperinterface(ClassName.get(type_IProviderGroup)).addModifiers(PUBLIC).addMethod(loadIntoMethodOfProviderBuilder.build()).build()).build().writeTo(mFiler);// 生成 root java 文件. 例如 ARouter$$Root$$app.javaString rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;JavaFile.builder(PACKAGE_OF_GENERATE_FILE,TypeSpec.classBuilder(rootFileName).addJavadoc(WARNING_TIPS).addSuperinterface(ClassName.get(elements.getTypeElement(ITROUTE_ROOT))).addModifiers(PUBLIC).addMethod(loadIntoMethodOfRootBuilder.build()).build()).build().writeTo(mFiler);}
总结一下RouteProcessor 做了什么事情
根据 @Route 配置 path, group 生成 ARouter$$Root$$"moduleName"
ARouter$$Root$$xxxx.java 以 group 为 key, 注入了生成的 ARouter$$Group$$xxxx 类
根据 @Route 配置 生成 ARouter$$Providers$$"groupName" 和 ARouter$$Group$$"groupName"
ARouter$$Providers$$xxxx.java 以类为 key, 注入了 IProvider 接口的实现类
ARouter$$Group$$xxxx.java 以 path 为 key, 注入 RouteMate实例, 描述实现类和 @Autowired 参数描述
同学们简单总结一下:RouteProcessor 完成 ARouter 大部分工作, 除了拦截器和自动注入
InterceptorProcessor init 方法和 RouteProcessor 类似, 直接跳过, 直接看核心方法
// InterceptorProcessor.javaprivate void parseInterceptors(Set<? extends Element> elements) throws IOException {for (Element element : elements) {// 验证必需实现了 IInterceptor 接口if (verify(element)) {Interceptor interceptor = element.getAnnotation(Interceptor.class);// 根据配置 @Interceptor 的 priority, 返回是否有重复配置的拦截器, 如果有抛出错误Element lastInterceptor = interceptors.get(interceptor.priority());if (null != lastInterceptor) {throw new IllegalArgumentException...}interceptors.put(interceptor.priority(), element);}}// 声明类型 Map<Integer, Class<? extends ITollgate>>ParameterizedTypeName inputMapTypeOfTollgate = ParameterizedTypeName.get(ClassName.get(Map.class),ClassName.get(Integer.class),ParameterizedTypeName.get(ClassName.get(Class.class),WildcardTypeName.subtypeOf(ClassName.get(type_ITollgate))));// 方法参数 interceptorsParameterSpec tollgateParamSpec = ParameterSpec.builder(inputMapTypeOfTollgate, "interceptors").build();// 声明方法 loadIntoMethodSpec.Builder loadIntoMethodOfTollgateBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(tollgateParamSpec);// 生成方法内代码, 例如 : interceptors.put(7, Test1Interceptor.class);for (Map.Entry<Integer, Element> entry : interceptors.entrySet()) {loadIntoMethodOfTollgateBuilder.addStatement("interceptors.put(" + entry.getKey() + ", $T.class)", ClassName.get((TypeElement) entry.getValue()));}// // 生成 Interceptor 的注入代码, 继承了 IInterceptorGroup. 例如 ARouter$$Interceptors$$app.javaJavaFile.builder(PACKAGE_OF_GENERATE_FILE,TypeSpec.classBuilder(NAME_OF_INTERCEPTOR + SEPARATOR + moduleName).addModifiers(PUBLIC).addJavadoc(WARNING_TIPS).addMethod(loadIntoMethodOfTollgateBuilder.build()).addSuperinterface(ClassName.get(type_ITollgateGroup)).build()).build().writeTo(mFiler);}
同学们注意:InterceptorProcessor 就是生成注入拦截器的代码, 拦截器有优先级, 并且一个优先级只能一个拦截器
AutowiredProcessor 负责生成自动注入的代码, 这点 butterknife 原理差不多
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {categories(roundEnvironment.getElementsAnnotatedWith(Autowired.class));generateHelper();}
private void categories(Set<? extends Element> elements) throws IllegalAccessException {for (Element element : elements) {TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();// @Autowired不能修饰 privateif (element.getModifiers().contains(Modifier.PRIVATE)) {throw new IllegalAccessException("The autowired fields CAN NOT BE 'private'!!! please check field ["+ element.getSimpleName() + "] in class [" + enclosingElement.getQualifiedName() + "]");}// 根据声明的类分组缓存if (parentAndChild.containsKey(enclosingElement)) {parentAndChild.get(enclosingElement).add(element);} else {List<Element> childs = new ArrayList<>();childs.add(element);parentAndChild.put(enclosingElement, childs);}}}
private void generateHelper() throws IOException, IllegalAccessException {// 声明方法参数 Object targetParameterSpec objectParamSpec = ParameterSpec.builder(TypeName.OBJECT, "target").build();// 遍历前面分析后的 parentAndChildfor (Map.Entry<TypeElement, List<Element>> entry : parentAndChild.entrySet()) {// 声明方法 public void inject(Object target)MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder(METHOD_INJECT).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(objectParamSpec);TypeElement parent = entry.getKey();List<Element> childs = entry.getValue();String qualifiedName = parent.getQualifiedName().toString();String packageName = qualifiedName.substring(0, qualifiedName.lastIndexOf("."));// 类名 : com.alibaba.android.arouter.demo.testactivity.Test1Activity// 生成 : com.alibaba.android.arouter.demo.testactivity.Test1Activity$$ARouter$$AutowiredString fileName = parent.getSimpleName() + NAME_OF_AUTOWIRED;// 声明 public class Test1Activity$$ARouter$$Autowired implements ISyringeTypeSpec.Builder helper = TypeSpec.classBuilder(fileName).addJavadoc(WARNING_TIPS).addSuperinterface(ClassName.get(type_ISyringe)).addModifiers(PUBLIC);// 声明变量 private SerializationService serializationService;FieldSpec jsonServiceField = FieldSpec.builder(TypeName.get(type_JsonService.asType()), "serializationService", Modifier.PRIVATE).build();helper.addField(jsonServiceField);// 生成代码 : serializationService = (SerializationService)ARouter.getInstance().navigation(SerializationService.class);injectMethodBuilder.addStatement("serializationService = $T.getInstance().navigation($T.class);", ARouterClass, ClassName.get(type_JsonService));// 生成代码 : Test1Activity substitute = (Test1Activity)target;injectMethodBuilder.addStatement("$T substitute = ($T)target", ClassName.get(parent), ClassName.get(parent));// 生成自动注入的代码for (Element element : childs) {Autowired fieldConfig = element.getAnnotation(Autowired.class);String fieldName = element.getSimpleName().toString();if (types.isSubtype(element.asType(), iProvider)) { if ("".equals(fieldConfig.name())) {// 生成代码 : substitute.helloService = (HelloService)ARouter.getInstance().navigation(HelloService.class);injectMethodBuilder.addStatement("substitute." + fieldName + " = $T.getInstance().navigation($T.class)",ARouterClass,ClassName.get(element.asType()));} else {// 生成代码 : substitute.helloService = (HelloService)ARouter.getInstance().build("/test/hello").navigation();injectMethodBuilder.addStatement("substitute." + fieldName + " = ($T)$T.getInstance().build($S).navigation();",ClassName.get(element.asType()),ARouterClass,fieldConfig.name());}} else { String statment = "substitute." + fieldName + " = substitute.";boolean isActivity = false;if (types.isSubtype(parent.asType(), activityTm)) {isActivity = true;// activity 直接用getIntent()statment += "getIntent().";} else if (types.isSubtype(parent.asType(), fragmentTm) || types.isSubtype(parent.asType(), fragmentTmV4)) {// fragment 直接用getArguments()statment += "getArguments().";} else {throw new IllegalAccessException("The field [" + fieldName + "] need autowired from intent, its parent must be activity or fragment!");}// 生成注入代码statment = buildStatement(statment, typeUtils.typeExchange(element), isActivity);// 判断serializationService注入if (statment.startsWith("serializationService.")) { injectMethodBuilder.beginControlFlow("if (null != serializationService)");injectMethodBuilder.addStatement("substitute." + fieldName + " = " + statment,(StringUtils.isEmpty(fieldConfig.name()) ? fieldName : fieldConfig.name()),ClassName.get(element.asType()));injectMethodBuilder.nextControlFlow("else");injectMethodBuilder.addStatement("$T.e(\"" + Consts.TAG + "\", \"You want automatic inject the field '" + fieldName + "' in class '$T' , then you should implement 'SerializationService' to support object auto inject!\")", AndroidLog, ClassName.get(parent));injectMethodBuilder.endControlFlow();} else {injectMethodBuilder.addStatement(statment, StringUtils.isEmpty(fieldConfig.name()) ? fieldName : fieldConfig.name());}}}helper.addMethod(injectMethodBuilder.build());// 生成 Autowired 的注入代码, 继承了 ISyringe. 例如 Test1Activity$$ARouter$$Autowired.javaJavaFile.builder(packageName, helper.build()).build().writeTo(mFiler);}}
ARouter 总结
我们已经底 ARouter 大部分代码都进行了一遍分析, 接下来总结一下 ARouter 工作原理.
编译期 arouter-compiler 负责生成了注入的的一些代码:
ARouter$$Group$$group-name 以 group-name 为文件名注入该 group 下声明了 @Route 的信息
ARouter$$Root$$app 按照 group 分组注入了 ARouter$$Group$$group-name
ARouter$$Providers$$app 注入实现 IProvider 接口的信息
ARouter$$Interceptors$$app 注入实现 IInterceptor 接口信息
Test1Activity$$ARouter$$Autowired @Autowired 自动注入的实现
ARouter 初始化的时候会把注入的信息进行缓存
在进行 navigation 的时候, 根据缓存进行懒加载, 然后获取实际对象或者跳转 activity.
自动注入就是调用 对应的 Test1Activity$$ARouter$$Autowired 实例, 对声明 @Autowired 的字段进行复制操作.
相关文章:
android组件化
学习流程:1.开源最佳实践:Android平台页面路由框架ARouter-阿里云开发者社区 (aliyun.com)2.中文ARouter使用API:https://github.com/alibaba/ARouter/blob/master/README_CN.md3.看当前文档后面的代码4.这是通俗易懂的文章:https…...
华为OD机试真题Python实现【特异性双端队列】真题+解题思路+代码(20222023)
🔥系列专栏 华为OD机试(Python)真题目录汇总华为OD机试(JAVA)真题目录汇总华为OD机试(C++)真题目录汇总华为OD机试(JavaScript)真题目录汇总文章目录 🔥系列专栏题目输入输出示例一输入输出解题思路核心知识点Python 代码实现代码运行结果版权说明<...
24.架构能力
文章目录24. 架构能力24.1 Competence of Individuals: Duties, Skills, and Knowledge of Architects 个人能力:架构师的职责、技能和知识24.2 Competence of a Software Architecture Organization 软件架构组织的能力24.3 Summary 小结24.4 For Further Reading …...
前端原生 CSS 跑马灯效果,无限轮播(横竖版本,带渐变遮罩,简单实用)
一、横版跑马灯 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-wid…...
4.8 注解与自定义注解
文章目录1.概述2.注解的分类2.1 JDK注解2.2 元注解2.2.1 Target ElementType…2.2.2 Retention RetentionPolicy…3 自定义注解1.概述 在注解刚出现时,曾受到过好多程序员的鄙夷,觉得这就是多此一举的操作; 但随着时间的推移,越…...
webpack 的热更新是如何做到的?原理是什么?
Hot Module Replacement,简称 HMR,在不需要刷新整个页面的同时更新模块,能够提升开发的效率和体验。热更新时只会局部刷新页面上发生了变化的模块,同时可以保留当前页面的状态,比如复选框的选中状态等。 在 webpack 中…...
嵌入式ARM设计编程(一) 简单数据搬移
文章和代码已归档至【Github仓库:hardware-tutorial】,需要的朋友们自取。或者公众号【AIShareLab】回复 嵌入式 也可获取。 一、实验目的 熟悉实验开发环境,掌握简单ARM汇编指令的使用方法。 二、实验环境 硬件:PC机 软件&am…...
【Selenium】十分钟手把手带你学会WebDriver API
目录 1、定位元素【8种】 2、操作测试对象 3、添加等待 4、弹窗类型 5、浏览器的操作 6、键盘事件 7、选择框 8、上传文件 1、定位元素【8种】 元素定位是自动化测试的核心,想要去操作一个对象,第一步就是需要我们先去识别这个对象。每个对象就会…...
3DMAX高级弯曲插件使用教程
3dMax高级弯曲插件是对3dmax原生“弯曲(Bend)”修改器的一个增强,给用户更多控制弯曲修改器的参数设置,它让用户输入宽度,插件脚本将移动中心以获得正确的宽度。 主要特性: - 使用智能捕捉捕捉到自定义网格…...
前端面试题之性能优化大杂烩
主要内容为下面几大类:移动端、图片、JavaScript、css、html、页面内容、服务器、cookie。 移动端性能优化: 保持单个文件小于25KB 移动网站页面要求下载资源,如果文件过大,会大大减慢页面加载速度。 打包内容为分段multipart文…...
SpringBoot+Vue实现养老智慧服务平台
文末获取源码 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9 浏…...
tigervnc2023
sudo apt-get install tigervnc-standalone-server 配置用户 /etc/tigervnc/vncserver.users :1user1 :2user2 :3user3 全局配置 /etc/tigervnc/vncserver-config-defaults $localhost"no"; $geometry "1920x1200"; 分别进入user1 user2 user3 用户…...
智能三子棋(人机大战)—— 你会是最终赢家吗?万字讲解让你实现与自己对弈
魔王的介绍:😶🌫️一名双非本科大一小白。魔王的目标:🤯努力赶上周围卷王的脚步。魔王的主页:🔥🔥🔥大魔王.🔥🔥🔥 ❤️…...
【自制开发板】自制STM32F407开发板(含TFT 8080串口屏幕接口)
【2023 年 2 月 14 日】 许久没有更新,最近做了个小开发板玩了玩。更新一下吧,作为记录!! 主要是象试一下LVGL在STM32上的应用,所以开发板的大小都是基于屏幕大小来设计的。 分享出来,给大家一个板子结构…...
openvino yolov5/ssd 实时推流目标检测在html上显示
安装ffmepg并添加到环境变量中,流媒体使用m7s 运行效果 SSD:检测在10ms左右,yolov5在100ms左右 app.py #!/usr/local/bin/python3 # encodin: utf-8import subprocess import threading import time import cv2 import osfrom OpenVinoYoloV…...
基于FPGA的 SPI通信 设计(1)
引言 低速通信目前搞过 UART串口通信、IIC通信。其实 SPI 也算是中低速(有时也可以用作高速通信)串行通信的范畴,但是一直还没真正实现过,所以此系列就 SPI的协议以及FPGA设计作几篇博客记录。欢迎订阅关注~ SPI 标准协议 x1模式…...
为什么西门子、美的等企业这样进行架构升级,看看改造效果就知道了
在工业领域, 生产、测试、运行阶段都可能会产生大量带有时间戳的传感器数据,这都属于典型的时序数据。时序数据主要由各类型实时监测、检查与分析设备所采集或产生,涉及制造、电力、化工、工程作业等多个行业,具备写多读少、量非常…...
open3d点云配准函数registration_icp
文章目录基本原理open3d调用绘图基本原理 ICP, 即Iterative Closest Point, 迭代点算法。 ICP算法有多种形式,其中最简单的思路就是比较点与点之间的距离,对于点云P{pi},Q{qi}P\{p_i\}, Q\{q_i\}P{pi},Q{qi}而言,如果二者是同一目标&am…...
HTML编码规范
本篇文章是基于王叨叨大佬师父维护的文档梳理的,有兴趣可以去看一下原文HTML编码规范。 1. 缩进与换行 【建议】 使用 2 个空格作为一个缩进层级,不允许使用tab字符 解释: 具体项目,可以使用2个空格,也可以使用…...
PDF SDK for Linux 8.4.2 Crack
PDF SDK for Linux 是适用于任何 Linux 企业或云应用程序的强大解决方案,非常适合需要完全可定制的 PDF 查看器或后端流程的任何 Linux 开发人员。 将 Foxit PDF SDK 嵌入到基于 Linux 的应用程序中非常容易。只需打开您最喜欢的 Linux IDE,复制您需要的…...
vb 模块和作用域的关系
模块在VB中有三种类型的模块,分别是窗体模块、标准模块和类模块。窗体模块窗体模块中包含了窗体以及窗体中所有控件的事件过程,文件扩展名为(*.frm),窗体文件中不仅包含窗体对象的外观设计,也包含窗体模块(…...
Redis分布式锁
一、背景 与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中。单机环境下࿰…...
京东前端经典面试题整理
img的srcset属性的作⽤? 响应式页面中经常用到根据屏幕密度设置不同的图片。这时就用到了 img 标签的srcset属性。srcset属性用于设置不同屏幕密度下,img 会自动加载不同的图片。用法如下: <img src"image-128.png" srcset&qu…...
django+mysql实现一个简单的web登录页面
目录 一、使用pyacharm创建一个django项目 二、启动django项目验证 三、配置mysql数据库 1、本地安装mysql数据库 1)安装mysql数据库 2)自己创建一个数据库 2、安装 pymysql 3、配置mysql数据库 1)在项目同名包下的_init_.py里面添加…...
python cartopy手动导入地图数据绘制底图/python地图上绘制散点图:Downloading:warnings/散点图添加图里标签
……开学回所,打开电脑spyder一看一脸懵逼,简直不敢相信这些都是我自己用过的代码,想把以前的自己喊过来科研了() 废话少说,最近写小综述论文,需要绘制一个地图底图+散点图ÿ…...
JavaScript中常用的数组方法
在日常开发中,我们会接触到js中数组的一些方法,这些方法对我们来说,可以很便利的达到我们想要的结果,但是因为方法比较多,有些方法也不常用,可能会过一段时间就会忘记,那么在这里我整理了一些数…...
磁疗为什么“没效果”?原来真相是这样!
很多人磁疗之后, 总爱迫不及待问一个问题: “这个多长时间见效啊?” …… 还有些人几天没有效果, 就果断下结论: “这东西没用!” …… 有不少人错误地把磁疗等同于“药品”一样看待,总觉得…...
【直击招聘C++】5.1函数模板
5.1函数模板一、要点归纳1.定义函数模板2.实例化函数模板3.重载模板函数4.函数调用的匹配顺序一、要点归纳 1.定义函数模板 定义函数模板的一般格式如下: template<类型形参表> 返回类型 函数名(形参表) {函数体; }例如以…...
谈谈Java多线程离不开的AQS
如果你想深入研究Java并发的话,那么AQS一定是绕不开的一块知识点,Java并发包很多的同步工具类底层都是基于AQS来实现的,比如我们工作中经常用的Lock工具ReentrantLock、栅栏CountDownLatch、信号量Semaphore等,而且关于AQS的知识点…...
国际化语言,多语言三种方式
可以用透传的方式,自己写local的json文件,不需要配置什么,直接传,自己写方法i18n nextjsi18n umi4一、透传的方式 export const AppContext React.createContext<any>({})app.tsx 用context包裹import type { AppProps } f…...
什么网站需要备案/十大广告投放平台
苹果cmsv10仿优酷模板在这里我的主题网需要提醒的是,不是1:1仿优酷的模板 只是首页的幻灯有些模仿,如果太多人都用仿优酷的模板不利于seo优化,有些差异化还是比较利于网站优化的。苹果cmsv10仿优酷模板1苹果cmsv10仿优酷模板演示地址…...
网站怎样注册备案/seo策略工具
这是我的电脑配置 硬盘和内存是 现在内存又重新加装了一根8G的,所以实际上是16G内存了.不过在安装这个双系统的时候还是8G的内存. 我原先的操作系统就是电脑自带的win10系统的最新版本,我有日常更新啦 开始安装了 首先制作一个启动盘 准备的材料 一个超过2G的U盘(我淘宝上…...
网站制作将栏目分类/seo企业培训班
sizeof,一个其貌不扬的家伙,引无数菜鸟竟折腰,小虾我当初也没少犯迷糊,秉着“ 辛苦我一个,幸福千万人”的伟大思想,我决定将其尽可能详细的总结一下。 但当我总结的时候才发现,这个问题既可以简…...
网站建设合同纠纷 延期可以终止合同吗/长沙seo管理
方案一: 文件名版本号,区别对待不同的版本控制,有设定值后会加上_v_x的后缀名。如:加载主文件 main.swf, 被命名为:Main_v_60.swf 。 方案二: loader.load(new URLRequest("assets/a.swf?"版本号 或者 随机…...
国内设计网站公司网站/上海百度seo牛巨微
TreeMap是jdk中基于红黑树的一种map实现。HashMap底层是使用链表法解决冲突的哈希表,LinkedHashMap继承自HashMap,内部同样也是使用链表法解决冲突的哈希表,但是额外添加了一个双向链表用于处理元素的插入顺序或访问访问。 既然TreeMap底层使…...
网页设计与制作培训班哪家好/湖南长沙seo教育
y gaussmf(x,[sig c]) 其中,c是位置参数,sig是尺度参数,控制图形的胖瘦。 x 0:0.1:10; y gaussmf(x,[2 5]); plot(x,y) xlabel(gaussmf, P[2 5]) ylabel(gaussmf) legend(gaussmf); %添加图例代码 结果图 更多《计算机视觉与图形学》知…...