如何阅读源码汇总

译文:从源码中学习(阅读源码,初学者的有效成长方式)

目录前言
为什么我们需要读源码
站在巨人的肩膀上
解决困难问题
扩展你的边界
应该读什么样的源码
如何读源码
预先准备
流程与技巧
结合上下文阅读代码
把实例跑起来并与之交互
了解数据结构间的关系
了解模块间的依赖关系与边界
使用测试用例
点评
一些好书
译者的话

这是一篇译制博客文章,原博客为 Learn from Source Code (an Effective Way to Grow for Beginners) ,原作者为Nick Mose 向Nick申请获得了此文的中文翻译权与中文翻译文章发布权。

前言

上周我在和一位年轻程序员聊天时,他问到我:“如何阅读源码?”,我们讨论了一段时间,我还列举了几种有效阅读源码的方式。 然后他说:“你应该就这个话题写篇文章,这对初学者很有帮助,而且这种经验无法从书籍和教程中获得。” 那么开始吧,下面是我关于阅读源码的小技巧。

为什么我们需要读源码

我们程序员每天都要和源码打交道。经过数年的学习,大多数程序员可以“写”代码,或者至少是拷贝并修改代码。 而且,我们教授编程的方式强调编写代码的艺术,而不是如何阅读代码。 当我说“阅读代码”,我是指有意地专门阅读代码

众所周知,编程和写作有诸多相同之处。 唐纳德·克努特甚至引入了文学编程(literate programming) 编程范式。 编程与写作有相同的理念:表达我们的想法 。 还记得你在学校是怎么学习写作的吗?我们的写作能力来源于从小学开始直到现在的大量的文本阅读。 多年以来,我们阅读了不同难度的伟大作家的作品,并练习了多种写作技巧。

“如果你没时间读,你就没时间(或工具)写,就这么简单。” —— 斯蒂芬·金,《写作这回事:创作生涯回忆录》

正如斯蒂芬·金所观察到的那样,一个作家必须广泛而频繁地阅读,才能形成自己的声音, 并学会写出促使读者拿起书并痴读的句式和故事结构。 和读书一样,有意地阅读代码可以帮助程序员加速成长,尤其是对中级(intermediate)程序员而言。 这样做有三个好处。

站在巨人的肩膀上

我们从他人身上学习。优秀的源代码就像文学杰作,它不仅仅只提供了知识和信息,还提供了启迪。

通过浏览Linux内核、Redis、Nginx、Rails或其他著名项目, 你可以从全球范围的成千上万的顶级程序员那里汲取智慧。 在这些项目中可以找到无数的良好编程示例、编程范式选择、设计和架构。 向他人学习的另一个好处是能够避免常见的坑,大多数坑早已被他人踩过。

解决困难问题

在你的职业生涯中,你终将会碰到谷歌都无法解决的问题。 如果你还没碰到过这种问题,这只是因为你编程的时间还不足够长 😃。 阅读源码是调查这类问题的好方法,也是学习新东西的好机会。

扩展你的边界

大多数程序员只在少数特别领域编过程。 一般而言,如果你不时常推自己一把,你的编程技能会维持在你同事间的平均水平。 不要满足于修补bug或在现有系统中添加琐碎特性的工作。 相反,你可以试着扩展到一个新的领域,持续尝试找到一个你在日常工作中接触不到、但你感兴趣的领域。 这将从整体上拓宽你对编程的理解。

应该读什么样的源码

综上,阅读源码是有益的。 那么下一个问题,有这么多优秀作品可供选择,我们该选择并阅读什么样的源码呢? 你必须从选择目标开始。如果不在这个步骤上下点功夫,你从源码中学习的效果就会打折扣。 这里有一些典型场景:

  • 当你想学习一门新语言。学新语言可不只是学会语法 。 不管怎样,阅读源码是一个非常有效的学习新语言的方式。我从rust-rosetta 项目中学到了很多Rust语言知识。Rosetta Code 是一个收集同一批通用任务在不同语言上的解决方案的项目,这是一个可用来学习新语言的有用资源。
  • 当你想了解一个特殊的算法或实现。 例如,我们都会使用标准库中的sort函数,你有没有好奇过它是怎么实现的? 或者当你要使用Redis中的Set结构,它是用什么数据结构实现的? 为了解决这些疑惑,你只需要读源码中与之相关的实现部分,通常只有很少的文件或函数。
  • 当你在特殊的框架中编程。这意味着你对该框架已经有了一定的经验, 这是个阅读一些框架本身的源码的好机会。 很显然,了解框架的源码有助于提高你对框架的理解。
  • 当你想拓展进入新的领域,你可以阅读这个领域的经典著名的项目的源码。 比如说,如果你在做Web开发的工作,你对分布式系统感兴趣吗? 如果你的答案是“是”而且你懂Golang,也许etcd 是你的选择。你想钻研操作系统的内部构成吗?那么也许xv6 是一个好的开始。我们处在一个许多优秀开源项目都托管在了Github的好时代,请试着寻找一些这种项目。

记住,选择与你当前的编程技能与知识水平相当的项目。 如果你选择了远超你当前技能水平的项目,最终你会感到沮丧。 读一些相对较小的项目,接着读更大的项目。 如果目前你不能理解某些特定的代码片段,这意味着你有个知识缺口(knowledge gap)。 把代码放到一边去,试着读一些相关的书、论文或其他文档,当你更有信心时再回来接着读代码。 我们总能在一个模式中取得进展:读(代码、书、论文),写,更多的读,更多的写。

如何读源码

《How to read a book》 是一本指导人进行明智地阅读的书。作为初学者,我们值得投入时间和精力去思考我们应该如何阅读代码。 阅读代码不是件容易的事。 光是阅读源码是不够的,你要试着去理解他人的设计和想法。

预先准备

为了更有效率地阅读代码,你需要提前在手边准备这些东西:

  • 一个你可以熟练使用的编辑器。你需要拥有快速搜索关键字或变量名的能力。 有时你需要查找函数的引用和定义。和你的编辑器相处融洽些。 为了更加有效率,试着学习仅使用键盘操作编辑器。 这会使你专注于代码而不受打扰(译:指额外思考编辑器操作)。
  • 掌握基本的Git或其他版本控制工具的技能,这样你就能比较代码在版本间的差异。
  • 与源码有关的文档。文档可以为你的阅读提供参考,尤其是设计文档、编码规范等文档。
  • 具有一定的编程语言与设计模式的知识和经验。这对(阅读)大项目是强制性的。 如果你很了解一门编程语言,你也会了解关于源码组织与编程范式的最佳实践。 当然,这需要时间来积累。要有耐心。

流程与技巧

阅读过程不是线性的。你不会就那么一个接一个地读源文件。 相反,大多数时候我们会从顶到底地阅读代码。 下面是一些更有效率阅读代码的小技巧:

结合上下文阅读代码

当你阅读代码时,请持续提出问题。 例如,如果一个应用有缓存策略,一个好问题就是:如果键无效了会怎样?缓存中的值如何更新? 带着这些问题阅读代码,就是结合上下文。或者说因为你有了一个目标,你会变得享受阅读的过程。 你甚至可以自己做一些假设,然后在代码中寻找验证。

你有点像侦探:你想发现代码的真相,代码的逻辑,代码是如何像故事一般上下流动的。

把实例跑起来并与之交互

源码就像乐高积木,只是已经组装好了。 如果你想了解它们是怎么组装在一起的,你需要和它交互,有时甚至要把它拆开。 阅读同一模块的老版本同样有帮助。从Git中阅读版本差异,试着弄清楚特定的特性是如何实现的 (修改日志在这个场景很有用)。 举个例子,我发现Lua的第一个版本相当简单,这可以帮助我了解作者最初的设计理念。

Debug是另一种与代码交互的方式。试着在代码中加一些断点(或打印一些变量值), 然后弄明白打印到控制台中的所有输出。

如果你对代码了解比较透彻了,试着对代码做一些修改,重新build并把它跑起来。 最简单的方式是试着调整配置项,去看不同配置的运行结果。 之后你可以试着添加一些细微的特性。 如果这些特性对其他人也有用,你应该把代码贡献到上游。

了解数据结构间的关系

“糟糕的程序员担心代码,优秀的程序员担心数据结构和它们的关系。” -Linus Torvalds

数据结构是一个程序中最重要的元素。用笔或者你喜欢的其他工具画出数据结构间的关系。 这个图就是源码的映射。你会在阅读过程中时常参考这个图。一些工具比如scitools 可以用来生成UML类图。 (译:这个方法用在写代码中能节约翻Model声明文件的时间,推荐用纸笔,不占屏幕)

了解模块间的依赖关系与边界

大项目中会包含许多模块,一个模块经常只拥有单一职责。 这有助于我们减少代码复杂度,在适当的层级上做抽象。 模块的接口是抽象的边界,我们可以一个接一个地阅读模块。 如果你在阅读一个使用Make构建的C/C++项目,Makefile是了解模块间如何组织的好切入点。

边界本身也很有用。优秀的代码组织得很好,变量名与函数名的命名风格体现着可读性。 你不需要阅读全部源文件,你可以忽略不重要的或你熟悉的部分。 如果你确定一个模块是仅仅是为了被解析而设计的(just designed for parsing), 那么你已经大致了解了它的功能;那么你就可以跳过不读这个模块。 当然,这将大大节约时间。

使用测试用例

测试用例也是帮助代码理解的一个很好的补充。测试用例就是文档。 当你在阅读一个类时,试着把对应的测试代码一起读了。 测试用例能帮你弄清一个类的接口,和该类的典型用法。 集成测试用例可以让你顺着走过程序的整体流程,适合输入一些特殊值并debug运行。

点评

为什么不在花了不少时间阅读一个项目后,写一篇代码点评呢?就像写一篇书评一样。 你可以写下代码中好的和不好的部分,还可以记下你从中学到了什么。 攥写这类文章可以帮助你阐明自己的理解,也有助于其他人阅读源码。

一些好书

我发现阅读代码是一个远超我想象的广泛话题。没有系统性训练该技能的方法。 总而言之,不断练习,找到你自己的方式。下面是一些帮助你提升代码阅读能力的好书:

《Design Patterns: Elements of Reusable Object-Oriented Software》

《Clean Architecture: A Craftsman’s Guide to Software Structure and Design》

《How to Read a Book: The Classic Guide to Intelligent Reading》

啊哈,这本书对程序员也很有用。

译者的话

这篇文章是为苦于不知从何开始阅读陌生项目代码的人(包括我)准备的。 截止撰文时间为止,我认为这篇文章是在同话题中较为务实的一篇, 不仅包含理念,还包含不少具体可实践的建议,因此我想把它分享给更多人。 而且我想如果它有中文版本,愿意读下去的人会多一些,我也方便向周围的人分享,于是我向Nic申请了翻译权。 我把标题改了改,这样搜索“源码”“学习”、“阅读源码”都能搜到。

这篇文章对我而言,最大作用是帮我突破了“不需要有意地专门阅读代码”的心理障碍。 我一直以为既然“写代码的时间中十之六七都是花在读既有代码上”(语出《Clean Code》), 就没必要再花时间专门读代码,我大错特错。

个人经验:驱动我“专门读代码”的最大动机是好奇心,和小孩拆小物件为了看内部构造差不多。 学习OO设计模式,我建议阅读《Head First 设计模式》 ,这本书超有趣,比四人帮那本删减了一些不常用的模式,但是你能轻松读下去。

阅读了解项目代码是参与(开源)项目的第一步,希望这篇文章能帮助你参与到心仪的项目中去。 这也可以帮助你在工作中了解同组同事的工作,而“了解同组同事的工作对工作有诸多潜在益处” 。 来吧,花点时间挑一个看上眼的项目(或者就读你手头的项目别人写的部分), 找到你最感兴趣的功能,读一读它是怎么实现的。

转载于:https://boholder.github.io/blogs/learn-from-source-code/#contents:%E5%BA%94%E8%AF%A5%E8%AF%BB%E4%BB%80%E4%B9%88%E6%A0%B7%E7%9A%84%E6%BA%90%E7%A0%81



我看到回答里也有人提到了我的书《通用源码阅读指导书》。这本书才刚上架还没几天,就有人提及了,十分荣幸。

本回答将按照下面的章节体系进行:

1 我为什么阅读源码
2 阅读源码的好处
3 阅读源码的困难
4 阅读源码的步骤
5 阅读源码的方法

1 我为什么阅读源码

我开始阅读源码是在进行互联网开发的第八九个年头。在此之前,我做过校园网站,接过网站开发的私活,进行过理论算法相关的研究,也设计开发了许多系统。我对我做过的系统都比较有信心,它们也都运行的不错,但是一个疑问却在我的心头逐渐浮现:我的架构和世界最优良架构之间的差距到底有多大?

阅读开源项目的源码能给我答案。

许多优秀的开源项目历经数千开发者的数万次提交,被数亿用户使用。这些项目从可扩展性、可靠性、可用性等各个角度考量,都是十分优良的。通过阅读这些项目的源码能让我找到自己在软件设计和开发上的不足。

于是我开始了我的源码阅读计划。

2 阅读源码的好处

阅读源码的好处很多,而且已经成为共识。光靠铺天盖地的源码阅读活动就能感知一二,虽然我觉着那些活动都太过浮躁,主要是个噱头,学不到啥。

但是不可否认,阅读源码的好处极多,包括但不限于:

  • 透彻地理解项目的实现原理
  • 接触到成熟和先进的架构方案
  • 学习到可靠与巧妙的实施技巧
  • 发现自身知识盲点,提升自身知识储备

所以,我有些后悔,我觉着我应该更早地开始源码阅读。我推荐进行源码阅读的时机是:学会编程语言的基本知识,并做过少量项目之后。

我们知道,学编程的过程中在不断接收新的知识,进步很快。刚开始做项目时,进入实践领域,进步很快然后就到了平台期,可能几年下来都是重复的增删改查,毫无进步。这段平台期实际是进行源码阅读的良好时机,能让你的技术持续进步。

3 阅读源码的困难

当我们阅读一份源码时,需要面对的困难通常有:

  • 难以归纳的凌乱文件
  • 稀奇古怪的类型组织
  • 混乱不堪的逻辑跳转
  • 不明其意的方法变量
  • ……

所以很多人即使知道阅读源码的好处,也坚持不下来。就因为以上困难难以克服。

而阅读源码的难,是正常的。

3.1 时间维度带来的困难

从时间维度上看,每一个优秀的工程项目都经历了从雏形到成熟的曲折演化过程。而整个过程都会被映射到一个时间点上,肯定很难。

于是有人提出了“阅读源码时,从第一次提交开始阅读”的主意。我要说一下,这是错误的!

这是一种纸上谈兵式的错误,提出这种方法的人肯定没怎么读过源码。事实上,我也有过这种想法,然后在实践中很快被抛弃了。

如果一个项目只经过了几次提交,这种方法获取可行。而一个优秀的通常有几千上万次提交,将这几千上万次提交都读完,工作量要比把当前最新版本的代码读完难的多得多!因为其时间维度的复杂度比空间维度复杂度大得多了。

我用下面的图片展示了项目发展的过程,一个项目从版本1到版本n,逐渐完善。版本1一般是最简单最基础的,但是,要想从版本1读起,一直读到版本n,可能要阅读几千个版本。难度极大,远不如直接去读版本n。

img

还有,许多项目的第一次提交并不是只有几行代码的init项目,往往第一次提交就是几百个类。而且,初期的代码组织还比较混乱。毕竟,作者也不知道该项目能火,往往比较随意。

更别说,历史提交中还夹杂很多的Bug、回退、依赖包升级等等。想要把每次Bug、回退、依赖包升级所产生的历史背景都搞清楚,这几乎是不可能的任务。

3.2 空间维度的困难

每一个优秀的工程项目都凝聚了众多开发者的缜密思维逻辑,而这些逻辑都被投射到了平面的代码上。这使得阅读源码的过程成了逆推开发者思维逻辑的过程,显然,逆推是很难的。

不过,这方面的困难真有解决方案:详细了解开源项目的功能,然后自己琢磨该如何实现

这样,就将**“逆推”的过程转化为了“正向推导+验证”的过程**,这样就相对简单了。

但无论如何,阅读源码总不会太简单。于是有人说读懂源码比编写源码更为困难,想必也是有一定道理的。

4 阅读源码的步骤

阅读源码推荐按照下面的步骤进行:

  1. 了解项目的背景。这相当于了解了项目的需求。
  2. 充分了解项目的功能,并熟练使用。我们说过,将“逆推”的过程转化为“正向推导+验证”的过程是阅读源码的一个重要方法。而了解项目的功能是这个方法的基础。
  3. 调试代码。通过断点调试,掌握整个代码的流程走向、主次关系、依赖关系。
  4. 划分源码。根据每个包、模块的功能不同,将源码划分为几个部分。
  5. 由底层到上层,逐个击破。先阅读不依赖其他模块的,或者依赖其他模块比较少的基础模块。然后逐步上移,完成整个项目源码的阅读。
  6. 横向总结。在由底层到上层阅读完成后,横向以功能点为维度串联源码的实现逻辑。

一般情况下,一个项目的源码要在3~4个月左右读完。否则,可能读到后面的,已经忘记前面的功能了。

当然越快越好,但考虑到我猿们还要加班,时间太短了不太现实。

5 阅读源码的方法

有人说读懂源码比编写源码更为困难,不无道理。而源码阅读和软件开发一样,是一个非常综合的能力,没有一个万全之法。但有很多不错的方法。

例如一些常用的方法:

  • 调试追踪:多数情况下,当我们对某些变量的含义产生疑惑时,借助开发工具的调试功能直接查看变量值的变化是一个非常好的方法。而且该方法还能指引代码逻辑的跳转过程,对于理解源码极为有用。
  • 归类总结:优秀的源码都遵循一定的设计规则,这些规则可能是项目间通用的,也可能是项目内独有的。在源码阅读的过程中将这些设计规则总结出来,将会使得源码阅读的过程越来越顺畅。
  • 上下文整合:有些对象、属性、方法等仅仅通过自身很难判断其作用和实现。此时可以结合其调用的上下文,查看对象何时被引用、属性怎样被赋值、方法为何被调用,这对于了解它们的作用和实现很有帮助。
  • 类比阅读:如果存在实现某功能的两条或者多条路径,可以类别阅读,找出他们处理逻辑的不同。
  • ……

还有很多方法,不再一一介绍。因为没有实在的源码案例,光介绍显得很浮。

如果大家真的决定阅读源码,开始提升自己架构能力和编程能力的新阶段,可以阅读《通用源码阅读指导书——MyBatis源码详解》。

是一本以MyBatis源码为材料,详细介绍源码阅读相关方法和技巧的源码阅读指导书籍。

该书以实际开源项目MyBatis为例,详细介绍了源码阅读的经验和方法。在源码阅读中,透彻分析了相关的背景知识、组织方式、逻辑结构、实现细节。在本书的讲解中,不漏过每一个类,不跳过每一个难点,做到深浅一致。并对这些源码阅读方法进行了进一步的总结整理。

阅读源码会使你有很大的提升,但是这个过程也确实需要你沉下心来慢慢进行。

但我保证,只要你读完一个项目的源码,就会收获巨大。

最后,用书籍前言中的一句话结尾。

img

我是高级软件架构师易哥,欢迎大家关注我。

我会偶尔分享软件架构编程相关的知识技巧。

转载于:https://www.zhihu.com/question/19625320



阅读源码是每个优秀开发工程师的必经之路,那么这篇文章就来讲解下为什么要阅读源码以及如何阅读源码。

首先来说下为什么要读源码,有学习源码的必要吗?

为什么要阅读源码?

关于为什么阅读和学习源码,我个人认为可能有以下几点:

(一)吊打面试官,应对面试

为了找到更好的工作,应对面试,因为在面试中肯定会问到源码级别的问题,比如:为什么 HashMap 是线程不安全的?

如果你没有阅读过源码,面试官可能会对回答的结果不满意,进而导致面试结果不太理想,但如果你对源码有所研究,并能够很好地问答面试官的问题,这可能就是你的加分点,可以形成自己独特的竞争力,吊打面试官,升职加薪不是梦。

(二)解决问题(bug)

在开发过程中,我们或多或少会遇到 bug,比如:在 foreach 循环里进行元素的 remove/add 操作,为啥有可能会报 ConcurrentModificationException 异常?

我们可以先在 Google、Stack Overflow 以及对应项目的 Issues 里看有没有类似问题以及解决办法,如果没有的话,我们只能通过阅读源码的方式去解决了。如果我们对相关源码有所涉猎,就可以快速定位到问题所在。

(三)提升编程能力

读一本好书,就是和许多高尚的人谈话。 -歌德

和阅读一本好书一样,阅读源码就是和编程大牛面对面交流的机会,在许多优秀的开源项目中,它们的编码规范和架构设计都是很棒的,另外在设计上也使用了大量的设计模式,通过阅读和学习源码,能够快速提升我们的编码水平,以及对设计模式有更深的理解。

同时,在我们阅读完一个源码后,可以触类旁通,能够快速地对其他框架的源码进行阅读和学习,减少时间成本。

除了上述提到的原因之外,可能还有许多,在这里就不一一赘述了,那么在确定了要阅读源码之后,就让我们看下如何阅读源码吧!

如何阅读源码?

如何阅读源码取决于你为什么要读源码,比如:

  • 如果为了应对面试,那就可以围绕常考的基础类、集合类、队列、线程、锁等内容进行阅读和学习;
  • 如果是为了解决 bug,那么就可以只围绕出现问题的相关类进行阅读分析,随着解决 bug 的增多,我相信阅读的源码也会越多,从而更容易去阅读和学习源码;

下面大概说下阅读源码的几点建议:

在阅读之前,可以先从开源项目的官网上看它的架构设计和功能文档,了解这个项目的整体架构、模块组成以及各个模块之间的联系

如果没有对应的项目文档,可以根据代码的模块进行梳理,以形成对项目的初步了解,或者查看已有的源码解析文章或者书籍,在阅读源码之前,了解项目的架构和思路会使阅读源码事半功倍。

在了解一个类的时候,可以使用 ctrl+F12 来查看类中的成员变量和方法。

img

可以通过 IDEA 的 Diagrams 功能去了解一个类的继承关系。

img

多打断点调试,断点追踪源码是很好的阅读源码的方式,可以先通过 debug 了解下调用逻辑,都和哪些类有关联,有大致了解后再通过 debug 了解整体代码的功能实现,各个类都起到了什么作用,有没有涉及到设计模式等。

另外,优秀的开源项目中肯定会有许多地方应用到了设计模式,建议在阅读源码之前,需要对常用的设计模式有大致的了解,不然阅读源码的效率会大大降低。

如果遇到读不懂某部分源码的时候,可以先跳过,之后再回来看,如果属于搞不懂这部分就茶不思饭不想的人,可以在网上找是否有该部分源码的解析或者文档,也可以自己通过源码注释和测试用例去阅读学习。

一般优秀的开源项目都会有单元测试,可以通过对应类的单元测试去了解方法的含义和用法,加深对源码逻辑的理解。

在阅读源码的时候,可以在代码上加上注释和总结,同时还可以画出时序图和类图,这样对阅读源码有很大的帮助,可以很清楚地知道类之间的调用关系和依赖关系,也方便以后回顾,重新阅读。

在这里推荐大家一个 IDEA 插件 SequenceDiagram,可以根据源码生成调用时序图,便于阅读源码。

img

刚开始阅读源码,不建议直接看框架源码,可以先从 jdk 源码看起:

img

jdk 源码也是非常庞大的,可以分模块来阅读,下面是建议的阅读顺序:

  1. java.lang 包下的基本包装类(Integer、Long、Double、Float 等),还有字符串相关类(String、StringBuffer、StringBuilder 等)、常用类(Object、Exception、Thread、ThreadLocal 等)。
  2. java.lang.ref 包下的引用类(WeakReference、SoftReference 等)
  3. java.lang.annotation 包下的注解的相关类
  4. java.lang.reflect 包下的反射的相关类
  5. java.util 包下为一些工具类,主要由各种容器和集合类(Map、Set、List 等)
  6. java.util.concurrent 为并发包,主要是原子类、锁以及并发工具类
  7. java.iojava.nio 可以结合着看
  8. java.time 主要包含时间相关的类,可以学习下 Java 8 新增的几个
  9. java.net 包下为网络通信相关的类,可以阅读下 SocketHTTPClient 相关代码

其他包下的代码也可以做下了解,JDK源码阅读笔记:https://github.com/wupeixuan/…

再有了一定的源码阅读经验后,可以再去学习 Spring、Spring Boot、Dubbo、Spring Cloud 等框架的源码。

总结

本文主要介绍了为什么读源码以及如何读源码,供大家参考,每个人都有适合自己的阅读源码的方式,希望可以在学习中去摸索出一套属于自己的方式。

阅读源码不是一蹴而就的,这是持久战,只要你能够坚持下来,肯定受益匪浅。阅读源码的过程比较枯燥,可以在社群里一起讨论学习,这样可能效率更高些。

写得不好的或者大家有什么更好的建议,也欢迎留言讨论。

最好的关系就是互相成就,大家的在看、转发、留言三连就是我创作的最大动力。

参考

《Java并发编程之美》

《程序员修炼之道:程序设计入门30讲》

面试官系统精讲Java源码及大厂真题

解锁大厂思维:剖析《阿里巴巴Java开发手册》



阅读源代码的能力算是程序员的一种底层基础能力之一,这个能力之所以重要,原因在于:

  • 不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目,比如接手一个其他人的项目。
  • 阅读优秀的项目源码是学习他人优秀经验的重要途径之一,这一点我自己深有体会。

读代码与写代码是两个不太一样的技能,原因在于“写代码是在表达自己,读代码是在理解别人”。因为面对的项目多,项目的作者有各自的风格,理解起来需要花费不少的精力。

我从业这些年泛读、精读过的项目源码不算少了,陆陆续续的也写了一些代码分析的文章,本文中就简单总结一下我的方法。

先跑起来

开始阅读一份项目源码的第一步,是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。

有的项目比较复杂,依赖的组件多,搭建起一个调试环境并不容易,所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来,那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。

就我的经验而言,一个项目代码,是否能顺利的搭建调试环境,效率大不一样。

跑起来之后,又要尽量的精简自己的环境,减少调试过程中的干扰信息。比如,Nginx使用多进程的方式处理请求,为了调试跟踪Nginx的行为,我经常把worker数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。

再比如,很多项目默认是会带上编译优化选项或者去掉调试信息的,这样在调试的时候可能会有困扰,这时候我会修改makefile编译成-O0 -g,即编译生成带上调试信息且不进行优化的版本。

总而言之,跑起来之后的调试效率能提升很多,而在跑起来的前提之下又要尽量精简环境排除干扰的因素。

明确自己的目的

尽管阅读项目源码很重要,但是并不见得所有项目都需要从头到尾看的清清楚楚。在开始展开阅读之前,需要明确自己的目的:是需要了解其中一个模块的实现,还是需要了解这个框架的大体结构,还是需要具体熟悉其中的一个算法的实现,等等。

比如,很多人看Nginx的代码,而这个项目有很多模块,包括基础的核心模块(epoll、网络收发、内存池等)和扩展具体某个功能的模块,并不是所有这些模块都需要了解的非常清楚,我在阅读Nginx代码的过程中,主要涉及了以下方面:

  • 了解Nginx核心的基础流程以及数据结构。
  • 了解Nginx如何实现一个模块。

有了这些对这个项目大体的了解,剩下的就是遇到具体的问题查看具体的代码实现了。

总而言之,并不建议毫无目的的就开始展开一个项目的代码阅读,无头苍蝇式的乱看只会消耗自己的时间和热情。

区分主线和支线剧情

有了前面明确的阅读目的,就能在阅读过程中区分开主线和支线剧情了。比如:

  • 想了解一个业务逻辑的实现流程,在某个函数中使用一个字典来保存数据,在这里,“字典这个数据结构是如何实现的”就属于支线剧情,并不需要深究其实现。

在这一原则的指导下,对于支线剧情的代码,比如一个不需要了解其实现的类,读者只需要了解其对外接口,了解这些接口的入口、出口参数以及作用,把这部分当成一个“黑盒”即可。

顺便一提的是,早年间看到一种C++的写法,头文件中只有一个类的对外接口声明,将实现通过内部的impl类转移到C++文件中,比如:

头文件:

// test.h
class Test {
public:
  void fun();

private:
  class Impl;
  Impl *impl_;
};

C++文件:

void Test::fun() {
  impl_->fun()
}

class Test::Impl {
public:
  void fun() {
    // 具体的实现
  }
}

这样的写法,让头文件清爽了很多:头文件中没有与实现相关的私有成员、私有函数,只有对外暴露的接口,使用者一目了然就能知道这个类对外提供的功能。

impl

“主线”和“支线”剧情在整个代码阅读的过程中经常切换,需要阅读者有一定的经验,清楚自己在这段代码的阅读中哪部分属于主线剧情。

纵向和横向

代码阅读过程中,分为两个不同的方向:

  • 纵向:顺着代码的顺序阅读,在需要具体了解一个流程、算法的时候,经常需要纵向阅读。
  • 横向:区分不同的模块进行阅读,在需要首先弄清楚整体框架时,经常需要横向阅读。

两个方向的阅读,应该交替进行,这需要代码阅读者有一定的经验,能够把握当前代码阅读的方向。我的建议是:过程中还是以整体为首,在不理解整体的前提之前,不要太过深入某个细节。把某个函数、数据结构当成一个黑盒,知道它们的输入、输出就好,只要不影响整体的理解就暂且放下接着往前看。

情景分析

假如有了前面的基础,已经能够让项目顺利在自己的调试环境跑起来了,也明确了自己想了解的功能,那么就可以对项目代码进行情景分析了。

所谓的“情景分析”,就是自己构造一些情景,然后通过加断点、调试语句等分析在这些场景下的行为。

以我自己为例,在写《Lua设计与实现》时,讲解到Lua虚拟机指令的解释和执行过程中,需要针对每个指令做分析,此时用的就是情景分析的方法。我会模拟出来使用该指令的Lua脚本代码,然后在程序里断点调试这些场景下的行为。

我惯用的做法,是在某个重要的入口函数上面加上断点,然后构造触发场景的调试代码,当代码在断点处停下,通过查看堆栈、变量值等等来观察代码的行为。

例如,Lua解释器代码中中,生成Opcode最终都会调用函数luaK_code,那么我就在这个函数上面加上断点,然后构造我想要调试的场景,只要在断点处中断,我通过函数堆栈就能看到完整的调用流程:

(lldb) bt
* thread #1: tid = 0xb1dd2, 0x00000001000071b0 lua`luaK_code, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001000071b0 lua`luaK_code
frame #1: 0x000000010000753e lua`discharge2reg + 238
frame #2: 0x000000010000588f lua`exp2reg + 31
frame #3: 0x000000010000f15b lua`statement + 3131
frame #4: 0x000000010000e0b6 lua`luaY_parser + 182
frame #5: 0x0000000100009de9 lua`f_parser + 89
frame #6: 0x0000000100008ba5 lua`luaD_rawrunprotected + 85
frame #7: 0x0000000100009bf4 lua`luaD_pcall + 68
frame #8: 0x0000000100009d65 lua`luaD_protectedparser + 69
frame #9: 0x00000001000047e1 lua`lua_load + 65
frame #10: 0x0000000100018071 lua`luaL_loadfile + 433
frame #11: 0x0000000100000eb9 lua`pmain + 1545
frame #12: 0x00000001000090cd lua`luaD_precall + 589
frame #13: 0x00000001000098c1 lua`luaD_call + 81
frame #14: 0x0000000100008ba5 lua`luaD_rawrunprotected + 85
frame #15: 0x0000000100009bf4 lua`luaD_pcall + 68
frame #16: 0x00000001000046fb lua`lua_cpcall + 43
frame #17: 0x00000001000007af lua`main + 63
frame #18: 0x00007fff6468708d libdyld.dylib`start + 1

情景分析的好处在于:不会在一个项目中大海捞针似的查找,而是能够把问题缩小到一个范围内展开来理解。

“情景分析”这一概念不是我想出来的名词,比如有这么几本分析代码的书籍,如:《Linux内核源代码情景分析》《Windows内核情景分析》

利用好测试用例

好的项目都会自带不少用例,这类型的例子有:etcd、google出品的几个开源项目。

如果测试用例写的很仔细,那么很值得好好去研究一下。原因在于:测试用例往往是针对某个单一的场景,独自构造出一些数据来对程序的流程进行验证。所以,其实跟前面的“情景分析”一样,都是让你从大的项目转而关注具体某个场景的手段之一。

厘清核心数据结构之间的关系

虽然说“程序设计=算法+数据结构”,然后我实际中的体会,数据结构更加重要。

因为结构定义了一个程序的架构,结构定下来了才有具体的实现。好比盖房子,数据结构就是房子的框架结构,如果一间房子很大,而你并不清楚这个房子的结构,会在这里面迷路。而对于算法,如果属于暂时不需要深究的细节部分,可以参考前面“区分主线和支线剧情”部分,先了解其入口、出口参数以及作用即可。

Linus说: “烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”

因此,在阅读一份代码时,厘清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系,我的源码分析类博客中有很多这样的例子,比如《Leveldb代码阅读笔记》《Etcd存储的实现》等等。

需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。

比如,你如果现在刚接手某个项目,需要简单的了解一下项目,可以先阅读代码了解都有哪些核心数据结构。理解了之后,如果不清楚某些情景下的流程,可以使用情景分析法。总而言之,交替进行直到解答你的疑问为止。

多问自己几个问题

学习的过程中离不开交互。

如果阅读代码只是输入(Input),那么还需要有输出(Output)。只有简单的输入好比喂东西给你吃,而只有更好的消化才能变为自己的营养,而输出就是更好消化知识的重要手段。

其实这个思想很常见,比如学生上课(Input)了需要做练习作业(Output),比如学了算法(Input)需要自己编码练习(Output),等等。简而言之,输出是学习过程中的一种及时反馈,质量越高学习效率越高。

输出的手段有很多,在阅读代码时,比较建议的是自己能够多问自己一些问题,比如:

  • 为什么选择这个数据结构来描述这个问题?类似的场景下,其他项目是怎么设计的?都有哪些数据结构做这样的事情?
  • 如果由我来设计这样的项目,我会怎么做?

等等等等。越是主动积极的思考,就越有更好的输出,输出质量与学习质量成正比关系。

写自己的代码阅读笔记

我从开始写博客,就是写不少各种项目的代码解读类文章,网名“codedump”也源于想把“code内部的实现原理dump出来”之意。

前面提到学习质量与输出质量成正比关系,这是我自己的深刻体会。也因为如此,所以才要坚持阅读源码之后写自己的分析类笔记。

写这类笔记,有以下几个需要注意的地方。

虽然是笔记,但是要想象着在向一个不太熟悉这个项目的人讲解原理,或者想象一下是几个月甚至几年后的自己回头来看这个文章。在这种情况下,会尽量的把语言组织好,循循善诱的解释。

尽量避免大段的贴代码。我认为在这类文章中,大段贴上代码有点自欺欺人:就是看上去自己懂了,其实并不见得。如果真要解释某段代码,可以使用伪代码或者缩减代码的方式。记住:不要自欺欺人,要真的懂了。如果真的想在代码上加上自己的注释,我有一个建议是fork出来一份该项目某个版本的代码,提交到自己的github上,上面随时可以加上自己的注释并且保存提交。比如我自己注释的etcd 3.1.10代码:etcd-3.1.10-codedump,类似的我阅读的其他项目都会在github上fork出一个带上codedump后缀的项目。

多画图,一图胜千言,使用图形展示代码流程、数据结构之间的关系。我最近才发现画图能力也是很重要的能力,自己在从头学习如何使用图像来表达自己的想法。

写作是很重要的基础能力,我一个朋友最近教育我,大体的意思是说:如果你在某方面的能力很强,如果再加上写作好、英语好,那么将极大放大你在这方面的能力。而类似写作、英语这样的底层基础能力,不是一撮而就的,需要长时间保持练习才可以。而写博客,对于技术人员而言,就是一种很好的锻炼写作的手段。

PS:如果很多事情,你当时做的时候能想到今后面对这个输出的人是你自己,比如自己写的代码后面要自己维护、自己写的文章后面给自己看,等等的,世界会美好很多。比如写技术博客这些事情,因为我在写的时候考虑到以后看这份文档的人可能就是我本人,所以在写的时候会尽量的清晰、易懂,力图我自己一段时间后再看到自己的这份文档时,能够马上回忆起当时的细节,也正是因为这样,我很少在博客里贴大段的代码,尽可能的补充图例。

总结

以上是我简单总结的一些阅读源码时候的手段和注意方法,大体而言有那么几点吧:

  • 只有更好的输出才能更好的消化知识,所谓的搭建调试环境、情景分析、多问自己问题、写代码阅读笔记等都是围绕输出来展开的。总而言之,不能像一条死鱼一样指望着光靠看代码就能完全理解它的原理,需要想办法跟它互动起来。
  • 写作是人的基础硬实力之一,不仅锻炼自己表达能力,还能帮助整理自己的思路。对程序员而言锻炼写作能力的手段之一就是写博客,越早开始锻炼越好。

最后,如同任何可以习得的技能一般,阅读代码这种能力也需要长时间、大量的反复练习,下一次就从自己感兴趣的项目开始锻炼自己的这种技能吧。

转载于:https://www.codedump.info/post/20200605-how-to-read-code-v2020/

  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小熊coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值