乐趣区

关于源代码:如何阅读一份源代码

浏览源代码的能力算是程序员的一种底层根底能力之一,这个能力之所以重要,起因在于:

  • 不可避免的须要浏览或者接手别人的我的项目。比方调研一个开源我的项目,比方接手一个其他人的我的项目。
  • 浏览优良的我的项目源码是学习别人优良教训的重要途径之一,这一点我本人深有体会。
    读代码与写代码是两个不太一样的技能,起因在于“写代码是在表白本人, 读代码是在了解他人”。因为面对的我的项目多,我的项目的作者有各自的格调,了解起来须要破费不少的精力。

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

先跑起来

开始浏览一份我的项目源码的第一步,是先让这个我的项目可能通过你本人编译通过并且顺利跑起来。这一点尤其重要。

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

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

跑起来之后,又要尽量的精简本人的环境,缩小调试过程中的烦扰信息。比方,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() {// 具体的实现}
}

这样的写法,让头文件清新了很多:头文件中没有与实现相干的公有成员、公有函数,只有对外裸露的接口,使用者高深莫测就能晓得这个类对外提供的性能。

“主线”和“干线”剧情在整个代码浏览的过程中常常切换,须要阅读者有肯定的教训,分明本人在这段代码的浏览中哪局部属于主线剧情。

纵向和横向

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

  • 纵向:顺着代码的程序浏览,在须要具体理解一个流程、算法的时候,常常须要纵向浏览。
  • 横向:辨别不同的模块进行浏览,在须要首先弄清楚整体框架时,常常须要横向浏览。
    两个方向的浏览,应该交替进行,这须要代码阅读者有肯定的教训,可能把握以后代码浏览的方向。我的倡议是:过程中还是以整体为首,在不了解整体的前提之前,不要太过深刻某个细节。把某个函数、数据结构当成一个黑盒,晓得它们的输出、输入就好,只有不影响整体的了解就暂且放下接着往前看。

情景剖析

如果有了后面的根底,曾经可能让我的项目顺利在本人的调试环境跑起来了,也明确了本人想理解的性能,那么就能够对我的项目代码进行情景剖析了。

所谓的“情景剖析”,就是本人结构一些情景,而后通过加断点、调试语句等剖析在这些场景下的行为。

以我本人为例,在写《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:如果很多事件,你过后做的时候能想到今后面对这个输入的人是你本人,比方本人写的代码前面要本人保护、本人写的文章前面给本人看,等等的,世界会美妙很多。比方写技术博客这些事件,因为我在写的时候思考到当前看这份文档的人可能就是我自己,所以在写的时候会尽量的清晰、易懂,力求我本人一段时间后再看到本人的这份文档时,可能马上回忆起过后的细节,也正是因为这样,我很少在博客里贴大段的代码,尽可能的补充图例。

总结

以上是我简略总结的一些浏览源码时候的伎俩和留神办法,大体而言有那么几点吧:

  • 只有更好的输入能力更好的消化常识,所谓的搭建调试环境、情景剖析、多问本人问题、写代码浏览笔记等都是围绕输入来开展的。总而言之,不能像一条死鱼一样指望着光靠看代码就能齐全了解它的原理,须要想方法跟它互动起来。
  • 写作是人的根底硬实力之一,不仅锤炼本人表达能力,还能帮忙整顿本人的思路。对程序员而言锤炼写作能力的伎俩之一就是写博客,越早开始锤炼越好。

最初,如同任何能够习得的技能个别,浏览代码这种能力也须要长时间、大量的重复练习,下一次就从本人感兴趣的我的项目开始锤炼本人的这种技能吧。

作者:codedump

起源:https://www.codedump.info/pos…

扫码关注微信公众号,获取更多精彩文章~

wechat:Databend

https://databend.rs

退出移动版