进击吧程序猿 发表于 2020-12-18 20:01:15

如何阅读源代码(建议收藏)

前言

读源码是大多数程序员进阶的紧张途径,最近相识到很多朋友反馈读了一些源码但是收获不是很大,看了一些源码总是功败垂成,有很多困惑。
主要表现为:

[*]读源码的时间并不知道该读啥
[*]很容易迷失在细节中,调试时跳来跳去跳晕了,很难坚持下去
[*]读完很快就忘掉了,无法机动运用
网上也会有一些讲某个具体开源代码的系列文章,通常比力冗长,教授的都是“鱼”而不是“渔”。 俗话说:授人以鱼不如授人以渔。
我相信大多数同学希望得到方法论级别、更加系统化介绍怎样更好地阅读源码的文章。 为此,在这里我打算将自己的读源码经验教授给各人,相信会让很多人理解问题的症结地点,给出一些“料想之外”的实用建议,让饱受读源码困惑的同学能够找到方向。
文章重点讲到如下内容:

[*]为什么很多人读源码收获不大?
[*]读源码究竟读什么?
[*]有哪些读源码紧张的思想?
[*]有哪些好的读源码切入点?
[*]有哪些读源码非常实用的技巧?
团体概览:
https://p26.toutiaoimg.com/large/pgc-image/7df4601f460f44acbfd8098c17e94854
为什么很多人读源码收获不大?

在我看来,大多数人读源码收获不大的主要原因如下:

[*]缺乏团体头脑,迷失在细节中(如调试源码时跳来跳去,最后跳晕了)
[*]缺乏思考(学而不思则罔,思而不学则殆!)
[*]不知道读源码究竟读什么(如源码的设计思想)
[*]角度单一(如从解决问题角度、性能优化角度、设计模式角度、每次提交、单元测试、解释等)
[*]方法单一(如不懂的高级的调试技巧,不懂的时序图插件)
[*]缺乏输出(不会输出成文章,不能讲给别人听)
读源码究竟读什么?

做事要“以终为始”,只有搞清晰读源码我们究竟想得到什么,我们才气制止“走马观花” 终极将收获无多的尴尬场景。
那么读源码读的是什么?我们要关注哪些方面呢?
读目标:该框架是为相识决什么问题?比同类框架相比的优劣是什么?这对理解框架非常紧张。
读解释:很多人读源码会忽略解释。建议各人读源码时一定要重视解释。由于优秀的开源项目,通常某个类、某个函数的目标、核心逻辑、核心参数的解释,非常的发生场景等都会写到解释中,这对我们学习源码,分析问题有极大的资助。
读逻辑:这里所谓的逻辑是指语句或者子函数的顺序问题。我们要重视作者编码的顺序,相识为什么先写 A 再写 B,背后的原因是什么。
读思想:所谓思想是指源码背后表现出了哪些设计原则,好比是不是和设计模式的六大原则相符?是不是符合高内聚低耦合?是不是表现某种性能优化思想?
读原理:读核心实现步骤,而不是记忆每行代码。核心原理和步骤最紧张。
读编码风格:一般来说优秀的源码的代码风格都比力优雅。我们可以通过源码来学习编码规范。
读编程技巧:作者是否采用了某种设计模式,某种编程技巧实现了料想之外的结果。
读设计方案:读源码不仅包罗具体的代码,更紧张的是设计方案。好比我们下载一个秒杀系统 / 商城系统的代码,我们可以学习暗码加密的方案,学习分布式事务处理的方案,学习幂等的设计方案,超卖问题的解决方案等。由于掌握这些方案之后对提升我们自己的工作经验非常有资助,我们工作中做技能方案时可以参考这些优秀项目标方案。
读源码的误区

很多人读源码不顺利,结果不好,通常都会有些共性。
那么读源码通常会有哪些误区呢?
开局打 Boss

经常打游戏的朋友都知道,开局直接打 Boss 无异于送人头。
一般开局先打野,练就了经验再去挑衅 Boss。
如果开始尝试学习源码就直接拿大型开源框架入手容易自信心受挫,导致放弃。
佛系青年

经常打游戏的朋友也都知道,打游戏要讲求策略,随便瞎打很容易失败。
有些朋友决定读源码,但又缺乏规划,随心所欲,往往结果不太好。
对着答案做题

我们知道很多小学生、初高中生,甚至很多大学生学习会出现眼高手低的情况。
有些人做题时并不是先思考,而是先看答案,然后对着答案的思路来理解标题。在这种模式下,大多数标题都理所当然地这么做,会误认为自己真正懂了。但是即使是原题,也会做错,想不出思路。
同样地,很多人读源码也会走到这个误区中。直接看源码的解析,直接看源码的写法,缺乏关键的前置步骤,即先自己思考再对照源码。
读源码的思想

先会用再读源码

学习某个源码之前一定要对源码的根本用法有一个初步相识。
如果对框架没有根本的相识就直接读源码,结果通常不会太好。
一般优秀的开源项目,都会给出一些简单的官方示例代码,各人可以将官方示例代码跑起来,相识根本用法。
各人也可以去 GitHub 上搜索并拉取某个技能的 Demo,某个技能的 hello world 项目,快速用起来。
如 Dubbo 官方文档就给出了快速上手示例代码 ;轻量级的分布式服务框架 jupiter README.md 就给出了简单的调用示例。一些开源项目给出了多个框架的示例代码,如 tutorials。
先易后难

循规蹈矩是学习的一大规律。
一方面,可以先尝试阅读较为简单的开源项目源码,好比 commons-lang、commons-collection、guava、mapstruct 等工具性质的源码。
别的还可以尝试寻找某个框架的简单版,先从简单版学起,看透了再学大型的开源项目就容易很多。
可能很多人会说不好找,实在大多数知名开源的项目都会有简单版,用心找大多数都可以找到, 好比 Spring 的简易版、Dubbo 简易版。
先团体后局部

先团体后局部黑白常紧张的一个认知规则,表现了“团体头脑”。
如果对框架缺乏团体认识,很容易陷入局部细节之中。
先团体后局部包罗多种含义,下面会介绍几种核心的含义。
先看架构再读源码

各人可以通过框架的官方文档相识其团体架构,相识其核心原理,然后再去看具体的源代码。
但是很多人总会忽视这个步骤。
如轻量级分布式服务框架 jupiter 框架 的 README.md 给出了框架的团体架构:
https://p26.toutiaoimg.com/large/pgc-image/7a436542d86542d1b81f8665891e7fed
(图片来自:jupiter 项目 README.md 文档)
对框架有了一个团体相识之后,再去看具体的实现就会容易很多。
先看项目结构再读源码

先团体后局部,还包罗先看项目标分包,再具体看源码。
https://p9.toutiaoimg.com/large/pgc-image/736ba17639a04f61bb0bf45a2cbb9821
(图片来自:jupiter 项目结构)
通过项目标报名,如 monitor、registry、serialization、example、common 等就可以明白该包下的代码意图。
先看类的函数列表再读源码

通过 IDEA 的函数列表功能,可以快速相识某个类包罗的函数,可以对这个类的核心功能有一个初步的认识。
这种方式在读某些源码时结果非常棒。
更紧张的是,如果能够养成查看函数列表的习惯,可以发现很多紧张但是被忽略的函数,在未来的项目开发中很可能会用到。
下图为 commons-lang3 的 3.9 版本中 StringUtils 类的函数列表表现图:
https://p26.toutiaoimg.com/large/pgc-image/2ca1f4ec7c374b9f954a9be606f70635
先看团体逻辑再看某个步骤

好比一个大函数可能分为多个步骤,我们先要理解某个步骤的意图,相识为什么先执行子函数 1, 再执行子函数 2 等。
然后再去观察某个子函数的细节。
以 spring-context 的 5.1.0.RELEASE 版本的 IOC 容器的核心 org.springframework.context.support.AbstractApplicationContext 的核心函数 refresh 为例:
@Overridepublic void refresh() throws BeansException, IllegalStateException {   synchronized (this.startupShutdownMonitor) {      // Prepare this context for refreshing.      // 1 初始化前的预处理      prepareRefresh();      // Tell the subclass to refresh the internal bean factory.      // 2 告诉子类去 refresh 内部的 bean Factory       ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();      // Prepare the bean factory for use in this context.      // 3 BeanFactory 的预处理配置      prepareBeanFactory(beanFactory);      try {         // Allows post-processing of the bean factory in context subclasses.      // 4 准备 BeanFactory 完成后举行后置处理         postProcessBeanFactory(beanFactory);         // Invoke factory processors registered as beans in the context.         // 5 执行 BeanFactory 创建后的后置处理器         invokeBeanFactoryPostProcessors(beanFactory);         // Register bean processors that intercept bean creation.         // 6 注册 Bean 的后置处理器         registerBeanPostProcessors(beanFactory);         // Initialize message source for this context.         // 7 初始化 MessageSource         initMessageSource();         // Initialize event multicaster for this context.         // 8 初始化事件派发器         initApplicationEventMulticaster();         // Initialize other special beans in specific context subclasses.         // 9 子类的多态 onRefresh         onRefresh();         // Check for listener beans and register them.         // 10 检查监听器并注册         registerListeners();         // Instantiate all remaining (non-lazy-init) singletons.         // 11 实例化所有剩下的单例 Bean (非懒初始化)         finishBeanFactoryInitialization(beanFactory);         // Last step: publish corresponding event.         // 12 最后一步,完成容器的创建         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn("Exception encountered during context initialization - " +                  "cancelling refresh attempt: " + ex);         }         // Destroy already created singletons to avoid dangling resources.      // 销毁已经常见的单例 bean          destroyBeans();         // Reset 'active' flag.         // 重置active 标记         cancelRefresh(ex);         // Propagate exception to caller.         // 将非常丢给调用者         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...      // 重置缓存         resetCommonCaches();      }   }}我们可以要特别重视每个步骤的含义,思考为什么这些要这么设计,然后再进入某个子函数中去相识具体的实现。
好比再去相识第 7 步的具体编码实现。
/** * Initialize the MessageSource. * Use parent's if none defined in this context. */protected void initMessageSource() {   ConfigurableListableBeanFactory beanFactory = getBeanFactory();   if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {      this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);      // Make MessageSource aware of parent MessageSource.      if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {         HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;         if (hms.getParentMessageSource() == null) {            // Only set parent context as parent MessageSource if no parent MessageSource            // registered already.            hms.setParentMessageSource(getInternalParentMessageSource());         }      }      if (logger.isTraceEnabled()) {         logger.trace("Using MessageSource [" + this.messageSource + "]");      }   }   else {      // Use empty MessageSource to be able to accept getMessage calls.      DelegatingMessageSource dms = new DelegatingMessageSource();      dms.setParentMessageSource(getInternalParentMessageSource());      this.messageSource = dms;      beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);      if (logger.isTraceEnabled()) {         logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");      }   }}从该子函数的角度,“团体”为 if 和 else 两个代码块,“部分”为 if 和 else 的代码块的具体步骤。
从设计者的角度学源码

从设计者的角度读源码是一条极其紧张的思想。表现了“先料想后验证”的思想。
这样就可以走出“对着答案做题”的误区。
学习源码时不管是框架的团体架构、某个具体的类还是某个函数都要设想如果自己是作者,该怎么设计框架、怎样编写某个类、某个函数的代码。
然后再和终极的源码举行对比,发现自己的设想和对方的差异,这样对源码的印象更加深刻,对作者的意图领会的会更加到位。
好比我们封装 HTTP 请求工具,获取相应后根据相应码判断是否成功,我们可能会这么写:
public boolean isSuccessful(Integer code) {    return 200 == code;}我们查看 okhttp 4.3.0 版本的源码,依赖:
    com.squareup.okhttp3    okhttp    4.3.0okhttp3.Response 类的 isSuccessful 函数源码解释和代码 (kotlin):
/**   * Returns true if the code is in 发现和自己设想的差异,相应码的范围是
通过这个简单的例子,我们发现自己对 HTTP 相应码的理解不够全面。
别的通过这个源码我们也相识到了源码解释的紧张性,通过源码解释可以清晰明白的理解该函数的意图。
从设计模式的角度学源码

很多优秀的开源项目都会用到各种设计模式,尤其是学习 Spring 源码。
因此,强烈建议要相识常见的设计模式。
相识常见设计模式的目标、核心场景、上风和劣势等。
要理解设计模式的六大原则:单一职责原则、开闭原则、依赖倒置原则、接口隔离原则、迪米特法则等。
在读源码时注意体会设计模式的六大原则在源码中的表现。
如 jupiter 1.3.1 版本的 org.jupiter.serialization.SerializerFactory 类就表现了工厂模式。该类通过在静态代码块中利用 SPI 机制加载序列化方式并存储到 serializers map 中,获取时从该 map 中直接取,实现了已有对象的重用。
https://p9.toutiaoimg.com/large/pgc-image/70b42594dca143fc9fc194f90cf47886
各人可以通过《设计模式之禅》、《Java 设计模式及实践》、《Head first 设计模式》等来学习设计模式。
从设计模式角度阅读源码,可以加深对设计模式应用场景的理解,自己编码时更容易选择得当的设计模式来应对项目中的变革。
读源码的粒度问题

很多开源项目代码行数非常多,几十万甚至上百万行,想都读完而且都能记下来不太实际。
前面也讲到读源码读什么的问题,个人建议各人读核心的原理,关键特性的实现,高抽象层的几个关键步骤。
不要追求读每一行代码,甚至“背诵”代码,由于工作之后学习的目标更多地是为了运用,而不是为了考试。
读源码的技巧

通过解释学习源码

我们以 Guava 源码 commit id 为 5a8f19bd3556 的提交版的 CacheBuilder 源码为例。
如果我们想相识 expireAfterWrite 函数的的用法。
可以通过读其解释相识该函数的功能,每个参数的含义,非常发生的原因等。对我们学习源码和实际工作中的利用资助极大。
/**   * Specifies that each entry should be automatically removed from the cache once a fixed duration   * has elapsed after the entry's creation, or the most recent replacement of its value.   * // 省略其他   *   * @param duration the length of time after an entry is created that it should be automatically   *   removed   * @param unit the unit that {@code duration} is expressed in   * @return this {@code CacheBuilder} instance (for chaining)   * @throws IllegalArgumentException if {@code duration} is negative   * @throws IllegalStateException if the time to live or time to idle was already set   */@SuppressWarnings("GoodTime") // should accept a java.time.Durationpublic CacheBuilder expireAfterWrite(long duration, TimeUnit unit) {    checkState(      expireAfterWriteNanos == UNSET_INT,      "expireAfterWrite was already set to %s ns",      expireAfterWriteNanos);    checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);    this.expireAfterWriteNanos = unit.toNanos(duration);    return this;}通过单元测试学源码

同样以学习 6.1 的函数为例,可以通过 find usages 找到对应的单元测试。
com.google.common.cache.CacheExpirationTest#testExpiration_expireAfterWrite可以执行在源码中断点,然后执行单元测试,相识源码细节。
public void testExpiration_expireAfterWrite() {FakeTicker ticker = new FakeTicker();CountingRemovalListener removalListener = countingRemovalListener();WatchedCreatorLoader loader = new WatchedCreatorLoader();LoadingCache cache =      CacheBuilder.newBuilder()          .expireAfterWrite(EXPIRING_TIME, MILLISECONDS)          .removalListener(removalListener)          .ticker(ticker)          .build(loader);checkExpiration(cache, loader, ticker, removalListener);}从入口开始学源码

如下面是常见的 springboot 的应用启动主函数:
@SpringBootApplicationpublic class DemoApplication {    public static void main(String[] args) {      SpringApplication.run(DemoApplication.class, args);    }}我们可以从 SpringApplication 的 run 函数不停跟下去。
有些朋友可能会说,跟着跟丢了怎么办?
各人可以在源码中打断点,然后通过左下角的调用栈实现源码的跳转,可以通过“drop frame”实现。
https://p3.toutiaoimg.com/large/pgc-image/74e0ee08b5324093a320cedae61d770f
利用插件来学源码

类图插件

可以利用 IDEA 自带的类图相识核心类的源码的关系。
如下图为 fastjson 的核心类的类图:
https://p5.toutiaoimg.com/large/pgc-image/eae480b513184ce38a61c3686674bf9f
时序图插件

可以利用 Stack trace to UML IDEA 插件绘制错误堆栈的时序图,相识源码的执行流程。
保举各人安装 SequenceDiagram IDEA 插件,读源码时可以查看调用的时序图,对理解源码调用关系资助很大。
https://p26.toutiaoimg.com/large/pgc-image/573d96ecfa3b499c8c0be76d18d2a25e
codota

强烈保举各人安装 codota 插件(支持 Eclipse、IDEA、Android Studio) 通过该插件或对应的 Java 代码搜索网站。
如下图所示,我们安装好 codota 插件后,想相识 org.springframework.beans.factory.support.BeanDefinitionRegistry 的 registerBeanDefinition 函数用法。
直接在该函数上右键然后选择“Get relevant examples”,即可查看其他知名开源项目中的相关用法。
https://p3.toutiaoimg.com/large/pgc-image/99d04684cb7e4df7b01a759a76359ff2
这对我们相识该源码的功能和用法有极大的资助,我们实际开发中也可以多用 codota 来快速学习怎样利用一个函数。
通过提交记载学源码

好比我们想研究某段源码的变动,可以拉取源代码,查看 Git 提交记载。
好比我们想研究某个感爱好类的演进,直接选取该类,查看提交记载即可。
下图为 commons-lang 项目标,StringUtils 工具类的一个变更记载:
https://p26.toutiaoimg.com/large/pgc-image/d462e9cfd3434dadb01e0087d15b4546
通过变更记载我们可以学习到早期版本有哪些问题,怎样举行优化。
根据 issue 学源码

issues 是学习源码的紧张途径,是我们提高开发经验的一个紧张途径。
如果我们想深入学习某个开源项目,可以翻阅汗青 issues 。
针对具体的 issue 中涉及的具体的问题入手相识各人对该问题的见解,学习问题的原因和解决办法。
着重相识有多种方案时作者举行了何种考量,做出了什么取舍。
如 Add Immutable*Array.reverse() #3965:
https://p9.toutiaoimg.com/large/pgc-image/5d393a4031924ee9aa0f62c4267a6b7b
搜索引擎大法

当我们对某些源码设计感到困惑时,可以在 Google 或者 Stack Overflow 上搜索问题的原因,往往会有些不测收获。
反编译大法

我们在读源码时经常会遇到类似下面的这种写法:
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer
    private WebServer startWebServer() {      WebServer webServer = this.webServer;      if (webServer != null) {            webServer.start();      }      return webServer;    }在函数中声明一个和成员变量同名的局部变量,然后将成员变量赋值给局部变量,再去利用。
看似很小的细节,隐含着一个优化思想。这就需要借助反编译大法,在字节码层面去分析。
具体解读拜见《为什么要保举各人学习字节码?》。
总结

总之,读源码要着重思考,思考为什么这么设计?可能的原因是什么?然后去验证。

[*]学习代码在平常,工作时如果项目开发工期不紧,编码过程中进入源码分析学习,积少成多;
[*]在开发过程中,如果遇到问题,可以选择进入源码调试,这样印象更深刻;
[*]此外,我们既要笃志苦干也要“仰望星空”(巩固专业底子),有些核心的软件设计原则,操作系统、计算机网络的设计原理,都是源码设计思想的紧张来源,如果专业底子不扎实,往往很难相识问题的本质。

amesty 发表于 2020-12-20 08:08:18

近期源码解读与作者想法甚是契合,深以为然.

强湛 发表于 2020-12-19 08:29:10

首先费点功夫从宏观上掌握代码整体架构的设计思路,就基本读懂多半了,当然这阶段也会细读某些关键局部功能代码。然后再细抠各小系统小功能的算法及其代码实现方式,甚至有些代码不用理会。

只为伊人纵使轻狂 发表于 2021-8-29 22:18:49

第一步是提炼核心流程,如果项目结构复杂,直接debug很容易迷失。最好的办法是找一个简易版项目。

唯爱这座城 发表于 2020-12-23 14:39:44

科技 如何阅读源代码

架构师成长记 发表于 2020-12-19 10:27:41

这得比较不错的源码阅读文章

乔wetyhdrujjzrbn 发表于 2020-12-19 17:45:43

笔记工具哪种好点

狼牙锤 发表于 2020-12-19 04:45:34

感谢作者[祈祷]

活泼画板CA 发表于 2020-12-20 21:00:27

转发学习

LOLer007 发表于 2020-12-19 20:10:17

收藏不看系列
页: [1] 2 3
查看完整版本: 如何阅读源代码(建议收藏)