阿里ARouter 路由框架解析
一、简介
众所周知,在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如果维护、扩展、解耦等成了一个非常头疼问题,随之孕育而生的诸如插件化、组件化、模块化等热门技术。 而其中组件化中一项的难点,就是实现各个组件之间的通讯,我们通常解决方案采用路由中间件,来实现页面之间跳转关系。 ARouter 是阿里开源路由框架,常被用于组件之间、模块之间的跳转,由于是国人团队开发的,所以你懂的,中文文档非常详细。
二、入门
2.1.添加依赖
android {defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = [AROUTER_MODULE_NAME: project.getName()]}}}
}dependencies {api'com.alibaba:arouter-api:1.5.0'annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
2.2.初始化
在 Application 中初始化
if (BuildConfig.DEBUG) {ARouter.openLog() // 打印日志ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this);
2.3.添加注解
@Route(path = "/main/main_main")
public class MainActivity extend BaseActivity {...
}
2.4.发起路由
// 1.应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/main/main_main").navigation();// 2.跳转并携带参数
ARouter.getInstance().build("/main/main_main").withLong("id", 10086L).withString("name", "Test").navigation();
三、原理解析
在原理解析之前,我们先了解 ARouter 使用关键技术-APT(Annotation Processing Tool),它是 javac 的一个工具,中文译名为编译时注解处理器,说白了,APT 用来编译时,扫描和处理注解,获取注解和被注解类等相关信息,拿到这些信息之后,自动生成一些代码,核心是 AbstractProcessor 这个类,APT 运用非常广泛,诸如 ButterKnife、EventBus、Dagger2 等使用运用到了 APT,ARouter 也不例外。运用 APT 技术我们可以自己写一些注解处理器,例如处理网络、打印出错信息等。
3.1 分析
我们来看看 ARouter 初始化
//1.初始化
ARouter.init(application)//2.ARouter#init
public static void init(Application application) {if (!hasInit) {logger = _ARouter.logger;_ARouter.logger.info(Consts.TAG, "ARouter init start.");//委托给_ARouter去初始化hasInit = _ARouter.init(application);if (hasInit) {_ARouter.afterInit();}//初始化之后调用afterInit_ARouter.logger.info(Consts.TAG, "ARouter init over.");}}
从上面我们可以看到 ARouter 采用门面模式,实际上委托给 *ARouter 处理,*ARouter 是整个框架的路由中心控制器,负责处理控制整个路由的流程。接下来,我们看看 _ARouter 初始化化。
protected static synchronized boolean init(Application application) {mContext = application;//调用 LogisticsCenter 的初始化LogisticsCenter.init(mContext, executor);logger.info(Consts.TAG, "ARouter init success!");hasInit = true;mHandler = new Handler(Looper.getMainLooper());return true;
}//LogisticsCenter#init
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {mContext = context;executor = tpe;try {long startInit = System.currentTimeMillis();loadRouterMap();if (registerByPlugin) {logger.info(TAG, "Load router map by arouter-auto-register plugin.");} else {Set<String> routerMap;//1.如果是debug模式,或者有更新if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {logger.info(TAG, "Run with debug mode or new install, rebuild router map.");//1.扫描包,获取ROUTE_ROOT_PAKCAGE(com.alibaba.android.arouter.routes)包下的类routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);if (!routerMap.isEmpty()) {context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();}PackageUtils.updateVersion(context); // Save new version name when router map update finishes.} else {logger.info(TAG, "Load router map from cache.");//2.获取本地缓存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();for (String className : routerMap) {//3.将 IRouteRoot,IRouteGroup和IProviderGroup的实现类,//通过注解生成,并且加载 Warehouse(数据仓库)// Warehouse:数据仓库,存储路由配置信息和具体生成的IProvider对象if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {//1.加载 IRouteRoot,每个moudle下都会生成一个该类型的实现类,//通过module名来区分的,作用是将每个module下所有的分组按照组名//和对应分组的实现类(IRouteGroup接口的实现类)的 Class 对象做一个映射,//然后保存在一个全局的 groupIndex 的 map 表中。((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {//2.加载 IInterceptorGroup 作用是将各个module下的 自定义的i nterceptor 按照优先级和 interceptor的 // Class 对象做一个映射,然后保存在一个全局的 interceptorIndex 的 map 表中。((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {//3.加载 IProviderGroup 该类的作用的是将项目中自定义的提供序//列化功能的类的相关信息以RouteMeta 类的对象保存在全局的providerIndex的map表中((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!");}if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));}} catch (Exception e) {throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");}}
从上面代码我们可以看到调用 LogisticsCenter,后勤中心主要完成两件事
-
获取到 com.alibaba.android.arouter.routes 包下的所有 class 文件类名
-
根据找到的类名去加载相关的实例到Warehouse中,com.alibaba.android.arouter.routes 包下面的 class 是注解解析器自动生成,主要 IRouteRoot,IRouteGroup 和 IProviderGroup 的实现类,当我们使用 @Route 注解某个类时,会将这个类的信息注入的到自动生成的上述实现类中。 接下来调用 afterInit()。
//_ARouter#afterInit
static void afterInit() {// 调用 Postcard#build 方法,获取 InterceptorService 拦截服务控制器interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();}//build 方法实际上,将 path 和 group 信息封装到 Postcard,生成一个要跳转的信息表protected Postcard build(String path) {if (TextUtils.isEmpty(path)) {throw new HandlerException(Consts.TAG + "Parameter is invalid!");} else {//生成具体实例PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);if (null != pService) {path = pService.forString(path);}return build(path, extractGroup(path));}}//Postcard#navigation 这个方法实现了跳转,我们点进去会发现,最后调用 _ARouter#navigation
//_ARouter#navigation
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)) {// Pretreatment failed, navigation canceled.return null;}try {//1.调用 LogisticsCenter#completion 的方法LogisticsCenter.completion(postcard);......//不是绿色通道,通过拦截控制器依次调用不同拦截器处理信息 if (!postcard.isGreenChannel()) { //每个拦截器的拦截方法调用都是在子线程中执行的interceptorService.doInterceptions(postcard, new InterceptorCallback() {@Overridepublic void onContinue(Postcard postcard) {_navigation(context, postcard, requestCode, callback);}@Overridepublic void onInterrupt(Throwable exception) {if (null != callback) {//只要有一个拦截器拦截该包裹,则回调onInterrupt方法宣告本次路由被终止callback.onInterrupt(postcard);}}});} else {//2.如果是绿色通道,调用_navigation方法进行具体的导航return _navigation(context, postcard, requestCode, callback);}return null;} }//LogisticsCenter#completion
public synchronized static void completion(Postcard postcard) {......//1.Warehouse.routes获取RouteMeta,RouteMeta 路由信息描述类,存储目标地址的类型,路径,参数等信RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());if (null == routeMeta) { //如果没有路由信息,则尝试去数据仓库查找Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); if (null == groupMeta) {throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");} else {// Load route and cache it into memory, then delete from metas.try {if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));}IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();iGroupInstance.loadInto(Warehouse.routes);Warehouse.groupsIndex.remove(postcard.getGroup());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(postcard); // Reload} else {//找到路由信息后,则将配置的路由信息填充到Postcard对象中postcard.setDestination(routeMeta.getDestination());//要跳转 Activity.class 路径postcard.setType(routeMeta.getType());postcard.setPriority(routeMeta.getPriority());postcard.setExtra(routeMeta.getExtra());Uri rawUri = postcard.getUri();if (null != rawUri) { //这里主要是完成参数的填充}//针对不同的路由类型进行处理switch (routeMeta.getType()) {case PROVIDER: //如果是服务提供者,则尝试获取其具体实例,如果没有,则根据路由信息构造一个实例,初始化并存储到数据仓库,Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();IProvider instance = Warehouse.providers.get(providerMeta);if (null == instance) { // There's no instance of this providerIProvider provider;provider = providerMeta.getConstructor().newInstance();provider.init(mContext);Warehouse.providers.put(providerMeta, provider);instance = provider;}postcard.setProvider(instance);//服务提供者被设置成绿色渠道,不用接受拦截检查postcard.greenChannel(); break;case FRAGMENT://fragment也不用拦截检查postcard.greenChannel(); default:break;}}//_ARouter#_navigation
//根据不同类型,路由处理和导航也不一样
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()) {//是 ACTIVITY 数据填充到intent,并且调用 startActivitycase ACTIVITY: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)) { // Non activity, need less one flag.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}// Set ActionsString action = postcard.getAction();if (!TextUtils.isEmpty(action)) {intent.setAction(action);}// Navigation in main looper.runInMainThread(new Runnable() {@Overridepublic void run() {startActivity(requestCode, currentContext, intent, postcard, callback);}});break;//PROVIDER类型,则直接返回其服务提供者case PROVIDER:return postcard.getProvider();//如果是BOARDCAST 、 CONTENT_PROVIDER 、 FRAGMENT,则创建其需要的实体,并填充数据,返回对象 case BOARDCAST:case CONTENT_PROVIDER:case 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 通过 apt 技术,生成保存路径(路由path)和被注解(@Router)的组件类的映射关系的类,利用这些保存了映射关系的类,根据用户的请求 postcard(明信片)寻找到要跳转的目标地址(class),使用 Intent 跳转。
ARouter的重要性主要体现在以下几个方面:
- 模块化开发:随着软件规模和复杂度的增加,越来越多的应用程序采用模块化开发方式。ARouter支持跨模块的调用,简化了模块间跳转的复杂度,降低了耦合性,提升了整个应用的可维护性和可扩展性。
- 代码解耦:在传统的Activity跳转方式中,一般是通过Intent来传递参数,不同页面之间的参数传递比较麻烦,还有可能导致代码冗余。ARouter采用注解方式传递参数,简化了代码的编写和阅读。
- 动态路由:路由是基于注解方式实现的,可以在代码中动态注册和删除路由。ARouter还支持路由重定向、降级等高级功能,可以根据不同的业务场景灵活处理路由逻辑。
- 统一管理:ARouter提供了统一管理路由配置的界面,可以直览整个项目的路由规则,方便开发人员进行维护和管理。
ARouter是一款非常实用的路由框架,可以帮助开发者在模块化开发、代码解耦、动态路由和统一管理等方面提高开发效率和应用质量。
下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接 通过点击此处↓↓↓ 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE
Android 架构学习手册
深入理解Gradle框架
相关文章:

阿里ARouter 路由框架解析
一、简介 众所周知,在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如果维护、扩展、解耦等成了一个非常头疼问题,随之孕育而生的诸如插件化、组件化、模块化等热门技术。 而其中组件化中一项的难点&…...

大型医院健康体检管理系统源码(PEIS)
一、体检管理系统(PEIS)概念 体检管理系统(PEIS)是以健康为中心的身体检查。一般医学家认为健康体检是指在身体尚未出现明显疾病时,对身体进行的全面检查。方便了解身体情况,筛查身体疾病。即应用体检手段对…...

java 获取时间的方法
Java的时间是通过字节码指令来控制的,所以 java程序的运行时间是通过字节码指令来控制的。但是由于 Java程序在运行时, JVM会产生一些状态,所以在执行 JVM指令时, JVM也会产生一些状态。 我们在执行 java程序时,主要是…...
Block原理(二)- 用白话说说底层源码,不扯代码
之前有一篇关于block的源码探究分析 Block原理(一),时至今日,总觉的那篇文章说得不够流畅,今天打算从顶层设计的角度试着拆解下block的设计思想,拗脑的源码部分就不必再次触碰了,尽量保障这篇文…...
springboot整合knife4j接口文档成公共模块使用
theme: smartblue 之前项目中一直用的是swagger-ui进行接口文档的调用和使用,最近前端一直反馈页面不美观,调用凌乱,等一系列问题。基于这个问题我决定将其进行更改调整,上网搜索了一下发现knife4j是目前接口文档不错的一款插件。…...

软件测试需要学什么
软件测试近些年也是比较热门的行业,薪资高、入门门槛低,让很多开发人员想纷纷加入软件开发这个行业,想要成为这一岗位的一员,想要进入软件测试行业,他们需要学习什么呢? 软件测试需要学习的还挺多的&#…...

【蓝桥杯省赛真题17】python删除字符串 青少年组蓝桥杯python编程省赛真题解析
目录 python删除字符串 一、题目要求 1、编程实现 2、输入输出 二、解题思路...
C# LINQ 查询语句和方法的区别及使用
C# LINQ(Language-Integrated Query)是一种强类型、编译时的查询技术,它可以通过统一的语法对多种数据源进行查询和操作,包括对象、集合、数据库等。LINQ 提供了两种查询方式:查询语句和扩展方法。 查询语句ÿ…...

【nacos配置中心】源码部分解析
启动初始化 SpringApplication.prepareContext applyInitializers 回调ApplicationContextInitializer的initialize方法 getInitializers()从applicationContext获取List<ApplicationContextInitializer<?>> initializers 这个集合是通过SpringApplication的…...
Kotlin 1.6.0 的新特性
1、稳定版对于枚举、密封类与布尔值主语穷尽 when 语句 一个详尽的when语句包含了所有主题可能的类型或值的分支,或者对于一些类型包含一个else分支。它覆盖了所有可能的情况,使代码更加安全。 即将禁止非详尽的when语句,以使行为与when表达…...

nextjs13临时笔记
动态路由 文件夹以中括号命名[id] -pages: --list: ---[id]: ----index.jsx(访问路径/list/1 即这种形式/list/:id) ---index.jsx(访问路径/list)[...params]gpt接口分析 初始化项目 npm install next react react-dom # or yarn add next react react-dom # or pnpm add n…...
云计算与区块链之间有什么区别?
区块链是一种去中心化的分布式数字账本,可实现安全透明的交易和数据存储。 它使用节点网络来验证和验证交易。 云计算通过互联网提供计算资源,例如服务器、存储和软件。区块链是一种分散且不可变的虚拟数据分类账,用于维护交易信息和监控网络…...
sed命令常用例子
替换文件中的文本 将文件file.txt中的所有"old_text"替换成"new_text": sed -i ‘s/old_text/new_text/g’ file.txt 删除文件中的某行 删除文件file.txt中的第5行: sed -i ‘5d’ file.txt 在文件中添加一行 在文件file.txt…...

MB510 3BSE002540R1在机器视觉工业领域最基本的应用
MB510 3BSE002540R1在机器视觉工业领域最基本的应用 大家都说人类感知外界信息的80%是通过眼睛获得的,图像包含的信息量是最巨大的。那么机器视觉技术的出现,就是为机器设备安上了感知外界的眼睛,使机器具有像人一样的视觉功能,…...

nightingale-0-介绍单机二进制部署
(一) 夜莺介绍 Nightingale | 夜莺监控,一款先进的开源云原生监控分析系统,采用 All-In-One 的设计,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。于…...

一个从培训学校走出来的测试工程师自述....
简单介绍一下我自己,1997年的,毕业一年了,本科生,专业是机械制造及其自动化。 在校度过了四年,毕业,找工作,填三方协议,体检,入职。我觉得我可能就这么度过我平平无奇的…...
关于pyqt的一些用法
QT原生是C,pyqt基于python语言。 关于插件: 安装一个PyUIC,一个Qt Designer 点击Qt Designer可以出来ui配置页面,和qt原生IDE基本一样 上面操作会生成.ui文件,选中此文件,点击PyUIC,会生成对…...

【Paper Note】ViViT: A Video Vision Transformer
ViViT: A Video Vision Transformer AbstractOverview of vision transformer 回顾ViTEmbedding video clips 视频编码方式Uniform frame sampling 均匀采样Tubelet embedding 时空管采样初始化3D卷积代码介绍视频编码输入到模型当中 Transformer Models for VideoSpatio-tempo…...

Java入坑之IO操作
目录 一、IO流的概念 二、字节流 2.1InputStream的方法 2.2Outputstream的方法 2.3资源对象的关闭: 2.4transferTo()方法 2.5readAllBytes() 方法 2.6BufferedReader 和 InputStreamReader 2.7BufferedWriter 和 OutputStreamWriter 三、路径:…...

校园小助手【GUI/Swing+MySQL】(Java课设)
系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设!!! 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址: 更多系统资源库地址:骚戴的博客_CSDN_更多系统资源 更多系统…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...