阅读源码前情提要
如何判断一个程序员水平的高低?
- 做过多少系统?
- 更重要-踩过多少坑?
阅读源码有哪些坑?
- 阅读源码本身不难,难的是坚持!!!
为什么一定要阅读源码?
1. 通用型基础技术必须细致打磨 比如 JAVA 集合、Java并发(JUC)等项目中高频使用的通用型基础技术,一定要阅读源码,深入细节掌握其工作机制,提前规避风险,提高应用程序的可用性、健壮性。
2. 打造个人亮点标签 比如公司用了 Dubbo、RocketMQ,积累了丰富的使用经验,突出在这两个领域的优势,详细阅读其源码,做成专栏发布,成为职业技能中非常亮眼的标签。
3. 成长效率:输出倒逼输入 给自己制造快速成长的环境。
阅读源码准备
- 找到所有源码相关的文档,尤其是设计文档、代码约定等;
- 熟练地使用项目
- 高效地使用IDE,如搜索关键字、查找变量和函数引用。熟悉到仅使用键盘来操作,使你专注于代码而不会中断思维;
- 掌握 Git 或其他版本控制工具,比如比较不同版本之间的差异;
- 了解UML,熟练使用纸和笔或熟悉一款绘图软件;
- 了解常用设计原则;
阅读源码步骤
步骤1:使用场景与技术背景
使用场景、架构设计中的承担责任
- 查阅官方文档(含博客),参考架构图、时序图
- 了解全局、理解概念、理解模型
- 画图 没有图自己画一些图,加快吸收
项目技术背景,开发项目的目的 很多项目迭代,核心功能并没有发生什么样的变化,变化的可能只是实现方式的不同。频繁变化的实际上不是需求,而是实现需求的技术。 阅读源码时,了解一下当时的技术背景,知道当时的技术限制,才能更好的理解代码为什么这么写。
步骤2:阅读文档,建立概念模型
- 建模:基于源码建立模型;
- 梳理:基于模型进行流程的梳理;
- 归纳:对梳理出的流程进行归纳,再整合进模型中;
- 延伸:在建模、梳理、归纳中触类旁通,与其它项目或技术点产生关联,扩大知识面
建模
通过对文档的理解,梳理出概念模型来完善黑盒模型。实际上随着对项目的理解,黑盒模型会慢慢的变成白盒模型。
看什么文档?
- 官方文档(强烈建议直接阅读原文,翻译多少带有个人理解,可能理解的是错的,仅作参考)
- 谷歌搜索的技术论坛或博客
- 把自己看到的东西给串起来
如何阅读文档?
- 有目的的去读文档
以Spring文档举例,Spring子项目很多,每个子项目都有文档,少的也有大几十页,多的甚至上千页,要一页一页的看完,是完全不可能的。理解项目的阶段不同,目的也就不同。比如现在要对项目有一个大致了解,要能构建项目的概念模型,就要去找描述项目概念的相关文档。
比如JUnit,找出官方网站里描述相关概念的文档。大概了解到JUnit中有哪些概念:
- Assertions:断言,用于判断测试结果是否符合预期
- TestRunners:测试执行,各种执行测试的方式。包括下面的参数化测试、理论测试,以及执行JUnit3的相关类等
- Suite:套件,批量执行测试的类
- Rules:规则,扩展JUnit的功能,灵活地改变测试方法的行为
从上面的几个概念,再结合我们之前的理解,就可以得到一个大致的概念模型:
- 各个Test是测试用例
- TestRunners用来执行各种测试
- Assertions用于验证测试结果是否符合预期
- Suite用于批量执行测试
- Rules用于改变测试的行为
通过概念模型验证与完善黑盒模型,通过上面的概念模型,结合前面的黑盒模型,我们可以完善黑盒模型:
- 既可以编写测试用例Test,也可以编写测试套件Suite
- 测试套件Suite可以添加多个测试用例Test
- 通过TestRunners来执行测试
- 通过Assertions来验证测试结果是否符合预期
- 通过Rules来改变测试的行为
- 最终通过Result展示结果
搭建开发调试环境
搭建开发调试环境,用官方Demo运行项目有两个目的:
- 知道这个项目运行前有哪些必须的前置条件
- 读代码出现疑惑,可以通过调试去解开自己的困惑
利用好测试用例 好的项目都会自带不少用例,这类型的例子有:etcd、google出品的几个开源项目。 如果测试用例写的很仔细,那么很值得好好去研究一下。原因在于:测试用例往往是针对某个单一的场景,独自构造出一些数据来对程序的流程进行验证。所以,其实跟前面的“情景分析”一样,都是让你从大的项目转而关注具体某个场景的手段之一。
梳理源码逻辑
先主流程再分支流程,注意切割,逐个击破;
纵向和横向阅读 代码阅读过程中,分为两个不同的方向: 纵向:顺着代码的顺序阅读,在需要具体了解一个流程、算法的时候,经常需要纵向阅读。 横向:区分不同的模块进行阅读,在需要首先弄清楚整体框架时,经常需要横向阅读。 两个方向的阅读,应该交替进行,这需要代码阅读者有一定的经验,能够把握当前代码阅读的方向。
我的建议是:过程中还是以整体为首,在不理解整体的前提之前,不要太过深入某个细节。把某个函数、数据结构当成一个黑盒,知道它们的输入、输出就好,只要不影响整体的理解就暂且放下接着往前看。
自顶向下梳理 越上层的模块,功能越多,但数量越少。我们可以从顶层的模块梳理出大致的流程关系,然后通过不断的深入,来梳理细化流程。就像思维脑图一样。
自底向上归纳 思维脑图的一个问题就是「只管拆分,不管归纳」!归纳其实是非常重要的一环。当你梳理了细化的流程后,需要将细化流程整合归纳到整体流程中,通过不断的归纳,你才能理解整体的流程。
先做减法,再做加法 无论项目代码多大、版本怎么变化,核心的功能是基本不变的。所以我们先自顶向下的去不断的剔除非核心的组件/包/类,找到核心的模块/包/类。梳理出核心流程后,在核心流程的基础上再进行扩展,引入其它流程,以构成完整的项目流程。
从接口找关系 个人认为「接口」这个词并不好,应该叫「协议」更合适。接口定义了一套可供外部访问的方法,其实就是交互的协议,外部对象、模块或者系统需要按照这个「协议」来访问我们的系统,所以我们可以从接口的调用关系,来梳理出模块之间的调用关系。
画图辅助阅读 有研究表明,我们接收的大部分信息都是通过眼睛接收的。绘图能加强我们的理解。同时,绘图相当于是有了存档,当再次看到绘制的流程图或结构图时,能快速的唤起你对项目的理解。
设计模式辅助阅读 如果你很熟悉设计模式,你就能从代码中的设计模式梳理出代码结构,也就能加快你对源码的理解。
厘清核心数据结构之间的关系 结构定义了一个程序的架构,结构定下来了才有具体的实现。好比盖房子,数据结构就是房子的框架结构,如果一间房子很大,而你并不清楚这个房子的结构,会在这里面迷路。而对于算法,如果属于暂时不需要深究的细节部分,可以参考前面“区分主线和支线剧情”部分,先了解其入口、出口参数以及作用即可。
Linus说: “烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”在阅读一份代码时,理清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系。
需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。
如果你刚接手某个项目,需要了解一下项目,可以先阅读代码了解都有哪些核心数据结构。 理解了之后,如果不清楚某些情景下的流程,可以使用情景分析法。 总而言之,交替进行直到解答你的疑问为止。
思考与质疑
阅读过程中多思考与质疑思维。 了解其他开发人员如何思考以及如何解决特定问题以及他们所欠缺的地方。
版本间比较阅读 项目的不同版本可能技术上、实现上有差异。通过比较阅读的方式,能够发现这些差异,再结合自己的思考:
- 为什么有这些差异?
- 哪种实现方式好?
- 哪种实现方式不好?
- 好在哪里?
- 不好在哪里?
通过设问法提高代入感? 理解使用场景后,结合官方文档,尝试理解该模块想要解决的问题?并思考如何解决?并不一定要求给出具体的答案,只是在真正步入源码阅读时能更快感悟其代码含义。 强调下:明确自己的目的非常重要。是需要了解其中一个模块的实现,还是需要了解这个框架的大体结构,还是需要具体熟悉其中的一个算法的实现,等等。
输出的手段有很多,在阅读代码时,比较建议的是自己能够多问自己一些问题,比如: 为什么选择这个数据结构来描述这个问题?类似的场景下,其他项目是怎么设计的?都有哪些数据结构做这样的事情? 如果由我来设计这样的项目,我会怎么做? 等等等等。越是主动积极的思考,就越有更好的输出,输出质量与学习质量成正比关系。
比如看Nginx的代码,这个项目有很多模块,包括基础的核心模块(epoll、网络收发、内存池等)和扩展具体某个功能的模块,并不是所有这些模块都需要了解的非常清楚,我在阅读Nginx代码的过程中,主要涉及了以下方面: 1、了解Nginx核心的基础流程以及数据结构。 2、了解Nginx如何实现一个模块。 有了这些对这个项目大体的了解,剩下的就是遇到具体的问题查看具体的代码实现了。并不建议毫无目的的就开始展开一个项目的代码阅读,无头苍蝇式的乱看只会消耗自己的时间和热情。
写自己的代码阅读笔记 前面提到学习质量与输出质量成正比关系,这是我自己的深刻体会。也因为如此,所以才要坚持阅读源码之后写自己的分析类笔记。
写这类笔记,有以下几个需要注意的地方。
虽然是笔记,但是要想象着在向一个不太熟悉这个项目的人讲解原理,或者想象一下是几个月甚至几年后的自己回头来看这个文章。在这种情况下,会尽量的把语言组织好,循循善诱的解释。
尽量避免大段的贴代码。我认为在这类文章中,大段贴上代码有点自欺欺人:就是看上去自己懂了,其实并不见得。如果真要解释某段代码,可以使用伪代码或者缩减代码的方式。记住:不要自欺欺人,要真的懂了。如果真的想在代码上加上自己的注释,我有一个建议是fork出来一份该项目某个版本的代码,提交到自己的github上,上面随时可以加上自己的注释并且保存提交。比如我自己注释的etcd 3.1.10代码:etcd-3.1.10-codedump,类似的我阅读的其他项目都会在github上fork出一个带上codedump后缀的项目。
多画图,一图胜千言,使用图形展示代码流程、数据结构之间的关系。我最近才发现画图能力也是很重要的能力,自己在从头学习如何使用图像来表达自己的想法。
写作是很重要的基础能力,我一个朋友最近教育我,大体的意思是说:如果你在某方面的能力很强,如果再加上写作好、英语好,那么将极大放大你在这方面的能力。而类似写作、英语这样的底层基础能力,不是一撮而就的,需要长时间保持练习才可以。而写博客,对于技术人员而言,就是一种很好的锻炼写作的手段。
PS:如果很多事情,你当时做的时候能想到今后面对这个输出的人是你自己,比如自己写的代码后面要自己维护、自己写的文章后面给自己看,等等的,世界会美好很多。比如写技术博客这些事情,因为我在写的时候考虑到以后看这份文档的人可能就是我本人,所以在写的时候会尽量的清晰、易懂,力图我自己一段时间后再看到自己的这份文档时,能够马上回忆起当时的细节,也正是因为这样,我很少在博客里贴大段的代码,尽可能的补充图例。
只有更好的输出才能更好的消化知识,所谓的搭建调试环境、情景分析、多问自己问题、写代码阅读笔记等都是围绕输出来展开的。总而言之,不能像一条死鱼一样指望着光靠看代码就能完全理解它的原理,需要想办法跟它互动起来。 写作是人的基础硬实力之一,不仅锻炼自己表达能力,还能帮助整理自己的思路。对程序员而言锻炼写作能力的手段之一就是写博客,越早开始锻炼越好。
阅读源码遇到难题怎么办? 遇到不理解作者实现意图,特别是一些自己不太熟悉的编程方式(如位运算),通常有两种解决方法: (1)DEBUG,结合运行时数据,方便对代码的理解。 (2)调整阅读顺序,先打基础,从易到难。先阅读JAVA集合框架的源码,提炼出一套自己的源码研究方法论。
提前声明:源码阅读其实最难的不是代码本身,也不是无法理解其设计理念,最难的是坚持。
阅读源码的技巧
- 需要了解某一类的相关调用展示,可以右键选择所需要查看的类,然后选择 Diagrams > Show Diagram... 可以打开类的继承图,示意图:
源码阅读踩过的坑
源码阅读是手段,但一定不是目的。 面试过程中发现好多候选者谈到某一项技术时,首先介绍其原理,而是一下子具体到某个类啥的,这些类是如何如何工作等等,其实这是不太妥当的。
源码阅读不建议一上来就直接DEBUG 如果一开始就使用DEBUG,很容易会迷失在代码的各个分支中,缺乏全局视角,从而变得没有头绪,极大的增加了源码理解的难度,很容易让我们半途而废。debug是在你理解了项目结构和流程后的一个验证手段,或者确认某些具体细节的方法。
没必要读最新版本的代码 以Linux内核为例,目前Linux内核的代码量已经超过了2500万行,就算你一目十行,那也需要250万秒,也就是说你要不眠不休看将近29天才能看完!实际上无论发布了多少版本,要解决的问题基本不变。所以可以找一个相对老一点的版本来阅读,理解了它的核心功能以后,再慢慢的进行必要的额外功能的源码阅读。
不需要读完所有的源码 了解项目架构的价值在了解系统的层次结构,理出项目的核心脉络,这样就能用有限时间阅读最有价值的代码。 应该有技巧的读,要知道如何跳过某些代码,要知道如何技巧的找到后续调用流程,还要知道如何把一些困难去集中攻克。
大型项目中通常包含多个模块,在设计良好的项目中一个模块通常具有单一职责,它的变量和函数以一种可读的风格命名,这也使得代码更容易维护。模块的接口是抽象边界,我们可以忽略掉那些我们暂时不关心的模块。和《如何阅读一本书》中介绍的精读和泛读一样,自己感兴趣的部分精读,其他部分则泛读,这将大大节省整体时间。
比如想了解一个业务逻辑的实现流程,在某个函数中使用一个字典来保存数据,在这里,“字典这个数据结构是如何实现的”就属于支线剧情,并不需要深究其实现。在这一原则的指导下,对于支线剧情的代码,比如一个不需要了解其实现的类,读者只需要了解其对外接口,了解这些接口的入口、出口参数以及作用,把这部分当成一个“黑盒”即可。
超难算法放最后 对于某些开源项目,它会采用很多经典的算法。很经典,当然也很难。可以先记下来位置。在后续集中就着算法资料,慢慢理解。 比如 logback 日志文件分割的算法。
调用关系需确定 如果你一旦读代码发现你找不到后续流程了,就得考虑考虑,作者是不是用了非顺序调用方式去调用后续方法或者对象。 一般来说,开发人员常用以下几种方式做非顺序调用:
- 通过中间件继续后续流程,比如 MQ
- 通过异步方式继续后续流程,比如 Future 模式、Promises 模式
- 通过回调方式继续后续流程
- 通过代理委托方式继续后续流程,比如动态代理
- 通过依赖注入方式继续后续流程,比如 Spring 的 autowired 注解
这些非顺序调用会严重影响我们阅读代码。而对于这几种情况,解决的办法大概有两种: 直接猜——其实后续流程我们在做业务流程映射到实际的代码对象的时候已经大概知道了,如果是接口,我们看看实现类不多,就可以大概挨个看下,一般都能猜着是哪个。 运行起来调试下——这种办法是很普遍的,对任何不确定的任何事情,其实都可以用这个方式。
示例 例子:阅读Spring源码 你泡了一杯咖啡,从github上下载最新的Spring源码,导入到IDE中。写了一个demo;然后就开始debug。你一边StepOver,一边嘴里嘟囔着「A类的a方法调用了B类的b方法,然后又调用了C类的c方法,然后又执行到了这里」。嗯,很清晰嘛,喝口咖啡。 咦,刚才看到哪里了?!没办法,只好重新debug! 「A类的a方法调用了B类的b方法,然后又调用了C类的c方法。。。。。。这里是个接口,要执行的是子类,子类在哪里呢」?你在IDEA里按下ALT+F7,发现有几十个实现,到底是哪个实现呢?你按下F7 StepIn想确认下。我去,一堆AOP的包装方法,按了半天,就是进不到最终的类! 最后,好不容易找到了执行的类。咦,刚才看的是什么来着?!唉,算了,看不下去了,先来把王者吧!然后,就没有然后了!!!
为什么你没办法把源码读下去呢? 因为你的方法不对:
- 不了解项目就读源码
- 一上来就读最新版本的源码
- 直接读完整的源码
- 通过debug的方式阅读源码
不了解项目就读源码 首先,你了解项目了吗?你有实际使用过项目吗?你对项目的执行流程了解多少?如果你还没有熟练的使用项目,那你就不要急着去读项目源码。 读源码需要在熟练使用的基础上进行。为什么呢?
你想想你读源码的目的是什么?你是希望能了解这个项目是如何设计实现的,代码如何组织的,有哪些技巧或思路可以学习。但是你连项目都不了解,你怎么去理解这个项目是怎么设计的? 就像上面,你只知道Spring是目前企业级开发事实上的标准,但是你不知道原因,也不知道它解决了什么问题,以及解决问题的方式。那你如何能理解代码为什么要这么写?!假如你最终千辛万苦的终于将Spring源码读完了,你也只知道Spring构建了Bean塞到了容器中,然后使用的时候获取这个Bean,而不清楚深层次的用意。
一上来就读最新版本的源码 很多开源项目经历了很多年,代码量成指数级增长。版本越新,代码量也就越多。以Spring来说,现在Spring已经到了5.*版本,从git上下载下来的spring-framework的源码大小有200多兆,你能想象这得有多少代码吗?一上来就读这种庞然大物,心里不瘆得慌才怪! 所以从相对老一点的版本入手,代码量上会少很多,阅读起来也会容易很多。但是版本也不能太老,太老的版本可能使用的技术已经过时了,读了也没有什么太大的用处。
直接看完整代码 虽然老版本的代码量相对少一点,但是一个相对较大一点的项目的代码量还是比较多的。比如Spring3的代码量也已经很多了。所以,无论是哪个版本的源码,你都不应该看完整的代码。
通过debug的方式阅读源码 很多源码分析的博客都是通过debug的方式来讲述源码。也有不少教读源码的文章,也是建议通过debug的方式来进行阅读。但是,实际上通过debug的方式来阅读源码不是一个好方法,至少一上来就进行debug,是个很不好的习惯。
大部分人应该都知道,人的记忆可以分为「短时记忆」和「长时记忆」!对于「短时记忆」来说,一般正常人一次只能记忆7(加减2)个左右的无规律信息。对于通过debug进行源码阅读的方式,每一次的方法跟进,实际就是一次信息记忆,所以最多7次左右的StepOver,你就没法记住所有的调用关系了。所以你就会出现看了后面忘了前面的情况。 同时像Spring这种使用了AOP的项目,由于在运行时有多层的代理,所以debug的层级更多,也就更加的难以阅读了。再加上众多的子类实现,很容易就迷失在了一堆没用的代码中。
例子:阅读logback 源码 步骤1:了解使用场景、承担责任 logback 的用途就是打日志。它的代码无论多复杂,就是要让 logback 能健壮高效的打印出日志来。
步骤2:把握设计理念 把握最有价值的代码。官方没有架构图,自己画了个架构
步骤3:搭建开发调试环境 能成功运行 logback 后,其必然存在了一个 logback.xml 文件,否则无法运行。这个 logback.xml 文件其实对于我们看源码非常重要,它点出了 logback 需要的关键元素。
步骤4:先总再分梳理 把问题分解:把大问题分解成小问题,分而克之。 对于过大的代码量,过多的功能,我们紧要的一件事儿就是把比较模糊的目标分解成能具体落地的精准的小目标。这些小目标对应到项目中,其实就是项目的一个一个的业务流程。
1、抽出一条完整的业务流程 纵向分解 与 横向扩展
把业务流程和代码逻辑给映射起来。
2、映射业务流程和代码逻辑
具体阅读源码的技巧: 技巧一:代码跳着看 最优先的,就是看正向流程的,核心的代码,其余代码皆可以跳过。
如何评估自己的源码阅读水平? 源码阅读的三层境界 第1层境界—流水账阶段 老粉应该能感觉到我初期的源码阅读文章,基本上是记流水账,其最直观的表现现象是对源码一行一行加注释,只关注底层实现细节,但并未形成更高层次认知,对其设计理念并未提炼与深度领悟。
第2层境界—能提问、思考、并提炼 随着技术类文章的持续分享,笔者认识了很多大牛、发现与大牛交流的时候,一开始并不会说细节,而是讲设计理念,这就要求我们在阅读源码的时候多思考,并反问:如果需要自己实现的话,我们该如何着手?如何设计?带着疑问去研究源码,通过对比,思考,会对其背后的理念有了更深刻的理解。
第3层境界—思考、质疑、验证 无论是哪个开源框架都会存在BUG或者实现并不合理的地方,如果大家在阅读源码的时候能够思考并开始质疑其不合理性,并能通过验证证明自己的观点,然后与官方取得联系,交流,建Isuue,共同促进社区的发展,说明我们的能力、思考得到了极大的提升。