如何高效、优雅、愉快地阅读项目源码?
代码是形式,逻辑是神韵。
引子
本文探索如何阅读成熟框架的源码。
温馨提示
欲速则不达。阅读源码很容易理解为就是直接去阅读代码本身。实际上,代码只是形式,逻辑才是神韵。
凡是有助于去理解逻辑,理解其原理、架构、实现的,都是值得阅读的。包括而不限于官方文档和 API 文档、架构设计分析文章、原理分析文章、源码阅读分析文章。磨刀不误砍柴工。准备工作做充足,充分借助各种资源辅助,阅读源码才能事半功倍。
预思考
有需求才有目标,有目标才有设计,有设计才有框架。在阅读某个源码模块之前,思考若干基本问题是必要的。
需求是什么?用一句话说清楚;
设计目标是什么?用一句话说清楚;
核心优势和适用场景是什么?分别用一句话说清楚;
基本原理是怎样的?先自己思考怎么实现,然后阅读框架原理文章;
整体设计是怎样的?先自己思考怎么设计,然后阅读架构设计的文章;
技术难点是什么?先自己思考其中的难点及解决方案,然后阅读相关文章;
数据结构及算法流程是如何设计的?阅读框架的源码解析文章。
比如 SpringBean 模块:
需求:有一套通用机制去创建和装配应用所需要的完整的 Bean 实例,使得应用无需关注 Bean 实例的创建和管理,只要按需获取;
设计目标:根据指定的配置文件或注解,生成和存储应用所需要的装配完整的 Bean 实例,并提供多种方式来获取 Bean 实例;
核心优势:支持多种装配方式、自动装配、依赖关系自动注入;支持不同作用域的 Bean 实例创建和获取;稳定高效;
适用场景:有大量的 Bean 需要创建,这些 Bean 存在复杂的依赖关系;
基本原理:反射机制 + 缓存;
算法流程:
创建 bean 工厂对象
->扫描资源路径,获得 bean 的 class 文件
->生成 bean 定义的 beanDefinition 实例
->根据 beanDefinitioin 实例创建 bean 实例并缓存到 bean 工厂对象
->依赖自动注入
->执行钩子方法
->完整的 bean 实例准备就绪
。技术难点:依赖自动装配、循环引用;解决自动依赖注入和循环引用问题需要用到缓存机制。
需求与目标
需求与目标往往容易混为一谈。但需求不等于目标。
需求是宽泛的,目标是具体的;
目标是需求的一种实现途径,往往是设计一个具备某些关键特性的系统或产品。
目标是功能与质量的结合体;除了功能部分,确定质量指标也是尤为关键的。
对于某个框架来说,需求、适用场景和核心优势,都是可以直接在官网或项目主页获取到的。如何还原框架的设计目标呢?可以从核心优势中获取基本说明,更多的就要从 API 文档里来提炼了。
方法
很多开发童鞋可能对阅读源码心生畏惧。其实读源码既不神秘也不复杂:写个 Demo,打断点,运行,然后细细揣摩。阅读源码就是观摩高手出招的过程。
确立目标,通常是理解某个模块的原理、设计或者为了解决实际问题;
写个 demo,能够将主流程运行起来;
找到框架运行的入口点,通过静态代码分析,大致了解整个实现流程;
在预估会经过的关键地方打断点,单步调试;
仔细查看主流程经过的主路径、每一个主要对象及其成员变量的值及变化,细细揣摩其设计意图和方法技巧;
绘制整体流程框图和类的交互图;
学习和理解关键类及关键方法及实现(代码)。
阅读源码,要把握主要与扩展:
首先把主流程及涉及到的主要类弄透彻;
理解其扩展机制;
理解主要扩展实现(需要的时候徐图之)。
阅读源码,常常要将“静态代码分析”和“单步调试”结合起来使用。
静态代码分析
静态代码分析,就是沿着方法调用链,“顺藤摸瓜”一路点击下去。通常能够对整体流程有一个大概的了解。
由于框架实现常常基于接口编程,有时会遇到有多个实现的情形。这时,可以根据直觉和经验,选择一个最有可能的默认实现继续跟下去,或者通过单步调试来弄清楚是哪个具体实现。
单步调试
单步调试,是看似笨拙却很实用的源码阅读方法。单步调试在以下情形尤其有用:
接口调用有多个实现,难以确定是哪个是具体实现时;
查看某个比较复杂的具体类的成员时;
理解实现细节时。
框架解析
框架的设计实现通常包括三层:
问题域及解决方案构成的抽象层,解决问题的核心部分;
封装和交互构成的设计层,确保灵活性、可扩展性和应用集成;
各种细节构成的实现层,用于保证性能和容错等。
阅读顺序是:抽象层 -> 封装与交互层 -> 细节实现层 或者 抽象层 -> 细节实现层 -> 封装与交互层。抽象层好比匣中的宝珠,不能干买椟还珠的事情。
抽象层
抽象层即是问题求解层。技术面试中问到的原理或实现机制,通常都属于这一层。
由于封装和交互、实现细节的大量代码往往会将用于解决问题的核心代码“淹没”,因此,在探索抽象层时,要学会大胆过滤封装和细节,直接跳过大量的分支条件语句,暂时跳过令人疑惑的地方,始终聚焦和直击解决问题的核心部分。用于解决基本问题的核心代码通常是不多的。
比如,Bean 实例创建的核心代码是 ClassPathBeanDefinitionScanner.doScan(扫描资源路径,生成 beanDefinition 对象) 和AbstractAutowireCapableBeanFactory.doCreateBean 方法(根据 beanDefinition 创建 bean 实例)。
设计层
要弄明白设计层,就要先弄清楚框架的整体设计:
有哪些子模块,子模块的设计意图是什么;
子模块之间的关联是怎样的,如何串联成一个完整的设计意图。
框架的设计实现常常会用到设计模式。
常用设计模式:工厂、单例、外观、策略、适配器、装饰、代理、模板、组合、观察者、迭代器;
不同问题域可能会用到的设计模式,比如 DB 驱动接口实现会用到生成器模式和桥接模式,web 请求处理用到职责链模式。
常用设计模式的使用场景:
如果需要创建实例,则通常离不开工厂和单例模式;
如果涉及较为复杂的算法流程,部分算法需要在子类实现,则会用到模板方法模式;
如果需要多种实现,并依据特定场景来选取使用,则会用到策略模式;
如果要将客户端接口及实现与框架的调用隔离,则会用到动态代理模式;
如果要灵活叠加多种功能,则会用到装饰器模式;
如果涉及到事件机制,则离不开观察者模式;
如果需要在库实现的基础上提供简洁接口,则通常用到外观模式;
如果要将多种实现与多种接口定义进行连接,则会用到桥接模式;
如果需要涉及大量配置(规格)并生成实例,则通常用到生成器模式;
如果涉及容器元素访问,则离不开迭代器模式;
如果需要以统一接口访问整体与部分的行为,且整体由部分组成,则通常用到组合模式。
理解基本设计模式的特征和适用场景,识别设计模式的使用,可以更自如地在框架源码之间穿梭。
细节层
细节是最考验源码阅读的心性了。细节藏魔鬼。关键细节考虑不周全,可能会导致整个设计的失败。因此,细节层也是值得仔细推敲的。技术面试中也常常考察实现细节。如果能够回答上来,大概率会让面试官眼前一亮。
有时,一些实现细节可能让人摸不到头脑。此时,可以上网搜索一下,往往会“茅塞顿开”。
克服障碍
阅读成熟框架源码,遇到的一大挑战就是对象之间的错综复杂的交互关系。令人生畏。这实际上考验着开发者的抽象和建模能力。
原理流程图
原理流程图非常重要,就像地图一样,指引人更容易地在“代码迷宫”中穿行而不迷失方向。
在阅读源码之前,设法弄到并理解框架的原理流程图,往往能起到事半功倍的效果。就如行兵打仗,先弄清楚天时与地形。不可不重视之。
概念图景
优秀的软件设计,往往是先建立一个比较完整的概念图景。概念图景,就是关于某个问题域的概念及其关联关系的整体图。
譬如盖房子吧。有的人盖房子就是:砌砖!砌砖!!砌砖!!!要安装窗户怎么办?把其中一大块砖墙锤空了再安。
有的人,则会“设计先行”:
原材料 => 子部件 => 组合与集成。
原材料:砖、石、木、铝、铜、玻璃等;
子部件:墙、窗框、窗户、门、地板、楼梯、锁、通道等;
房子:由子部件进行组合和集成而成;
机制:子部件的组合与集成的原理支撑,比如形状的组合与契合、承压计算等。
如何理清其中的复杂交互关系,从而理解其中蕴含的设计思想呢?需要先理清楚框架的概念图景。
有两种技巧可以结合使用:
由于接口定义了具体类的行为规范,可以通过阅读接口定义及文档来了解其设计思路和骨架;
查看具体类的实例成员(暂不涉及方法),根据经验揣摩其设计意图。
核心类成员
要深入到具体实现,则无法避免核心类的阅读。核心类往往拥有十几个甚至几十个成员及方法,展示出了十足的源码阅读劝退诚意。面对这种情况怎么办呢?有三个技巧可以结合使用:
按快捷键 Alt+7,可以查看该类的所有成员及方法,概览一下,大致猜测其意图;
首先只关注那些对核心问题求解有重要影响的成员,暂时忽略那些用来提升性能、可扩展性等方面的成员;
单步调试,仔细看看运行时的成员对象如何,这样会更加直观具体一些。比如 DefaultListableBeanFactory 这个类,单步调试后得到如下图示:
技术难点
技术难点也是理解源码实现的一个主要障碍。技术难点主要有三类:
数据结构与算法:比如 HashMap 用到了哈希表和红黑树,需要先阅读文献(比如《算法导论》)理解其结构与算法;
原理机制:比如 IO 读写、内存管理、文件系统、编译原理、网络协议,先学习相关的原理机制,夯实基础;
编程模型:特别的编程手法和技巧,比如读 hystrix 源码,就要先熟悉函数式编程和响应式编程。
如何找到论述原理机制的相关文献呢?有一些基本方法可循:
经典书籍:比如数据结构与算法,就有《算法导论》、《算法》、《计算机程序设计艺术》(排序与查找)等;
经典论文:一些还没来得及写入书籍的论文解读,比如 Raft 算法等;
技术书籍:比如 Linux 操作系统内核实现,TCP 协议详解等;
官方文档:优秀项目主页的文档部分,往往有相关原理机制的介绍,比如 ES 的官方文档;
JavaDoc:优秀源码的 Java Doc 往往会引用相关出处,比如 AQS 的源码;
优秀博文:优秀博文往往有一些文献引用,可以阅读相关文献引用;
百科与搜索:在维基百科上搜索出处和引用来源;或者使用搜索引擎。
越到后面,就会发现,真正需要仔细阅读和钻研的书籍和论文,其实并不多。花费很多时间阅读网络文章,这些偷懒和捷径,反而是走了弯路。
耐心与意志
阅读框架源码需要很大的耐心和意志。有点像蚕宝宝吃桑叶,需要一点一点地啃。各个击破。在这个过程中,需要克服不少障碍,才能“修得正果”。
可以使用多种辅助手段:
边听音乐边阅读代码;
拉取代码分支,边读边做标记并提交;
阅读原理、架构及源码分析文章。
小结
源码阅读技能,可以说是程序员的“内功心法”之一。若是能读通优秀源码,则应对日常编程工作游刃有余,而应对难题则有路可循。
路漫漫其修远兮,吾将上下而求索。
程序汪资料链接
卧槽!字节跳动《算法中文手册》火了,完整版 PDF 开放下载!
卧槽!阿里大佬总结的《图解Java》火了,完整版PDF开放下载!
欢迎添加程序汪个人微信 itwang007 进粉丝群或围观朋友圈