2024年最全如何快速读懂开源代码?_怎么读懂软件代码(2),前方高能

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

电子书目录如下图所示:

img

img

img

《gdb 高级调试实战教程》电子书下载链接:

链接: https://pan.baidu.com/s/1fS8571m6KYcR4tQ05Lbkgw 提取码: wiw3

相关的配套资源:

链接:https://pan.baidu.com/s/1f4Y275wEhljVK1-ChEdUkw 提取码:snwb

再比如,后来我创业了,我们的项目需要用到 Nginx,另外一点就是很早就听说 Nginx 的性能非常高,使用非常广,我一直也想找个时间去系统地研究一下 Nginx 的源码,例如 Nginx 多进程模式是如何设计的、反向代码是如何实现的等等。我学习 Nginx 源码仍然是调试大法。

注意:Nginx 的功能点比较多,涉及到的新概念和设计思路对于新手也不是特别友好,我建议在了解Nginx 的一些基本用法之后,再通过调试来学习 Nginx 源码。

1 下载 Nginx 源码

从 Nginx 官网下载最新的 Nginx 源码,然后编译安装(回答此问题时,nginx 最新稳定版本是 1.18.0)。

 ## 下载nginx源码
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# wget http://nginx.org/download/nginx-1.18.0.tar.gz
 --2020-07-05 17:22:10--  http://nginx.org/download/nginx-1.18.0.tar.gz
 Resolving nginx.org (nginx.org)... 95.211.80.227, 62.210.92.35, 2001:1af8:4060:a004:21::e3
 Connecting to nginx.org (nginx.org)|95.211.80.227|:80... connected.
 HTTP request sent, awaiting response... 200 OK
 Length: 1039530 (1015K) [application/octet-stream]
 Saving to: ‘nginx-1.18.0.tar.gz’
 
 nginx-1.18.0.tar.gz                            100%[===================================================================================================>]   1015K   666KB/s    in 1.5s    
 
 2020-07-05 17:22:13 (666 KB/s) - ‘nginx-1.18.0.tar.gz’ saved [1039530/1039530]
 
 ## 解压nginx
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# tar zxvf nginx-1.18.0.tar.gz
 
 ## 编译nginx
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# cd nginx-1.18.0
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]# ./configure --prefix=/usr/local/nginx
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make CFLAGS="-g -O0"
 
 ## 安装,这样nginx就被安装到/usr/local/nginx/目录下
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make install

注意:使用 make 命令编译时我们为了让生成的 Nginx 带有调试符号信息同时关闭编译器优化,我们设置了"-g -O0"选项。

2 调试 Nginx

可以使用如下两种方式对 Nginx 进行调试:

方法一

启动 Nginx:

 [root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
 [root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
 [root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
 nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
 nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)

如上所示,Nginx 默认会开启两个进程,在我的机器上以 root 用户运行的 Nginx 进程是父进程,进程号 5246,以 nobody 用户运行的进程是子进程,进程号 5247。我们在当前窗口使用gdb attach 5246命令将 gdb 附加到 Nginx 主进程上去。

 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5246
 ...省略部分输出信息...
 0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 zlib-1.2.11-10.el8.x86_64
 (gdb)

此时我们就可以调试 Nginx 父进程了,例如使用 bt 命令查看当前调用堆栈:

 (gdb) bt
 #0  0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
 #2  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) f 1
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
 164             sigsuspend(&set);
 (gdb) l
 159                 }
 160             }
 161
 162             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");
 163
 164             sigsuspend(&set);
 165
 166             ngx_time_update();
 167
 168             ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
 (gdb)

使用 f 1 命令切换到当前调用堆栈#1,我们可以发现 Nginx 父进程的主线程挂起在src/core/nginx.c:382处。

此时你可以使用 c 命令让程序继续运行起来,也可以添加断点或者做一些其他的调试操作。

再开一个 shell 窗口,使用gdb attach 5247将 gdb 附加到 Nginx 子进程:

 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5247
 ...部署输出省略...
 0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libblkid-2.32.1-17.el8.x86_64 libcap-2.26-1.el8.x86_64 libgcc-8.3.1-4.5.el8.x86_64 libmount-2.32.1-17.el8.x86_64 libselinux-2.9-2.1.el8.x86_64 libuuid-2.32.1-17.el8.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 pcre2-10.32-1.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 systemd-libs-239-18.el8_1.2.x86_64 zlib-1.2.11-10.el8.x86_64
 (gdb)

我们使用 bt 命令查看一下子进程的主线程的当前调用堆栈:

 (gdb) bt
 #0  0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 #2  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
 #3  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #4  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #5  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #6  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
 #7  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) f 1
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 (gdb)

可以发现子进程挂起在src/event/modules/ngx_epoll_module.c:800的 epoll_wait 函数处。我们在 epoll_wait 函数返回后(src/event/modules/ngx_epoll_module.c:804)加一个断点,然后使用 c 命令让 Nginx 子进程继续运行。

 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 (gdb) list
 795         /* NGX_TIMER_INFINITE == INFTIM */
 796
 797         ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
 798                        "epoll timer: %M", timer);
 799
 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 801
 802         err = (events == -1) ? ngx_errno : 0;
 803
 804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
 (gdb) b 804
 Breakpoint 1 at 0x44e560: file src/event/modules/ngx_epoll_module.c, line 804.
 (gdb) c
 Continuing.

接着我们在浏览器里面访问 Nginx 的站点,我这里的 IP 地址是我的云主机地址,读者实际调试时改成自己的 Nginx 服务器所在的地址,如果是本机就是 127.0.0.1,由于默认端口是 80,所以不用指定端口号。

 http://你的IP地址:80
 等价于
 http://你的IP地址

此时我们回到 Nginx 子进程的调试界面发现断点被触发:

 Breakpoint 1, ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
 (gdb) 

使用 bt 命令可以获得此时的调用堆栈:

 (gdb) bt
 #0  ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 #1  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
 #2  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #3  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #4  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #5  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
 #6  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) 

使用 info threads 命令可以查看子进程所有线程信息,我们发现 Nginx 子进程只有一个主线程:

 (gdb) info threads
   Id   Target Id                                Frame 
 * 1    Thread 0x7fd42b17c740 (LWP 5247) "nginx" ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 (gdb) 

Nginx 父进程不处理客户端请求,处理客户端请求的逻辑在子进程中,当单个子进程客户端请求数达到一定数量时,父进程会重新 fork 一个新的子进程来处理新的客户端请求,也就是说子进程数量可以有多个,你可以开多个 shell 窗口,使用 gdb attach 到各个子进程上去调试。

然而,方法一存在一个缺点,即程序已经启动了,我们只能使用 gdb 观察程序在这之后的行为,如果我们想调试程序从启动到运行起来之间的执行流程,方法一可能不太适用。有些读者可能会说:用 gdb 附加到进程后,加好断点,然后使用 run 命令重启进程,这样就可以调试程序从启动到运行起来之间的执行流程了。问题是这种方法不是通用的,因为对于多进程服务模型,有些父子进程有一定的依赖关系,是不方便在运行过程中重启的。这个时候方法二就比较合适了。

方法二

gdb 调试器提供一个选项叫 follow-fork,通过 set follow-fork mode 来设置:当一个进程 fork 出新的子进程时,gdb 是继续调试父进程(取值是 parent)还是子进程(取值是 child),默认是父进程(取值是 parent)。

 # fork之后gdb attach到子进程
 set follow-fork child
 # fork之后gdb attach到父进程,这是默认值
 set follow-fork parent

我们可以使用 show follow-fork mode 查看当前值:

 (gdb) show follow-fork mode
 Debugger response to a program call of fork or vfork is "child".

我们还是以调试 Nginx 为例,先进入 Nginx 可执行文件所在的目录,将方法一中的 Nginx 服务停下来:

 [root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin/
 [root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -s stop

Nginx 源码中存在这样的逻辑,这个逻辑会在程序 main 函数处被调用:

 //src/os/unix/ngx_daemon.c:13行
 ngx_int_t
 ngx_daemon(ngx_log_t *log)
 {
     int  fd;
 
     switch (fork()) {
     case -1:
         ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
         return NGX_ERROR;
     
     //fork出来的子进程走这个case
     case 0:
         break;
     
     //父进程中fork返回值是子进程的PID,大于0,因此走这个case
     //因此主进程会退出
     default:
         exit(0);
     }
 
     //...省略部分代码...
 }

如上述代码中注释所示,为了不让主进程退出,我们在 Nginx 的配置文件中增加一行:

 daemon off;

这样 Nginx 就不会调用 ngx_daemon 函数了。

接下来,我们执行gdb nginx,然后通过设置参数将配置文件 nginx.conf 传给待调试的 Nginx 进程:

 Quit anyway? (y or n) y
 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb nginx 
 ...省略部分输出...
 Reading symbols from nginx...done.
 (gdb) set args -c /usr/local/nginx/conf/nginx.conf
 (gdb) 

接着输入 run 命令尝试运行 Nginx:

 (gdb) run
 Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
 [Thread debugging using libthread_db enabled]
 ...省略部分输出信息...
 [Detaching after fork from child process 7509]

如前文所述,gdb 遇到 fork 指令时默认会 attach 到父进程去,因此上述输出中有一行提示”Detaching after fork from child process 7509“,我们按 Ctrl + c 将程序中断下来,然后输入 bt 命令查看当前调用堆栈,输出的堆栈信息和我们在方法一中看到的父进程的调用堆栈一样,说明 gdb在程序 fork 之后确实 attach 了父进程:

 ^C
 Program received signal SIGINT, Interrupt.
 0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
 (gdb) bt
 #0  0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:164
 #2  0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
 (gdb) 

如果想让 gdb 在 fork 之后去 attach 子进程,我们可以在程序运行之前设置 set follow-fork child,然后使用 run 命令重新运行程序。

 (gdb) set follow-fork child 
 (gdb) run
 The program being debugged has been started already.
 Start it from the beginning? (y or n) y
 Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib64/libthread_db.so.1".
 [Attaching after Thread 0x7ffff7fe7740 (LWP 7664) fork to child process 7667]
 [New inferior 2 (process 7667)]
 [Detaching after fork from parent process 7664]
 [Inferior 1 (process 7664) detached]
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib64/libthread_db.so.1".
 ^C
 Thread 2.1 "nginx" received signal SIGINT, Interrupt.
 [Switching to Thread 0x7ffff7fe7740 (LWP 7667)]
 0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
 (gdb) bt
 #0  0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x71f720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 #2  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x71f720) at src/event/ngx_event.c:247
 #3  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x71f720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #4  0x000000000044926f in ngx_spawn_process (cycle=0x71f720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #5  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x71f720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #6  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:131
 #7  0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
 (gdb) 

我们接着按 Ctrl + C 将程序中断下来,然后使用 bt 命令查看当前线程调用堆栈,结果显示确实是我们在方法一中子进程的主线程所在的调用堆栈,这说明 gdb 确实 attach 到子进程了。

我们可以利用方法二调试程序 fork 之前和之后的任何逻辑,是一种较为通用的多进程调试方法,建议读者掌握。

总结起来,我们可以综合使用方法一和方法二添加各种断点调试 Nginx 的功能,慢慢就能熟悉 Nginx 的各个内部逻辑了。

3 推荐一些 Nginx 学习书单

必看 Nginx 经典书籍(含下载方式)mp.weixin.qq.com/s/uP6_2UaTFZkwvGhNopWzSg

以上三种方式都是不错的阅读源码的方式,读者可以根据自己的水平、目的和所处阶段去使用。

最后,阅读源码不是做给别人看的,如果你之前从未意识到阅读各种大大小小的开源项目的源码的重要性,从现在开始,循序渐进,少买点在线课程,少囤点书,多读些开源代码吧。




开源代码一行一行读肯定是不行,因为人脑的缓存和内存都非常低,短期之内记不住几行代码,阅读量上来,如果没有纲领,基本就是浪费时间。下面的几个方法可以快速的建立纲领:

  • 阅读文档
  • 运行实例
  • Debug
  • 阅读 commit 历史
前置准备

首先,需要准备该语言的 IDE 或者具有 IDE 功能的编辑器,比如代码调转、补全、类型推断等等功能。

然后,把代码拉到本地,用 IDE 打开。

最后,确保可以编译 或者 解释 某个实例。

阅读文档

如果对一个开源项目感兴趣,特别是想学到一些新知识,阅读文档是最佳的途径。一般的开源项目文档组织如下:

  • 用户说明
    • 项目说明:解释这个项目的目的、设计原则等等
      • 快速开始:一般包括几个运行实例
  • API 说明

一般如果某个项目属于你比较熟悉的业务领域,那么稍微阅读一下用户说明,就可以看看 API 的组成,主要关注:

上面这些内容有助于理解项目整体的一些特性,非常有助于后面阅读源代码。

运行实例

这部分就是把代码 Pull 回本地,然后运行之前看到的一些实例。注意最好不要安装二进制或者包后运行。而是pull代码回本地后编译运行,这样可以为下一步 Debug 关键功能做好准备。

通过运行实例,可以熟悉该项目一些基本的使用方法和规则,找到自己感兴趣的部分,就可以开始进行 Debug 操作了。

Debug

阅读文档和源代码属于静态分析,软件的运行时需要在大脑中就行模拟(大脑就变成了一个解释器)。可是有些项目抽象比较复杂,大脑往往很难完成这个解释运行时的工作。Debug 就成了快速熟悉代码运行时的最佳方式。

把一个自己感兴趣的实例运行在 Debug 模式,然后一步一步的 Step in,观察数据是如何变化、函数调用如何发展等等。

很多语言都有函数调用分析的静态分析工具,但是个人感觉对于大项目,函数的调用栈非常复杂,今天分析工具生成的图表不一定非常直观。大部分时候,还是 Debug 某个 code path 比较直接。

阅读 commit 历史

再熟悉了一些 Code path 以后就可以聚焦在自己最为感兴趣的功能或者模块上。这时候我们应该利用好版本控制工具。

比如,对于一个项目,我们可以回滚的某个早期的版本,观察代码的演变;也可以观察某个函数的变化历史。这些 Commit 往往体现了贡献者对该函数或者模块理解的变化。会帮助我们进一步理解项目。




为什么人们都愿意“重新造轮子”?一个重要的原因就是:读别人的代码比自己写还难!

开源代码不适合新手学习,除非是那种具有良好文档的,而且深入浅出的优秀代码。

但这几乎是不可能的。因为:

1、文档的写作是很枯燥的;

2、开源代码是免费的,其作者没有义务为你解释服务;

3、开源代码更倾向于使用一些“复杂”的“新”技术,而不是朴实的保守的旧技术。

如果题主英语好,情况可能会好一点,因为有限的开源代码文档资料绝大部分都是英文的。

如果题主有兴趣的话,我开源了两个项目,正在进行中:

英雄帖:开源项目招募英才

。但不得不说,写文档真是件苦逼的事!




"没有经验的技术差底子薄的初级程序员,如何阅读项目源码? "

“有人阅读过 mybatis 的源码吗 ?就看一个初始化过程就看的已经头晕眼花了,小伙伴们支支招吧!”

"源码应该怎么阅读,我曾经尝试阅读一些源码,例如alibaba的druid中sqlparser部分,spring-mvc,但是发现很吃力,都说debug是最好的阅读方式,我在debug时经常有跟丢的现象……就是走着走着感觉好像进入了一些我当前不太关注细枝末节。 "

。。。。。。

估计很多人都有这样的疑惑。

我非常能理解小伙伴们的痛苦,因为我也是这么痛苦着走过来的。

阅读优秀源码的好处想必大家都知道,学习别人优秀的设计,合理的抽象,简洁的代码… 总之是好处多多。

但是真的把庞大的代码放到你的面前,就如同一个巨大的迷宫,要在其中东转西转寻出一条路来,把迷宫的整个结构搞清楚,理解核心思想,真心不容易。

在阅读由面向对象的语言如Java写的代码时,会发现接口和具体的实现经常对应不起来,不太清楚一个功能到底是怎么在哪个实现类中才能找到。 不像C语言,就是函数调用函数,相对还好点。

如果是动态语言如Ruby,Python, 一个变量的类型甚至都不容易知道,阅读的难度大大增加。

还有一个重要的原因,现在我们看到的源码基本上都经过若干年发展、经过很多人不断地完善的,枝枝蔓蔓非常多,魔鬼都在细节中。 阅读的时候很容易陷进去, 看了几十层函数调用以后,就彻底懵了,就放弃了: 甭管你把源码吹得天花乱坠, 老子再也不看了。

经过很多痛苦的挣扎以后,我也算有一些成功的经历,今天用治学的三个境界来类比, 给大家分享一下:

昨夜西风凋碧树,独上高楼,望尽天涯路

想把源码搞懂,吃透,首先得登高望远,瞰察路径,明确目标与方向,了解源码的概貌。

所以有些准备工作必须得做。

1. 阅读源码之前,需要有一定的技术储备。

比如设计模式,在很多Java源码中几乎就是标配,尤其是这几个:模板方法,单例,观察者,工厂方法,代理,策略,装饰者。

再比如阅读Spring源码,肯定得先了解IoC是怎么回事,AOP的实现方式,CGLib,Java动态代理等,自己动手,写点相关的代码,把这些知识点掌握了。

2. 必须得会使用这个框架/类库, 最好是精通各种各样的用法。

上面刚提过,魔鬼都在细节中,如果有些用法根本不知道,可能你能看明白代码是什么意思,但是不知道它为什么这些写。

3. 先去找书,找资料,了解这个软件的整体设计。

都有哪些模块? 模块之间是怎么关联的?怎么关联的?

可能一下子理解不了,但是要建立一个整体的概念,就像一个地图,防止你迷航。

在读源码的时候可以时不时看看自己在什么地方。

4. 搭建系统,把源代码跑起来!

相信我,Debug是非常非常重要的手段, 你想通过只看而不运行就把系统搞清楚,那是根本不可能的!

衣带渐宽终不悔,为伊消得人憔悴

5. 根据你对系统的理解,设计几个主要的测试案例,定义好输入,输出。

运行系统,慢慢地debug ,一步步地走,这是个死功夫,没有办法绕过。

Debug一遍肯定是不行的,需要Debug很多遍。

第一遍尽可能抛弃细节,抓住主要流程, 比如有些看起来不重要的方法就不进去看了。

第二遍、第三遍…再去看那些细节。

一个非常重要的工作就是记笔记(又是写作!),画出系统的类图(不要依靠IDE给你生成的), 记录下主要的函数调用, 方便后续查看。

文档工作极为重要,因为代码太复杂,人的大脑容量也有限,记不住所有的细节。 文档可以帮助你记住关键点, 到时候可以回想起来,迅速地接着往下看。

要不然,你今天看的,可能到明天就忘个差不多了。

给大家看看我做的一些笔记, 格式不重要,很随意,方便自己看懂就行。

img

img

img

img

6. 主要的测试案例搞明白了,丰富测试案例,考虑一些分支流程。

继续Debug…

总之,静态地看代码 + 动态地debug (从业务的角度), 就会慢慢揭开这个黑暗森林的面纱。

这一步会非常非常地花费时间,但是你做完了,对系统的理解绝对有质的飞跃。

众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

没有千百度的上下求索,不会有瞬间的顿悟和理解,衷心祝愿阅读源码的朋友们都能达到这一境界。

最后一点,也是最关键的一点: 要能坚持下去。

我不是一个聪明人, 但是笨人自有笨办法:什么事都架不住不断的重复,一遍看不明白,再来第二遍, 两遍搞不明白,再来第三遍…

可能有人要问: 你怎么能这么坚持地刨根问底呢?

答案就是好奇心: 这玩意儿到底是怎么实现的?!




今天一文分享怎么学习开源代码,纯干货纯干货!可以复制到自己的笔记本!

一、看什么样子的开源

  • 开源项目中要有**全套的源码和配套工具,**刚开始学开源的时候可别找那种没有工具或者少源码的给自己增加难度。
  • 需要有完善的文档,比如新手指南、项目整体的架构设计文档、模块详细的设计文档,配置说明文档和注意事项
  • 社区是否活跃,是否还在更新和完善。

这几点非常非常重要,基本上是一个初学者学开源的必备内容。

二、初步了解

2.1 顺一遍文档

很少有小伙伴可以好好的看一遍官方文档,因为一开始的心态就是扎进去学。

这样非常容易造成:

  1. 给自己添加难度,很多细节的内容,根本不懂。
  2. 着急动手实践,发现自己连基本的使用方式都没弄明白。
  3. 或者你能找到和黑马程序员一样给大家按照官方文档讲解的课程
2.2 重点学习思路

首先是项目的背景:项目用了什么技术?提供了什么特性?它实际解决了什么问题

项目的适用场景是:优点是什么?缺点呢?适合与不适合的场景是什么

哪些公司在生产环境中使用?

有欧少公司在生产环境中部署并维护过此开源项目?

一方面经历过生产环境的质量相对较好,及时用bug其他公司或多或少也遇到过,自己解决起来也不至于孤立无援。

2.3 了解代码目录结构

在下载源码之后,先看下代码目录的结构,或者是从示例测试代码入手,比如:

  • base代表基础库
  • net代表网络库
  • demo/example代表示例代码
  • tests:测试代码
  • docs:文档目录(类图、流程图、活动图、业务知识相关资料等)

刚刚入手的时候,建议从标粗的内容开始看起来。

2.4 在安装部署前补充新概念、新技术

在已有的知识体系纸上去学习新的开源项目,需要对概念或者新技术有个大概的了解,才能更好地理解项目的整体实现思路,做起来才可以事倍功半。

千万不要一上来就看代码,核心概念不清楚,原理不懂,核心算法没吃透,看代码非常费劲。

三、安装部署、运行

好的开源项目,文档都是比较完善的,安装部署文档一般会有的,更好的甚至都会有rpm安装包和docker镜像。

先把程序运行起来,这只是第一遍!

运行起来之后,精简自己的环境,减少后面调试过程中会出现的干扰信息。

比如,Nginx使用多进程的方式处理请求,为了调试跟踪Nginx的行为,我经常把worker数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。

3.2 成功运行的意义

第一:先来体验项目的功能,对开源项目的功能从上帝视角了解。

第二:下断点>调试>修改代码>观察>再调试,从这个反复的步骤中了解程序的逻辑。

3.3 举例子

安装配置环境>从最简单的例子入门>研究复杂一点的例子>自己写个demo;

常见的安装目录是conf存放配置文件,logs存放日志文件,bin存放日志文件。有一些特殊情况,Nginx有html目录,这种目录能促使我们带着相关疑问继续研究学习,带着问题学习是最高效的。

四、要清楚自己的学习目的

我遇到很多同学上来就是找我,老师给我推荐点什么的开源项目,我说为什么学?很多同学的回答都是感觉该学点源码了。但是完全不知道自己为什么要学。

是要了解其中一个模块,比如是基础模块还是业务模块

4.2 学习顺序
  • 第一步:业务逻辑的实现流程,中间调用了第三方库函数、utils函数、定制的数据结构和算法等;还要了解对外的接口,这些接口的入口,出口参数以及作用。
  • 第二步:看看内存池的实现代码、调度器代码、dpdk中对于海量数据包是如何处理的。
  • 第三步:制作成excel表格,记录走读进度
  • 第四步:批判性的思维,为什么要这样做?

比如:从main函数进入,使用gdb单步跟踪清理一次完整流程(如程序初始化)的代码调用路径,这可以通过debug来观察运行时的变量和行为

如果实在是读不下去的话,先找到自己的兴趣所在,如果你对网络通讯感兴趣,就阅读网络层代码,深入到实现细节。比如:它用了什么库、采用了什么设计模式、为什么这么做?如果可以,debug的细节是什么?

或者是,看1.0版本的源码,1.0版本的内容,从复杂度上来说,都小很多,比较容易

4.3 数据结构和算法

如果上面的步骤都完成了,在来这里。如果真的不明白可以跳过去!比如判断参数的就可以跳过不看的,其次就是代码中有没有一些非顺序的代码,如果有你能理解么?比如通过中间件MQ继续后续的流程等等,所以大家要学会分析。

优秀的开源项目当中都有很多经典的算法,可以全部跳过之后再来学习

因为结构定义了一个程序的架构,结构定下来了才有具体的实现,好比盖房子。数据结构就是框架的结构,至于算法,暂时属于不需要深究的细节。先了解入口出口参数以及作用就可以了。

linus说:烂程序员关心的是代码,好程序员关心的是数据结构以及他们的关系。

所以在,读一份代码的时候,理清楚核心的数据结构之间的关系很重要。

五、划重点

将上面说的学习事项再次总结梳理一遍!

安装运行:按照相关文档,安装运行项目:
  • 系统的依赖组件:
    **因为依赖组件是系统设计和实现的基础,可以了解系统关键信息。**比如Memcached最重要的一来是高性能的网络库libevent,我们大概就能推测出Memached的网络实现应该是Reacyor模型。
  • 系统提供的工具:
    **需要特别关注命令行和配置文件,通过这两个非常重要的信息,**我们可以知道系统具备哪些能力和系统将会如何运行。这些信息是我们学习系统内部机制和原理的窗口。
系统测试

如果只是自己学习和研究是可以参考网上测试和分析的文档,但是大家是要在生产环境投入使用必须进行测试。思路如下:

  • 核对每个配置项的作用和影响,识别出关键配置项
  • 进行多场景的性能测试
  • 进行压力测试:连续跑几天,观察CPU、内存、磁盘IO等指标拨动
  • 进行故障测试:kill、断点、拔网线等等
关键学习

有人学了源码跟没学一样,主要是读了,连API都没有调用过,这是灾难式学习。所以这个阶段的关键学习如下:

在IDE拿到调用栈:
在IDE里面读,这里方便跳转,方便查看定义,比网页上效率高。
通过IDE工具,运行example程序进行跟踪调试,通过打断点可以的刀片程序运行的调用栈。
尽可能编译调试,只要调试了,基本没有看不懂的代码

重点注意是,平常的时候多了解一些设计模式,这样看到名字里比如有proxy之类的直接就明白了。代码都是分模块的,要知道自己看的是哪个层哪个模块。

小的项目分层不明显的话就无所谓了,更多是注重语法的技巧。读没读懂,最简单的标准是,有没有信心可以写出一个差不多的东西。

六、一些建议

上面说的全部内容都是认真学开源的步骤,完全不能少的步骤。

大家如果抱着多学几个开源的心态,不如集中全部时间,把一个项目吃透,哪怕用了半年的时间。积累几年下来的数量还是非常可观的。而且很多项目的思想是共同的,比如高可用方案、分布式协议等。

一定要记得,不断的总结复盘,最好可以不仅写笔记还在论坛上分享。一方面锻炼自己的思维,一方面建立知识索引。




阅读源码的速度与你自身的水平成正比,所以经验不足,基础不好是快不起来的。一般来说,建议先读一些小而全的代码,对类似的工程有初步的认识,然后再去看成熟的开源项目。

具体阅读的时候要确定目标,从庞大的系统中抽离出你想研究的内容, 同时最好有好的ide工具方便搜索。剩下的按照

1.运行程序,观察表现

2.打断点调试,增加log输出

3.查看堆栈、画流程图

4.提出问题

5.解决问题

的流程梳理与分析,需要重复执行。

我之前写过一篇文章,描述了我在一个百万代码级别的项目里是如何学习的,可以参考一下

原创: Jerish 微信公众号——游戏开发那些事

最近有朋友突然问我一个问题 “你怎么把UE4引擎代码看的那么深入的?”

看到问题后我还愣了一下,因为这是第一次有人给我打了个"深入UE4"的标签。其实我接触虚幻引擎满打满算也就两年,确实谈不上深入。只是靠着平时的学习习惯积累,写了一些相关的技术文章。

但这个事却让我突然意识到最容易被我忽略的学习习惯很可能是有一定价值和意义的。我只想着分享我对引擎学习的心得总结,却从没有想过分享我的学习方法,或许后者更为重要。

每一个人做事都有自己的风格与习惯。当你发现身边一个人很优秀的时候,你去看一下他的24小时是怎么度过的,然后再对比一下你的24小时,答案就很明了了。同理,如果你觉得学习源码很困难,不妨请教一下那些比较牛的"过来人",看一下别人学习源码模块的流程。当然具体来说,影响一个事物的维度,细节,前提条件都很多,别人的方法照搬过来可能是行不通的,比如说别人能一天雷打不动地学10个小时,这个放到有些人身上几乎不可能。这些道理大家都明白,我也不过多阐述。


回归主题,既然标题是“如何学习大型项目的源码”,所以下面我把自己学习虚幻引擎源码(C++)的思路和过程给分享给大家。

虚幻引擎源码大概有几百万行(没有确切统计过,可以参考下面的纯代码加静态库文件夹截图),最早可以追溯到1998年Epic自主研发的3D游戏——虚幻。对于一个提供了如此完善功能的游戏引擎,可以想象到他的代码是相当复杂的。所以,在学习的一开始你要明确,你的目的不应该是从头到尾地读遍他所有的源码,而是确定好学习目标后,抽丝剥茧地且有条理的整理出某一个具体模块的内容。

img

UE4纯源码文件夹信息

这里先给出简化版的总结,然后我会针对每条做进一步的阐述。

**准备工作:**建议准备大块且连续的时间(细碎的时间容易中断类关系的梳理),一个比较方便查找的IDE或工具(VS,Notepad++,UltraEdit,Source Insight等。评论区有朋友推荐Source Insight效果与VS+Visual AssistX差不多),类图工具(staruml,Edraw等)

学习步骤(简化版):

1.决定要学习的模块,查找官方文档、相关的总结文章,整理出大概的学习内容与目标

2.运行程序,观察表现

3.运行源码,断点调试,从头跟一边源码的执行流程,注意函数堆栈

4.画类图、流程图,先把遇到的重要类记录下来,表明各个类的关系

5.记录问题,把不理解的类或者内容以问题的方式记录下来

6.写文章、笔记,尝试逐个解决之前遗留的问题

2-6可能需要持续的重复进行

学习步骤(详细版):

1. 查找官方文档、相关的总结文章

比如说我想研究网络模块,首先去官方文档、论坛、wiki里面过一遍网络相关的所有内容,这时候遇到不懂的问题尽可能解决,解决不了的就把问题记下来,先去官方文档看我觉得是非常有必要的,因为这里的文章是最权威的,错误率非常低。然后,去Google、百度搜索相关的文章与帖子,同时可以加入一些技术qq群(有一些水群果断退了就行,保留一些优质的交流群),过一遍这些文章和资料。目前能看到比较好的技术网站大体上就是各个技术对应的官方网站(论坛)、StackOverflow、知乎、博客园、简书、CSDN、一些个人网站等,当然有些网站复制粘贴现象严重,需要自己筛选。建议能找到原文链接的尽量去原文里面看,因为你有可能从原创作者那里看到更多优秀的文章。

2. 在运行程序的时候,我们需要调整各种参数来执行不同的情况,进而观察其表现效果来验证我们的猜想与结论

比如说,对于一个处于休眠状态的Actor属性是否能正常同步,如果客户端属性与服务器一样是否还会执行回调函数等。执行程序可以快速的得到结论,然后根据结论我们可以更快速准确的进行分析。为了提高效率,最好在一开始就设置不同的配置、GM等来在项目运行时动态改变运行内容,因为大型项目一般都是编译型语言,我们可能可能需要频繁的修改代码编译再重新运行。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

章,整理出大概的学习内容与目标

2.运行程序,观察表现

3.运行源码,断点调试,从头跟一边源码的执行流程,注意函数堆栈

4.画类图、流程图,先把遇到的重要类记录下来,表明各个类的关系

5.记录问题,把不理解的类或者内容以问题的方式记录下来

6.写文章、笔记,尝试逐个解决之前遗留的问题

2-6可能需要持续的重复进行

学习步骤(详细版):

1. 查找官方文档、相关的总结文章

比如说我想研究网络模块,首先去官方文档、论坛、wiki里面过一遍网络相关的所有内容,这时候遇到不懂的问题尽可能解决,解决不了的就把问题记下来,先去官方文档看我觉得是非常有必要的,因为这里的文章是最权威的,错误率非常低。然后,去Google、百度搜索相关的文章与帖子,同时可以加入一些技术qq群(有一些水群果断退了就行,保留一些优质的交流群),过一遍这些文章和资料。目前能看到比较好的技术网站大体上就是各个技术对应的官方网站(论坛)、StackOverflow、知乎、博客园、简书、CSDN、一些个人网站等,当然有些网站复制粘贴现象严重,需要自己筛选。建议能找到原文链接的尽量去原文里面看,因为你有可能从原创作者那里看到更多优秀的文章。

2. 在运行程序的时候,我们需要调整各种参数来执行不同的情况,进而观察其表现效果来验证我们的猜想与结论

比如说,对于一个处于休眠状态的Actor属性是否能正常同步,如果客户端属性与服务器一样是否还会执行回调函数等。执行程序可以快速的得到结论,然后根据结论我们可以更快速准确的进行分析。为了提高效率,最好在一开始就设置不同的配置、GM等来在项目运行时动态改变运行内容,因为大型项目一般都是编译型语言,我们可能可能需要频繁的修改代码编译再重新运行。

[外链图片转存中…(img-iLJTT1qv-1715726881148)]
[外链图片转存中…(img-mZd0rsG6-1715726881148)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值