• ARouter路由解析


    目录介绍

    • 01.原生跳转实现
    • 02.实现组件跳转方式
      • 2.1 传统跳转方式
      • 2.2 为何需要路由
    • 03.ARouter配置与优势
    • 04.跨进程组件通信
      • 4.1 URLScheme
      • 4.2 AIDL
      • 4.3 BroadcastReceiver
      • 4.4 路由通信注意要点
    • 05.ARouter的结构
    • 06.ARouter的工作流程
      • 6.1 初始化流程
      • 6.2 跳转页面流程
    • 07.ARouter简单调用api
      • 7.1 最简单调用
      • 7.2 build源码分析
      • 7.3 navigation分析
    • 08.Postcard信息携带
    • 09.LogisticsCenter
    • 10.DegradeService降级容错服务
    • 11.Interceptor拦截器
    • 12.数据传输和自动注入
    • 13.多dex的支持
    • 14.InstantRun支持
    • 15.生成的编译代码

    好消息

    • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
    • 链接地址:https://github.com/yangchong211/YCBlogs
    • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议或者问题,万事起于忽微,量变引起质变!

    注解学习小案例

    • 注解学习小案例,比较系统性学习注解并且应用实践。简单应用了运行期注解,通过注解实现了setContentView功能;简单应用了编译器注解,通过注解实现了防暴力点击的功能,同时支持设置时间间隔;使用注解替代枚举;使用注解一步步搭建简单路由案例。结合相应的博客,在来一些小案例,从此应该对注解有更加深入的理解……
    • 开源项目地址:https://github.com/yangchong211/YCApt

    01.原生跳转实现

    • Google提供的原声路由主要是通过intent,可以分成显示和隐式两种。显示的方案会导致类之间的直接依赖问题,耦合严重;隐式intent需要的配置清单中统一声明,首先有个暴露的问题,另外在多模块开发中协作也比较困难。只要调用startActivity后面的环节我们就无法控制了,在出现错误时无能为力。

    02.实现组件跳转方式

    2.1 传统跳转方式

    • 第一种,通过intent跳转
    • 第二种,通过aidl跳转
    • 第三种,通过scheme协议跳转

    2.2 为何需要路由

    • 显示Intent:项目庞大以后,类依赖耦合太大,不适合组件化拆分
    • 隐式Intent:协作困难,调用时候不知道调什么参数
    • 每个注册了Scheme的Activity都可以直接打开,有安全风险
    • AndroidMainfest集中式管理比较臃肿
    • 无法动态修改路由,如果页面出错,无法动态降级
    • 无法动态拦截跳转,譬如未登录的情况下,打开登录页面,登录成功后接着打开刚才想打开的页面
    • H5、Android、iOS地址不一样,不利于统一跳转

    03.ARouter配置与优势

    3.1 ARouter的优势

    • 如下所示
      • 直接解析URL路由,解析参数并赋值
      • 支持多模块项目
      • 支持InstantRun
      • 允许自定义拦截器
      • ARouter可以提供IoC容器
      • 映射关系自动注册
      • 灵活的降级策略

    3.2 至于配置和使用

    • 直接看https://www.jianshu.com/p/fed5d5b95bae

    04.跨进程组件通信

    4.1 URLScheme【例如:ActivityRouter、ARouter等】

    • 优势有:
      • 基因中自带支持从webview中调用
      • 不用互相注册(不用知道需要调用的app的进程名称等信息)
    • 劣势有:
      • 只能单向地给组件发送信息,适用于启动Activity和发送指令,不适用于获取数据(例如:获取用户组件的当前用户登录信息)
      • 需要有个额外的中转Activity来统一处理URLScheme
      • 如果设备上安装了多个使用相同URLScheme的app,会弹出选择框(多个组件作为app同时安装到设备上时会出现这个问题)
      • 无法进行权限设置,无法进行开关设置,存在安全性风险

    4.2 AIDL

    • 优势有:
      • 可以传递Parcelable类型的对象
      • 效率高
      • 可以设置跨app调用的开关
    • 劣势有:
      • 调用组件之前需要提前知道该组件在那个进程,否则无法建立ServiceConnection
      • 组件在作为独立app和作为lib打包到主app时,进程名称不同,维护成本高

    4.3 BroadcastReceiver

    • BroadcastReceiver + Service + LocalSocket。该方案是参考cc路由框架!
    • 跨组件间通信实现的同时,应该满足以下条件:
      • 每个app都能给其它app调用
      • app可以设置是否对外提供跨进程组件调用的支持
      • 组件调用的请求发出去之后,能自动探测当前设备上是否有支持此次调用的app
      • 支持超时、取消

    4.4 路由通信注意要点

    05.ARouter的结构

    • ARouter主要由三部分组成,包括对外提供的api调用模块、注解模块以及编译时通过注解生产相关的类模块。
      • arouter-annotation注解的声明和信息存储类的模块
      • arouter-compiler编译期解析注解信息并生成相应类以便进行注入的模块
      • arouter-api核心调用Api功能的模块
    • annotation模块
      • Route、Interceptor、Autowired都是在开发是需要的注解。
      • image
    • compiler模块
      • AutoWiredProcessor、InterceptorProcessor、RouteProcessor分别为annotation模块对应的Autowired、Interceptor、Route在项目编译时产生相关的类文件。
    • api模块
      • 主要是ARouter具体实现和对外暴露使用的api。

    06.ARouter的工作流程

    6.1 初始化流程

    • 初始化代码如下所示
      /**
       * Init, it must be call before used router.
       */
      public static void init(Application application) {
          //如果没有初始化,则
          if (!hasInit) {
              logger = _ARouter.logger;
              _ARouter.logger.info(Consts.TAG, "ARouter init start.");
              
              //做初始化工作
              hasInit = _ARouter.init(application);
      
              if (hasInit) {
                  _ARouter.afterInit();
              }
      
              _ARouter.logger.info(Consts.TAG, "ARouter init over.");
          }
      }
      
    • 之后接着看_ARouter.init(application)这行代码,点击去查看
      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里面做了什么
      public class LogisticsCenter {
          /**
           * 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();
                  Set<String> routerMap;
                  //debug或者版本更新的时候每次都重新加载router信息
                  // It will rebuild router map every times when debuggable.
                  if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                      logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                      // These class was generate by arouter-compiler.
                      //加载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 finish.
                  } else {
                      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();
      
                  for (String className : routerMap) {
                      if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                          // This one of root elements, load root. 
                          //导入ARouter$$Root$$app.java,初始化Warehouse.groupsIndex集合
                          ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                      } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                          // Load interceptorMeta
                          //导入ARouter$$Interceptors$$app.java,初始化Warehouse.interceptorsIndex集合
                          ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                      } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                          // Load providerIndex
                          //导入ARouter$$Providers$$app.java,初始化Warehouse.providersIndex集合
                          ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                      }
                  }
      
                  /*******部分代码省略********/
              } catch (Exception e) {
                  throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
              }
          }
      }
      
    • 综上所述,整个初始化的流程大概就是:
      • 初始化运行时的上下文环境
      • 初始化日志logger
      • 寻找router相关的类
      • 解析并且缓存路由相关信息
      • 初始化拦截服务

    6.2 跳转页面流程

    • image

    07.ARouter调用api

    7.1 最简单调用

    • 最简单的调用方式
      ARouter.getInstance()
                      .build("/user/UserFragment")
                      .navigation();
      

    7.2 build源码分析

    • 这个主要是添加跳转的路径
      public Postcard build(String path) {
          return _ARouter.getInstance().build(path);
      }
      
    • 然后把这个路径添加到默认的组中
      /**
       * 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 pService = ARouter.getInstance().navigation(PathReplaceService.class);
              if (null != pService) {
                  path = pService.forString(path);
              }
              return build(path, extractGroup(path));
          }
      }
      

    7.3 navigation分析

    • 如下所示
      final class _ARouter {
          protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
              try {
                  LogisticsCenter.completion(postcard);
              } catch (NoRouteFoundException ex) {
                  /**************部分代码省略***************/
                  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);
              }
              //是否为绿色通道,是否进过拦截器处理
              if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
                  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) {
                              callback.onInterrupt(postcard);
                          }
                      }
                  });
              } else {
                  return _navigation(context, postcard, requestCode, callback);
              }
      
              return null;
          }
      
          private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
              //没有上下文环境,就用Application的上下文环境
              final Context currentContext = null == context ? mContext : context;
              switch (postcard.getType()) {
                  case ACTIVITY:
                      // Build intent 构建跳转的intent
                      final Intent intent = new Intent(currentContext, postcard.getDestination());
                      intent.putExtras(postcard.getExtras());
                      // Set flags. 设置flag
                      int flags = postcard.getFlags();
                      if (-1 != flags) {
                          intent.setFlags(flags);
                      } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                          //如果上下文不是Activity,则添加FLAG_ACTIVITY_NEW_TASK的flag
                          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                      }
                      // Navigation in main looper.  切换到主线程中
                      new Handler(Looper.getMainLooper()).post(new Runnable() {
                          @Override
                          public void run() {
                              if (requestCode > 0) {  // Need start for result
                                  ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                              } else {
                                  ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                              }
      
                              if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                                  ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                              }
      
                              if (null != callback) { // Navigation over.
                                  callback.onArrival(postcard);
                              }
                          }
                      });
      
                      break;
                  case PROVIDER:
                      return postcard.getProvider();
                  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;
          }
      }
      

    08.Postcard信息携带

    • Postcard主要为信息的携带者,内容是在构造一次路由信息的时候生产的,其继承于RouteMeta。RouteMeta是在代码编译时生成的内容,主要在初始化WareHouse时对跳转信息做了缓存。
    • 看看代码如下所示
      //Postcard继承于RouteMeta
      public final class Postcard extends RouteMeta
      
      
      //然后看看编译生成的文件
      /**
       * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
      public class ARouter$$Group$$me implements IRouteGroup {
        @Override
        public void loadInto(Map<String, RouteMeta> atlas) {
          atlas.put("/me/ExperienceCouponActivity", RouteMeta.build(RouteType.ACTIVITY, ExperienceCouponActivity.class, "/me/experiencecouponactivity", "me", null, -1, -2147483648));
          atlas.put("/me/ServiceActivity", RouteMeta.build(RouteType.ACTIVITY, ServiceActivity.class, "/me/serviceactivity", "me", null, -1, -2147483648));
          atlas.put("/me/SettingActivity", RouteMeta.build(RouteType.ACTIVITY, SettingActivity.class, "/me/settingactivity", "me", null, -1, -2147483648));
          atlas.put("/me/UdeskServiceActivity", RouteMeta.build(RouteType.ACTIVITY, UdeskServiceActivity.class, "/me/udeskserviceactivity", "me", null, -1, -2147483648));
        }
      }
      

    10.DegradeService降级容错服务

    • 首先,自定义一个类,需要继承DegradeService类,如下所示
      /**
       * <pre>
       *     @author 杨充
       *     blog  : https://github.com/yangchong211
       *     time  : 2018/08/24
       *     desc  : ARouter路由降级处理
       *     revise:
       * </pre>
       */
      @Route(path = DegradeServiceImpl.PATH)
      public class DegradeServiceImpl implements DegradeService {
      
          static final String PATH = "/service/DegradeServiceImpl";
      
          @Override
          public void onLost(Context context, Postcard postcard) {
              if (context != null && postcard.getGroup().equals("activity")) {
                  Intent intent = new Intent(context, WebViewActivity.class);
                  intent.putExtra(Constant.URL, Constant.GITHUB);
                  intent.putExtra(Constant.TITLE, "github地址");
                  ActivityCompat.startActivity(context, intent, null);
              }
          }
      
          @Override
          public void init(Context context) {
      
          }
      }
      
    • 如何使用该降级方案,十分简单。
      NavigationCallback callback = new NavCallback() {
          @Override
          public void onArrival(Postcard postcard) {
              LogUtils.i("ARouterUtils"+"---跳转完了");
          }
      
          @Override
          public void onFound(Postcard postcard) {
              super.onFound(postcard);
              LogUtils.i("ARouterUtils"+"---找到了");
          }
      
          @Override
          public void onInterrupt(Postcard postcard) {
              super.onInterrupt(postcard);
              LogUtils.i("ARouterUtils"+"---被拦截了");
          }
      
          @Override
          public void onLost(Postcard postcard) {
              super.onLost(postcard);
              LogUtils.i("ARouterUtils"+"---找不到了");
              DegradeServiceImpl degradeService = new DegradeServiceImpl();
              degradeService.onLost(Utils.getApp(),postcard);
          }
      };
      

    11.Interceptor拦截器

    • 在ARouter模块的时候讲述Interceptor的使用,如果本次路由跳转不是走的绿色通道那么则会触发拦截器进行过滤。
      final class _ARouter {
          protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
              /************部分代码省略************/
      
              if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
                  interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                      /**
                       * Continue process
                       *
                       * @param postcard route meta
                       */
                      @Override
                      public void onContinue(Postcard postcard) {
                          _navigation(context, postcard, requestCode, callback);
                      }
      
                      /**
                       * Interrupt process, pipeline will be destory when this method called.
                       *
                       * @param exception Reson of interrupt.
                       */
                      @Override
                      public void onInterrupt(Throwable exception) {
                          if (null != callback) {
                              callback.onInterrupt(postcard);
                          }
      
                          logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                      }
                  });
              } else {
                  return _navigation(context, postcard, requestCode, callback);
              }
              return null;
          }
      }
      
    • 拦截器的初始化
      • 在刚开始初始化的时候,就已经做了这个操作。
      final class _ARouter {
          static void afterInit() {
              // Trigger interceptor init, use byName.
              interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
          }
      }
      
    • InterceptorServiceImpl的init方法:
      @Route(path = "/arouter/service/interceptor")
      public class InterceptorServiceImpl implements InterceptorService {
          @Override
          public void init(final Context context) {
              LogisticsCenter.executor.execute(new Runnable() {
                  @Override
                  public void run() {
                      if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                          //循环遍历仓库中的拦截器
                          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.add(iInterceptor);
                              } catch (Exception ex) {
                                  throw new HandlerException(TAG + "ARouter init interceptor error! name = [" + interceptorClass.getName() + "], reason = [" + ex.getMessage() + "]");
                              }
                          }
                          interceptorHasInit = true;
                          logger.info(TAG, "ARouter interceptors init over.");
                          synchronized (interceptorInitLock) {
                              interceptorInitLock.notifyAll();
                          }
                      }
                  }
              });
          }
      }
      
    • 拦截器的工作过程
      @Route(path = "/arouter/service/interceptor")
      public class InterceptorServiceImpl implements InterceptorService {
          @Override
          public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
              if (null != Warehouse.interceptors && Warehouse.interceptors.size() > 0) {
                  //检测是否初始化完所有的烂机器
                  checkInterceptorsInitStatus();
                  //没有完成正常的初始化,抛异常
                  if (!interceptorHasInit) {
                      callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));
                      return;
                  }
                  //顺序遍历每一个拦截器,
                  LogisticsCenter.executor.execute(new Runnable() {
                      @Override
                      public void run() {
                          CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
                          try {
                              _excute(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(new HandlerException(postcard.getTag().toString()));
                              } else {
                                  callback.onContinue(postcard);
                              }
                          } catch (Exception e) {
                              callback.onInterrupt(e);
                          }
                      }
                  });
              } else {
                  callback.onContinue(postcard);
              }
          }
      
          //执行拦截器的过滤事件
          private static void _excute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
              if (index < Warehouse.interceptors.size()) {
                  IInterceptor iInterceptor = Warehouse.interceptors.get(index);
                  iInterceptor.process(postcard, new InterceptorCallback() {
                      @Override
                      public void onContinue(Postcard postcard) {
                          // Last interceptor excute over with no exception.
                          counter.countDown();
                          //如果当前没有拦截过滤,那么使用下一个拦截器
                          _excute(index + 1, counter, postcard);  // When counter is down, it will be execute continue ,but index bigger than interceptors size, then U know.
                      }
      
                      @Override
                      public void onInterrupt(Throwable exception) {
                          // Last interceptor excute over with fatal exception.
                          postcard.setTag(null == exception ? new HandlerException("No message.") : exception.getMessage());    // save the exception message for backup.
                          counter.cancel();
                      }
                  });
              }
          }
      }
      

    12.数据传输和自动注入

    13.多dex的支持

    • 可查看multidex源码:
      public class ClassUtils {  
        /**
           * Identifies if the current VM has a native support for multidex, meaning there is no need for
           * additional installation by this library.
           *
           * @return true if the VM handles multidex
           */
          private static boolean isVMMultidexCapable() {
              boolean isMultidexCapable = false;
              String vmName = null;
      
              try {
                  if (isYunOS()) {    // YunOS需要特殊判断
                      vmName = "'YunOS'";
                      isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
                  } else {    // 非YunOS原生Android
                      vmName = "'Android'";
                      String versionString = System.getProperty("java.vm.version");
                      if (versionString != null) {
                          Matcher matcher = Pattern.compile("(\d+)\.(\d+)(\.\d+)?").matcher(versionString);
                          if (matcher.matches()) {
                              try {
                                  int major = Integer.parseInt(matcher.group(1));
                                  int minor = Integer.parseInt(matcher.group(2));
                                  isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                                          || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                                          && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                              } catch (NumberFormatException ignore) {
                                  // let isMultidexCapable be false
                              }
                          }
                      }
                  }
              } catch (Exception ignore) {
      
              }
      
              Log.i(Consts.TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
              return isMultidexCapable;
          }
      }
      

    14.InstantRun支持

    • 什么是InstantRun支持?
      • Android Studio 2.0 中引入的 Instant Run 是 Run 和 Debug 命令的行为,可以大幅缩短应用更新的时间。尽管首次构建可能需要花费较长的时间,Instant Run 在向应用推送后续更新时则无需构建新的 APK,因此,这样可以更快地看到更改。

    15.生成的编译代码

    • 如下所示
      • image

    关于其他内容介绍

    01.关于博客汇总链接

    02.关于我的博客

  • 相关阅读:
    httprunner学习-参数化与数据驱动
    httprunner学习4-variables变量声明与引用
    httprunner学习3-extract提取token值参数关联(上个接口返回的token,传给下个接口请求参数)
    HttpRunner基础使用一:环境安装
    yml运行时报错SSL: CERTIFICATE_VERIFY_FAILED 解决verify设置False.
    Selenium+Python日期控件小案例
    使用Navicat Keygen激活(破解)Navicat Premium 12
    robotframework(rf)中对时间操作的datetime库常用关键字
    【Selenium】不同chrome版本对应的chrome驱动版本
    1、jmeter工具&soapui工具做接口测试
  • 原文地址:https://www.cnblogs.com/yc211/p/10408181.html
Copyright © 2020-2023  润新知