路由框架 ARouter 原理及源码解析

文章目录

  • 前言
  • 一、ARouter 简介
  • 二、ARouter 使用
    • 1.添加依赖和配置
    • 2.添加注解
    • 3.初始化SDK
    • 4.发起路由操作
  • 三、ARouter 成员
    • 1. PostCard 明信片
    • 2. Interceptor 拦截器
    • 3. Warehouse 路由仓库
    • 4. ARouter 注解处理
  • 四、ARouter 原理
  • 五、ARouter 源码分析
    • 1. ARouter 初始化
      • 1.1 ARouter#init()
      • 1.2 _ARouter#init()
      • 1.3 LogisticsCenter#init()
      • 1.4 ClassUtils#getFileNameByPackageName()
    • 2. ARouter 路由跳转
      • 2.1 ARouter.getInstance()
      • 2.2 ARouter#build()
      • 2.3 _ARouter#build()
      • 2.3 _ARouter#navigation()
        • 2.3.1 LogisticsCenter#completion()
        • 2.3.2 _ARouter#_navigation()
    • 3. ARouter 获取 Service
  • 总结
    • 1. ARouter 初始化
    • 2. ARouter 路由跳转
  • ARouter 思考
  • 参考


前言

在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如何维护、扩展、解耦等成了一个非常头疼问题。为解决此问题而衍生出的诸如:插件化组件化模块化等热门技术。 使用组件化来改造项目时的难点,就是实现各个组件之间的通讯,通常解决方案采用路由中间件,来实现页面之间的跳转关系。本文要解析的 ARouter 路由框架就是众多解决方案中比较优秀的一个开源库,并且是国人团队开发的,所以中文文档非常详细,以便使用者快速接入。


一、ARouter 简介

ARouter 是阿里开源的一款用于帮助 Android App 进行组件化改造的路由框架,支持模块间的路由、通信、解耦,是 Android 平台中对页面、服务提供路由功能的中间件,以实现在不同模块的 Activity 之间跳转。

ARouter 功能介绍:

  1. 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  2. 支持多模块工程使用
  3. 支持添加多个拦截器,自定义拦截顺序
  4. 支持依赖注入,可单独作为依赖注入框架使用
  5. 支持InstantRun
  6. 支持MultiDex(Google方案)
  7. 映射关系按组分类、多级管理,按需初始化
  8. 支持用户指定全局降级与局部降级策略
  9. 页面、拦截器、服务等组件均自动注册到框架
  10. 支持多种方式配置转场动画
  11. 支持获取 Fragment
  12. 完全支持 Kotlin 以及混编
  13. 支持第三方 App 加固(使用 arouter-register 实现自动注册)
  14. 支持生成路由文档
  15. 提供 IDE 插件便捷的关联路径和目标类
  16. 支持增量编译(开启文档生成后无法增量编译)
  17. 支持动态注册路由信息

ARouter 的典型应用场景有:

  1. 从外部URL映射到内部页面,以及参数传递与解析
  2. 跨模块页面跳转,模块间解耦
  3. 拦截跳转过程,处理登陆、埋点等逻辑
  4. 跨模块API调用,通过控制反转来做组件解耦

ARouter 工作原理:
ARouter 工作原理
ARouter 架构:

ARouter 源码架构

  • appARouter 提供的一个测试 Demo
  • arouter-annotation:模块中声明了很多注解信息和一些枚举类,如:供业务侧使用的注解 @Route 等
  • arouter-api:框架的核心逻辑实现,并提供便于业务侧使用的 ARouter 的核心 api
  • arouter-compiler:定义注解处理器,用于生 Java 类,即用来自动生成路由表
  • arouter-gradle-plugin:编译期使用的 Plugin 插件,主要作用是用于编译器自动加载路由表,节省应用的启动时间

ARouter 架构图


二、ARouter 使用

1.添加依赖和配置

android {defaultConfig {...javaCompileOptions {annotationProcessorOptions {// ARouter参数arguments = [AROUTER_MODULE_NAME: project.getName()]}}}
}dependencies {// 替换成最新版本, 需要注意的是 api 要与 compiler 匹配使用,均使用最新版可以保证兼容compile 'com.alibaba:arouter-api:1.2.4'annotationProcessor 'com.alibaba:arouter-compiler:1.2.4'...
}

2.添加注解

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级:/xx/xx
@Route(path = "/first/activity")
public class FirstActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_first);......}
}

3.初始化SDK

if (BuildConfig.DEBUG) {    // 这两行必须写在init之前,否则这些配置在init过程中将无效ARouter.openLog();      // 打印日志ARouter.openDebug();    // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

4.发起路由操作

public class MainActivity extends AppCompatActivity implements View.OnClickListener {@Overridepublic void onClick(View v) {if (v.getId() == R.id.first_button) {// 1. 应用内简单的跳转ARouter.getInstance().build("/first/activity").navigation();// 2. 跳转并携带参数ARouter.getInstance().build("/test/1").withLong("key1", 666L).withString("key3", "888").withObject("key4", new Test("Jack", "Rose")).navigation();}}
}

三、ARouter 成员

1. PostCard 明信片

public final class Postcard extends RouteMeta {// Base	private Uri uri;				// 使用 Uri 方式发起路由private Object tag;             // A tag prepare for some thing wrong. inner params, DO NOT USE!private Bundle mBundle;         // 需要传递的参数使用 Bundle 存储private int flags = 0;          // 启动 Activity 的标志,如:NEW_FALGprivate int timeout = 300;      // 路由超时时间private IProvider provider;     // 使用 IProvider 的方式跳转private boolean greenChannel;	// 绿色通道,可以不经过拦截器// 序列化服务serializationService:需要传递Object自定义类型对象,就需要实现这个服务private SerializationService serializationService; private Context context;        // 在使用应用程序或活动之前,需检查实例类型private String action;			// Activity 跳转的 Action// Animationprivate Bundle optionsCompat;   // Activity 的过渡动画private int enterAnim = -1;private int exitAnim = -1;......
}

Postcard 明信片,跟我们去各大旅游景点购买然后寄给朋友的明信片是一样的,包含需要传递的参数、跳转方式等等,PostCard 继承自 RouteMeta,来看看其包含哪些重要参数:

public class RouteMeta {private RouteType type;         // 路由类型:如Activity,Fragment,Provider等private Element rawType;        // 路由原始类型,在编译时用来判断private Class<?> destination;   // 目标 Class 对象private String path;            // 路由注册的 pathprivate String group;           // 路由注册的 group 分组private int priority = -1;      // 路由执行优先级,priority 越低,优先级越高,这个一般在拦截器中使用private int extra;              // 额外数据private Map<String, Integer> paramsType;  // 参数类型,例如 Activity 中使用 @Autowired 的参数类型private String name;			// 路由名字,用于生成 javadoc   // 参数配置(对应paramsType).private Map<String, Autowired> injectConfig;  // 缓存注入配置(对应paramsType)......
}

RouteMeta 主要存储的是一些目标对象的信息,这些对象是在路由注册的时候才会生成。

2. Interceptor 拦截器

ARouter 中存在一套拦截器机制,所有的路由调用在 completion 的过程中都需经过自定义的一系列拦截器,实现一些 AOP 切面编程。因此先来看看其拦截器机制的实现,首先看一下 IInterceptor 接口类:

public interface IInterceptor extends IProvider {/*** The operation of this interceptor.** @param postcard meta* @param callback cb*/void process(Postcard postcard, InterceptorCallback callback);
}

IInterceptor 继承了 IProvider,所以其也是一个服务类型,只需要实现 process() 方法就可以实现拦截操作,在其内部对 Postcard 进行处理,拦截器的执行是通过 InterceptorServiceImpl#doInterceptions() 方法实现的:

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService {private static boolean interceptorHasInit;private static final Object interceptorInitLock = new Object();@Overridepublic void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {// 检查拦截器 IInterceptor 的初始化状态,如还未初始化完成,则等待 10s,如仍未初始化完成则报错checkInterceptorsInitStatus(); if (!interceptorHasInit) { // 回调通知拦截器初始化耗时太多异常callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));return;}LogisticsCenter.executor.execute(new Runnable() {@Overridepublic void run() {// 构建值为 interceptors 个数的 CountDownLatch 计数器,用于倒数计数,子线程每执行成功一个// 调用 CountDownLatch.countDown() 方法,interceptors 的个数减 1,直到 interceptors 的个数减为 0// CountDownLatch.await() 就会自动解除等待状态, 不再阻塞主线程, 进入运行状态CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());try {// 调用 _execute() 方法执行一个拦截器的 iInterceptor.process() 方法_execute(0, interceptorCounter, postcard);interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);if (interceptorCounter.getCount() > 0) {    // Cancel the navigation this time, if it hasn't return anythings.callback.onInterrupt(new HandlerException("The interceptor processing timed out."));} else if (null != postcard.getTag()) {    // Maybe some exception in the tag.callback.onInterrupt((Throwable) postcard.getTag());} else {callback.onContinue(postcard);}} catch (Exception e) {callback.onInterrupt(e);}}});} else {callback.onContinue(postcard);}}/*** Excute interceptor** @param index    current interceptor index* @param counter  interceptor counter* @param postcard routeMeta*/private static void _execute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {if (index < Warehouse.interceptors.size()) {// 获取 index 对应的拦截器 IInterceptorIInterceptor iInterceptor = Warehouse.interceptors.get(index); // 调用拦截器 IInterceptor#process() 方法执行拦截操作,对 Postcard 进行处理iInterceptor.process(postcard, new InterceptorCallback() {@Overridepublic void onContinue(Postcard postcard) {// Last interceptor excute over with no exception.counter.countDown(); // 计数器 CountDownLatch 的 interceptors 的个数减 1// 继续调用 _execute() 方法,只是 index 的数值 + 1,来执行下一个拦截器的 iInterceptor.process() 方法_execute(index + 1, counter, postcard);  // When counter is down, it will be execute continue ,but index bigger than interceptors size, then U know.}@Overridepublic void onInterrupt(Throwable exception) {// Last interceptor execute over with fatal exception.// save the exception message for backup.postcard.setTag(null == exception ? new HandlerException("No message.") : exception);   counter.cancel();}});}}......
}

InterceptorServiceImpl#doInterceptions() 方法中,通过 ThreadPoolExecutor 线程池和 CountDownLatch 计数器,实现对每个拦截器 IInterceptor 调用其 process() 方法执行拦截操作,对 Postcard 进行处理。

3. Warehouse 路由仓库

Warehouse 意为仓库,用于存放被 @Route@Interceptor 注释的路由相关的信息,也就是我们关注的 destination 等信息。

class Warehouse {// 保存所有 IRouteGroup 实现类的class对象,在 ARouter 初始化中赋值,key 是 path 第一级//(IRouteGroup 实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务)static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();// 保存所有路由元信息 RouteMeta,是在 completion 中赋值,key是path// 首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务static Map<String, RouteMeta> routes = new HashMap<>();// 保存所有服务provider实例,在completion中赋值,key是IProvider实现类的classstatic Map<Class, IProvider> providers = new HashMap<>();// 保存所有provider服务的元信息(实现类的class对象),在 ARouter 初始化中赋值,key是IProvider实现类的全类名// 主要用于使用IProvider实现类的class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class)static Map<String, RouteMeta> providersIndex = new HashMap<>();// 保存所有拦截器实现类的class对象,在 ARouter 初始化时收集到,key是优先级 static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");// 保存所有拦截器,在 ARouter 初始化完成后立即创建static List<IInterceptor> interceptors = new ArrayList<>();......
}

其中 groupsIndexprovidersIndexinterceptorsIndexARouter 初始化时就准备好的基础信息,为业务中随时发起路由操作(Activity 跳转、服务获取、拦截器处理)做好准备。

4. ARouter 注解处理

ARouter 通过注解处理器 AnnotationProcessor 配合 AutoService 来实现的,并通过 JavaPoet 实现对 Java 文件的编写。

AnnotationProcessor:注解处理器是一种工具,它通过检索源代码中的注解信息,执行特定的代码生成任务或对代码进行检查。ARouter 使用注解处理器在源码编译的阶段,通过 APT 获取被 @Route@Interceptor 等注解的路由相关信息,在对应模块下动态下动态生成 .java 源文件,通常是自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率。详细可以看看 arouter-complier 包下的具体实现。

AutoService:是 Google 开发的一个自动生成 SPI(全称是 Service Provider Interface) 清单文件的框架,用来自动帮我们注册 APT 文件(全称是 Annotation Process Tool,或者叫注解处理器,AbstractProcessor 的实现类)。

JavaPoetJava 诗人,名字是不是很优雅,JavaPoet 是由 Square 推出的开源 Java 代码生成框架,能够提供 Java 生成源代码文件的能力,通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式替代掉繁琐冗杂的重复工作。

SPIService Provider Interface 的简称,是 JDK 默认提供的一种将接口和实现类进行分离的机制。这种机制能将接口和实现进行解耦,大大提升系统的可扩展性。

SPI 机制约定:当一个 Jar 包需要提供一个接口的实现类时,该 Jar 包需要在 META-INF/services 目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 JarMETA-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。


四、ARouter 原理

ARouter 工作原理主要包括:路由表生成、路由匹配和页面跳转三个方面:

  • ARouter 通过注解处理器来生成路由表,在每个组件的 build.gradle 文件中,配置注解处理器的依赖和配置信息,在编译时,注解处理器会扫描项目中所有使用了 @Route 注解的类,然后根据注解中的信息生成一个路由表。该路由表包含了每个被注解标记的类对应的路由信息,如:路径、组件名、优先级等;
  • ARouter 在运行时会根据路由表来进行路由匹配,当需要跳转到某个页面时,通过调用 ARouter.getInstance().build(path) 方法来获取一个路由构建器,构建器中的 path 参数对应了要跳转的页面路径。随后 ARouter 根据该路径在路由表中进行匹配,找到对应的 Class 对象并返回;
  • ARouter 通过反射来进行页面跳转,当找到目标 Class 对象后,调用 navigation() 方法进行页面跳转,ARouter 自动调用目标页面的构造方法来创建一个实例,并且会根据传递的参数来进行参数的注入(ARouter.withString() 方法)。 然后 navigation() 方法的内部调用 startActivity(intent) 方法进行页面跳转,至此便实现两个相互没有依赖的 module 顺利的启动对方的 Activity 的目标。

五、ARouter 源码分析

1. ARouter 初始化

ARouter 在使用前需要通过调用 ARouter#init() 方法并传入 Application 进行初始化:

1.1 ARouter#init()

public final class ARouter {private volatile static ARouter instance = null;private volatile static boolean hasInit = false;public static ILogger logger;/*** 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.");// 继续调用 _ARouter.init() 函数进行初始化hasInit = _ARouter.init(application);if (hasInit) {// ARouter 创建了一个 InterceptorServiceImpl 服务的实例对象,后面讲到拦截器的时候会用到_ARouter.afterInit();}_ARouter.logger.info(Consts.TAG, "ARouter init over.");}}
}

继续调用 _ARouter#init() 函数进行初始化:

1.2 _ARouter#init()

final class _ARouter {static ILogger logger = new DefaultLogger(Consts.TAG);private volatile static _ARouter instance = null;private volatile static boolean hasInit = false;private volatile static ThreadPoolExecutor executor = DefaultPoolExecutor.getInstance();private static Handler mHandler;private static Context mContext;protected static synchronized boolean init(Application application) {mContext = application;LogisticsCenter.init(mContext, executor);logger.info(Consts.TAG, "ARouter init success!");hasInit = true;mHandler = new Handler(Looper.getMainLooper());return true;}
}

内部初始化并赋值一些 mContextmHandler 以及字段信息,最重要的是调用 LogisticsCenter#init(mContext, executor) 函数来初始化。

1.3 LogisticsCenter#init()

public class LogisticsCenter {private static Context mContext;static ThreadPoolExecutor executor;private static boolean registerByPlugin;/*** LogisticsCenter init, load all metas in memory. Demand initialization*/public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {mContext = context;executor = tpe;try {long startInit = System.currentTimeMillis();// 首先使用 AGP 插件进行路由表的自动加载(要先引入插件才行 apply plugin: 'com.alibaba.arouter')loadRouterMap();if (registerByPlugin) { // 如果 registerByPlugin 被设置为true,说明使用的是插件加载,直接跳过logger.info(TAG, "Load router map by arouter-auto-register plugin.");} else { // loadRouterMap() 中设置 registerByPlugin 为 false,因此调用下面步骤加载Set<String> routerMap;// 如果是 debug 模式或者是新版本的,则每次都会去加载 routerMap,这会是一个耗时操作if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {logger.info(TAG, "Run with debug mode or new install, rebuild router map.");// 获取 arouter-compiler 模块生成的存储 ClassName 集合的 routerMap// 根据指定的 packageName 获取 package 下的所有 ClassNamerouterMap = 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 {// 如果是其他的情况,如:release 模式下,已经缓存了 ClassName 列表,则直接去文件中读取logger.info(TAG, "Load router map from cache.");routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));}logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");startInit = System.currentTimeMillis();// 遍历 routerMap 获取 ClassNamefor (String className : routerMap) {// 如果className = "com.alibaba.android.arouter.routes.ARouter$$Root"格式// 则加载类构建对象后通过 loadInto() 方法将路由组信息添加到 Warehouse.groupsIndex 中if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {// This one of root elements, load root.((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {// 如果className = "com.alibaba.android.arouter.routes.ARouter$$Interceptors"格式// 则加载类构建对象后通过 loadInto() 方法将拦截器信息添加到 Warehouse.interceptorsIndex 中((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {// 如果className = "com.alibaba.android.arouter.routes.ARouter$$Providers"格式// 则加载类构建对象后通过 loadInto() 方法将服务 Provider 信息添加到 Warehouse.providersIndex 中((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);}}}logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");if (Warehouse.groupsIndex.size() == 0) {logger.error(TAG, "No mapping files were found, check your configuration please!");}......} catch (Exception e) {throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");}}
}

LogisticsCenter#init() 函数执行流程如下:

  • 获取 com.alibaba.android.arouter.routes 下存储 ClassName 的集合 routerMap。如果是 debug 模式或者是新版本的,则每次都会调用 ClassUtils#getFileNameByPackageName() 函数根据指定的 packageName 获取 package 下的所有 ClassName 并保存到集合 routerMap 中;如果不是 debug 模式且之前已经解析过,则直接从 SP 中读取(已有缓存)。注意debug 模式每次都需要更新,因为类会随着代码的修改而变动。
  • 遍历 routerMap 中的 ClassName,如果是 RouteRoot,则加载类构建对象后通过 loadInto() 方法将路由组信息添加到 Warehouse.groupsIndex 中;如果是 InterceptorGroup,则加载类构建对象后通过 loadInto() 方法将拦截器信息添加到 Warehouse.interceptorsIndex 中;如果是 ProviderGroup,则加载类构建对象后通过 loadInto() 方法将服务 Provider 信息添加到 Warehouse.providersIndex 中。

1.4 ClassUtils#getFileNameByPackageName()

public class ClassUtils {private static final String EXTRACTED_NAME_EXT = ".classes";private static final String EXTRACTED_SUFFIX = ".zip";/*** 通过指定包名,扫描包下面包含的所有的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<>();// 通过 getSourcePaths 方法获取 dex 文件 path 集合List<String> paths = getSourcePaths(context);// 通过 CountDownLatch 对 path 的遍历处理进行控制final CountDownLatch parserCtl = new CountDownLatch(paths.size());// 遍历 path,通过 DefaultPoolExecutor 并发对 path 进行处理for (final String path : paths) {DefaultPoolExecutor.getInstance().execute(new Runnable() {@Overridepublic void run() {DexFile dexfile = null; // 加载 path 对应的 dex 文件try {if (path.endsWith(EXTRACTED_SUFFIX)) {// NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"// 如果是 .zip 结尾的文件通过 DexFile.loadDex 进行加载dexfile = DexFile.loadDex(path, path + ".tmp", 0);} else {// 否则通过 new DexFile 加载dexfile = new DexFile(path);}Enumeration<String> dexEntries = dexfile.entries();// 遍历 dex 中的 Entrywhile (dexEntries.hasMoreElements()) {// 如果是对应的 package 下的类,则添加到 classNames 中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) {}}// 计数器 CountDownLatch 的 paths 的个数减 1parserCtl.countDown();}}});}// 所有 path 处理完成后,CountDownLatch.await() 自动解除等待状态, 不再阻塞主线程, 进入运行状态继续向下走流程parserCtl.await();Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");return classNames;}
}

ClassUtils#getFileNameByPackageName() 函数的执行流程如下:

  • 通过 getSourcePaths() 方法获取 dex 文件的 path 集合;
  • 创建了一个 CountDownLatch 计数器控制 dex 文件的并行处理,以加快速度;
  • 遍历 path 列表,通过 DefaultPoolExecutor 线程池对 path 并行处理;
  • 加载 path 对应的 dex 文件,并对其内部的 Entry 进行遍历,若发现了对应 package 下的 ClassName,将其加入结果集合 classNames 中。

流程至此,ARouter 初始化过程就完成了对自动生成的路由相关类 RouteRootInterceptorProviderGroup 的加载,并对它们通过反射构造后将信息加载进了 Warehouse 类中。

2. ARouter 路由跳转

以第二节 ARouter 用法中的发起路由操作的代码为例:

// 1. 应用内简单的跳转
ARouter.getInstance().build("/first/activity").navigation();

首先看一下 ARouter#getInstance() 函数获取 ARouter 实例对象:

2.1 ARouter.getInstance()

public final class ARouter {private volatile static ARouter instance = null;private volatile static boolean hasInit = false;private ARouter() {}    public static ARouter getInstance() {if (!hasInit) {throw new InitException("ARouter::Init::Invoke init(context) first!");} else {if (instance == null) {synchronized (ARouter.class) {if (instance == null) {instance = new ARouter();}}}return instance;}}
}

首先检查 ARouter 是否已经初始化,如果已经初始化则直接新建 ARouter 实例对象。

2.2 ARouter#build()

public Postcard build(String path) {return _ARouter.getInstance().build(path);
}

_ARouter#getInstance() 函数跟 ARouter 的类似,不再继续贴代码,继续转调 _ARouter 的同名 build() 方法:

2.3 _ARouter#build()

final class _ARouter {/*** Build postcard by path and default group*/protected Postcard build(String path) {if (TextUtils.isEmpty(path)) {throw new HandlerException(Consts.TAG + "Parameter is invalid!");} else {// 获取 PathReplaceService 接口的实例,该接口需要用户来实现,若没有实现则返回 null// 若有实现则调用其 forString() 方法传入用户的 RoutePath 进行路径的预处理PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);if (null != pService) {path = pService.forString(path);}// 通过 extractGroup() 方法对字符串 path 进行截取处理,取出 Route Group 的名称部分// 然后继续调用重载的 build() 方法,传入路径 path、Route Group 的名称部分return build(path, extractGroup(path), true);}}/*** Build postcard by path and group*/protected Postcard build(String path, String group, Boolean afterReplace) {if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {throw new HandlerException(Consts.TAG + "Parameter is invalid!");} else {if (!afterReplace) {PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);if (null != pService) {path = pService.forString(path);}}return new Postcard(path, group);}}
}

_ARouter#build() 的重载方法中,由于入参 afterReplacetrue,因此根据入参 pathgroup 直接新建 Postcard 实例对象并返回。

2.3 _ARouter#navigation()

通过上面的分析可知,ARouter#navigation() 方法,最终也是委托给 _ARouter#navigation() 进行处理的,因此不再贴 ARouter#navigation() 方法的代码,直接看 _ARouter#navigation() 方法的实现:

final class _ARouter {/*** Use router navigation.** @param context     Activity or null.* @param postcard    Route metas* @param requestCode RequestCode* @param callback    cb*/protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {return null; // 预处理失败,取消页面跳转}// 如果没有传入 Context,则使用 ARouter 初始化时传入的 Application 作为 Contextpostcard.setContext(null == context ? mContext : context);try {// 通过 LogisticsCenter.completion() 方法对 postcard 进行补全//(目前只有path、group,还需要知道具体目的地,例如要跳转到的activity信息)LogisticsCenter.completion(postcard);} catch (NoRouteFoundException ex) {logger.warning(Consts.TAG, ex.getMessage());if (debuggable()) {// Show friendly tips for user.runInMainThread(new Runnable() {@Overridepublic void run() {Toast.makeText(mContext, "There's no route matched!\n" +" Path = [" + postcard.getPath() + "]\n" +" Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();}});}// 通过 NavigationCallback 对 navigation 的过程进行监听if (null != callback) {callback.onLost(postcard);} else {// No callback for this invoke, then we use the global degrade service.DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);if (null != degradeService) {degradeService.onLost(context, postcard);}}return null;}if (null != callback) {callback.onFound(postcard);}// 如果设置了 greenChannel,会跳过所有拦截器的执行if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.// 调用 InterceptorService.doInterceptions() 方法,对 postcard 的所有拦截器进行执行interceptorService.doInterceptions(postcard, new InterceptorCallback() {/*** Continue process* @param postcard route meta*/@Overridepublic void onContinue(Postcard postcard) {_navigation(postcard, requestCode, callback);}/*** Interrupt process, pipeline will be destory when this method called.* @param exception Reson of interrupt.*/@Overridepublic void onInterrupt(Throwable exception) {if (null != callback) {callback.onInterrupt(postcard);}logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());}});} else {return _navigation(postcard, requestCode, callback);}return null;}
}

_ARouter#navigation() 方法的执行流程如下:

  • 通过 LogisticsCenter#completion() 方法对 Postcard 进行补全;
  • 如果 Postcard 没有设置 greenChannel,则对 Postcard 的拦截器进行执行,执行完成后调用 _navigation 方法真正实现跳转;如果设置了 greenChannel,即绿色免检通道,则直接跳过所有拦截器,直接调用 _navigation 方法来跳转。
2.3.1 LogisticsCenter#completion()
public class LogisticsCenter {public synchronized static void completion(Postcard postcard) {if (null == postcard) {throw new NoRouteFoundException(TAG + "No postcard!");}// 通过 Warehouse.routes.get 由 postcard.path 路径尝试获取 RouteMetaRouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());if (null == routeMeta) { // 若 routeMeta 为 null,可能是并不存在,或是还没有加载进来if (!Warehouse.groupsIndex.containsKey(postcard.getGroup())) {throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");} else {// 加载路由并将其缓存到内存中,然后 routeMeta 中删除try {// 调用 addRouteGroupDynamic() 方法,如果 Warehouse.groupsIndex 中包含 postcard.getGroup(),但是还未加载// 则将其移除后,重新 loadInto 进来addRouteGroupDynamic(postcard.getGroup(), null);if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));}} catch (Exception e) {throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");}// 重新调用 completion() 方法对其进行补全completion(postcard);}} else {// 如果找到了对应的 routeMeta,将它的信息设置进 postcard 中postcard.setDestination(routeMeta.getDestination());postcard.setType(routeMeta.getType());postcard.setPriority(routeMeta.getPriority());postcard.setExtra(routeMeta.getExtra());Uri rawUri = postcard.getUri();if (null != rawUri) { // 将获取到的 uri 中的参数设置进 bundle 中Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);Map<String, Integer> paramsType = routeMeta.getParamsType();if (MapUtils.isNotEmpty(paramsType)) {// 针对由 @Param 注释的参数,按其类型设置值for (Map.Entry<String, Integer> params : paramsType.entrySet()) {setValue(postcard,params.getValue(),params.getKey(),resultMap.get(params.getKey()));}// Save params name which need auto inject. 保存需要自动注入的参数名postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));}// Save raw uripostcard.withString(ARouter.RAW_URI, rawUri.toString());}// 对 provider 和 fragment,进行特殊处理switch (routeMeta.getType()) {case PROVIDER:  // if the route is provider, should find its instance// 如果是一个 provider,尝试从 Warehouse 中查找它的类并构造对象,然后将其设置到 providerClass<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();IProvider instance = Warehouse.providers.get(providerMeta);if (null == instance) { // 如果没有找到 provider 的实例对象IProvider provider;try { // 新建 provider 实例对象并初始化,然后加入到 Warehouse.providers 中provider = providerMeta.getConstructor().newInstance();provider.init(mContext);Warehouse.providers.put(providerMeta, provider);instance = provider;} catch (Exception e) {logger.error(TAG, "Init provider failed!", e);throw new HandlerException("Init provider failed!");}}postcard.setProvider(instance);postcard.greenChannel();    // Provider 需要跳过所有的拦截器,因此需设置 greenChannel 为 truebreak;case FRAGMENT:postcard.greenChannel();    // Fragment 也不需要拦截器,因此设置 greenChannel 为 truedefault:break;}}}
}

LogisticsCenter#completion() 的执行流程如下:

  • 通过 Warehouse.routes.get 由 path 路径尝试获取 RouteMeta 对象;
  • 若获取不到 RouteMeta 对象,可能是不存在或是还没有进行加载(第一次都未加载),尝试获取 RouteGroup 调用其 loadInto 方法将 RouteMeta 加载进 Warehouse.routes,最后调用 completion 重新尝试补全;
  • 若获取到 RouteMeta 对象,则将 RouteMeta 的信息设置到 postcard 中,其中会将 rawUri 的参数设置进 Bundle
  • 对于 ProviderFragment 特殊处理,其中 Provider 会从 Warehouse 中加载并构造它的对象,然后设置到 postcard注意ProviderFragment 都会跳过拦截器。
2.3.2 _ARouter#_navigation()
final class _ARouter {private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {final Context currentContext = postcard.getContext();switch (postcard.getType()) { // 根据 postcard 的 type 来分别处理case ACTIVITY:// 对 Activity,构造 Intent,将参数设置进去final Intent intent = new Intent(currentContext, postcard.getDestination());intent.putExtras(postcard.getExtras());// 设置 flagsint flags = postcard.getFlags();if (0 != flags) {intent.setFlags(flags);}// 不是 activity,需要设置 FLAG_ACTIVITY_NEW_TASK,新建一个栈if (!(currentContext instanceof Activity)) {intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}// 设置 ActionsString action = postcard.getAction();if (!TextUtils.isEmpty(action)) {intent.setAction(action);}// 切换到主线程,调用 startActivity() 方法,启动新的页面runInMainThread(new Runnable() {@Overridepublic void run() {startActivity(requestCode, currentContext, intent, postcard, callback);}});break;case PROVIDER:// provider 类型的直接返回对应的 providerreturn postcard.getProvider();case BOARDCAST:case CONTENT_PROVIDER:case FRAGMENT:// 对于 broadcast、contentprovider、fragment,构造对象,设置参数后返回Class<?> fragmentMeta = postcard.getDestination();try {Object instance = fragmentMeta.getConstructor().newInstance();if (instance instanceof Fragment) {((Fragment) instance).setArguments(postcard.getExtras());} else if (instance instanceof android.support.v4.app.Fragment) {((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());}return instance;} catch (Exception ex) {logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));}case METHOD:case SERVICE:default:return null;}return null;}
}

_ARouter#_navigation() 函数的执行流程如下:

  • 对于 Activity,会构造一个 Intent 并将之前 postcard 中的参数设置进去,之后会切换到主线程,调用 startActivity() 方法,启动新的页面;
  • 对于 Provider,直接返回其对应的 provider 对象;
  • 对于 BroadcastContentProviderFragment,反射构造对象后,将参数设置进去并返回。

通过分析 ARouter 的初始化和路由跳转的整体逻辑,可以发现其实整体还是不复杂的,实际上就是对 ActivityFragment 的调转过程进行了包装。

3. ARouter 获取 Service

ARouter 除了可以通过 ARouter#getInstance()#build()#navigation() 这样的方式实现页面跳转之外,还可以通过ARouter#getInstance()#navigation(XXService.class) 这样的方式实现跨越组件的服务获取,来看看其是如何实现的:

public <T> T navigation(Class<? extends T> service) {return _ARouter.getInstance().navigation(service);
}

ARouter 还是委托给 _ARouter 来实现:

final class _ARouter {protected <T> T navigation(Class<? extends T> service) {try {// 通过 LogisticsCenter.buildProvider 传入 service.class 的 name 构建一个 postcardPostcard postcard = LogisticsCenter.buildProvider(service.getName());// Compatible 1.0.5 compiler sdk.// 早期版本没有使用完全限定名来获取服务if (null == postcard) {// 没有服务,或者这个服务是旧版本,因此再次通过 LogisticsCenter.buildProvider // 传入 simpleName 来构建 postcardpostcard = LogisticsCenter.buildProvider(service.getSimpleName());}if (null == postcard) {return null; // 仍然获取不到则返回 null}// Set application to postcard.postcard.setContext(mContext);// 对 postcard 进行补全,前面分析过LogisticsCenter.completion(postcard);// 通过 postcard.getProvider 获取对应的 Provider 并返回return (T) postcard.getProvider();} catch (NoRouteFoundException ex) {logger.warning(Consts.TAG, ex.getMessage());return null;}}
}

_ARouter#navigation() 函数,首先通过 LogisticsCenter#buildProvider() 方法传入 service.class 的类名 name 构建出一个 postcard。如果获取不到,则再次通过 LogisticsCenter#buildProvider() 方法传入 simpleName 来构建出一个 postcard。随后通过 LogisticsCenter#completion() 方法对 postcard 进行补全,最后通过 postcard#getProvider() 方法获取对应的 Provider 实例对象。


总结

1. ARouter 初始化

  1. 遍历 Apkdex 文件,查找存放自动生成类的包下的类 ClassName 集合。其中为了加快查找速度,通过一个线程池 DefaultPoolExecutor 进行异步查找,并通过 CountDownLatch 来等待所有异步查找任务的结束。这个查找过程在非 debug 模式下是有缓存的,因为 releaseApk 其自动生成的类信息必然不会变 化;
  2. 根据 ClassName 的类型,分别构建 RouteRootInterceptorGroupProviderGroup 的对象并调用了其 loadInto() 方法将这些 Group 的信息装载进 Warehouse,这个过程并不会将具体的 RouteMeta 装载。这些 Group 中主要包含了一些其对应的下一级的信息(如:RouteGroupClass 对象等),之后就只需要取出下一级的信息并从中装载,不再需要遍历 dex 文件。

2. ARouter 路由跳转

ARouter 路由调用时序图
结合 ARouter 路由跳转时序图,对 ARouter 的路由过程做个总结:

  1. 通过 ARouter 中的 build(path) 方法构建出一个 Postcard,或直接通过其 navigate(serviceClass) 方法构建一个 Postcard
  2. 通过 Postcard 中提供的一系列方法对这次路由进行配置,包括携带的参数,是否跳过拦截器等;
  3. 通过 ARouter#navigation() 方法完成路由的跳转,其步骤如下:
    - 通过 LogisticsCenter#completion() 方法根据 Postcard 的信息结合 Warehouse 中加载的信息对 PostcardDestinationType 等信息进行补全,这个过程中会实现对 RouteMeta 信息的装载,并且对于未跳过拦截器的类会逐个调用拦截器进行拦截器处理;
    - 根据补全后 Postcard 的具体类型,调用对应的方法进行路由的过程(如:对于 Activity 调用其 startActivity() 方法,对于 Fragment 构建对象并调用其 setArgument() 方法)。

ARouter 思考

问题ARouter 有没有什么可优化点?或者说有没有缺点?
:任何框架或多或少都会有这样那样的问题存在,也都在一点点儿优化更新,ARouter 路由框架也有缺点,那就是耗时较多,尤其是对于大型 App 来说,在某些低端 Android 机型影响更大。

  • 首先,通过本文的分析可知,在初始化阶段要生成路由对照表(group,Class<? extends IRouteGroup>),需要遍历 dex 文件,如果是 .zip 文件还需要调用 DexFile#loadDex(path, path + “.tmp”, 0) 方法先获取到 dex 文件,在拿到所有包名为:com.alibaba.android.arouter.routes 的路径 path,最后通过 SharedPreferences 将获取到的路径和版本号存储起来,以便下次获取。虽说同版本第二次运行时不会再进行遍历,但依然要调用 SharedPreferences 去获取之前存储的路由表数据,这也是一个耗时操作。
  • 其次,需根据包名调用反射将对照表缓存到一个单例类 Warehouse 中,此时缓存中已经有了 Map<String, Class<? extends IRouteGroup>> routes,其存储了 group 和 ARouter$ $Group$ $.class 的一个映射表。运行时通过 navigate 方法路由导航的时候,依然需要由 group 拿到了 Root$$*** 类,然后用反射创建它的实例,调用它的方法将 pathRouteMeta (里面存储了 Activity 等信息)缓存起来。

以上这些原因是因为,每个 Module 打包时编译产物是相互隔离的,分别生成 Module 内的映射类,因此运行时,需要将各个 Module 生成的类进行汇总,生成总的映射表,但这个生成过程也是很耗时的,会影响到 App 的启动时长,给 App 体验造成负面的影响。那么,如何优化它呢?

优化方案
可以通过 ARouter 提供的注册插件 (power by AutoRegister) 进行路由表的自动加载,通过 Gradle 插件进行自动注册可以缩短初始化时间解决应用加固导致无法直接访问 dex 文件。通过判断如果使用了插件,在编译期就自动注册,完成注册表信息的存储,避免了扫描 dex 文件这种耗时的操作。

需要注意的是,该插件必须搭配 api 1.3.0 以上版本使用!

AutoRegister:基于字节码插桩,在 Android 中实现跨 module 自动注册的 Gradle 插件,可用于模块解耦。使用此插件后,在编译期(代码混淆之前)扫描所有打到 apk 包中的类,将符合条件的类收集起来,并生成注册代码到指定的类的 static 块中,自动完成注册。

参考

github.com/alibaba/ARouter

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

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

相关文章

基于SSM的足球联赛管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

分布式系列之限流组件

概述 在高并发场景下&#xff0c;请求量瞬间到达&#xff0c;后端服务器即使有缓存、集群主备、分库分表、容错降级等措施&#xff0c;也有可能扛不住这请求量&#xff0c;因此可考虑引入限流组件。限流的目的&#xff1a;防止恶意请求流量或流量超出系统承载。 应用场景&…

计算机毕业设计师hadoop+spark+hive知识图谱医生推荐系统 医生数据分析可视化大屏 医生爬虫 医疗可视化 医生大数据 机器学习 大数据毕业设计

流程&#xff1a; 1.Python爬虫采集中华健康网约10万医生数据&#xff0c;最终存入mysql数据库&#xff1b; 2.使用pandasnumpy/hadoopmapreduce对mysql中的医生数据进行数据分析&#xff0c;使用高德地图解析地理位置&#xff0c;并将结果转入.csv文件同时上传到hdfs文件系统&…

百度文心智能体平台(想象即现实):轻松上手,开启智能新时代!创建属于自己的智能体应用。

目录 1.1、文心智能体平台 1.2、创建智能体 1.3、智能体报名入口 1.4、古诗词小助手 1.5、访问我的智能体 我的智能体访问地址&#xff1a;https://mbd.baidu.com/ma/s/7u8kBFYA。 在这个全新的时代里&#xff0c;人工智能技术正以前所未有的速度发展&#xff0c;渗透到我…

LinkedHashMap详解

目录 LinkedHashMap详解1、LinkedHashMap的继承体系2、LinkedHashMap的特性介绍和代码示例①、特性②、适用场景使用LinkedHashMap 实现最简单的 LRU缓存 3、LinkedHashMap的构造函数4、LinkedHashMap是如何存储元素的&#xff0c;底层数据结构是什么&#xff1f;LinkedHashMap…

用LoRA微调 Llama 2:定制大型语言模型进行问答

Fine-tune Llama 2 with LoRA: Customizing a large language model for question-answering — ROCm Blogs (amd.com) 在这篇博客中&#xff0c;我们将展示如何在AMD GPU上使用ROCm对Llama 2进行微调。我们采用了低秩适配大型语言模型(LoRA)来克服内存和计算限制&#xff0c;…

SAP赋能食品行业,确保安全与品质的双重飞跃

品安全与品质是消费者最关心的问题&#xff0c;也是食品企业的生命线。随着科技的发展和消费者需求的日益多样化&#xff0c;食品行业正面临着前所未有的挑战和机遇。SAP作为全球领先的企业资源规划&#xff08;ERP&#xff09;系统&#xff0c;为食品行业提供了全面的解决方案…

RealityCheck™电机监测和预测性维护模型

RealityCheck™电机 一个附加的软件工具箱&#xff0c;可实现条件监测和预测性维护功能&#xff0c;而无需依赖额外的传感器。相反&#xff0c;它使用来自电机控制过程的电子信息作为振动和其他传感器的代理。凭借其先进的信号处理和机器学习(ML)模型&#xff0c;RealityCheck …

惠普8596E频谱分析仪

8590E系列频谱分析仪具有各种各样的性能、功能&#xff0c;其价格亦是为适应用户的承受能力而确定的。用户可以从价格低廉、具有基本性能的分析仪直至高性能分析仪中进行挑选&#xff0c;无论选择哪种分析仪&#xff0c;都会感受到8590系列频谱分析仪便于使用且高度可靠。这些仪…

Palo Alto GlobalProtect App 6.3 (macOS, Linux, Windows, Andriod) - 端点网络安全客户端

Palo Alto GlobalProtect App 6.3 (macOS, Linux, Windows, Andriod) - 端点网络安全客户端 Palo Alto Networks 远程访问 VPN 客户端软件 请访问原文链接&#xff1a;https://sysin.org/blog/globalprotect-6/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。…

从0开始C++(三):构造函数与析构函数详解

目录 构造函数 构造函数的基本使用 构造函数也支持函数重载 构造函数也支持函数参数默认值 构造初始化列表 拷贝构造函数 浅拷贝和深拷贝 析构函数 总结 练习一下ヽ(&#xffe3;▽&#xffe3;)&#xff89; 构造函数 构造函数的基本使用 构造函数是一种特殊的成…

腾讯地图撒点并默认显示点位信息

实现步骤如下&#xff1a; 1、注册腾讯位置服务账号并获取 Key 2、需要创建一个地图容器&#xff0c;并使用腾讯地图的 API 初始化地图。通常涉及到设置地图的中心点、缩放级别和地图样式。 map new TMap.Map(document.getElementById(‘container’), { center: center, zo…

Vue56-组件的自定义事件

一、什么是自定义事件 二、子组件—【传值】—>父组件 2-1、prop属性 2-2、自定义事件 v-on在谁身上&#xff0c;就给谁绑定事件&#xff01; 给谁绑定的事件&#xff0c;想触发就找谁&#xff01; 2-3、prop属性VS自定义属性 2-4、简写形式 2-5、ref属性实现 加了ref属性…

软件监控发展简史

软件监控简史&#xff0c;从 00 年代开始。发生了什么变化&#xff1f;为什么事情变得如此神秘&#xff1f; 终端设备上日益重要的用户体验通过边缘计算和分布式计算不断得到改善。然而&#xff0c;服务质量的测量仍然使用基于服务器的原语进行。 我们的 2000 年软件监控是这样…

程序员兼职接单有哪些渠道?一篇文章带你了解!

2024年&#xff0c;程序员兼职接单别只盯着朋友圈啦&#xff01;这些兼职接单渠道你一个都不容错过&#xff01;想要通过兼职接单获取收入的程序员&#xff0c;一定不能错过这篇文章&#xff01; 程序员兼职接单的渠道可以简单的分类为兼职平台和程序员论坛和自身人脉拓展三个…

【SD3辅助工具推荐】InstantX发布了三种SD3专属的ControlNet模式——Pose、Canny和Tile

InstantX 是一家专注于人工智能内容生成的独立研究机构。此前&#xff0c;曾开源著名的InstantID和论文《InstantID : Zero-shot Identity-Preserving Generation in Seconds》。随着本月12号&#xff0c;Stability AI正式开源了其产品 Stable Diffusion 3&#xff0c;这家机构…

高效设计必选!5款好用的UI动效工具

UI 动态设计是应用程序设计的重要组成部分。随着技术的积累&#xff0c;UI设计中的动态效果遍地开花&#xff0c;UI动态效果可以使我们的页面更时尚、更有趣、更人性化。5G网络的快速发展也使美丽的动态效果几乎无缝地嵌入到UI界面中。今天&#xff0c;毫不夸张地说&#xff0c…

Visual Studio2022+cuda环境配置及代码调试

环境配置 下载并安装CUDA Toolkit 打开Visual Studio&#xff0c;新建项目。如下图所示&#xff0c;已经包含CUDA编程选项 代码调试 1、打开cu文件的属性页&#xff0c;按下图所示&#xff0c;将Host中的Generate Host Debug Information设置为“是" 2、不可勾选Nsight…

ARM功耗管理框架之LPI

安全之安全(security)博客目录导读 思考&#xff1a;功耗管理框架&#xff1f;SCP&#xff1f;PPU&#xff1f;LPI&#xff1f;之间的关系&#xff1f;如何配合&#xff1f; 目录 一、功耗管理框架中的LPI 二、LPI分类 三、Q-Channel和P-Channel对比 四、Q-Channel和P-Ch…

第28讲:Ceph集群使用RBD块存储与K8S Volumes集成

文章目录 1.Ceph集群使用RBD块存储与K8S集成简介2.Ceph集群RBD块存储与K8S Volume集成2.1.在Ceph集群中创建K8S集群使用的块存储2.2.创建用于K8S访问Ceph RBD块设备的认证用户2.3.将认证用户的Key存储在K8S Secret资源中2.4.在K8S集群的所有节点中安装Ceph命令2.5.创建Pod资源使…