如何阅读源码
编程的世界里一直流传着这样一句充满极客风格的话:Talk is cheap.Show me the code.
中文翻译的版本有很多,不论是“屁话少说,放码过来”还是“代码胜于雄辩”,都牛气的传达着极客们对技术的推崇和热爱。 其实这句话是出自Linux之父林纳斯·托瓦兹(Linus Torvalds)。
为什么我们需要读源码
我们程序员每天都要和源码打交道。经过数年的学习,大多数程序员可以“写”代码,或者至少是拷贝并修改代码。 而且,我们教授编程的方式强调编写代码的艺术,而不是如何阅读代码。 当我说“阅读代码”,我是指有意地专门阅读代码。
站在巨人的肩膀上
我们从他人身上学习。优秀的源代码就像文学杰作,它不仅仅只提供了知识和信息,还提供了启迪。
通过浏览Linux内核、Redis、Nginx、Rails或其他著名项目, 你可以从全球范围的成千上万的顶级程序员那里汲取智慧。 在这些项目中可以找到无数的良好编程示例、编程范式选择、设计和架构。 向他人学习的另一个好处是能够避免常见的坑,大多数坑早已被他人踩过。
解决困难问题
在你的职业生涯中,你终将会碰到谷歌都无法解决的问题。 如果你还没碰到过这种问题,这只是因为你编程的时间还不足够长。 阅读源码是调查这类问题的好方法,也是学习新东西的好机会。
扩展你的边界
大多数程序员只在少数特别领域编过程。 一般而言,如果你不时常推自己一把,你的编程技能会维持在你同事间的平均水平。 不要满足于修补bug或在现有系统中添加琐碎特性的工作。 相反,你可以试着扩展到一个新的领域,持续尝试找到一个你在日常工作中接触不到、但你感兴趣的领域。 这将从整体上拓宽你对编程的理解。
调研开源项目
不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目,比如接手一个其他人的项目。
应该读什么样的源码
综上,阅读源码是有益的。 那么下一个问题,有这么多优秀作品可供选择,我们该选择并阅读什么样的源码呢? 你必须从选择目标开始。如果不在这个步骤上下点功夫,你从源码中学习的效果就会打折扣。 这里有一些典型场景:
新技术
当你想学习一门新语言。学新语言可不只是学会语法 。 不管怎样,阅读源码是一个非常有效的学习新语言的方式。
新算法
当你想了解一个特殊的算法或实现。 例如,我们都会使用标准库中的sort函数,你有没有好奇过它是怎么实现的? 或者当你要使用Redis中的Set结构,它是用什么数据结构实现的? 为了解决这些疑惑,你只需要读源码中与之相关的实现部分,通常只有很少的文件或函数。
新领域
当你想拓展进入新的领域,你可以阅读这个领域的经典著名的项目的源码。 比如说,如果你在做Web开发的工作,你对分布式系统感兴趣吗? 如果你的答案是“是”而且你懂Golang,也许etcd 是你的选择。你想钻研操作系统的内部构成吗?那么也许xv6 是一个好的开始。我们处在一个许多优秀开源项目都托管在了Github的好时代,请试着寻找一些这种项目。
如何读源码
预先准备
为了更有效率地阅读代码,你需要提前在手边准备这些东西:
- 一个你可以熟练使用的编辑器。你需要拥有快速搜索关键字或变量名的能力。 有时你需要查找函数的引用和定义。和你的编辑器相处融洽些。 为了更加有效率,试着学习仅使用键盘操作编辑器。 这会使你专注于代码而不受打扰(译:指额外思考编辑器操作)。
- 掌握基本的Git或其他版本控制工具的技能,这样你就能比较代码在版本间的差异。
- 与源码有关的文档。文档可以为你的阅读提供参考,尤其是设计文档、编码规范等文档。
- 具有一定的编程语言与设计模式的知识和经验。这对(阅读)大项目是强制性的。 如果你很了解一门编程语言,你也会了解关于源码组织与编程范式的最佳实践。 当然,这需要时间来积累。要有耐心。
流程与技巧
首先了解项目背景
前面我们已经涉及到,项目背景是非常重要的,比如Hadoop,我们是否对他所了解。Hadoop为何产生,是为了解决在大数据量的情况下,单机很难计算和处理的数据的情况下,所以产生了价格和成本都非常昂贵的超级计算机。所以有的人就想如何通过廉价的普通的计算机来实现计算大数据量,所以Hadoop应运而生。Hadoop又分为Hdfs、Yarn等组件,当然这里面又会细分,我们了解的越详细,对我们阅读源码越方便和快捷。
了解项目功能、结构
了解项目功能、结构,比如哪些是通用部分,哪些是功能部分。所以这里我们需要一定猜测,这个猜测我们同样需要去验证。有的大神称其为““正向推导+验证””,这里其实也和我们的学习方法和思维关联。我们在学习比如当前的源码,你的学习思路是什么?是一直跟着文件或者文档的思路去学习,还是自己提前有一定的想法,然后去文档或者文件中去验证自己的想法,这二者的学习效果和速度是不一样的。
结合上下文阅读代码
当你阅读代码时,请持续提出问题。 例如,如果一个应用有缓存策略,一个好问题就是:如果键无效了会怎样?缓存中的值如何更新? 带着这些问题阅读代码,就是结合上下文。或者说因为你有了一个目标,你会变得享受阅读的过程。 你甚至可以自己做一些假设,然后在代码中寻找验证。 你有点像侦探:你想发现代码的真相,代码的逻辑,代码是如何像故事一般上下流动的。
把实例跑起来并与之交互
源码就像乐高积木,只是已经组装好了。 如果你想了解它们是怎么组装在一起的,你需要和它交互,有时甚至要把它拆开。 阅读同一模块的老版本同样有帮助。从Git中阅读版本差异,试着弄清楚特定的特性是如何实现的 (修改日志在这个场景很有用)。 举个例子,我发现Lua的第一个版本相当简单,这可以帮助我了解作者最初的设计理念。
Debug是另一种与代码交互的方式。试着在代码中加一些断点(或打印一些变量值), 然后弄明白打印到控制台中的所有输出。
如果你对代码了解比较透彻了,试着对代码做一些修改,重新build并把它跑起来。 最简单的方式是试着调整配置项,去看不同配置的运行结果。 之后你可以试着添加一些细微的特性。 如果这些特性对其他人也有用,你应该把代码贡献到上游。
开始阅读一份项目源码的第一步,是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。
有的项目比较复杂,依赖的组件多,搭建起一个调试环境并不容易,所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来,那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。
就我的经验而言,一个项目代码,是否能顺利的搭建调试环境,效率大不一样。
跑起来之后,又要尽量的精简自己的环境,减少调试过程中的干扰信息。比如,Nginx 使用多进程的方式处理请求,为了调试跟踪 Nginx 的行为,我经常把 worker 数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。
再比如,很多项目默认是会带上编译优化选项或者去掉调试信息的,这样在调试的时候可能会有困扰,这时候我会修改 makefile 编译成 -O0 -g,即编译生成带上调试信息且不进行优化的版本。
总而言之,跑起来之后的调试效率能提升很多,而在跑起来的前提之下又要尽量精简环境排除干扰的因素。
明确自己的目的
尽管阅读项目源码很重要,但是并不见得所有项目都需要从头到尾看的清清楚楚。在开始展开阅读之前,需要明确自己的目的:是需要了解其中一个模块的实现,还是需要了解这个框架的大体结构,还是需要具体熟悉其中的一个算法的实现,等等。
比如,很多人看 Nginx 的代码,而这个项目有很多模块,包括基础的核心模块( epoll 、网络收发、内存池等)和扩展具体某个功能的模块,并不是所有这些模块都需要了解的非常清楚,我在阅读 Nginx 代码的过程中,主要涉及了以下方面:
了解 Nginx 核心的基础流程以及数据结构。 了解 Nginx 如何实现一个模块。 有了这些对这个项目大体的了解,剩下的就是遇到具体的问题查看具体的代码实现了。 总而言之,并不建议毫无目的的就开始展开一个项目的代码阅读,无头苍蝇式的乱看只会消耗自己的时间和热情。
区分主线和支线剧情
有了前面明确的阅读目的,就能在阅读过程中区分开主线和支线剧情了。比如:
想了解一个业务逻辑的实现流程,在某个函数中使用一个字典来保存数据,在这里,“字典这个数据结构是如何实现的”就属于支线剧情,并不需要深究其实现。 在这一原则的指导下,对于支线剧情的代码,比如一个不需要了解其实现的类,读者只需要了解其对外接口,了解这些接口的入口、出口参数以及作用,把这部分当成一个“黑盒”即可。
纵向和横向
代码阅读过程中,分为两个不同的方向:
- 纵向:顺着代码的顺序阅读,在需要具体了解一个流程、算法的时候,经常需要纵向阅读。
- 横向:区分不同的模块进行阅读,在需要首先弄清楚整体框架时,经常需要横向阅读。 两个方向的阅读,应该交替进行,这需要代码阅读者有一定的经验,能够把握当前代码阅读的方向。我的建议是:过程中还是以整体为首,在不理解整体的前提之前,不要太过深入某个细节。把某个函数、数据结构当成一个黑盒,知道它们的输入、输出就好,只要不影响整体的理解就暂且放下接着往前看。
了解数据结构间的关系
数据结构是一个程序中最重要的元素。用笔或者你喜欢的其他工具画出数据结构间的关系。 这个图就是源码的映射。你会在阅读过程中时常参考这个图。一些工具比如scitools 可以用来生成UML类图。 (译:这个方法用在写代码中能节约翻Model声明文件的时间,推荐用纸笔,不占屏幕) 虽然说“程序设计=算法+数据结构”,然后我实际中的体会,数据结构更加重要。
因为结构定义了一个程序的架构,结构定下来了才有具体的实现。好比盖房子,数据结构就是房子的框架结构,如果一间房子很大,而你并不清楚这个房子的结构,会在这里面迷路。而对于算法,如果属于暂时不需要深究的细节部分,可以参考前面“区分主线和支线剧情”部分,先了解其入口、出口参数以及作用即可。 Linus 说:“烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”
因此,在阅读一份代码时,厘清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系,我的源码分析类博客中有很多这样的例子,比如 《Leveldb代码阅读笔记》、《Etcd存储的实现》 等等。
需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。
比如,你如果现在刚接手某个项目,需要简单的了解一下项目,可以先阅读代码了解都有哪些核心数据结构。理解了之后,如果不清楚某些情景下的流程,可以使用情景分析法。总而言之,交替进行直到解答你的疑问为止。
了解模块间的依赖关系与边界
大项目中会包含许多模块,一个模块经常只拥有单一职责。 这有助于我们减少代码复杂度,在适当的层级上做抽象。 模块的接口是抽象的边界,我们可以一个接一个地阅读模块。 如果你在阅读一个使用Make构建的C/C++项目,Makefile是了解模块间如何组织的好切入点。
边界本身也很有用。优秀的代码组织得很好,变量名与函数名的命名风格体现着可读性。 你不需要阅读全部源文件,你可以忽略不重要的或你熟悉的部分。 如果你确定一个模块是仅仅是为了被解析而设计的(just designed for parsing), 那么你已经大致了解了它的功能;那么你就可以跳过不读这个模块。 当然,这将大大节约时间。
情景分析
假如有了前面的基础,已经能够让项目顺利在自己的调试环境跑起来了,也明确了自己想了解的功能,那么就可以对项目代码进行情景分析了。
所谓的“情景分析”,就是自己构造一些情景,然后通过加断点、调试语句等分析在这些场景下的行为。
以我自己为例,在写 《 Lua 设计与实现》 时,讲解到Lua虚拟机指令的解释和执行过程中,需要针对每个指令做分析,此时用的就是情景分析的方法。我会模拟出来使用该指令的 Lua 脚本代码,然后在程序里断点调试这些场景下的行为。
我惯用的做法,是在某个重要的入口函数上面加上断点,然后构造触发场景的调试代码,当代码在断点处停下,通过查看堆栈、变量值等等来观察代码的行为。
使用测试用例
测试用例也是帮助代码理解的一个很好的补充。测试用例就是文档。 当你在阅读一个类时,试着把对应的测试代码一起读了。 测试用例能帮你弄清一个类的接口,和该类的典型用法。 集成测试用例可以让你顺着走过程序的整体流程,适合输入一些特殊值并debug运行。 好的项目都会自带不少用例,这类型的例子有:etcd、google 出品的几个开源项目。 如果测试用例写的很仔细,那么很值得好好去研究一下。原因在于:测试用例往往是针对某个单一的场景,独自构造出一些数据来对程序的流程进行验证。所以,其实跟前面的“情景分析”一样,都是让你从大的项目转而关注具体某个场景的手段之一。
阅读由易到难
先阅读基础模块,然后阅读依赖较多的模块。我们在刚开始阅读文档,就想去攻克难点,这样会非常容易让我们放弃,所以其实这也是做事的策略和方法
多问自己几个问题
学习的过程中离不开交互。 如果阅读代码只是输入 (Input),那么还需要有输出 (Output)。只有简单的输入好比喂东西给你吃,而只有更好的消化才能变为自己的营养,而输出就是更好消化知识的重要手段。
其实这个思想很常见,比如学生上课 (Input) 了需要做练习作业 (Output),比如学了算法 (Input) 需要自己编码练习 (Output),等等。简而言之,输出是学习过程中的一种及时反馈,质量越高学习效率越高。 输出的手段有很多,在阅读代码时,比较建议的是自己能够多问自己一些问题,比如: 为什么选择这个数据结构来描述这个问题?类似的场景下,其他项目是怎么设计的?都有哪些数据结构做这样的事 ? 如果由我来设计这样的项目,我会怎么做? 等等等等。越是主动积极的思考,就越有更好的输出,输出质量与学习质量成正比关系。
写自己的代码阅读笔记
写这类笔记,有以下几个需要注意的地方。
虽然是笔记,但是要想象着在向一个不太熟悉这个项目的人讲解原理,或者想象一下是几个月甚至几年后的自己回头来看这个文章。在这种情况下,会尽量的把语言组织好,循循善诱的解释。
尽量避免大段的贴代码。我认为在这类文章中,大段贴上代码有点自欺欺人:就是看上去自己懂了,其实并不见得。如果真要解释某段代码,可以使用伪代码或者缩减代码的方式。记住:不要自欺欺人,要真的懂了。如果真的想在代码上加上自己的注释,我有一个建议是 fork 出来一份该项目某个版本的代码,提交到自己的github上,上面随时可以加上自己的注释并且保存提交。 多画图,一图胜千言,使用图形展示代码流程、数据结构之间的关系。我最近才发现画图能力也是很重要的能力,自己在从头学习如何使用图像来表达自己的想法。
写作是很重要的基础能力,我一个朋友最近教育我,大体的意思是说:如果你在某方面的能力很强,如果再加上写作好、英语好,那么将极大放大你在这方面的能力。而类似写作、英语这样的底层基础能力,不是一撮而就的,需要长时间保持练习才可以。而写博客,对于技术人员而言,就是一种很好的锻炼写作的手段。
PS:如果很多事情,你当时做的时候能想到今后面对这个输出的人是你自己,比如自己写的代码后面要自己维护、自己写的文章后面给自己看,等等的,世界会美好很多。比如写技术博客这些事情,因为我在写的时候考虑到以后看这份文档的人可能就是我本人,所以在写的时候会尽量的清晰、易懂,力图我自己一段时间后再看到自己的这份文档时,能够马上回忆起当时的细节,也正是因为这样,我很少在博客里贴大段的代码,尽可能的补充图例。
总结
以上是我简单总结的一些阅读源码时候的手段和注意方法,大体而言有那么几点吧:
- 只有更好的输出才能更好的消化知识,所谓的搭建调试环境、情景分析、多问自己问题、写代码阅读笔记等都是围绕输出来展开的。总而言之,不能像一条死鱼一样指望着光靠看代码就能完全理解它的原理,需要想办法跟它互动起来。
- 写作是人的基础硬实力之一,不仅锻炼自己表达能力,还能帮助整理自己的思路。对程序员而言锻炼写作能力的手段之一就是写博客,越早开始锻炼越好。 最后,如同任何可以习得的技能一般,阅读代码这种能力也需要长时间、大量的反复练习,下一次就从自己感兴趣的项目开始锻炼自己的这种技能吧。