如何閱讀一份源代碼?
閱讀源代碼的能力算是程序員的一種底層基礎能力之一,這個能力之所以重要,原因在于:
- 不可避免的需要閱讀或者接手他人的項目。比如調研一個開源項目,比如接手一個其他人的項目。
- 閱讀優秀的項目源碼是學習他人優秀經驗的重要途徑之一,這一點我自己深有體會。
然而,讀代碼比寫代碼還是更難一些,原因在于“寫代碼是在表達自己,讀代碼是在理解別人”。因為面對的項目多,項目的作者有各自的風格,理解起來需要花費不少的精力。
我從業這些年泛讀、精讀過的項目源碼不算少了,陸陸續續的也寫了一些代碼分析的文章,本文中就簡單總結一下我的方法。
先跑起來
開始閱讀一份項目源碼的***步,是先讓這個項目能夠通過你自己編譯通過并且順利跑起來。這一點尤其重要。
有的項目比較復雜,依賴的組件多,搭建起一個調試環境并不容易,所以并不見得是所有項目都能順利的跑起來。如果能自己編譯跑起來,那么后面講到的情景分析、加上調試代碼、調試等等才有展開的基礎。
就我的經驗而言,一個項目代碼,是否能順利的搭建調試環境,效率大不一樣。
跑起來之后,又要盡量的精簡自己的環境,減少調試過程中的干擾信息。比如,Nginx使用多進程的方式處理請求,為了調試跟蹤Nginx的行為,我經常把worker數量設置為1個,這樣調試的時候就知道待跟蹤的是哪個進程了。
總而言之,跑起來之后的調試效率能提升很多,而在跑起來的前提之下又要盡量精簡環境。
調試手段
調試手段,大體分為以下兩種:
- 加調試語句。為了做到這一點,你需要先了解項目如何加調試日志,可能需要修改項目的日志級別支持輸出一些在調試級別的日志,等等。
- 斷點調試。并不是所有項目代碼,跑起來之后都自帶調試信息能夠斷點調試的。所以在自己的調試環境里需要先確定這一點。比如一些C相關的項目,基本都是”./configure & make”來編譯,但是makefile中的編譯flags使用了O2之類的優化選項,此時需要自己先手動修改成”-O0 -g”,即編譯生成的二進制中不優化且帶上調試信息。
總之,在能夠搭建自己的調試環境之后,還需要想辦法確定一下如何加上調試日志以及斷點調試。
使用順手的工具
好的工具會讓你事半功倍,這一點應該很多人都同意。
我閱讀Go代碼的時候,喜歡使用IDEA,這個IDE工具可以***的做到以下幾點:
符號的定位、跳轉、查找符號被引用的地方。
左邊能夠展開一個源碼文件中的所有符號。
反之,很多人推崇的VSCode,我幾次嘗試使用用來閱讀Go和C類代碼,都覺得不夠順手,查找符號能力不行、也沒有地方可以看到一個文件中出現的符號。
C\C++類的代碼,在嘗試各種工具之后,還是使用Vim+Ctags+Cscope來寫C、C++代碼。
情景分析
假如有了前面的基礎,已經能夠讓項目順利在自己的調試環境跑起來了,那么就可以對項目代碼進行情景分析了。
所謂的“情景分析”,我的理解就是自己構造一些情景,然后通過加斷點、調試語句等分析在這些場景下的行為。
以我自己為例,在寫《Lua設計與實現》時,講解到Lua虛擬機指令的解釋和執行過程中,需要針對每個指令做分析,此時用的就是情景分析的方法。我會模擬出來使用該指令的Lua腳本代碼,然后在程序里斷點調試這些場景下的行為。
我慣用的做法,是在某個重要的入口函數上面加上斷點,然后構造觸發場景的調試代碼,當代碼在斷點處停下,通過查看堆棧、變量值等等來觀察代碼的行為。
情景分析的好處在于:不會在一個項目中大海撈針似的查找,而是能夠把問題縮小到一個范圍內展開來理解。
“情景分析”這一概念不是我想出來的名詞,比如有這么幾本分析代碼的書籍,如:《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后綴的項目。
多畫圖,一圖勝千言,使用圖形展示代碼流程、數據結構之間的關系。我最近才發現畫圖能力也是很重要的能力,自己在從頭學習如何使用圖像來表達自己的想法。
寫作是很重要的基礎能力,我一個朋友最近教育我,大體的意思是說:如果你在某方面的能力很強,如果再加上寫作好、英語好,那么將極大放大你在這方面的能力。而類似寫作、英語這樣的底層基礎能力,不是一撮而就的,需要長時間保持練習才可以。而寫博客,對于技術人員而言,就是一種很好的鍛煉寫作的手段。
總結
以上是我簡單總結的一些閱讀源碼時候的手段和注意方法,大體而言有那么幾點吧:
只有更好的輸出才能更好的消化知識,所謂的搭建調試環境、情景分析、多問自己問題、寫代碼閱讀筆記等都是圍繞輸出來展開的。總而言之,不能像一條死魚一樣指望著光靠看代碼就能完全理解它的原理,需要想辦法跟它互動起來。
寫作是人的基礎硬實力之一,不僅鍛煉自己表達能力,還能幫助整理自己的思路。對程序員而言鍛煉寫作能力的手段之一就是寫博客,越早開始鍛煉越好。
***,如同任何可以習得的技能一般,閱讀代碼這種能力也需要長時間、大量的反復練習,下一次就從自己感興趣的項目開始鍛煉自己的這種技能吧。