组件化框架 ARouter 完全解析(一)

news2024/11/29 12:48:51

一、路由认知

ARouter从命名即可知,这是一个路由框架。那么路由是个啥呢?

路由routing)就是通过互联的网络把信息源地址传输到目的地址的活动。-- 百科
可见 路由 是个动词,这是网络传输中的概念,完成路由这个操作的实体设备就是 路由器(Router)。

另外,生活中的 信件邮寄 也可以理解为一个 路由过程:邮局把信件从邮寄方 传输到接收人的手上。首先 邮寄方 和 接收人 是无法接触的(无耦合依赖),只能通过 邮局这个第三方 完成邮寄;邮局根据信封上的地址,例如 “深圳市 深圳大学粤海校区”,决定分发到 开往深圳的车上,然后深圳的邮递员找到 深圳大学粤海校区 对应的 "南山区南海大道3688号”,最终找到接收人。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HGOmWj6M-1658750174332)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c3428ead70384bd4a3856937338889a9~tplv-k3u1fbpfcp-watermark.image?)]

对应地, ARouter 也是个“路由器”,也是个“邮政系统”。通行根据组件化介绍的,ARouter 帮助 无相互依赖的组件间 进行跳转和通信。

抽象一下,邮局、ARouter 都是 路由系统 ——— 给 无依赖的双方 提供 通信和路由的能力。

官方文档 有详细的引入和各种功能使用介绍,包括基础使用步骤、参数解析、拦截器、服务获取等进阶用法。这里不再搬运。

二、原理解析

使用ARouter在进行Activity跳转非常简单:初始化ARouter、添加注解@Route、发起路由。

// 在module app中
//1.初始化SDK
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化
// moduleA
// 2.在支持路由的页面上添加注解(必选)
// 路径注意至少需有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
// moduleB(没有依赖 moduleA)
// 3.发起路由跳转
ARouter.getInstance().build("/test/activity").navigation();

这样就使得 没有依赖moduleA的moduleB能跳转到moduleA的Activity了。服务获取也是类似简单的代码就可实现。

那么 ARouter 是如何做到只通过简单2步 就完成 解耦组件间的路由操作呢?我们通过源码一步步理解。

2.1 构建PostCard

我们知道 想要跳转Activity最终必定是走到了 startActivity(intent)方法,而intent是一般需要目标Activity的Class。所以我们猜想 navigation()中应该是有寻找目标Activity的Class这一过程的。

下面就来跟踪源码分析这一过程。先看ARouter.getInstance():

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单实例,没有初始化则报异常。再看它的build(string)方法:

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

这里是调用了 _ARouter 的同名方法,返回了 Postcard(意为明信片)。ARouter实际是使用了外观模式(设计模式的一种),其所有方法都是调用了_ARouter的同名方法。 进入_ARouter:

protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) { //path不能为空
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        //path替换,这是预处理
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path), true);
    }
}

这里对path做了空校验和预处理替换。如果想重写path,可以写一个PathReplaceService实现类。接着又走到重载方法:

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 {
        ...
        return new Postcard(path, group);
    }
}

其中参数group是通过extractGroup(path)获取,也就是path的第一级,即"/test/activity"中的"test"。group的作用是作为路由的默认分组。

路由中的分组概念:

  • SDK中针对所有的路径(/test/1、/test/2)进行分组,分组只有在分组中的某一个路径第一次被访问的时候,该分组才会被初始化
  • 可以通过 @Route 注解主动指定分组,否则使用路径中第一段字符串(/*/)作为分组
  • 注意:一旦主动指定分组之后,应用内路由需要使用 ARouter.getInstance().build(path, group) 进行跳转,手动指定分组,否则无法找到
    @Route(path = "/test/1", group = "app")

最后返回创建的Postcard实例。Postcard是明信片的意思,承载了一次跳转/路由 需要的所有信息,它继承自路由元信息 RouteMeta

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;         //启动activity的传入的Bundle
    private int flags = 0;         // 启动activity的Flags
    private int timeout = 300;      // 路由超时时间
    private IProvider provider;     // 如果是获取provider,就有值
    private boolean greenChannel;  //是绿色通道
    private SerializationService serializationService;
    private Context context;        //context
    private String action;          //启动activity的action
    
    // activity转场动画相关
    private Bundle optionsCompat;
    private int enterAnim = -1;
    private int exitAnim = -1;
    
    public Postcard(String path, String group) {
        this(path, group, null, null);
    }

    public Postcard(String path, String group, Uri uri, Bundle bundle) {
        setPath(path);
        setGroup(group);
        setUri(uri);
        this.mBundle = (null == bundle ? new Bundle() : bundle);
    }
    ...
}
public class RouteMeta {
    private RouteType type;         // 路由类型;activity、service、fragment、IProvider等,编译时会根据被@Route注解的类的类型来设置
    private Element rawType;        // 路由原始类型,在编译时用来判断
    private Class<?> destination;   // 目的地:具体的 XxxActivity.class等
    private String path;            // Path
    private String group;           // Group
    private int priority = -1;      // 优先级,越小优先级越高
    private int extra;              // Extra
    private Map<String, Integer> paramsType;  // 参数类型,例如activity中使用@Autowired的参数类型
    private String name; //路由名字,用于生成javadoc
    private Map<String, Autowired> injectConfig;  // 参数配置(对应paramsType).
}
  • Postcard:路由的信息。 理解为是生活中的明信片。继承自RouteMeta。例如Postcard中的mBundle等则是 明信片上寄件人写的 “你好,xxx 节日快乐!” 这种文字内容。
  • RouteMeta:路由元信息,即基础信息。 理解就是 明信片上的 收件人地址 这种必备的基础信息。 明信片上可以没有文字内容,但想要被邮寄就必须有收件人地址

2.2 路由过程

2.2.1 整体步骤

通过path构建了PostCard后调用了其navigation()方法,也就是开始了路由过程:

public Object navigation() {
    return navigation(null);
}
public Object navigation(Context context) {
    return navigation(context, null);
}
public Object navigation(Context context, NavigationCallback callback) {
    return ARouter.getInstance().navigation(context, this, -1, callback);
}

看到连续走了两个重载方法,最后走到ARouter的navigation方法,并且把自己传了进去。ARouter的navigation方法同样会走到_ARouter的同名方法:

 // @param context     Activity or null.
 // @param postcard    Route metas
 // @param requestCode RequestCode,startActivity的requestCode
 // @param callback    cb,路由回调:找到目的地、未找到、中断、到达
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    //若有PretreatmentService的实现,就进行预处理。可以在真正路由前进行一些判断然后中断路由。
    PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
    if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {
        // 预处理失返回false,路由取消.
        return null;
    }

    // 给路由设置context,例如启动activity需要。如果没有传就使用mContext,即初始化ARouter时传入的Application
    postcard.setContext(null == context ? mContext : context);

    try {
        // 1. 完善postcard信息(目前只有path、group,还需要知道具体目的地,例如要跳转到的activity信息)
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        //这里就是debug包中,没找到路由目的地时 经常出现的提示
        if (debuggable()) {
            runInMainThread(new Runnable() {
                public void run() {
                    Toast.makeText(mContext, "There's no route matched!\n" +" Path = [" + postcard.getPath() + "]\n" +
                            " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
                }
            });
        }
        //没找到的回调
        if (null != callback) {
            callback.onLost(postcard);
        } else {
            // 没有callback的话, (如果有)就回调到降低服务 
            DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
            if (null != degradeService) {
                degradeService.onLost(context, postcard);
            }
        }
        return null;
    }
    //找到的回调
    if (null != callback) {
        callback.onFound(postcard);
    }
    // 2. 不是绿色通道的话,要先走拦截器
    if (!postcard.isGreenChannel()) {
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            
            //拦截器处理结果:继续路由
            @Override
            public void onContinue(Postcard postcard) {
                // 3. 获取路由结果
                _navigation(postcard, requestCode, callback);
            }

             //拦截器处理结果:中断路由,回调中断
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }
            }
        });
    } else {
        //绿色通道,不走拦截器,就获取路由结果
        return _navigation(postcard, requestCode, callback);
    }

    return null;
}

使用前面构建好的PastCard经过整体3个步骤,就完成了路由:

  1. 完善postcard信息:只有path、group还不行,还需要知道具体目的地,例如要跳转到的Activity信息。
  2. 拦截器处理:不是绿色通道的话,要先经过路由器的处理,结果是中断或者继续。
  3. 获取路由结果:例如进行目标Activity的跳转、获取IProvider实现对象、获取Fragment等。
2.2.2 获取路由结果

先来看比较简单的最后一个步骤——路由结果获取过程,也就是_navigation()方法:

private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    final Context currentContext = postcard.getContext();
    //根据路由类型来走对应逻辑
    switch (postcard.getType()) {
        case ACTIVITY:
            // Activity, 使用postcard.getDestination()来构建intent、传入Extras、设置 flags、action
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            intent.putExtras(postcard.getExtras());
            int flags = postcard.getFlags();
            if (0 != flags) {
                intent.setFlags(flags);
            }
            // 当前的context不是activity,需要添加flag:FLAG_ACTIVITY_NEW_TASK
            //(启动startActivity需有任务栈的,application/service启动activity没有任务栈,所以必须要new_task_flag新建一个任务栈)
            if (!(currentContext instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            String action = postcard.getAction();
            if (!TextUtils.isEmpty(action)) {
                intent.setAction(action);
            }

            // 最后在主线程执行 熟悉的startActivity,
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    startActivity(requestCode, currentContext, intent, postcard, callback);
                }
            });
            break;
        case PROVIDER:
            //provider,指的是想要获取的服务,即IProvider的实现类。直接从postCard中获取。
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
        //Broadcast、ContentProvider、Fragment,都是使用postcard.getDestination()反射创建实例
            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;
            } ...
    }
    return null;
}

从上面可见,postcard 经过完善后,路由类型type、目的地destination等都已经被赋了值。destination就是目标类的class对象。

方法内容就是根据路由类型来走对应逻辑:

  • Activity, 使用postcard.getDestination()来构建intent、传入Extras、设置 flags、action,最后在主线程执行 熟悉的startActivity
  • provider,指的是想要获取的服务,即IProvider的实现类。是直接从postCard中获取的,因为服务类是单例,只会在首次获取时创建()。
  • Broadcast、ContentProvider、Fragment,都是使用postcard.getDestination()反射创建实例

整体逻辑还是比较简单的。这里你可能好奇 destination的值是如何获取的,因为无论哪种类型的路由,都是要使用目标class,这个就是ARouter最为核心的内容——如何获取 无直接依赖的模块的 class对象,也就是完善postcard信息的过程。 不过我们先来把拦截器逻辑分析完,最后再来看这个核心点。

2.2.3 拦截器

拦截器模式是开发中常用设计模式之一,路由中也可以设置拦截器,对路径进行判断决定是否需要中断。

未设置绿色通道的路由需要经过拦截器处理,也就是interceptorService的doInterceptions()方法。interceptorService是啥呢?

final class ARouter {
    ...
    private static InterceptorService interceptorService;
    ...
    //ARouter的初始化方法
    public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            hasInit = _ARouter.init(application);
            if (hasInit) {
                _ARouter.afterInit();
            }
        }
    }
   ...
}
    //_ARouter.java
    static void afterInit() {
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }

InterceptorService继承IProvider,可见interceptorService也是一个服务,在ARouter初始化后 获取,用于处理拦截器的逻辑。具体的实现类是InterceptorServiceImpl:

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService {
...
    @Override
    public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
        //有拦截器
        if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
            ...
            LogisticsCenter.executor.execute(new Runnable() { //放入线程池异步执行
                @Override
                public void run() { //interceptorCounter 用于保证所有拦截器都走完,并且设置了超时
                    CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
                    try {//执行第一个拦截器,如果没有中断 则递归调用继续后面的拦截器
                        _execute(0, interceptorCounter, postcard);
                        interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);
                        if (interceptorCounter.getCount() > 0) {    // count>0说明超时了,拦截器还没处理完.
                            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
                        } else if (null != postcard.getTag()) {    //Tag!=null说明被某个拦截器回调中断了
                            callback.onInterrupt((Throwable) postcard.getTag());
                        } else {
                            callback.onContinue(postcard); // 所有拦截器处理完、没超时、也没异常,则继续路由
                        }
                    }...
                }
            });
        } else {
            //没有拦截器则继续路由
            callback.onContinue(postcard);
        }
    }

    private static void _execute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
        if (index < Warehouse.interceptors.size()) {
            //从Warehouse.interceptors中获取第index个拦截器,走process方法,如果回调到onContinue就继续下一个;
            IInterceptor iInterceptor = Warehouse.interceptors.get(index);
            iInterceptor.process(postcard, new InterceptorCallback() {
                @Override
                public void onContinue(Postcard postcard) {
                    counter.countDown();
                    _execute(index + 1, counter, postcard);  // 继续下一个
                }
                @Override
                public void onInterrupt(Throwable exception) {
                    postcard.setTag(null == exception ? new HandlerException("No message.") : exception);    // save the exception message for backup.
                    counter.cancel();
                    ...
                }
            });
        }
    }
    
    @Override //此init方法会在服务被创建后调用。这里就是反射创建所有的拦截器实例
    public void init(final Context context) {
        LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run() {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                    //遍历Warehouse.interceptorsIndex ,使用存储与其中的拦截器class对象反射创建拦截器实例
                    for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                        Class<? extends IInterceptor> interceptorClass = entry.getValue();
                        try {
                            IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                            iInterceptor.init(context);
                            //存入 Warehouse.interceptors
                            Warehouse.interceptors.add(iInterceptor);
                        }...
                    }
                    interceptorHasInit = true;
                    ...
                }
            }
        });
    }...
}

doInterceptions()方法中判断如果有拦截器,就放入线程池异步执行第一个拦截器,且使用interceptorCounter 保证所有拦截器都走完,同时也设置了超时。 如果第一个拦截器没有回调中断 则递归调用继续后面的拦截器。

拦截器的执行,是从Warehouse.interceptors中获取第index个拦截器,走process方法,如果回调到onContinue就继续下一个;若回调onInterrupt就中断路由。

拦截器的执行逻辑还是比较清晰的。那么拦截器是怎么获取的呢?我们来看下InterceptorServiceImpl的init方法:init()方法会在服务被创建后立即调用,如上所示就是遍历Warehouse.interceptorsIndex ,使用存储在其中的拦截器class对象 反射创建拦截器实例,然后存在存入 Warehouse.interceptors。 也即是说,ARouter初始化完成后就获取到了所有拦截器实例

那么Warehouse又是啥呢?interceptorsIndex是如何存储的所有拦截器的class的?

2.2.4 路由元信息的收集

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

前面举的例子:moduleB发起路由跳转到moduleA的activity,moduleB没有依赖moduleA,只是在moduleA的activity上增加了@Route注解。 由于进行activity跳转需要目标Activity的class对象来构建intent,所以必须有一个中间人,把路径"/test/activity"翻译成Activity的class对象,然后moduleB才能实现跳转。(因此在ARouter的使用中 moduleA、moduleB 都是需要依赖 arouter-api的)

这个中间人那就是ARouter了,而这个翻译工所作用到的词典就是 Warehouse,它存着所有路由信息。

class Warehouse {
    //所有IRouteGroup实现类的class对象,是在ARouter初始化中赋值,key是path第一级
    //(IRouteGroup实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务)
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); 
    //所有路由元信息,是在completion中赋值,key是path
    //首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务
    static Map<String, RouteMeta> routes = new HashMap<>();
    
    //所有服务provider实例,在completion中赋值,key是IProvider实现类的class
    static 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<>("...");
    //所有拦截器实例,是在ARouter初始化完成后立即创建
    static List<IInterceptor> interceptors = new ArrayList<>();
...
}

Warehouse存了哪些信息呢?

  • groupsIndex所有路由组元信息。是所有IRouteGroup实现类的class对象,是在ARouter初始化中赋值,key是path第一级。IRouteGroup实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务)。
  • routes所有路由元信息。是在LogisticsCenter.completion中赋值,key是path。首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务
  • providers所有服务provider实例。在LogisticsCenter.completion中赋值,key是IProvider实现类的class
  • providersIndex所有provider服务元信息(实现类的class对象)。是在ARouter初始化中赋值,key是IProvider实现类的全类名。用于使用IProvider实现类class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class)
  • interceptorsIndex所有拦截器实现类class对象。是在ARouter初始化时收集到,key是优先级
  • interceptors所有拦截器实例。是在ARouter初始化完成后立即创建

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

那么Warehouse的信息是如何收集到的呢? 下面就先来看下ARouter初始化具体做了哪些事情:

final class _ARouter {
    ...
    protected static synchronized boolean init(Application application) {
        mContext = application;
        LogisticsCenter.init(mContext, executor);
        ...
        return true;
    }

_ARouter的init方法中 调用了LogisticsCenter的init方法。LogisticsCenter意为物流中心,上面提到的完善postcard的completion操作也是此类提供。

//LogisticsCenter.java
//LogisticsCenter初始化,加载所有的路由元信息
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {...
    try {
        long startInit = System.currentTimeMillis();
        //先尝试使用AGP transform 收集 根帮助类 后 写好的注入代码(要先引入插件才行 apply plugin: 'com.alibaba.arouter')
        loadRouterMap();
        if (registerByPlugin) {
            //registerByPlugin为true说明使用AGP加载ok了(通常都会用AGP,即registerByPlugin为true)
            logger.info(TAG, "Load router map by arouter-auto-register plugin.");
        } else {
            //若没有使用 AGP transform,就用ClassUtils.getFileNameByPackageName来搜集dex中ROUTE_ROOT_PAKCAGE包下的所有类,即编译时生成的所有帮助类
            //这样的话,就是运行时 遍历搜集 会比较耗时,也就是init会较为耗时;而AGP transform 是在编译时完成收集的。
            //当前app是新安装时才会走(收集到的帮助类会缓存到SP文件)
            Set<String> routerMap;
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                // 这写帮助类是在编译时由arouter-compiler生成
                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);
            } else {
                //不是新安装的版本,就从SP文件中读取
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }...
            //遍历帮助类,区分是哪种帮助类,然后反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map
            for (String className : routerMap) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Root
                    //填充Warehouse.groupsIndex,即所有IRouteGroup实现类的class对象
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Interceptors
                    //填充Warehouse.interceptorsIndex,即所有IInterceptor实现类的class对象
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Providers
                    //填充Warehouse.providersIndex,即所有provider的RouteMeta
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }...
    }
}

LogisticsCenter初始化 就是加载所有的路由元信息的过程,有两种方式:

  1. 走loadRouterMap()方法:直接使用在编译时收集好的帮助类信息,然后反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。这需要开发者要先引入插件才行 apply plugin: ‘com.alibaba.arouter’。如果加载成功,registerByPlugin这个值就为true,否则false。
  2. 若第一步没有加载成功,即registerByPlugin为false:就会使用ClassUtils.getFileNameByPackageName在运行时搜集dex中"com.alibaba.android.arouter.routes"包下的所有类(即帮助类),然后遍历,区分是哪种帮助类。接着反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。

两种方式对比:

  • 相同点:都是使用帮助类信息反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。
  • 不同点:在于帮助类信息的收集方式。前者是在编译时搜集就完毕了,后者是运行时。

一般都是使用第一种,因为运行时遍历dex搜集会比较耗时,而第一种在编译时已经收集好了。 第一种方式的loadRouterMap()方法的实现逻辑 我们稍后再看。

先看两个问题:

  • 上面提到的帮助类是个啥? 如何使用帮助类填充Warehouse的?
  • 为啥帮助类还要收集?还分 编译时收集、运行时收集?
2.2.4.1 拦截器元信息

我们先来看拦截器元信息(拦截器class信息)是如何通过帮助类填充的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tdm0Wd2d-1658750174333)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4cf903d3ab0a48d981b7e093af6e9a4f~tplv-k3u1fbpfcp-watermark.image?)]

上图是ARouter工程编译后module-java的build目录,ARouter$$ 开头的这些类都是在ARouter在编译过程中生成,它们就是所谓的帮助类:

ARouter$$Interceptors$$modulejava 这个类就是一个帮助类,帮助WareHouse填充WareHouse.interceptorsIndex。它实现接口IInterceptorGroup,loadInfo方法接受一个Map<Integer, Class<? extends IInterceptor>>,也就是WareHouse.interceptorsIndex的类型。loadInfo方法体内,是用接收的map来put当前module所有拦截器的class,即使用 @Interceptor 注解并实现 IInterceptor 接口的类。

在上面LogisticsCenter的init方法中第二种加载方式中看到,确实是遍历收集到的帮助类,然后使用类名判断是 ARouter$$Interceptors$$modulejava,接着就调用loadInfo方法,这就实现了对WareHouse.interceptorsIndex的赋值。 也就是说,有了ARouter$$Interceptors$$modulejava ,我们就能在ARouter初始化时对WareHouse.interceptorsIndex进行赋值,就为创建所有拦截器实例做好了准备。

那么到这里,关于拦截器还有一个问题,ARouter$$Interceptors$$modulejava的loadInfo方法中 拦截器实现类class是如何获取的呢?—— 当然是编译时对注解 @Interceptor 的解析,解析过程将在下篇中介绍。

拦截器帮助类我们看完了,再来看看其他三种帮助类。

2.2.4.2 路由组元信息

路由组元信息的收集是通过 —— ARouter$$Root$$xxx —— 根帮助类:即用来帮助对 WareHouse.groupsIndex 赋值。这样就会把path第一级相同的所以路由分到同一个组中。 一个module对应一个根帮助类。xxx是module名,就是在build.gradle中配置的 AROUTER_MODULE_NAME 。

在这里插入图片描述

如上图,ARouter$$Root$$modulejava 就是根帮助类,帮助WareHouse填充Warehouse.groupsIndex。实现自IRouteRoot接口,loadInfo方法接受一个Map<String, Class<? extends IRouteGroup>>,也就是WareHouse.groupsIndex 的类型。loadInfo方法体内,是用接收的map来put当前module所有路由组帮助类的class 。

在上面LogisticsCenter的init方法中同样 对遍历收集到的帮助类判断类名,接着就调用loadInfo方法,这就实现了对WareHouse.groupsIndex 的赋值。

根帮助类,目的就是对路由进行分组,分组的好处是避免一次性加载所有路由,减少反射耗时和内存占用的性能问题。

根帮助类也是在编译时生成,具体生成过程将在下篇中介绍。

2.2.4.3 路由元信息

路由元信息的收集是通过 —— ARouter$$Group$$xxx —— 组帮助类:即用来帮助对 WareHouse.routes 赋值。也就是把同组的路由put到WareHouse.routes。 一个module可能有多个组,即对应有多个根帮助类,xxx是组名,即path第一级。

在这里插入图片描述

如上图,ARouter$$Group$$test 就是组帮助类,帮助WareHouse填充Warehouse.routes。实现自IRouteGroup接口,loadInfo方法接受一个Map<String, RouteMeta>,也就是WareHouse.routes 的类型。loadInfo方法体内,是用接收的map来put 当前组 的所有路由元信息 。 其中最重要的就是 每个路由的目标类class

在上面LogisticsCenter的init方法中 没有看到对组帮助类的处理。ARouter的设计是:在使用时才进行加载,即首次使用某个组的路由时,才会使用组帮助类对 WareHouse.routes 进行填充。

组帮助类,目的就是 首次使用时 一次性加载本组所有路由元信息。这比较符合实际使用场景:一般同组的路由都是同业务的内容,当前用户进入此业务时,就把本组路由元信息准备好,是比较合理的。

组帮助类也是在编译时生成。

2.2.4.4 provider元信息

provider元信息 其实在上面 路由元信息 中已经包含了,为啥还要单独拎出来呢?我们回头看下 _ARouter的navigation方法:

protected <T> T navigation(Class<? extends T> service) {
    Postcard postcard = LogisticsCenter.buildProvider(service.getName());
    if (null == postcard) {
        postcard = LogisticsCenter.buildProvider(service.getSimpleName());
    }
    if (null == postcard) {
        return null;
    }
    postcard.setContext(mContext);
    LogisticsCenter.completion(postcard);
    return (T) postcard.getProvider();
    ...
}

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    ...
}

前面介绍了4个参数的方法,而上面这个传服务class的重载方法 就是单独给获取provider服务使用的。看到通过LogisticsCenter使用服务类name获取到了PostCard,然后经过完善PostCard,直接获取provider服务。

//LogisticsCenter.java
public static Postcard buildProvider(String serviceName) {
    RouteMeta meta = Warehouse.providersIndex.get(serviceName);
    if (null == meta) {
        return null;
    } else {
        return new Postcard(meta.getPath(), meta.getGroup());
    }
}

其中Postcard其实就是从 Warehouse.providersIndex 中获取到的RouteMeta后构建的。而Warehouse.providersIndex的赋值就是通过 ——ARouter$$Providers$$xxx —— Provider帮助类

在这里插入图片描述

帮助类可能在其他文章中叫路由表,之前想叫做代理类,但觉得帮助类更合适,它们就是用来帮助填充WareHouse中的元数据的。

2.2.5 AGP方式加载路由

我们来看下路由信息收集的第一种方式,这是一般都会使用到的方式,也就是loadRouterMap()方法:

public class LogisticsCenter {
    private static boolean registerByPlugin;
    
    private static void loadRouterMap() {
        registerByPlugin = false;
        //主动注册插件 会在此处插入代码。调用此方法就注册了全部的 Routers、Interceptors、Provider
        
    }
   ...

你会惊奇地发现,loadRouterMap()竟然只有一行代码?!

反编译 ARouter demo APK后,查看LogisticsCenter:

在这里插入图片描述

看到编译后的loadRouterMap()方法,多了几行register()方法的调用,而参数就是 所有的根帮助类、拦截器帮助类、provider帮助类。

//LogisticsCenter.java
...
 private static void register(String className) {
        if (!TextUtils.isEmpty(className)) {
            try {
                Class<?> clazz = Class.forName(className);
                Object obj = clazz.getConstructor().newInstance();
                if (obj instanceof IRouteRoot) {
                    registerRouteRoot((IRouteRoot) obj);
                } else if (obj instanceof IProviderGroup) {
                    registerProvider((IProviderGroup) obj);
                } else if (obj instanceof IInterceptorGroup) {
                    registerInterceptor((IInterceptorGroup) obj);
                } ...
            } ...
        }
    }
    private static void registerRouteRoot(IRouteRoot routeRoot) {
        markRegisteredByPlugin();
        if (routeRoot != null) {
            routeRoot.loadInto(Warehouse.groupsIndex);
        }
    }
    private static void registerInterceptor(IInterceptorGroup interceptorGroup) {
        markRegisteredByPlugin();
        if (interceptorGroup != null) {
            interceptorGroup.loadInto(Warehouse.interceptorsIndex);
        }
    }
    private static void registerProvider(IProviderGroup providerGroup) {
        markRegisteredByPlugin();
        if (providerGroup != null) {
            providerGroup.loadInto(Warehouse.providersIndex);
        }
    }
    private static void markRegisteredByPlugin() {
        if (!registerByPlugin) {
            registerByPlugin = true; //标记通过AGP加载成功了
        }
    }

register()方法很简单,就是反射创建帮助类实例,调用loadInto方法对Warehouse进行填充,和第二种路由信息收集方式的是一致的。 而第二种是在运行时遍历dex才找到的帮助类,在第一种方式就神奇的直接出现了?

这个神奇的操作,我们将在第三篇文章做详细介绍,目前只需知道是在编译时进行扫描并动态在loadRouterMap()中插入代码就可以了。

2.2.3 路由信息的完善

上面兜了一大圈,从路由整体过程、获取路由结果、拦截器、路由信息记载,到各个帮助类的介绍,也即是说 我们了解了 路由的发起、路由整体过程、路由结果获取,以及路由元信息的加载,那么现在就来看看路由元信息是如何使用的。

//LogisticsCenter.java
public synchronized static void completion(Postcard postcard) {
    //完善postcard信息(目前只有path、group,还需要知道具体目的地,例如要跳转到的activity信息)
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {
        //没有从Warehouse.routes获取到:要么不存在、要么还没有加载本组路由
        //先看路由仓库中是否有这个组帮助类,没就异常。(仓库里的已有 组帮助类 是谁放进仓库的呢?就是 在 ARouter.init中调用 LogisticsCenter.init的时候。它里面的 loadRouterMap() 中执行代码是 transform时收集到的 APT 生成 根帮助类 的load方法。)
        if (!Warehouse.groupsIndex.containsKey(postcard.getGroup())) {
            throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
        } else {
            try {
                //这里,仓库中有这个组的帮助类,那么就可以 加载 这个组的所有路由 到内存
                addRouteGroupDynamic(postcard.getGroup(), null);
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }
            //仓库有了这个组的路由信息,再重新完善
            completion(postcard);
        }
    } else {
        //有路由信息,就完善postcard
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());
        ...
        switch (routeMeta.getType()) {
            case PROVIDER:  //provider, 获取实例
                // 要实现自IProvider
                Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                IProvider instance = Warehouse.providers.get(providerMeta);
                if (null == instance) { // 没有,就反射创建
                    IProvider provider;
                    try {
                        provider = providerMeta.getConstructor().newInstance();
                        provider.init(mContext);
                        Warehouse.providers.put(providerMeta, provider); //实例存入仓库
                        instance = provider;
                    } catch (Exception e) {...
                    }
                }
                postcard.setProvider(instance); //实例通过PostCard带出去
                postcard.greenChannel();    // Provider 不用经过拦截器
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment 不用经过拦截器
        }
    }
}


public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) {
    if (Warehouse.groupsIndex.containsKey(groupName)){//这里,仓库中 有这个组的帮助类
        //拿到这个 组帮助类,实例化,调loadInfo 把 这个组所有的路由信息加载 到 仓库中的routes。
        Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
        Warehouse.groupsIndex.remove(groupName);
    }...
}
  1. 尝试通过path从仓库中获取对应的路由元信息,如果没有获取到:要么不存在、要么还没有加载本组路由。
  2. 先看路由仓库中是否有这个组帮助类,没就抛出异常;有就通过addRouteGroupDynamic()加载这个组的所有路由,然后再调completion
  3. 有了path对应的路由元信息,就同步到postCard中,其中最重要的就是 目标类class——routeMeta.getDestination()。并且判断如是provider就创建服务实例并存入仓库。

好了,到这里,我们终于可以解答最开始提出的问题了:ARouter最为核心的内容——如何获取 无直接依赖的模块的 class对象 :

  • 编译时ARouter根据注解 @Route 生成了各个帮助类,帮助类的loadInfo方法中包含了路由目标信息,最重要的是注解的类class,然后在ARouter初始化时根据 根帮助类、provider帮助类、拦截器帮助类 对仓库WareHouse的groupsIndex进行赋值(以及providersIndex、interceptorsIndex),然后在路由发起后,根据path通过WareHouse的groupsIndex 加载 同组的所有路由元信息,也就是拿到了目标class。
  • 抽象一下就是:moduleA先把目标class存入第三方仓库——ARouter的WareHouse,然后muduleB发起路由时从仓库中根据path获取目标class,ARouter就是这个仓库的管理者。 就好比 邮政是信件的管理者,它是两方通信者的中间人。

2.3 流程图

以上分析内容梳理成流程图:

在这里插入图片描述

三、总结

我们从路由发起开始使,介绍了整个路由详细过程:moduelA通过中间人ARouter把路由信息的存到仓库WareHouse;moduleB发起路由时,再通过中间人ARouter从仓库WareHouse取出路由信息,这要就实现了没有依赖的两者之间的跳转与通信。其中涉及Activity的跳转、服务provider的获取、拦截器的处理等。

需要重点理解的是:路由框架的整体思路,通过中间人ARouter使用WareHouse加载和获取路由信息;路由信息加载实现原理,各帮助类作用和路由完善过程。

其中ARouter在编译时生成的帮助类,是用于对所有使用@Route、@Interceptor注解的类信息的分组和收集,编译运行时对路由信息仓库Warehouse的填充和使用。这里涉及到的是Annotation Process ToolAPT)技术,即注解处理工具。

如何使用编译时生成的帮助类呢?除了运行时查找dex,还可以在编译时扫描帮助类信息,并且直接在物流中心LogisticsCenter loadRouterMap()方法中直接插入使用帮助类的代码,这里涉及 Android Gradle PluginAGP)技术,即Android的gradle插件相关技术。

ARouter如何在编译时解析注解、如何生成帮助类以及对APT技术的介绍将在本系列第二篇中详细介绍。

ARouter如何在编译时扫描目标帮助类、如何注入代码以及对AGP技术的介绍将在本系列第三篇中详细介绍。

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

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

相关文章

跨境电商测评、采购大额下单自养号需要解决哪些技术原理?

市场上有许多伪装工具&#xff0c;但大多数只是为了方便开发人员测试系统程序&#xff0c;它们并不能针对特定的电商平台进行伪装。每个电商平台都有其独特的风控机制&#xff0c;因此&#xff0c;我们需要从硬件环境的底层配合软件控制&#xff0c;以满足各平台的检测规则。 …

Spring boot 使用AbstractRoutingDataSource实现数据源动态切换

目录 一、AbstractRoutingDataSource 二、具体实现 1、pom.xml 2、新建UserMapper 3、在spring boot 启动类上添加扫描mapper注解 4、在配置文件 application.properties 中添加多个(我这里是两个)数据源的配置信息 5、集成动态数据源模块 5.1、新建注解 CurDataSource…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】梯度的反向传播算法

矩阵微积分&#xff08;Matrix Calculus&#xff09; 在开始之前&#xff0c;需要先了解矩阵微积分的一些计算规则。 首先&#xff0c;对于矩阵微积分的表示&#xff0c;通常由两种符号约定&#xff1a; 分母布局 标量关于向量的导数为列向量 向量关于标量的导数为行向量 N维…

C# Winform Datagridview查询项目实例

在项目中&#xff0c;我们经常要遇到查询和展示内容&#xff0c;常用的做法是通过文本框&#xff0c;时间控件&#xff0c;按键和datagridview查询和展示内容。下面是一个常见的综合实例&#xff0c;并支持Excel(csv)导入导出&#xff0c;表格列动态调整的功能。 实例代码链接&…

【python-AI篇】人工智能技能树思维导图

大致总结一下得出如下思维导图&#xff0c;如不完善日后迭代更新 1. python基础三方库 1.1 科学计算库 ---- numpy库 1.2 科学计算库 ---- Scipy库 1.3 数据分析处理库 ---- pandas库 1.4 可视化库 ---- matplotlib库 1.5 可视化库 ---- seaborn库 1.6 机器学习和数据挖掘库 …

springboot网上书店管理系统-计算机毕业设计源码03780

摘 要 网上书店管理系统采用B/S结构、java开发语言、以及Mysql数据库等技术。系统主要分为管理员和用户两部分&#xff0c;管理员管理主要功能包括&#xff1a;首页、站点管理&#xff08;轮播图&#xff09;用户管理&#xff08;管理员、注册用户&#xff09;内容管理&#x…

OpenCV 4.10 发布

OpenCV 4.10 JPEG 解码速度提升 77%&#xff0c;实验性支持 Wayland、Win ARM64 根据 “OpenCV 中国团队” 介绍&#xff0c;从 4.10 开始 OpenCV 对 JPEG 图像的读取和解码有了 77% 的速度提升&#xff0c;超过了 scikit-image、imageio、pillow。 4.10 版本的一些亮点&…

第N4周:中文文本分类-Pytorch实现

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制&#x1f680; 文章来源&#xff1a;K同学的学习圈子 目录 一、准备工作 1.任务说明 文本分类流程图&#xff1a; 2.加载数据 ​编辑 二、…

第2章 Rust初体验7/8:错误处理时不关心具体错误类型的下划线:提高代码可读性:猜骰子冷热游戏

讲动人的故事,写懂人的代码 2.6.6 用as进行类型转换:显式而简洁的语法 贾克强:“大家在查看Rust代码时,可能会注意到这一句。在这里,如果我们不使用as i32,编译器会报错,因为它在u32中找不到abs()方法。这是因为prev和sum_of_two_dice都是u32类型,u32类型并不支持abs(…

【LLM】吴恩达『微调大模型』课程完全笔记

Finetuning Large Language Models 版权说明&#xff1a; 『Finetuning Large Language Models』是DeepLearning.AI出品的免费课程&#xff0c;版权属于DeepLearning.AI(https://www.deeplearning.ai/)。 本文是对该课程内容的翻译整理&#xff0c;只作为教育用途&#xff0c;不…

4-字符串-11-反转字符串-LeetCode344

4-字符串-11-反转字符串-LeetCode344 LeetCode: 题目序号344 更多内容欢迎关注我&#xff08;持续更新中&#xff0c;欢迎Star✨&#xff09; Github&#xff1a;CodeZeng1998/Java-Developer-Work-Note 技术公众号&#xff1a;CodeZeng1998&#xff08;纯纯技术文&#xff0…

【牛客面试必刷TOP101】Day32.BM68 矩阵的最小路径和和BM69 把数字翻译成字符串

文章目录 前言一、BM68 矩阵的最小路径和题目描述题目解析二、BM69 把数字翻译成字符串题目描述题目解析总结 前言 一、BM68 矩阵的最小路径和 题目描述 描述&#xff1a; 给定两个字符串str1和str2&#xff0c;输出两个字符串的最长公共子序列。如果最长公共子序列为空&#x…

Ubuntu系统环境配置

Ubuntu安装terminator以及美化 安装 sudo apt-get install terminator美化 修改或者创建.config/terminator/config文件&#xff0c;添加如下配置 [global_config]suppress_multiple_term_dialog Truetitle_font Sans 11title_use_system_font False [keybindings] [lay…

supOS助力中核陕西铀浓缩有限公司迈向智能化、数字化、绿色化

中核陕西铀浓缩有限公司是中国核工业集团有限公司所属大型生产骨干企业&#xff0c;建于1969年10月&#xff0c;是我国第一座采用离心法生产浓缩铀的工厂。 蓝卓基于supOS工业操作系统&#xff0c;以“三化一平台一基础”的顶层架构&#xff0c;面向陕铀公司工艺优化、设备管理…

【Photoshop】PS修改文字内容

Photoshop(PS)修改图片上文字内容&#xff0c;网上教材不少&#xff0c;本人整理实践过的方法&#xff0c;分享给各位。本人实践方法&#xff1a; 内容识别填充&#xff1a;适用于背景色复杂的图片内容修补工具&#xff1a;适用于背景色为纯色的图片 方式一&#xff1a;内容识…

el-table表头文字换行或者修改字体颜色样式

例如 <el-table:data"tableData":header-cell-style"headClass" style"width: 100%;" border ><el-table-columnprop"address"label"生产工序"align"center"></el-table-column> //重点看这里…

uniapp 开发版小程序之间跳转

uni.navigateToMiniProgram({appId: urL,path: patH,envVersion: release,//我使用develop会给我返回&#xff1a;开发版小程序已过期&#xff0c;请在开发者工具重新扫码确定success(res) {console.log(res);// 打开成功uni.showToast({title: 跳转成功})},fail(err) {console…

希亦、追觅、云鲸洗地机:究竟有何不同?选择哪款更合适

最近收到很多私信里&#xff0c;要求洗地机测评的呼声特别高&#xff0c;作为宠粉的测评博主&#xff0c;当然是马上安排起来&#xff0c;满足大家对想看洗地机的愿望。这次洗地机测评&#xff0c;我挑选了三款热门的品牌型号&#xff0c;并从多个维度对它们进行使用测评&#…

【STM32】CubeIDE下载安装使用全记录

文章目录 0 前言1 下载安装2 基本使用2.0 编译下载2.1 字体和代码高亮设置2.2 快速格式化代码2.3 快速定位函数/变量的声明和定义2.4 设置代码折叠2.5 生成hex文件 3 设置代码自动提示4 设置中文界面5 遇到的问题和解决办法 0 前言 作为ST官方主推的集成开发环境&#xff08;ID…

深入浅出 Go 语言的 GPM 模型(Go1.21)

引言 在现代软件开发中&#xff0c;有效地利用并发是提高应用性能和响应速度的关键。随着多核处理器的普及&#xff0c;编程语言和框架如何高效、简便地支持并发编程&#xff0c;成为了软件工程师们评估和选择工具时的一个重要考量。在这方面&#xff0c;Go 语言凭借其创新的并…