阿里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() {
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }
                @Override
                public 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 provider
                        IProvider 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,并且调用 startActivity
            case 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 Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public 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的重要性主要体现在以下几个方面:

  1. 模块化开发:随着软件规模和复杂度的增加,越来越多的应用程序采用模块化开发方式。ARouter支持跨模块的调用,简化了模块间跳转的复杂度,降低了耦合性,提升了整个应用的可维护性和可扩展性。
  2. 代码解耦:在传统的Activity跳转方式中,一般是通过Intent来传递参数,不同页面之间的参数传递比较麻烦,还有可能导致代码冗余。ARouter采用注解方式传递参数,简化了代码的编写和阅读。
  3. 动态路由:路由是基于注解方式实现的,可以在代码中动态注册和删除路由。ARouter还支持路由重定向、降级等高级功能,可以根据不同的业务场景灵活处理路由逻辑。
  4. 统一管理:ARouter提供了统一管理路由配置的界面,可以直览整个项目的路由规则,方便开发人员进行维护和管理。

ARouter是一款非常实用的路由框架,可以帮助开发者在模块化开发、代码解耦、动态路由和统一管理等方面提高开发效率和应用质量。

下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接 通过点击此处↓↓↓ 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

Android 架构学习手册

深入理解Gradle框架

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入理解Linux多线程

致前行的人&#xff1a; 昨日渐多&#xff0c;明日愈少&#xff0c;今日还在&#xff0c;不要为成功而努力&#xff0c;要为做一个有价值的人而努力。人生道路上充满了坎坷&#xff0c;谁也不可能一帆风顺。只有在最困难的时刻&#xff0c;才能体会到无助的含义。 目录 1.理解…

SpringBoot集成MyBatis-yml自动化配置原理详解

SpringBoot集成MyBatis-yml自动化配置原理详解 简介&#xff1a;spring boot整合mybatis开发web系统目前来说是市面上主流的框架&#xff0c;每个Java程序和springboot mybatis相处的时间可谓是比和自己女朋友相处的时间都多&#xff0c;但是springboot mybatis并没有得到你的真…

适用于 Windows 的 5 个最好的 PDF 转换器应用程序

由于稳定性、高分辨率、高安全性、易于传输等特点&#xff0c;PDF已经成为我们日常工作中最常用的格式。我们在享受PDF带来便利的同时&#xff0c;也发现PDF带来了一些不便&#xff0c;其中最大的问题就是PDF内容的编辑难度。同时&#xff0c;并不是所有的文件都是PDF格式的&am…

代码优化- 前端优化

常量折叠 基本思想&#xff1a;在编译期间计算表达式的值&#xff08;编译时静态计算&#xff09; 例如&#xff1a;a 3 5 > a 8&#xff0c;if (true && false) ... > if (false) 好处是&#xff1a;语法树的节点数量减少了&#xff0c;意味着编译器要维护…

Ubuntu上跑通PaddleOCR

书接上文。刚才说到我已经在NUC8里灌上了Windows Server 2019。接下来也顺利的启用了Hyper-V角色并装好了一台Ubuntu 22.04 LTS 的虚机。由于自从上回在树莓派上跑通了Paddle-Lite-Demo之后想再研究一下PaddleOCR但进展不顺&#xff0c;因此决定先不折腾了&#xff0c;还是从x6…

【论文写作】如何写科技论文?万能模板!!!(以IEEE会议论文为例)

0. 写在前面 常言道&#xff0c;科技论文犹如“八股文”&#xff0c;有固定的写作模式。本篇博客主要是针对工程方面的论文的结构以及写作链条的一些整理&#xff0c;并不是为了提高或者润色一篇论文的表达。基本上所有的论文&#xff0c;都需要先构思好一些点子&#xff0c;有…

一文搞懂Session和JWT登录认证

前言 目前在开发的小组结课项目中用到了JWT认证&#xff0c;简单分享一下&#xff0c;并看看与Session认证的异同。 登录认证&#xff08;Authentication&#xff09;的概念非常简单&#xff0c;就是通过一定手段对用户的身份进行确认。 我们都知道 HTTP 是无状态的&#xf…

强化学习技巧

此软件包处于维护模式&#xff0c;请使用Stable-Baselines3 (SB3)获取最新版本。您可以在 SB3 文档中找到迁移指南。 本节的目的是帮助您进行强化学习实验。它涵盖了有关 RL 的一般建议&#xff08;从哪里开始、选择哪种算法、如何评估算法等&#xff09;&#xff0c;以及使用自…

【Linux】System V 共享内存、消息队列、信号量

&#x1f34e;作者&#xff1a;阿润菜菜 &#x1f4d6;专栏&#xff1a;Linux系统编程 system V共享内存介绍 System V 共享内存是一种进程间通信的机制&#xff0c;它允许多个进程共享一块物理内存区域&#xff08;称为“段”&#xff09;。System V 共享内存的优点是效率高&…

OTG是什么意思?

OTG是什么意思&#xff1f; OTG是怎么样实现的&#xff1f; TYPE-C接口的手机如何实现同时充电OTG功能&#xff1f; OTG是什么意思&#xff1f; OTG是On-The-Go的缩写&#xff0c;是一项新兴技术&#xff0c;主要应用于不同的设备或移动设备间的联接&#xff0c;进行数据交…

基于遥感的自然生态环境检测——实验三:生态因子提取

实验三&#xff1a;生态因子提取 一、实验目标 生态因子生成&#xff1b;生态因子归一化&#xff1b;生态环境评价 二、实验内容 根据经过大气校正后的影像生产土地覆盖指数、土壤指数以及坡度等&#xff0c;对土地覆盖指数、土壤指数以及坡度进行密度分割归一化&#xff1…

“SCSA-T学习导图+”系列:下一代防火墙

本期引言&#xff1a; 近年来&#xff0c;随着数字化业务带给我们高效和便捷的同时&#xff0c;信息暴露面的增加、网络边界的模糊化以及黑客攻击的产业化&#xff0c;使得网络安全事件相较以往成指数级增加。传统防火墙基于五元组的方式对进出网络的数据流量进行访问控制&…

JavaScript(JS)-1.JS基础知识

1.JavaScript概念 (1)JavaScript是一门跨平台&#xff0c;面向对象的脚本语言&#xff0c;来控制网页行为的&#xff0c;它能使网页可交互 (2)W3C标准&#xff1a;网页主要由三部分组成 ①结构&#xff1a;HTML负责网页的基本结构&#xff08;页面元素和内容&#xff09;。 …

【Linux网络服务】Linux网络设置

一、查看网络配置 1.1ifconfig 1.2ip a 1.3什么是mtu 最大传输单元MTU&#xff0c;是指网络能够传输的最大数据包大小&#xff0c;以字节为单位。MTU的大小决定了发送端一次能够发送报文的最大字节数。如果MTU超过了接收端所能够承受的最大值&#xff0c;或者是超过了发送路径…

EIGRP 配置,详解拓扑表,路由汇聚

1.3 EIGRP 拓扑&#xff0c;路由以及汇聚 1.3.1 实验目的 通过对 EIGRP 拓扑&#xff0c;路由以及汇聚相关实验的练习&#xff0c;掌握 EIGRP 建立拓扑信息的方式&#xff0c; 度量计算方法&#xff0c;如何调整度量&#xff0c;非等价负载均衡&#xff0c;以及 EIGRP 末节路…

做完自动化测试,但别让不会汇报毁了你...

pytest 是一个成熟的全功能Python测试工具&#xff0c;可以帮助您编写更好的程序。它与 python 自带的 unittest 测试框架类似&#xff0c;但 pytest 使用起来更简洁和高效&#xff0c;并且兼容 unittest 框架。pytest 能够支持简单的单元测试和复杂的功能测试&#xff0c;pyte…

Verilog带参数的`define用法

宏除了可以进行简单的文本替换,还可以像函数和任务一样传递指定多个参数分别对文本进行对应的替换. 示例1&#xff1a; define Disp(pa,pb,pc) \initial \begin \#1200; \$display("%d \n",(papbpc)); \$display(" data_ pa data_ pb data_ pc %d",(…

C#中用程序代码修改了datagridview中的数据,保存时只对光标当前行有保存解决办法

C#中DataGridView绑定了DataTable后&#xff0c;通过代码修改DataGridView中的数据&#xff0c;总有一行&#xff08;被修改过并被用户选中的行集合中索引为0的行&#xff09;不能被UpDate回数据库的问题和解决办法 长江黄鹤 2017-06-26 | 300阅读 | 1转藏 转藏全屏朗读分…

真题详解(UML部署图)-软件设计(五十二)

真题详解&#xff08;地址索引&#xff09;-软件设计&#xff08;五十一)https://blog.csdn.net/ke1ying/article/details/130211684 瀑布模式&#xff1a;适应 开发大型项目&#xff0c;且需求明确。 演化模式&#xff1a;适应 对软件需求缺乏准确认知。 螺旋模式&#xff…

【linux】yum “应用商店” 的基本用法

好多工具 yum软件包查看软件包安装软件卸载软件 yum 通俗的讲&#xff0c;这就似我们手机上的应用商店&#xff0c;只不过是在linux下的。 我们可以用yum来下载东西。 软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序。 但是这样…
最新文章