从Mybatis源码角度,分析一些常见技术点

   日期:2020-11-04     浏览:95    评论:0    
核心提示:文章目录前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结前言提示:这里可以添加本文要记录的大概内容:例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。提示:以下是本篇文章正文内容,下面案例可供参考一、pandas是什么?示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。二、使用步骤1.引入库代码如下(示例):import numpy as npimport

文章目录

  • 懒加载
    • 简介
    • 实现原理
  • 缓存
    • 简介
    • 缓存具体实现
    • 二级缓存
      • 配置二级缓存
      • 二级缓存实现
  • 插件
    • 简介
    • 实现原理
      • 初始化
      • 加载
      • 调用
  • 流式读取
    • 简介
    • 实现原理
  • 标签的id可以重复吗
  • 总结

懒加载

简介

Mybatis在进行关联查询时,可以开启懒加载的功能,懒加载避免了一开始就去加载关联属性,而是在需要时再通过关联表来查询关联属性。
开启懒加载的配置

<settings> 
   <!--开启懒加载-->
   <setting name="lazyLoadingEnabled" value="true"/> 
   <setting name="aggressiveLazyLoading" value="false"/> 
</settings>

在resultMap标签配置映射规则时,关联查询的子标签中可以指定select和column属性,select属性代表延迟加载需要执行的statement的id,如果不在当前mapper文件中,需要加上namespace。column属性代表同select查询关联的字段。

<!-- 配置延迟加载 -->
<association property="position" fetchType="lazy"  column="position_id" select="com.stu.mapper.PositionMapper.selectByPrimaryKey" />

实现原理

Mybatis对查询的结果集进行映射处理过程中,会读取ResultSet中的每一行记录然后调用getRowValue方法进行映射

 private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException { 
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //根据resultMap的type属性,实例化目标对象(并确认关联属性是否开启懒加载,开启则为当前对象创建代理对象)
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { 
      //对目标对象进行封装得到metaObjcect,为后续的赋值操作做好准备
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;//取得是否使用构造函数初始化属性值
      if (shouldApplyAutomaticMappings(resultMap, false)) { //是否使用自动映射
    	 //一般情况下 autoMappingBehavior默认值为PARTIAL,对未明确指定映射规则的字段进行自动映射
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
      }
       //映射resultMap中明确指定需要映射的列
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
      ....
    }
    return rowValue;
  }

映射过程中会先调用createResultObject方法根据ResultMap标签中配置的type属性指定的目标对象的类名,然后先通过反射实例化目标对象。接下来,会遍历ResultMap对象中的所有ResultMapping(ResultMap标签中的每一个子标签,都会封装成ResultMapping)对象,判断关联查询的子标签是否开启了懒加载

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { 
    ...
    //返回实际的结果集对象(通过反射创建Type指定类型的对象)
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { 
      //获得所有的ResultMapping
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) { 
    	//这里是判断association、collection子标签是否开启了懒加载
        // issue gcode #109 && issue #149
    	//嵌套查询id存在,并且开始懒加载
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { 
          //这里创建了延迟加载的代理对象
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

如果开启了懒加载就会通过Javassist或者Cglib为目标对象创建一个代理对象,并指定代理类的处理类EnhancedResultObjectProxyImpl。

static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { 
    ProxyFactory enhancer = new ProxyFactory();
    //要生成代理对象的原生类
    enhancer.setSuperclass(type);
    ...
    try { 
       //创建带参数的代理对象
      enhanced = enhancer.create(typesArray, valuesArray);
    } catch (Exception e) { 
      throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
    }
    ((Proxy) enhanced).setHandler(callback);//指定代理类的处理类
    return enhanced;
  }

当调用目标对象的指定方法时,就会被代理对象拦截,然后执行到EnhancedResultObjectProxyImpl该处理类invoke方法,其中PropertyNamer. isProperty(methodName)这段代码,它会判断调用的方法是不是get,is类型的方法或者lazyLoadTriggerMethods集合中指定的方法,如果是的话,就会触发懒加载。

private static class EnhancedResultObjectProxyImpl implements MethodHandler { 
   ....
   @Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable { 
      final String methodName = method.getName();
      try { 
        synchronized (lazyLoader) { 
          if (WRITE_REPLACE_METHOD.equals(methodName)) { 
            .....
          } else { 
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { 
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { 
            	//全部加载
                lazyLoader.loadAll();
              //判断是否为set方法,set方法不需要延迟加载
              } else if (PropertyNamer.isSetter(methodName)) {  
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
              } else if (PropertyNamer.isGetter(methodName)) { 
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) { 
                  //延迟加载单个属性
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) { 
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
}

懒加载过程会根据resultLoader记录的嵌套查询的信息,调用具体的方法查询,最后将查询结果反射set到目标对象中,完成懒加载过程。

public void load(final Object userObject) throws SQLException { 
      .....
      //关键点就在这,查询出关联对象,然后通过metaObject给目标对象的关联属性赋值
      this.metaResultObject.setValue(property, this.resultLoader.loadResult());
    }
public Object loadResult() throws SQLException { 
	//这里就会查询出关联对象
    List<Object> list = selectList();
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
}

其中ResultLoader的生成是在进行嵌套查询映射时生成的。

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException { 
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    if (nestedQueryParameterObject != null) { 
      .....
      if (executor.isCached(nestedQuery, key)) { 
        executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
        value = DEFERED;
      } else { 
    	//重点,ResultLoader 就在这里构造,记录嵌套查询的信息
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
        //懒加载的处理
        if (propertyMapping.isLazy()) { 
          //property:嵌套查询配置的type对象 metaResultObject:目标代理对象 resultLoader: 嵌套查询信息
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERED; //标识为懒加载
        } else { 
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }

缓存

简介

Mybatis的缓存分为一级缓存和二级缓存,一级缓存存在于SqlSession的生命周期中,默认会启用。二级缓存也叫应用缓存,存在于SqlSessionFactory的生命周期中,可以理解为跨SqlSession的,缓存是以 namespace为单位的,默认未启用。
对一级缓存来说同一个SqlSession在查询时, Mybatis会把执行的方法和参数等信息通过算法生成缓存的键值,将键值和查询结果存入一个 Map 对象中。大多数情况下同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中己经存在该键值时,则查询时会返回缓存中的对象。任何的 INSERT 、UPDATe 、DELETE 操作都会清空一级缓存;

缓存具体实现

Mybatis缓存的底层是基于一个HashMap进行存储的,具体的实现为:

//缓存的具体实现类
public class PerpetualCache implements Cache { 

  private final String id; //Mapper中namespace的值

  //这个的key: cachekey value : sql语句处理过程
  //cachekey: sql语句、入参、分页信息、MappedStatement的id
  private Map<Object, Object> cache = new HashMap<>();
  ...
}

而Mybatis中缓存的key生成是非常严谨的,在查询时会调用一个createCacheKey方法创建一个CacheKey对象,CacheKey的生成主要由四大约束条件:
1、MappedStatement的id
2、Mybatis内置分页对象的参数信息
3、sql语句
4、sql语句对应的实际参数值

//创建CacheKey对象,做为缓存Map中访问的key
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { 
    if (closed) { 
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId()); //MappedStatement的id加入计算
    cacheKey.update(rowBounds.getOffset());  //分页信息
    cacheKey.update(rowBounds.getLimit());   //分页信息
    cacheKey.update(boundSql.getSql()); // 将sql语句加入计算
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); //sql语句的参数映射集
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); 
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) { 
      if (parameterMapping.getMode() != ParameterMode.OUT) { 
        Object value;
        String propertyName = parameterMapping.getProperty(); // 获取参数的属性
        // 以下获取参数的值
        if (boundSql.hasAdditionalParameter(propertyName)) { 
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) { 
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 
          value = parameterObject;
        } else { 
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value); // 将参数值加入计算
      }
    }
    if (configuration.getEnvironment() != null) { 
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId()); // 存在Environment,则将Environment的id也加入计算
    }
    return cacheKey;
  }

二级缓存

配置二级缓存

在 Mybatis 的核心配置文件中cacheEnabled参数是二级缓存的全局开关,默认值是 true,如果把这个参数设置为false,即使有后面的二级缓存配置,也不会生效。
要开启二级缓存,只需要在某一个Mapper文件中添加配置,如下:

<cache eviction=LRU" flushInterval="60000" size="512" readOnly="true"/>

注意: 二级缓存是以namespace为单位的,属于SqlSession共享的,容易出现脏读现象,应该避免去使用二级缓存。

二级缓存实现

在Mapper文件中开启二级缓存时,是可以动态的配置一些属性,比如:淘汰策略、定时刷新、同步、日志、序列化功能等能力。Mybatis针对这种场景使用了装饰器模式进行了二级缓存功能的动态增强,二级缓存实现类图如下:

底层在初始化二级缓存时,是通过XMLMapperBuilder在解析每一个mapper文件时,会解析每一个cache标签,然后为当前mapper文件生成一个PerpetualCache缓存具体实现类,之后会根据cache标签的配置的属性以及一些默认的属性,创建对应的缓存装饰器对象(比如SynchronizedCache装饰器对象,默认为二级缓存添加同步功能),对PerpetualCache进行具体的装饰。

public Cache build() { 
	  //设置缓存的主实现类为PerpetualCache
    setDefaultImplementations();
    //通过反射实例化PerpetualCache对象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);//根据cache节点下的<property>信息,初始化cache
    // issue #352, do not apply decorators to custom caches
    
    if (PerpetualCache.class.equals(cache.getClass())) { //如果cache是PerpetualCache的实现,则为其添加标准的装饰器
      for (Class<? extends Cache> decorator : decorators) { //为cache对象添加装饰器,这里主要处理缓存清空策略的装饰器
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      //通过一些属性为cache对象添加装饰器
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { 
      //如果cache不是PerpetualCache的实现,则为其添加日志的能力
      cache = new LoggingCache(cache);
    }
    return cache;
  }

插件

简介

插件是用来改变或者扩展Mybatis的原有的功能,Mybatis的插件就是通过继承Interceptor拦截器来实现的。在没有完全理解插件之前禁止使用插件对Mybatis进行扩展,有可能会导致严重的问题;
Mybatis中能使用插件进行拦截的接口和方法如下:

  1. Executor(update、query 、 flushStatment 、 commit 、 rollback 、 getTransaction 、 close 、 isClose)
  2. StatementHandler(prepare 、 paramterize 、 batch 、 update 、 query)
  3. ParameterHandler( getParameterObject 、 setParameters )
  4. ResultSetHandler( handleResultSets 、 handleCursorResultSets 、 handleOutputParameters )

实现原理

初始化

在Mybatis的配置文件中引入一个插件

<plugins>
  	 <plugin interceptor="com.github.pagehelper.PageInterceptor">
		<property name="pageSizeZero" value="true" />
     </plugin>
</plugins>

之后,配置文件在解析时会通过XMLConfigBuilder这个类,解析所有的plugin标签,然后根据指定的插件类名,将对应的插件进行反射实例化,最后添加到Configuration配置类中的InterceptorChain对象中,其内部通过list记录所有的插件。

private void pluginElement(XNode parent) throws Exception { 
    if (parent != null) { 
      //遍历所有的插件配置
      for (XNode child : parent.getChildren()) { 
    	//获取插件的类名
        String interceptor = child.getStringAttribute("interceptor");
        //获取插件的配置
        Properties properties = child.getChildrenAsProperties();
        //实例化插件对象
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //设置插件属性
        interceptorInstance.setProperties(properties);
        //将插件添加到configuration对象,底层使用list保存所有的插件并记录顺序
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

加载

当创建Executor、StatementHandler、ParameterHandler、ResultSetHandler这些接口实现类时,就会尝试添加插件功能。在Mybatis的Configuration配置类中提供了new这些对象的方法,其中创建Executor实现如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) { 
    ....
    //通过interceptorChain遍历所有的插件为executor增强,添加插件的功能
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

遍历初始化时收集的所有插件,为目标对象添加插件功能

public class InterceptorChain { 

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) { 
    for (Interceptor interceptor : interceptors) { 
      //为目标对象添加插件功能
      target = interceptor.plugin(target);
    }
    return target;
  }
  ...
}

通常情况下插件的plugin方法都会执行到Plugin这个代理处理类的wrap方法,通过这个wrap方法会解析当前插件上的@Intercepts注解内部的@Signature注解信息,然后根据每个Signature拦截的类型来确认是否能够拦截到当前目标对象,如果能就会基于JDK动态代理为当前目标对象创建一个代理对象。

//静态方法,用于帮助Interceptor生成动态代理
  public static Object wrap(Object target, Interceptor interceptor) { 
	//解析Interceptor上@Intercepts注解得到的signature信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();//获取目标对象的类型
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);//获取目标对象实现的接口(拦截器可以拦截4大对象实现的接口)
    if (interfaces.length > 0) { 
      //使用jdk的方式创建动态代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

调用

加载过程我们知道如果目标对象能够被任意一个插件拦截就会为其生成一个代理对象,之后当目标对象执行相应方法时,就会被代理对象拦截,然后执行到Plugin这个类的invoke方法,invoke执行过程中会先判断当前调用的方法是否被拦截,如果被拦截就会执行插件的intercept方法,调用具体插件逻辑。

public class Plugin implements InvocationHandler { 
  //封装的真正提供服务的对象
  private final Object target;
  //插件拦截器
  private final Interceptor interceptor;
  //解析@Intercepts注解得到的signature信息
  private final Map<Class<?>, Set<Method>> signatureMap;
  ....
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
    try { 
      //获取当前接口可以被拦截的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) { //如果当前方法需要被拦截,则调用interceptor.intercept方法进行拦截处理
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //如果当前方法不需要被拦截,则调用对象自身的方法
      return method.invoke(target, args);
    } catch (Exception e) { 
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

流式读取

简介

Mybatis针对大数量读取提供了一种流式读取的方式,避免查询数据过多,导致OOM。具体实现:
首先,自定义ResultHandler实现类来处理结果集

public class MyResultHandler<T> implements ResultHandler<T>{ 

	//如果需要批量处理,可以定义一个容器进行保存每条数据
	//当超过BATCH_SIZE再对容器中数据进行处理
	private final int BATCH_SIZE= 100;
	private List<T> list=new ArrayList<T>(); 
	
	@Override
	public void handleResult(ResultContext resultContext) { 
		// TODO 流式读取,每次只返回单条结果
		Object result=resultContext.getResultObject();
		// TODO 对获取到的结果数据进行相应的业务处理
	}

}

其次,在指定mapper接口中定义一个以ResultHandler作为入参的查询方法,并且查询方法不接收返回值


void seachUserDataList(ResultHandler handler);

最后,在调用seachUserDataList()查询方法时,会将查询到的每一条记录都调用一次MyResultHandler的handleResult()方法,这就是流式读取的效果,避免一次性在内存中加载过大的对象。

实现原理

DefaultResultSetHandler在进行查询的结果集处理时,会判断当前查询方法是否有指定ResultHandler做为入参。如果有,就会使用指定的resultHandler进行后续处理,处理过程中会通过handleRowValuesForSimpleResultMap()对查询的结果集的每条数据进行处理,将ResultSet结果集的每一行数据映射成目标对象,再调用storeObject方法保存映射目标对象,之后就会执行callResultHandler()方法,将目标对象添加到resultContext中,最后根据指定的resultHandler调用它的handleResult()方法,达到流式读取的效果。

//处理结果集
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { 
    try { 
      if (parentMapping != null) { //处理多结果集的嵌套映射
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else { 
        if (resultHandler == null) { //如果resultHandler为空,实例化一个人默认的resultHandler
         ....
        } else { 
          //使用指定的resultHandler进行处理
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally { 
      // issue #228 (close resultsets)
      //调用resultset.close()关闭结果集
      closeResultSet(rsw.getResultSet());
    }
  }
//简单映射处理
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException { 
	//创建结果上下文,所谓的上下文就是专门在循环中缓存结果对象的
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    skipRows(rsw.getResultSet(), rowBounds);
    //shouldProcessMoreRows判断是否需要映射后续的结果,实际还是翻页处理,避免超过limit
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { 
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
      //读取resultSet中的一行记录并进行映射,转化并返回目标对象
      Object rowValue = getRowValue(rsw, discriminatedResultMap);
      //保存映射结果对象
      storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
    }
}
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException { 
    if (parentMapping != null) { 
      linkToParents(rs, parentMapping, rowValue);
    } else { //普通映射则把对象保存至resultHandler和resultContext
      callResultHandler(resultHandler, resultContext, rowValue);
    }
  }
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) { 
    resultContext.nextResultObject(rowValue);
    //流式读取
    ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
  }

标签的id可以重复吗

Mybatis中约束了同一种类型的标签(ResultMap、增删改查)不能存在相同的namespace+id。原因是Mybatis的配置类Configuration中,会通过Map去记录每个标签封装的对象,其中namespace+id 是作为Map的key使用的,而Map是使用的自定义StrictMap继承与HashMap,在进行put()插入标签元素时,会判断namespace+id是否重复,重复就会抛出异常。

protected static class StrictMap<V> extends HashMap<String, V> { 
    ...
    public V put(String key, V value) { 
      //插入之前判断namespace+id,是否已经存在,已经存在抛出异常
      if (containsKey(key)) { 
        throw new IllegalArgumentException(name + " already contains value for " + key);
      }
      ....
      return super.put(key, value);
    }
}

总结

以上介绍了一些Mybatis内部的一些技术点,也结合源码去简单分析了技术点底层实现,在代码截图中对一些非关键的代码进行了删除,避免关注到其它知识点,个人认为在源码学习的过程中,每一个点的学习,要去抓住关键点,去排除掉一些无用不相关的代码,这样整体的结构就会更清晰点。

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服