什麼是BUG,簡單點說就是,程序沒有按照我們預想的方式運行。我比較喜歡把BUG分成兩類:
- Crash掉的
- 沒有Crash掉的
可能在平時的編程實踐中,往往簡單的把BUG與Crash基本等價了。而且我們很多精力也都放在解決Crash的Bug上面。而對於沒有Crash掉的BUG,似乎沒有過多的關注。但是,實際情況上那些讓人痛徹心扉的“天坑”往往是那些沒有Crash掉的BUG造成的,比如前一段時間OpenSSL心臟大出血。爲什麼這麼說呢?且聽我慢慢道來。
如何合理的製造BUG
Crash掉的BUG,用程序的死證明了你的程序存在問題,你必須抓緊時間來解決程序的問題了。而沒有Crash掉的Bug,像是一個善於撒謊的人,僞裝成可以正常運轉的樣子,讓整個程序運行在一個不穩定的狀態下。雖然外表看起來好好地(沒有crash),但是裏子早就爛透了,一旦報露出問題往往是致命的,比如OpenSSL的心臟大出血。這就是前人總結的“死程序不說謊”。
Crash不可怕,可怕的是程序沒有Crash而是運行在一個不穩定的狀態下,如果程序還操作了數據,那帶來的危害將是災難性的。
所以放心的讓程序Crash掉吧,因爲當他Crash掉的時候,你還有機會去修正自己的錯誤。如果沒有Crash,那就有可能要給整個程序和產品收屍了。因此合理製造“BUG”的原則之一,也是最大的原則就是:儘量製造Crash的BUG,減少沒有Crash的BUG,如果有可能將沒有Crash掉的Bug轉換成Crash的BUG以方便查找。
NSAssert
這個應該都比較熟悉,他的名字叫做“斷言”。斷言(assertion)是指在開發期間使用的、讓程序在運行時進行自檢的代碼(通常是一個子程序或宏)。斷言爲真,則表明程序運行正常,而斷言爲假,則意味着它已經在代碼中發現了意料之外的錯誤。斷言對於大型的複雜程序或可靠性要求極高的程序來說尤其有用。而當斷言爲假的時候,幾乎所有的系統的處理策略都是,讓程序死掉,即Crash掉。方便你知道,程序出現了問題。
斷言其實是“防禦式編程”的常用的手段。防禦式編程的主要思想是:子程序應該不因傳入錯誤數據而被破壞,哪怕是由其他子程序產生的錯誤數據。這種思想是將可能出現的錯誤造成的影響控制在有限的範圍內。斷言能夠有效的保證數據的正確性,防止因爲髒數據讓整個程序運行在不穩定的狀態下面。
關於如何使用斷言,還是參考《代碼大全2》中“防禦式編程”一章。這裏簡單的做了一點摘錄,概括其大意:
1. 用錯誤處理代碼來處理預期會發生的狀況,用斷言來處理絕不應該發生的狀況。 2. 避免把需要執行的代碼放到斷言中 3. 用斷言來註解並驗證前條件和後條件 4. 對於高健壯性的代碼,應該先使用斷言再處理錯誤 5. 對來源於內部系統的可靠的數據使用斷言,而不要對外部不可靠的數據使用斷言,對於外部不可靠數據,應該使用錯誤處理代碼。
而在IOS編程中,我們可以使用NSAssert來處理斷言。比如:
1
2
3
4
5
|
- ( void )printMyName:(NSString *)myName
{ NSAssert(myName == nil, @ "名字不能爲空!" );
NSLog(@ "My name is %@." ,myName);
} |
我們驗證myName的安全性,需要保證其不能爲空。NSAssert會檢查其內部的表達式的值,如果爲假則繼續執行程序,如果不爲假讓程序Crash掉。
每一個線程都有它自己的斷言捕獲器(一個NSAssertionHanlder的實例),當斷言發生時,捕獲器會打印斷言信息和當前的類名、方法名等信息。然後拋出一個NSInternalInconsistencyException
異常讓整個程序Crash掉。並且在當前線程的斷言捕獲器中執行handleFailureInMethod:object:file:lineNumber:description:
以上述信息爲輸出。
當時,當程序發佈的時候,不能把斷言帶入安裝包,你不想讓程序在用戶機器上Crash掉吧。打開和關閉斷言可以在項目設置中設置:
在release版本中設置了NS_BLOCK_ASSERTIONS之後斷言失效。
儘可能不要用Try-Catch
並不是說Try-Catch這樣的異常處理機制不好。而是,很多人在編程中,錯誤了使用了Try-Catch,把異常處理機制用在了核心邏輯中。把其當成了一個變種的GOTO使用。把大量的邏輯寫在了Catch中。弱弱的說一句,這種情況幹嘛不用ifelse呢。
而實際情況是,異常處理只是用戶處理軟件中出現異常的情況。常用的情況是子程序拋出錯誤,讓上層調用者知道,子程序發生了錯誤,並讓調用者使用合適的策略來處理異常。一般情況下,對於異常的處理策略就是Crash,讓程序死掉,並且打印出堆棧信息。
而在IOS編程中,拋出錯誤的方式,往往採用更直接的方式。如果上層需要知道錯誤信息,一半會傳入一個NSError的指針的指針:
1
2
3
4
5
6
7
8
9
|
- ( void ) doSomething:(NSError* __autoreleasing*)error { ... if (error != NULL) { *error = [NSError
new ]; } .... } |
而能夠留給異常處理的場景就極少了,所以在IOS編程中儘量不要使用Try-Catch。
(PS:見到過使用Try-Catch來防止程序Crash的設計,如果不是迫不得已,儘量不要使用這種策略)
儘量將沒有Crash掉的BUG,讓它Crash掉
上面主要講的是怎麼知道Crash的“BUG”。對於合理的製造“BUG”還有一條就是儘量把沒有Crash掉的“BUG”,讓他Crash掉。這個沒有比較靠譜的方法,靠暴力吧。比如寫一些數組越界在裏面之類的。比如那些難調的多線程BUG,想辦法讓他Crash掉吧,crash掉查找起來就比較方便了。
總之,就是抱着讓程序“死掉”的心態去編程,向死而生。
如何查找BUG
其實查找BUG這個說法,有點不太靠譜。因爲BUG從來都不需要你去找,他就在那裏,只增不減。都是BUG來找你,你很少主動去找BUG。程序死了,然後我們就得加班加點。其實我們找的是發生BUG的原因。找到引發BUG的罪魁禍首。說的比較理論化一點就是:在一堆可能的原因中,找到那些與BUG有因果性的原因(注意,是因果性,不是相關性)。
於是解決BUG一般可以分兩步進行:
- 合理性假設,找到可能性最高的一系列原因。
- 對上面找到的原因與BUG之間的因果性進行分析。必須確定,這個BUG是由某個原因引起的,而且只由改原因引起。即確定特定原因是BUG的充分必要條件。
找到原因之後,剩下的事情就比較簡單了,改代碼解決掉。
合理性假設
其實,BUG發生的原因可以分成兩類:
- 我們自己程序的問題。
- 系統環境,包括OS、庫、框架等的問題。
前者找到了,我們可以改。後者就比較無能爲力了,要麼發發牢騷,要麼email開發商,最後能不能被改掉就不得而知了。比如IOS製作framework的時候,category會報方法無法找的異常,到現在都沒有解決掉。
當然,一般情況下導致程序出問題的原因的99.999999%都是我們自己造成的。所以合理性假設第一條:
首先懷疑自己和自己的程序,其次懷疑一切。
而程序的問題,其實就是開發者自己的問題。畢竟BUG是程序員的親子親孫,我們一手創造了BUG。而之所以能夠創造BUG,開發者的原因大致有三:
- 知識儲備不足,比如IOS常見的空指針問題,發現很多時候就是因爲對於IOS的內存管理模型不熟悉導致。
- 錯心大意,比較典型的就是數組越界錯誤。還有在類型轉化的時候沒注意。比如下面這個程序:
//array.count = 9 for (int i = 100; array.count - (unsigned int)i > 10 ; ) { i++ ..... }
按道理講,這應該是個可以正常執行的程序,但是你運行的話是個死循環。可能死循環的問題,你改了很多天也沒解決。直到同事和你說array.count返回的是NSUInterge,當與無符號整形相間的時候,如果出現負值是回越界的啊。你才恍然大悟:靠,類型的問題。
- 邏輯錯誤
這個就是思維方式的問題,但是也是問題最嚴重的。一旦發生,很難查找。人總是最難懷疑自己的思維方式。比如死循環的問題,最嚴重的是函數間的循環引用,還有多線程的問題。
但是慶幸的是絕大多數的BUG都是由於知識儲備不足和粗心大意造成的。所以合理性假設的第二條:
首先懷疑基礎性的原因,比如自己知識儲備和粗心大意等人爲因素,通過這些原因查找具體的問題。之後再去懷疑難處理的邏輯錯誤。
有了上面的合理性懷疑的一些基本策略,也不能缺少一些基本的素材啊。就是常見的Crash原因,最後我們還是得落地到這些具體的原因或者代碼上,卻找與BUG的因果性聯繫。
- 訪問了一個已經被釋放的對象,比如
123
NSObject * aObj = [[NSObject alloc] init];
[aObj release];
NSLog(@
"%@"
, aObj);
- 訪問數組類對象越界或插入了空對象
- 訪問了不存在的方法
- 字節對齊,(類型轉換錯誤)
- 堆棧溢出
- 多線程併發操作
- Repeating NSTimer
合理性假設第三條:儘可能的查找就有可能性的具體原因。
因果性分析
首先必須先說明的是,我們要找的是“因果性”而不是“相關性“。這是兩個極度被混淆的概念。而且,很多時候我們錯誤的把相關性當成了因果性。比如,在解決一個多線程問題的時候,發現了一個數據混亂的問題,但是百思不得其解。終於,有一天你意外的給某個對象加了個鎖,數據就正常了。然後你就說這個問題是這個對象沒有枷鎖導致的。
但是,根據上述你的分析,只能夠得出該對象枷鎖與否與數據異常有關係,而不能得出就是數據異常的原因。因爲你沒能證明對象加鎖是數據異常的充分必要條件,而只是使用了一個單因變量實驗,變量是枷鎖狀態,取值x=[0,1],x爲整形。然後實驗結果是枷鎖與否與數據異常呈現正相關性。
相關性:在概率論和統計學中,相關(Correlation,或稱相關係數或關聯繫數),顯示兩個隨機變量之間線性關係的強度和方向。在統計學中,相關的意義是用來衡量兩個變量相對於其相互獨立的距離。在這個廣義的定義下,有許多根據數據特點而定義的用來衡量數據相關的係數。 因果性:因果是一個事件(即“因”)和第二個事件(即“果”)之間的關係,其中後一事件被認爲是前一事件的結果。
錯誤的把相關性等價於因果性。不止是程序員,幾乎所有人常見的邏輯錯誤。爲了加深認識,可以看一下這篇小科普:相關性 ≠ 因果性。
因果性分析的首要問題就是,別被自己的邏輯錯誤欺騙,正確的分辨出相關性和因果性之間的區別。不要把相關性等價於因果性。
之後便是因果性分析的內容了,之前一直反覆說,因果性分析的目的就是確定特定原因是BUG發生的充分必要條件。那麼確定這個事情,就需要兩步:
- 充分性證明
- 必要性證明
關於充分性證明,這個基本上就是正常的邏輯推理。基本思路就是,能夠還原出BUG出現的路徑,從原因到BUG發生處的代碼,走了怎樣的函數調用和控制邏輯。確定了這個基本上就能夠證明充分性。一般情況下根據Crash的堆棧信息能夠,非常直接的證明充分性。
關於必要性證明,這個就比較困難了。充分性和必要性的定義如下:當命題“若A則B”爲真時,A稱爲B的充分條件,B稱爲A的必要條件。那麼必要性就是,BUG能夠作爲導致BUG的原因的原因。這個說法比較拗口。換種說法,就是你得確認這個BUG能夠解釋原因,這個BUG就是而且只是這個原因造成的。
只有證明了充分必要性,才能算是真正找到了BUG的原因。
參考:
什麼是BUG,簡單點說就是,程序沒有按照我們預想的方式運行。我比較喜歡把BUG分成兩類:
- Crash掉的
- 沒有Crash掉的
可能在平時的編程實踐中,往往簡單的把BUG與Crash基本等價了。而且我們很多精力也都放在解決Crash的Bug上面。而對於沒有Crash掉的BUG,似乎沒有過多的關注。但是,實際情況上那些讓人痛徹心扉的“天坑”往往是那些沒有Crash掉的BUG造成的,比如前一段時間OpenSSL心臟大出血。爲什麼這麼說呢?且聽我慢慢道來。
如何合理的製造BUG
Crash掉的BUG,用程序的死證明了你的程序存在問題,你必須抓緊時間來解決程序的問題了。而沒有Crash掉的Bug,像是一個善於撒謊的人,僞裝成可以正常運轉的樣子,讓整個程序運行在一個不穩定的狀態下。雖然外表看起來好好地(沒有crash),但是裏子早就爛透了,一旦報露出問題往往是致命的,比如OpenSSL的心臟大出血。這就是前人總結的“死程序不說謊”。
Crash不可怕,可怕的是程序沒有Crash而是運行在一個不穩定的狀態下,如果程序還操作了數據,那帶來的危害將是災難性的。
所以放心的讓程序Crash掉吧,因爲當他Crash掉的時候,你還有機會去修正自己的錯誤。如果沒有Crash,那就有可能要給整個程序和產品收屍了。因此合理製造“BUG”的原則之一,也是最大的原則就是:儘量製造Crash的BUG,減少沒有Crash的BUG,如果有可能將沒有Crash掉的Bug轉換成Crash的BUG以方便查找。
NSAssert
這個應該都比較熟悉,他的名字叫做“斷言”。斷言(assertion)是指在開發期間使用的、讓程序在運行時進行自檢的代碼(通常是一個子程序或宏)。斷言爲真,則表明程序運行正常,而斷言爲假,則意味着它已經在代碼中發現了意料之外的錯誤。斷言對於大型的複雜程序或可靠性要求極高的程序來說尤其有用。而當斷言爲假的時候,幾乎所有的系統的處理策略都是,讓程序死掉,即Crash掉。方便你知道,程序出現了問題。
斷言其實是“防禦式編程”的常用的手段。防禦式編程的主要思想是:子程序應該不因傳入錯誤數據而被破壞,哪怕是由其他子程序產生的錯誤數據。這種思想是將可能出現的錯誤造成的影響控制在有限的範圍內。斷言能夠有效的保證數據的正確性,防止因爲髒數據讓整個程序運行在不穩定的狀態下面。
關於如何使用斷言,還是參考《代碼大全2》中“防禦式編程”一章。這裏簡單的做了一點摘錄,概括其大意:
1. 用錯誤處理代碼來處理預期會發生的狀況,用斷言來處理絕不應該發生的狀況。 2. 避免把需要執行的代碼放到斷言中 3. 用斷言來註解並驗證前條件和後條件 4. 對於高健壯性的代碼,應該先使用斷言再處理錯誤 5. 對來源於內部系統的可靠的數據使用斷言,而不要對外部不可靠的數據使用斷言,對於外部不可靠數據,應該使用錯誤處理代碼。
而在IOS編程中,我們可以使用NSAssert來處理斷言。比如:
1
2
3
4
5
|
- ( void )printMyName:(NSString *)myName
{ NSAssert(myName == nil, @ "名字不能爲空!" );
NSLog(@ "My name is %@." ,myName);
} |
我們驗證myName的安全性,需要保證其不能爲空。NSAssert會檢查其內部的表達式的值,如果爲假則繼續執行程序,如果不爲假讓程序Crash掉。
每一個線程都有它自己的斷言捕獲器(一個NSAssertionHanlder的實例),當斷言發生時,捕獲器會打印斷言信息和當前的類名、方法名等信息。然後拋出一個NSInternalInconsistencyException
異常讓整個程序Crash掉。並且在當前線程的斷言捕獲器中執行handleFailureInMethod:object:file:lineNumber:description:
以上述信息爲輸出。
當時,當程序發佈的時候,不能把斷言帶入安裝包,你不想讓程序在用戶機器上Crash掉吧。打開和關閉斷言可以在項目設置中設置:
在release版本中設置了NS_BLOCK_ASSERTIONS之後斷言失效。
儘可能不要用Try-Catch
並不是說Try-Catch這樣的異常處理機制不好。而是,很多人在編程中,錯誤了使用了Try-Catch,把異常處理機制用在了核心邏輯中。把其當成了一個變種的GOTO使用。把大量的邏輯寫在了Catch中。弱弱的說一句,這種情況幹嘛不用ifelse呢。
而實際情況是,異常處理只是用戶處理軟件中出現異常的情況。常用的情況是子程序拋出錯誤,讓上層調用者知道,子程序發生了錯誤,並讓調用者使用合適的策略來處理異常。一般情況下,對於異常的處理策略就是Crash,讓程序死掉,並且打印出堆棧信息。
而在IOS編程中,拋出錯誤的方式,往往採用更直接的方式。如果上層需要知道錯誤信息,一半會傳入一個NSError的指針的指針:
1
2
3
4
5
6
7
8
9
|
- ( void ) doSomething:(NSError* __autoreleasing*)error { ... if (error != NULL) { *error = [NSError
new ]; } .... } |
而能夠留給異常處理的場景就極少了,所以在IOS編程中儘量不要使用Try-Catch。
(PS:見到過使用Try-Catch來防止程序Crash的設計,如果不是迫不得已,儘量不要使用這種策略)
儘量將沒有Crash掉的BUG,讓它Crash掉
上面主要講的是怎麼知道Crash的“BUG”。對於合理的製造“BUG”還有一條就是儘量把沒有Crash掉的“BUG”,讓他Crash掉。這個沒有比較靠譜的方法,靠暴力吧。比如寫一些數組越界在裏面之類的。比如那些難調的多線程BUG,想辦法讓他Crash掉吧,crash掉查找起來就比較方便了。
總之,就是抱着讓程序“死掉”的心態去編程,向死而生。
如何查找BUG
其實查找BUG這個說法,有點不太靠譜。因爲BUG從來都不需要你去找,他就在那裏,只增不減。都是BUG來找你,你很少主動去找BUG。程序死了,然後我們就得加班加點。其實我們找的是發生BUG的原因。找到引發BUG的罪魁禍首。說的比較理論化一點就是:在一堆可能的原因中,找到那些與BUG有因果性的原因(注意,是因果性,不是相關性)。
於是解決BUG一般可以分兩步進行:
- 合理性假設,找到可能性最高的一系列原因。
- 對上面找到的原因與BUG之間的因果性進行分析。必須確定,這個BUG是由某個原因引起的,而且只由改原因引起。即確定特定原因是BUG的充分必要條件。
找到原因之後,剩下的事情就比較簡單了,改代碼解決掉。
合理性假設
其實,BUG發生的原因可以分成兩類:
- 我們自己程序的問題。
- 系統環境,包括OS、庫、框架等的問題。
前者找到了,我們可以改。後者就比較無能爲力了,要麼發發牢騷,要麼email開發商,最後能不能被改掉就不得而知了。比如IOS製作framework的時候,category會報方法無法找的異常,到現在都沒有解決掉。
當然,一般情況下導致程序出問題的原因的99.999999%都是我們自己造成的。所以合理性假設第一條:
首先懷疑自己和自己的程序,其次懷疑一切。
而程序的問題,其實就是開發者自己的問題。畢竟BUG是程序員的親子親孫,我們一手創造了BUG。而之所以能夠創造BUG,開發者的原因大致有三:
- 知識儲備不足,比如IOS常見的空指針問題,發現很多時候就是因爲對於IOS的內存管理模型不熟悉導致。
- 錯心大意,比較典型的就是數組越界錯誤。還有在類型轉化的時候沒注意。比如下面這個程序:
//array.count = 9 for (int i = 100; array.count - (unsigned int)i > 10 ; ) { i++ ..... }
按道理講,這應該是個可以正常執行的程序,但是你運行的話是個死循環。可能死循環的問題,你改了很多天也沒解決。直到同事和你說array.count返回的是NSUInterge,當與無符號整形相間的時候,如果出現負值是回越界的啊。你才恍然大悟:靠,類型的問題。
- 邏輯錯誤
這個就是思維方式的問題,但是也是問題最嚴重的。一旦發生,很難查找。人總是最難懷疑自己的思維方式。比如死循環的問題,最嚴重的是函數間的循環引用,還有多線程的問題。
但是慶幸的是絕大多數的BUG都是由於知識儲備不足和粗心大意造成的。所以合理性假設的第二條:
首先懷疑基礎性的原因,比如自己知識儲備和粗心大意等人爲因素,通過這些原因查找具體的問題。之後再去懷疑難處理的邏輯錯誤。
有了上面的合理性懷疑的一些基本策略,也不能缺少一些基本的素材啊。就是常見的Crash原因,最後我們還是得落地到這些具體的原因或者代碼上,卻找與BUG的因果性聯繫。
- 訪問了一個已經被釋放的對象,比如
123
NSObject * aObj = [[NSObject alloc] init];
[aObj release];
NSLog(@
"%@"
, aObj);
- 訪問數組類對象越界或插入了空對象
- 訪問了不存在的方法
- 字節對齊,(類型轉換錯誤)
- 堆棧溢出
- 多線程併發操作
- Repeating NSTimer
合理性假設第三條:儘可能的查找就有可能性的具體原因。
因果性分析
首先必須先說明的是,我們要找的是“因果性”而不是“相關性“。這是兩個極度被混淆的概念。而且,很多時候我們錯誤的把相關性當成了因果性。比如,在解決一個多線程問題的時候,發現了一個數據混亂的問題,但是百思不得其解。終於,有一天你意外的給某個對象加了個鎖,數據就正常了。然後你就說這個問題是這個對象沒有枷鎖導致的。
但是,根據上述你的分析,只能夠得出該對象枷鎖與否與數據異常有關係,而不能得出就是數據異常的原因。因爲你沒能證明對象加鎖是數據異常的充分必要條件,而只是使用了一個單因變量實驗,變量是枷鎖狀態,取值x=[0,1],x爲整形。然後實驗結果是枷鎖與否與數據異常呈現正相關性。
相關性:在概率論和統計學中,相關(Correlation,或稱相關係數或關聯繫數),顯示兩個隨機變量之間線性關係的強度和方向。在統計學中,相關的意義是用來衡量兩個變量相對於其相互獨立的距離。在這個廣義的定義下,有許多根據數據特點而定義的用來衡量數據相關的係數。 因果性:因果是一個事件(即“因”)和第二個事件(即“果”)之間的關係,其中後一事件被認爲是前一事件的結果。
錯誤的把相關性等價於因果性。不止是程序員,幾乎所有人常見的邏輯錯誤。爲了加深認識,可以看一下這篇小科普:相關性 ≠ 因果性。
因果性分析的首要問題就是,別被自己的邏輯錯誤欺騙,正確的分辨出相關性和因果性之間的區別。不要把相關性等價於因果性。
之後便是因果性分析的內容了,之前一直反覆說,因果性分析的目的就是確定特定原因是BUG發生的充分必要條件。那麼確定這個事情,就需要兩步:
- 充分性證明
- 必要性證明
關於充分性證明,這個基本上就是正常的邏輯推理。基本思路就是,能夠還原出BUG出現的路徑,從原因到BUG發生處的代碼,走了怎樣的函數調用和控制邏輯。確定了這個基本上就能夠證明充分性。一般情況下根據Crash的堆棧信息能夠,非常直接的證明充分性。
關於必要性證明,這個就比較困難了。充分性和必要性的定義如下:當命題“若A則B”爲真時,A稱爲B的充分條件,B稱爲A的必要條件。那麼必要性就是,BUG能夠作爲導致BUG的原因的原因。這個說法比較拗口。換種說法,就是你得確認這個BUG能夠解釋原因,這個BUG就是而且只是這個原因造成的。
只有證明了充分必要性,才能算是真正找到了BUG的原因。
參考: