开源项目源码阅读指北

4,119
原文链接: www.jianshu.com

写在前面

作为一个程序员,阅读大牛们优秀的开源项目源码是一个提升个人编程能力、扩展思维的重要途径。在实际工作中,相信并不是所有人接手的项目代码都很优雅和优秀,而且很大可能因为历史遗留、赶进度等原因,导致代码冗余、模块耦合严重、扩展性差和兼容性差等等, 这就有可能导致在工作中无法使个人能力得到很好的提高,并且会导致个人的思维和眼界有所局限。

其实一种想法的实现往往是多种的,而欠缺能力的人往往采用简单粗暴的方式,另一方面,而有能力的人总能使用优雅的方式,尽可能考虑各种可能的需求变动、适应各种使用途径和场景、想到未来扩展的方式来实现。

优秀的开源项目正是这种有能力的人用优雅方式实现想法的结晶!所以,阅读优秀的开源项目对个人编程的思考方式、知识扩展都是非常非常有帮助的。

作为经常阅读别人的优秀开源项目的人,想给大家分享下我的阅读经验,希望能对大家有所帮助~

正文

下面将通过我最近阅读的奇虎360的开源项目 Replugin 作为例子,说说我阅读源码的方法。

1.寻找驱动力

当你开始阅读开源项目首先你得有目的性,工作需要?个人学习?这都是很好的驱动力。

没驱动力是很难坚持的,特别是开源项目涉及到很多你不怎么了解的知识点,很容易会觉得枯燥、晦涩。毕竟阅读别人的代码并不是一件快乐的事情,我们很难去完成理解代码作者当时的思路和想法,这个过程是很痛苦的。但如果你有目标、有意图地去阅读,就能在一定程度上减少这痛苦。每天给自己打下鸡血,未来的你等下会为这份坚持感到骄傲!

2.浏览官方文档,对开源项目的功能、架构有大概的印象

好了,有了驱动力,先别急,看看官方文档,看看这个项目能完成什么事情和不能完成什么事情,还有官方对这个项目的定位。例如 Replugin 写得满满的十几页的 wiki ,官方定位:

RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。
完整的:让插件运行起来“像单品那样”,支持大部分特性
稳定的:如此灵活完整的情况下,其框架崩溃率仅为业内很低的“万分之一”
适合全面使用的:其目的是让应用内的“所有功能皆为插件”
占坑类:以稳定为前提的Manifest占坑思路
插件化方案:基于Android原生API和语言来开发,充分利用原生特性

可以看到,wiki很详细地介绍了Replugin定位和优点,这时相信对技术有追求的人都会冒出一个疑问:“他们如何做到的!?” 这又大大激起了你的好奇心,让你更有动力坚持下去。

很多人急功近利,马上就开始源码阅读之旅了,包括我。但经过多个项目源码的阅读的我,会告诉你,别急!我们还需要知道它怎么用。

3.在工作中或实践中使用开源项目

本节讲的不是怎么使用,这官网文档肯定会有说明的,而是讲为什么使用。一个东西你连使用都不会,就想去了解他的原理?就像你开车都不会,就去了解刹车怎么把车停下来。而事实是你开车都不会,你可能都分不清刹车、离合和油门是哪个跟哪个,这就去了解原理,往往会迷失方向。

所以,阅读前先使用吧。但我不建议在实际工作项目中立刻使用,因为你原理都不清楚,有问题不好排查,会影响线上用户,这就很糟糕了。我建议的是看官方demo,然后在自己的个人练手项目中使用。当对项目的使用有一定地理解了,ok,可以走下一步了。

4.网上搜索针对该开源项目进行分析的优秀文章

一个优秀的开源项目总是有很多人阅读并分析,然后整理写出总结文章。既然前人都帮我们分析好了,我们为什么不站在前人的肩膀上继续往上爬,这样就省了从脚到肩膀的力气了。但要注意我的字眼,是“优秀”的文章!现在很多人都写博客,很多都是潦潦而谈,只能说是笔记,而非总结。

像 Replugin 这样一个“巨型”的开源项目,老实说,对我这种菜鸡来说,很多知识点都只是略知一二,例如多进程通信、gradle编译脚本等,在实际工作中很少接触的难免会觉得难懂。另外,官方文档往往不会对实现细节讲得很细,这时,看前人的分析就很有必要了。这样可以让你对项目的实现有一定地了解,当你自己看时,你能很快懂得作者这样做的意图。

当然,如果你不想看别人的分析总结也未必不可,可能在自己阅读过程中多点磕磕碰碰,但你总不能跳过下一步!

5.对开源项目提出自己的疑问

前面做了这么多准备,你总会产生疑问吧。什么?没有!好吧,这开源项目对你来说太简单,已经不值得你一读了。带着疑问去阅读是我认为最高效的阅读方式,当你有了目的,而不至于在阅读过程中迷失了方向,并且在阅读过程中针对性的看。对一个开源项目的疑问一般可以从以下方向提出:

  • 这块功能为什么这么做?有什么好处?
  • 有没有另外一种实现方式?
  • 我缺少哪些知识会阻碍我看源码(需要去补)?

例如我在阅读 Replugin 之前提出了几个疑问:

  • 如何做到一处hook?借助gradle?
  • 查找坑位策略?如何替换真正的启动组件?
  • 为什么需要声明这么多坑位?
  • 为什么不用注入Service?

好了,当你有了好奇心、驱动力、目的,你已经准备好了。但开始阅读前还有一件事情先搞定:编译源码。

6.把开源项目下载到本地,并导入IDE,方便调试、测试

工欲善其事,必先利其器。没有一个好的调试环境怎么能顺心地看源码。但幸亏GitHub让我们能简单地把源码download或clone下来,很多情况都是直接用IDE打开项目就搞定了。但也有像 Replugin 一样的,分为多个项目,每个项目都是单独编译的,这样我们就无法只打开一个窗口来调试,很不爽。这时就需要点导入技巧来搞定了。这里不打算讲了,需要点篇幅,留到后面我写Replugin阅读总结时讲。

当然,我们还可以借助一些小技巧来帮忙我们快速阅读源码。例如我会建一个能打印方法调用堆栈的Log工具类,在一些核心的部分打印log,看下它的调用堆栈:

package com.leo;
import android.util.Log;

public class Logger {

    private final static boolean showTrace = true;
    private final static boolean onlyShowTopStack = false;

    // 过滤的方法堆栈
    private final static String[] FILTER_TRACE_PREFIX = {
            "java.lang",
            "android",
            "com.android",
            "com.leo.Logger" // 过滤掉自己
    };

    public static void i(Object obj) {
        StringBuilder msg = new StringBuilder();
        if (showTrace) {
            StackTraceElement[] stes = Thread.currentThread().getStackTrace();
            for (int i = stes.length - 1, indent = 0; i > 0; i--) {
                boolean needFilter = false;
                String clsName = stes[i].getClassName();
                for (String filter : FILTER_TRACE_PREFIX) {
                    if (clsName.startsWith(filter)) {
                        needFilter = true;
                        break;
                    }
                }
                if (needFilter) continue;

                if (i != stes.length - 1)
                    msg.append(indent(indent)).append("-> ");
                msg.append(stes[i].getClassName() + "#" + stes[i].getMethodName() + "\n");
                indent++;
            }
        }
        msg.append(" >>> " + obj + "\n");
        String showMsg = msg.toString();
        if (onlyShowTopStack) {
            String[] trace = showMsg.split("->");
            showMsg = trace[trace.length - 1];
            showMsg = showMsg.replace("->", "")
                    .replace("\n", "")
                    .trim();
        }
        Log.i("TEST", showMsg);
    }

    private static String indent(int i) {
        if (i < 0) {
            i = 0;
        }
        StringBuilder ret = new StringBuilder();
        for (int n = 0; n < i; n++) {
            ret.append(" ");
        }
        return ret.toString();
    }
}

到这里,你已经全部准备就绪,战争一触即发!开始 READ THE FUCKING SOURCE CODE 吧!

7.带着疑问阅读源码

战争打响,在充满迷雾的大海中,我方对敌人的方位还不甚了解,但不怕,我们的指北针 —— 疑问 —— 会带领我们直达敌方腹地,我们终会揭开它的露出庐山真面目。

开源项目往往是庞大而复杂的,我们在阅读过程中真的非常容易会纠结于细节,而导致阅读混乱,迷失了方向,这对阅读的动力打击很沉重的,往往会使人放弃。

而有了疑问就不同了,你知道自己为何要看,你会思考,会有自己的目的,不拘泥于细节实现,能准确地找到源码的核心实现。

对于纠结细节是很多人在阅读源码犯的错误,有些细节我们根本不需要去搞清楚它怎么做的,知道它做什么就可以了。一些具体的实现可以放到当你使用过程中遇到问题,或者对该具体实现产生另一个疑问时才去深究,也就是说,还是带着疑问阅读代码。因为一个开源项目往往是多个优秀的人花了很多时间写出来的结晶,你想在短时间内把它完成消化,是不科学的。我们专注于最感兴趣的、最有参考价值和最核心的部分就可以了。

8.阅读源码过程中多添加注释、多做笔记

我得承认,我的记忆力不好,而我也不信我的记忆。好记性不如烂笔头,记忆终将遗忘,但所做的笔记除非被销毁,否则永远都会在那里,等着你去翻阅回顾。

我们把整个项目都下载下来了,首先当然是在阅读源码过程中添加下自己的注释了,写下自己的理解、疑惑,或者标记下值得借鉴参考的实现等等。另外,我们还需要做些简单的总结笔记。可以纸质或者网上很多的笔记类应用。对于我这种无法直视自己的手写字的,更倾向于用笔记类应用,这也是我推荐大家用的,多端同步,不能再省心。

9.做阅读总结,吸收和再创造

当你对开源项目阅读到一定程度了,对该项目有了深刻的理解,并有了自己的见解,你是不是有话要说?别憋着了,讲出来吧!跟大家分享!写篇博客总结下阅读经验、心得和成长等等,既能加深自己的印象,又能帮助到他人,何乐而不为呢?!

阅读开源项目我们最终的目的是把其涉及到的知识点和设计实现思路吸收,并且转化为自己的功力。这个转化不是说你阅读完了就转化成功了,往往阅读是不够的,你还需要实践。

例如喜欢打球的我深知看NBA球星在球场上各种变向戏耍对手,对我的过人能力几乎没任何帮助,只是让我知道:“原来还能这么做呀!” 我还得自己去球场一招一式的练习,反复练习,或者我根据我的身体条件,做些简单的变种,直到这招转化为我的肌肉记忆,我才能在比赛中自然而然地使用出来。

所以我提倡再创造。所谓再创造不是让你重复造轮子,而是能根据自己的工作需求,把开源项目应用到工作中。这里的应用不一定是直接引用开源项目来使用,我是不建议这么做的,因为开源项目往往考虑全面,考虑到非常多的情况的,而你项目根本不存在这样的情况,这就是浪费。所以我建议的是:根据自己工作的需求,把开源项目的核心实现抽取出来,转化为能满足自己需求的库来使用

而这个抽取的过程就是吸收的过程。在这个过程你遇到的问题并解决,会使你对开源项目有更深刻的理解。这个过程如果你对开源项目的某个实现不太认同,可以尝试改为自己的实现,这就是吸收。

在写最后

非常感谢看到这里的童鞋,毕竟这些经验谈没什么干货,能耐心读到这里真的非常感谢!我们来总结一波阅读源码的步骤:

  1. 寻找驱动力
  2. 浏览官方文档,对开源项目的功能、架构有大概的印象
  3. 在工作中或实践中使用开源项目
  4. 网上搜索针对该开源项目进行分析的优秀文章
  5. 对开源项目提出自己的疑问
  6. 把开源项目下载到本地,并导入IDE,方便调试、测试
  7. 带着疑问阅读源码
  8. 阅读源码过程中多添加注释、多做笔记
  9. 做阅读总结,吸收和再创造

以上步骤有些可以根据实际情况跳过,程序员都是聪明人,总也会随机应变~

好了,完结撒花~再次感谢大家~