你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 開發過程中危機四伏的調試

開發過程中危機四伏的調試

編輯:IOS開發基礎

1-YbKdfZCx8wf8kKf1xx9N1A (1).gif

本文由風鳴_Jan(微博)翻譯自BigNerdRanch,原文:A Lurking Horror in Debugging
通過深入調查這難以言說的恐怖——一個詭異到幾乎動搖了可憐的人類根基的bug,我克服了一種之前不熟悉的恐懼乃至近似恐怖的情緒。

上個禮拜我在我們的一個高級iOS訓練營教一門課程。教授這樣的課程的樂趣之一,就是有很多機會動態調試,因為我們的學生常常會遇到一些非常有趣的問題。在大多數情況下,調試過程會演變成兩個課程:“如何解決問題”和 “如何定位到問題” (也許是更重要的)。

個別bug初看起來是無害的。它是這樣開始的,“我遇到了一個程序crash,我不知道為什麼。”這個crash是100%可重現的:“啟動app,做一個兩指捏合(pinch)的手勢,然後它就掛了”。這類問題通常都比較簡單,八成是:“你在調用數據源委托前忘了……”。

當一個學生重現了那個問題,我們擠到了一個電腦前面。這是調試器裡的證據:

63.png

啊。

我的腦海裡立即跳出了兩件事。第一件,crash是發生在編譯器合成的property setter方法裡,對應的屬性是:

@property (strong, nonatomic)
    id interactiveTransition;

如果你回想一下我在 Thoughts on Debugging裡列的潛在批評層次,你應該記得編譯器是在我的批評列表的最底部。 但現在這個crash是編譯器產生的代碼導致的。要麼編譯器出了問題,要麼這個屬性值相關代碼出了問題。

然後調試器給出了傳給方法的地址:1. 等同於0x1或者0x00000001。這是個奇怪的地址。它不是nil,因為nil是全0. 它也肯定不是一個合法的地址——合法地址的值不但比它大得多,而且一定是16的倍數,因為對象是以16字節對齊的。也許這是個迷路的枚舉值或者其它什麼的。

我們來做個假設:-setInteractiveTransition 被傳進了一個假的值。那麼這個值的類型是什麼呢?也許有個模式可以導致0x0000001產生。一個驗證的簡單辦法是把編譯器產生的setter替換成原始的調試方法:

- (void) setInteractiveTransition: (id) transition {
    NSLog (@"Got set a transition of %p", transition);
    _interactiveTransition = transition;
}

這段代碼打出了兩個nil:

2014-07-29 19:05:12.225 FieldTech[22852:60b] Got set a transition of 0x0
2014-07-29 19:05:20.588 FieldTech[22852:60b] Got set a transition of 0x0

然後在進入函數之前崩潰了:

69.png

尼瑪, 真奇怪。在工作了一段時間後,在打log之前,程序卻crash了,這讓我陷入了迷霧之中。為了安全,ARC在進入函數之前會retain傳進來的指針。在對crash附近的代碼快速反匯編之後我們發現,內存管理是工作的:

(lldb) disassemble
...
   0x446a:  movl   %ecx, 0x4(%esp)
   0x446e:  movl   %eax, -0x18(%ebp)
   0x4471:  calll  0x5a8e          ; symbol stub for: objc_storeStrong
-> 0x4476:  movl   -0x18(%ebp), %eax
   0x4479:  leal   0x4c8f(%eax), %ecx

這些反編譯的數據是否有用呢?好像不是。不管是編譯器生成的setter,還是我自己的代碼,看起來ARC都崩潰在了一個瘋狂的地址裡。

那麼這是怎麼發生的呢?堆棧跟蹤顯示調用來自-[UINavicationController _startCustomTransition:]。也許從這兒開始看應該比較靠譜。但是我們沒有可用的UIKit代碼,所以只能靠反匯編了。我使用的是Hopper Disassembler(http://hopperapp.com/),它能生成偽代碼。

這是個非常大的函數,但是它的構建過程很有趣:

 

70.png

它檢查了寄存器r5,r5是在調用_interactionController的時候初始化的。如果它的值是非0,那麼就把寄存器r2設置成0x1,然後調用setInteractiveTransition,並把r2傳給它。

所以0x1不是一個錯誤地址。它看起來像是個boolean值!為什麼會把一個boolean值傳給我們的方法呢?更奇怪的是,為什麼它會首先調用我們的這個方法呢?聽起來甚至像UINavigationController有它自己的interactiveTransitions屬性。

是時候讓Class-dump登場了!我們把UINavigationController dump出來:

@interface UINavigationController : UIViewController
{
    UIView *_containerView;
    ...
    BOOL _interactiveTransition;
}
...
@property(nonatomic, getter=isInteractiveTransition) BOOL interactiveTransition;

尼瑪,猜到了開頭,猜不到結尾啊。一個沒有文檔說明的名叫interactiveTransition的屬性(property)潛伏在類的腹地,而且它的類型是BOOL。(這個名字看起來一點兒都不像BOOL型)這就是這個問題的原因。

編譯器不知道已經存在一個BOOL interactiveTransition的事,所以它無法告訴我們:“嘿,你在覆蓋一個不同類型的方法。你真的要這樣做麼?”然後Clang很歡地為這個Objective-C指針生成了適當的代碼,包括ARC的內存管理在內。

另外UINavigationController,也很歡地把BOOL值傳了進去。看到setInteractiveTransition打印出來的nil值了?實際上NO.nil和NO的值都是0,他們在運行時是沒法區分的。這是徒勞無功的事。

把那個property重命名一下就解決了這個問題。

干貨大派送

Leveling Up一文中提到的工具非常強大,它的用處不僅僅局限於破解系統。他們能給你提供信息。在調試的時候,信息就是王道。尤其是在Hopper Disassembler給了我們那個“HuH?“的驚喜的時候,其實就是在啟發我們用class-dump探查探查發生了什麼。

這個bug也給我們展示了Objective-C在某些情況下是多麼危險。編譯器有時根本無法知道有些錯誤發生了,所以它也沒辦法警告我們。作為C的衍生品,這個語言假設我們知道我們在干什麼,所以BOOL值傳給了指針,傳的很歡。

那麼Swift會有這個問題麼?

在修了這個bug之後,我在我們內部的一個iOS討論渠道裡發了個帖子。一個哥們Nerd冒出來說:”我相信Swift裡的private能解決這個問題。如果你有父/子類(在不同文件中)都定義了private func foo(),他們可以都存在而且彼此看不見對方。你不能在子類中調用super;在父類中的調用肯定會指向父類的版本,在子類中的調用會指向子類的版本。”Swift再得一分。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved