感谢这个时代,我们有了github,有了近乎无穷的开源项目可以看,可以学。
记住,看的目的是学,但是看不等于学!
从小代码集看起
对于一个新手,是绝对不适合一上来就追求spring,web容器,数据库这种级别的代码。万事开头总是要从简单的来。如果没有太多阅读开源代码经验的话,请从一个代码量在千行级别或更小的repository开始吧。在阅读代码中,也慢慢留意一些约定,如
- 代码的文档一般写哪里了(README.md ? 或者 docs目录?)
- 代码的起名字和目录组织大概遵循什么规范
- 代码如何配置
- 代码如何build
这些将帮你构建一个初步的可以深入代码的路径,并为进一步深入理解代码打基础。
这一点不得不夸一下javascript的npm。npm有相当多的很小的好代码。老牌web框架express的代码数才4000多行。一些小工具比如随机字符串产生器、分布式ID产生器只有数十行。非常适合入门学习。
聚焦
有多少人是抱着一颗不切实际的预期去看代码的?有人说,我Java想提升一下去看看Spring吧;有人说我Web没有太理解,去看看Tomcat吧。结果可想而知。正像问问题时不应该问过于宽泛的问题,看任何代码都不应该抱有看一遍全都看懂的期望——因为就连代码作者自己都做不到。
写代码时往往都会做一些抽象,把某个特定问题拆解。比如分层、比如抽象为一个class代表一个实际的概念等等。每个抽象都可能解决一个具体的问题。看代码也是如此,一定要先聚焦,把看代码的scope限制住,不要贪多。
比如当看Java Collections代码,可能是希望学习其数据结构的实现方式——到底链表、树、跳表等是怎么实现的,内存中一个个object是如何关联起来的,如何被快速访问的;又或者是特定算法的实现(比如Collections.sort用的是什么排序算法)。此时,其他的部分就不要太过于在意,直接忽略那些抽象隐藏起来的地方,以及不相关的细节。
对于更复杂的服务就更要聚焦。比如Jetty解决了相当多问题,比如:如何启动、如何找到Java Runtim、如何加载配置文件、如何load核心class、如何打log如何处理IO、如何解析HTTP协议的数据、如何将数据转换为Servlet标准的处理、如何管理集群…… 并且,这些代码里还会夹杂着一些设计模式的层,比如XXXXFactory,XXXXAdaptor…… 如此复杂的代码,即使是很有经验的人也不可能兼顾着在短时间内全看懂。所以,每次看代码之前,务必先确定一个要学习的目的。如果代码量很庞大,就可以安排一个学习计划,每次聚焦于一个目的。
对于像Java这样的面向对象语言,优秀的设计往往都是基于一组代表概念的类生成的对象的相互交互。学习代码时,优先去看类名,组织起高层的全局感非常重要。比如,如果希望学习一个“驾驶模拟”系统的代码中有关“如何驾驶”的部分,一定会找到代表操作人、油门和方向盘概念的类。然后去观察他们是如何互动的。此时绝对不该扯上变速箱、发动机和传动轴。
作为像C这样的语言,其源码大致是面向过程的,即分多个步骤做一件事,每个步骤再细分为更多子步骤。例如,nginx分配一段内存来存储一个http请求头就大概包括
- 利用一个工具函数分配指定大小的内存
- 从socket中将数据读出来,并填充到分配的内存上
而第一步可以进一步细化为:从一个内存池把一段内存借出来,如果没有可用内存了就得找操作系统要,要到了内存可能还需要填充为零……等等子步骤。这时预先画画流程图对理解代码会非常有帮助。
总之,如果你被庞大的源码打败,大概率不是因为你笨,而是因为你过于贪心急躁了。
请先看文档
任何代码,总得有个出现的动机和大致的工作原理。好的代码一般会在文档里(一般是README.md)里讲得比较明白。比如Redis的源代码的README.md就给出了非常概要性的信息,见https://github.com/antirez/redis。如果还需要对某个特定的主题的解释,Redis官网也提供了大量的文字来说明,比如
- 解释Redis的常用数据类型 —— https://redis.io/topics/data-types-intro
- 解释如何实现一个key过期 —— https://redis.io/commands/expire
- 解释如何做主从复制 —— https://redis.io/topics/replication
- ……
这些文档能非常好的指导阅读相关的源代码。
更复杂的开源系统往往都有对应的书籍来解释其内部工作原理。比如,《MySQL技术内幕:InnoDB存储引擎》是很好的指引如何理解InnoDB源代码的书,大名几乎每个做业务的同学都会接触到;当年侯捷先生的《深入浅出MFC》非常细致的剖析了MFC内部的C++是怎么把本来C++运行时做不到的事情通过一些歪招给搞定的;《C专家编程》(又称鱼书),用很多小例子来解释比如一个复杂的函数指针的是怎么被parse的,变量是如何被保存和传递的。等等等等。
如果你阅读的是著名系统的源代码,请尽量先从文档/书籍入手找到切入点,往往能事半功倍。对于可读性,给人写的东西总是好过给机器写的东西啊。
关注资源的生命周期
有生产意义的系统一般总是会有一些核心的资源需要管理,而这些资源的生命周期的维护往往是这类系统代码的核心。
比如
- 对于spring-core来讲,其核心资源是“Bean”。一个Bean被创建、初始化、被使用、被解构,是整套代码的核心;
- 对于spring-webmvc,其核心资源是“HTTP 请求”。一个http请求从被收到开始、其数据被注入到请求handler,其返回的数据结构被设定,是整套代码的核心;
- 对于一个池 (比如commons-pool,thread-pool),其核心资源是池中的Object。Object从创建,被借出,被使用,被归还,到最后被销毁,是整套代码的核心
数据库系统、队列系统、web系统、一些业务系统(比如做活动、发红包)、安全系统等等,都有这样的资源的存在。把握住核心资源的生命周期就能掐到代码的命门。
找一个好工具
很多年前我们做C开发时都喜欢用一款叫做"Source Insight"的软件来学习代码。他可以开很多窗口,在不同的函数间跳来跳去,还可以做书签方便定位。
如今,基本上是个IDE都会有这些功能,就连没有类型的js也能很方便的在VS Code中做各种符号跳转和多串口切换。
代码嘛,了解其执行顺序,而非其写作顺序更有利于学习。
建立调试环境
如果对于某些系统需要特别细致的理解,就需要把代码跑起来。通过打断点,输出log验证等方式印证自己的想法。
对于js,python这类会相当简单,因为npm/pip等工具解决了很多依赖问题,而且无需编译,直接启动-修改-重启-修改-……即可不断的尝试。jvm类的系统只要能满足mvn install
或者gradle build
这样的通用编译约定,或者通过IDE直接加载,也能相对容易的把系统跑起来。
对于C/C++的系统就要麻烦许多,需要自己建立一个虚拟机,然后自行安装必要的包。对于它们,远程GDB之类的技术是非常必要的。
对于前端代码,使用jsfiddle配合chrome开发工具这样的工具可让你快速的构建一段js+html+css代码的片段,并且实时的看到效果。
值得提一句的是,有些系统可能涉及到比较复杂的多进程/线程并发执行,对调试学习非常不利。此时需要优先寻找将系统退化到单线程/进程的运行方式(毕竟代码作者自己也得靠这个模式调试不是:)。一个典型的例子是nginx
通常会有1个master进程+多个worker进程并发运行,但在开发时只要配置中设定:
daemon off;
worker_processer 1;
即可使其退化为单进程运行时模式。
看代码很累,要坚持
就像跑步能跑多远,跑多久,都是要靠自己。阅读代码的确能极大地提高个人能力。但是能走多远要靠毅力坚持。而坚持的大敌就是过大的挫折感。上面提到的种种方法——从简单的入手、聚焦、有计划、找好工具,说白了就是尽量减少挫败感,最大限度的提高“正反馈”。但是即便如此,不可否认的是,对于大多数人,看代码是一项非常枯燥的过程。这时你的恒心,你的毅力,你的“熬”的劲头决定了你能走多远。毕竟,想成为高级程序员并不是轻轻松松的事情。
横下一条心来,挑战自我吧。
最后附上部分我曾经看过的高质量开源代码:
- nginx
- MFC
- Redis
- Lucene
- Jetty
- Kafka
- Thrift
- Java Executors
- Java Collections
- Java Concurrecy
- express
- koa