野指針就是指向一個已刪除的對象或者受限內存區域的指針。 咱們寫C++的時候強調指針初始化爲NULL,強調用完後也爲其賦值爲NULL,誰分配的誰回收,來避免野指針的問題。 比較常見的就是這個指針指向的內存,在別處被回收了,可是這個指針不知道,依然還指向這塊內存。 MRC 時代由於引用計數手動控制,因此內存很容易在別處被回收。ARC解決了大部分這種問題。、 在iOS9以前,系統庫的delegate
和target-action
有一部分是assign(unsafe_unretain)
的形式,這時候若是內存在別處被回收了,也是會出現野指針的。 因此iOS9以後這些地方就改爲了weak內存修飾符,內存被回收的時候經過weak表,把這些指針設爲nil。也大幅度減小了野指針的出現。c++
若是如今在工程中依然頻繁出現野指針,幾乎能夠確定是錯誤地使用了內存。git
對於Mach
、Unix
、NSException
三種不一樣層級的crash,NSException比較好說,能夠直接定位到OC代碼。問題主要來自EXC_BAD_ACCESS(SIGSEGV)
這種異常,難以在咱們的應用代碼中定位。github
實際咱們遇到Mach Exception
絕大部分都是野指針的問題。SIGSEGV/SIGABRT/SIGTRAP 比較多見。 野指針問題表現千奇百怪,並且由於崩潰的地方並非形成野指針的地方,並且難以重現,因此問題每每難以定位。 macos
Mach Exception
。
這是目前幫助最大的調試模式。實現原理就是 hook 住了對象的dealloc方法,經過調用本身的__dealloc_zombie
方法來把對象進行殭屍化。xcode
id object_dispose(id obj) {
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
複製代碼
正常的對象釋放方法如上,可是殭屍對象調用了objc_destructInstance
後就直接return了,再也不free(obj);
。同時生成一個"_NSZombie_" + clsName
類名,調用objc_setClass(self, zombieCls);
修改對象的 isa 指針,令其指向特殊的殭屍類。 若是這個對象再次收到消息,objc_msgsend
的時候,調用abort()崩潰並打印出調用的方法。bash
野指針指向的內存沒有被覆蓋的時候,或者被覆蓋成能夠訪問的內存的時候,不必定會出現崩潰。這個時候向對象發送消息,不必定會崩潰(可能恰好有這個方法),或者向已經釋放的對象發送消息。 可是若是野指針指向的是殭屍對象,那就必定會崩潰了,會崩潰在殭屍對象第一次被其它消息訪問的時候。app
殭屍對象必須在鏈接Xcode中debug的時候使用,若是咱們想跟咱們的崩潰收集工具集成在一塊兒,就須要本身實現相似Zombie Object的東西。 邏輯是經過hook住NSObject的根類的dealloc方法,而後在新的dealloc方法中將原本即將釋放的對象的isa指針改成指向咱們建立的一個新的殭屍類。iphone
iOS使用代碼排查野指針錯誤 和 開發本身的NSZombie這兩篇文章裏介紹了在代碼裏實現相似Zoombie Object的方法,然而其實是沒法使用的,這兩種實現跟 Zombie Object 實現上不小的區別,實際應用中有大量誤判的狀況。ide
誤判的緣由主要是dealloc的實現和殭屍類的實現跟Zombie Object不同。 參考Apple的源碼,能夠看到Apple是徹底調用了objc_destructInstance
函數的。而其它人的實現要麼沒有調用這個函數,要麼只作了一部分。對於一個OC對象的dealloc來講,主要包括兩部分,一部分是objc_destructInstance
,一部分是free(self)
。objc_destructInstance
裏包括了移除弱引用,移除關聯對象,c++析構等等。這些邏輯不能省略。函數
- (void)dealloc
{
const char *className = object_getClassName(self);
char *zombieClassName = NULL;
do {
//...
Class zombieClass = objc_getClass(zombieClassName);
objc_destructInstance(self); //關鍵
object_setClass(self, zombieClass);
} while (0);
if (zombieClassName != NULL)
{
free(zombieClassName);
}
}
複製代碼
而對於殭屍類的實現,Zombie Object的實現簡潔並且有效。不像其它人的實現那麼臃腫。就只是申明瞭一個沒有任何方法的根類而已,因此任何消息發給它都會crash。
NS_ROOT_CLASS
@interface _NSZombie_ {
Class isa;
}
@end
複製代碼
因此我從Apple的源碼中提取出來的一套實現NSZombie,跟Zombie Object的實現保證徹底一致,解決誤判的狀況。
Scribble 工具可以在alloc的時候填上0xAA,dealloc的時候填上0x55,就是對象釋放後在內存上填上不可訪問的數據,若是再次訪問對象就會必現crash。
Bugly的這篇文章如何定位Obj-C野指針隨機Crash 就是採用這種方式提升crash率,來方便定位問題。 爲了避免限制在xcode中使用,本身在代碼中實現了相似的邏輯。經過fishhook去hook free
函數的方法,實現以下:
void safe_free(void* p){
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
複製代碼
雖然已經給被釋放的對象寫上了0x55,可是若是是內存在被訪問(觸發crash)以前被其它覆蓋了,則可能沒法觸發crash。 這種狀況也很多見。 因此Bugly爲了內存不被覆蓋,就再也不調用free來釋放這個內存。保持這個內存一直在。 這樣的原理就很是相似Zombie Object
了。
製造crash的方式也是採用修改rsa指針的方式,當對象收到消息的時候abort()。
將malloc/free
函數進行了替換。在malloc函數中額外的分配了禁止訪問區域的內存。 在free函數中將全部分配的內存區域設爲禁止訪問,並放到了隔離區域的隊列中(保證在必定的時間內不會再被malloc函數分配)。 若是訪問到禁止訪問的區域,就直接crash。
對CPU影響2~5⨉, 增長內存消耗 2~3⨉。
可以檢查出來的問題:
不能用於檢查內存泄漏。有些文章說ASan能檢查內存泄漏是不對的,Google的LSan能夠,可是Xcode的Asan不行。
以前介紹的工具都是提升崩潰機率,以拿到崩潰的對象和內存地址。拿到崩潰的對象以後也很難定位,由於崩潰地方離釋放的地方已經很遠了。並且有些對象在工程中初始化了不少個,不知道是對應的哪一個地方出了問題。因此若是能知道對象是在哪初始化的就行了。 Malloc Stack 可以記錄下來全部對象的malloc調用時的堆棧信息。而後咱們執行命令:
script import lldb.macosx.heap
malloc_info --stack-history 0x7fbf0dd4f5c0
複製代碼
就能夠在lldb中打印出來該對象初始化位置的堆棧信息。 Malloc Stack可是有兩個巨大的缺點,一個是隻能在模擬器上使用,第二是沒有打印出dealloc的信息。若是想在真機上使用須要越獄。
公司內部的大神開發的的lldb插件,基於Malloc Stack開發的,經過調用私有函數拿到Malloc Stack記錄的數據。可以支持真機調試,可以打印出dealloc的堆棧信息。 能打印出dealloc的緣由是hook了-dealloc方法,調用__disk_stack_logging_log_stack
函數記錄當前的堆棧信息。
遇到的這個例子多是比較經典的野指針,崩潰日誌中出現了各類各樣的表現。
第一種表現是dealloc對象時崩潰:
0 libsystem_kernel.dylib 0x252fac5c __pthread_kill + 4
1 libsystem_c.dylib 0x2528f0ac abort + 103
2 libsystem_malloc.dylib 0x25324ef6 free + 431
3 libobjc.A.dylib 0x24e13e08 object_dispose + 19
4 Foundation 0x25de3cf2 -[NSIndexPath dealloc] + 66
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
6 libsystem_blocks.dylib 0x25243ac2 _Block_release + 215
7 CoreFoundation 0x25583384 -[__NSArrayI dealloc] + 64
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
9 UIKit 0x29e934f2 __runAfterCACommitDeferredBlocks + 310
10 UIKit 0x29e9f7da __cleanUpAfterCAFlushAndRunDeferredBlocks + 90
11 UIKit 0x29bddb1c __afterCACommitHandler + 84
複製代碼
能夠看到這裏徹底是系統library的崩潰,跟工程代碼毫無關係,最開始也是一頭霧水。 這裏只有兩個線索,一個是NSIndexPath
,另外一個是隻發生於10.3.3以前的iphone5機型上。 由於10.3.3是iphone5支持的最後一個版本,因此用戶量並很多。
第二種表現是objc_msgsend, isEqual:
是經過讀取ARM寄存器lr
獲取到的方法名,這個是Bugly幫咱們查到的。
0 libobjc.A.dylib 0x1a1b0dd6 objc_msgSend (isEqual:) + 15
1 UIKit 0x201afdfa -[UICollectionReusableView _setLayoutAttributes:] + 60
2 UIKit 0x209d0280 -[UICollectionView _applyLayoutAttributes:toView:] + 138
3 UIKit 0x209daf26 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 28
4 UIKit 0x2015b5c2 +[UIView(Animation) performWithoutAnimation:] + 84
5 UIKit 0x209dae40 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2156
6 UIKit 0x201af68a -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 160
7 XXXXXXProject 0x00404c02 -[XXXXXXCollectionView collectionView:cellForItemAtIndexPath:] (XXXXXXClass.m:77)
8 UIKit 0x209cf850 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] + 420
9 UIKit 0x201af5e0 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:] + 42
10 UIKit 0x201ad7f6 -[UICollectionView _updateVisibleCellsNow:] + 4076
11 UIKit 0x201a83d6 -[UICollectionView layoutSubviews] + 398
12 UIKit 0x2014b482 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1224
複製代碼
這裏線索就比較豐富,能夠找到對應的類了,XXXXXXProject
是咱們的工程,明顯崩潰在UICollectionView
中。在重用collectionViewCell的過程當中,調用_setLayoutAttributes
的方法,在+60的位置調用了isEqual:
,通過反編譯這個方法得知調用isEqual:
的對象的是UICollectionViewLayoutAttributes
(反編譯過程省略)。 這裏也是隻發生於10.3.3以前的iphone5機型上。因此基本肯定是同一個問題。
可是並無什麼了卵用,正如以前所說的,野指針崩潰的地方跟出錯的地方相去甚遠。 惟一能肯定的地方,就是引發崩潰的對象是NSIndexPath
。
第三種表現比較奇怪,報[UITransitionView initialize] unrecognized selector
,這個類一臉懵逼。不知道在哪使用過
Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: -[UITransitionView initialize]: unrecognized selector sent to instance 0x165f22c0 at 0x1c4d1acc
Crashed Thread: 0
0 CoreFoundation 0x1cd03b3d ___exceptionPreprocess + 129
1 libobjc.A.dylib 0x1bf8b067 objc_exception_throw + 31
2 CoreFoundation 0x1cd08fd1 ___methodDescriptionForSelector + 1
3 CoreFoundation 0x1cd070c3 ____forwarding___ + 697
4 CoreFoundation 0x1cc2fdc8 _CF_forwarding_prep_0 + 24
5 libobjc.A.dylib 0x1bf8bbad _CALLING_SOME_+initialize_METHOD + 23
6 libobjc.A.dylib 0x1bf8bdf3 __class_initialize + 579
7 libobjc.A.dylib 0x1bf92c15 _lookUpImpOrForward + 173
8 libobjc.A.dylib 0x1bf92b65 __class_lookupMethodAndLoadCache3 + 27
9 libobjc.A.dylib 0x1bf991af __objc_msgSend_uncached + 15
10 UIKit 0x21f98167 -[UICollectionViewLayoutAttributes isEqual:] + 95
11 UIKit 0x21f97dfb -[UICollectionReusableView _setLayoutAttributes:] + 61
12 UIKit 0x227b8281 -[UICollectionView _applyLayoutAttributes:toView:] + 139
13 UIKit 0x227c2f27 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 29
14 UIKit 0x21f435c3 +[UIView(Animation) performWithoutAnimation:] + 85
15 UIKit 0x227c2e41 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2157
16 UIKit 0x21f9768b -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 161
複製代碼
看下面的堆棧就發現仍是同一個問題,可是爲啥會報這麼奇怪的錯? 這就是野指針的表現。這一塊內存被別的東西覆蓋了。
實際上還有其它的表現,可是比較具備表明性的就這三個了。從崩潰日誌中只能獲得有限的信息,一個是這個是野指針問題。第二個是這個野指針對象極可能是一個NSIndexPath
對象(也不能徹底肯定)。
若是不知道是野指針的問題,就很容易誤入歧途,花大量時間在研究UICollectionView
或者在研究UITransitionView
上。其實都是浪費時間,由於形成野指針的地方地方已經很遠了。
正如Bugly這篇文章說的,定位野指針最重要仍是增大野指針出現的機率。 因此此次我是採用Zombie Object,而且限制在iPhone5 和 iOS10.3.3的狀況下重現的。
通過屢次重現,肯定了是NSIndexPath
的問題,並且全部的UICollectionView
和UITableView
都受到了影響。因此我開始懷疑是否是工程中有全局的代碼被hook了。果真不出所料:
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
if (kiOS9Later) {
if ([NSStringFromSelector(invocation.selector) isEqualToString:@"collectionView:didSelectItemAtIndexPath:"]) {
//無痕打點
__unsafe_unretained UICollectionView *collectionView = nil;
id indexPath;
[invocation getArgument:&collectionView atIndex:2];
[invocation getArgument:&indexPath atIndex:3];
[FPPVHelper reportMTAEventId:[collectionView hotTagId] Index:[indexPath row] info:nil];
}
}
}
複製代碼
這是某一段神奇的打點代碼,不知道誰寫的。很明顯indexPath
此處的修飾符應爲__unsafe_unretained
,若是爲strong
的話對象在這裏就會被ARC釋放掉,然而由於傳遞的是C指針,其它地方的某個指針不知道這裏釋放了,依然指向了這裏。產生了野指針。
在iOS9以前的tableview的delegate和datasource都是assign
內存修飾符的。iOS9以後才使用weak
。
// iOS 8 以前
@property(nonatomic, assign) id<UITableViewDataSource> dataSource
@property(nonatomic, assign) id<UITableViewDelegate> delegate
// iOS 9 以後
@property(nonatomic, weak, nullable) id<UITableViewDataSource> dataSource
@property(nonatomic, weak, nullable) id<UITableViewDelegate> delegate
複製代碼
這種狀況,若是delegate
比tableview
自己更早被釋放,此時的dataSource
就會成爲一個野指針。常見的狀況好比block調用延長了tableview的生命週期,就可能會發生這種狀況,致使野指針crash。 通常崩潰日誌裏是objc_msgsend + 15
的崩潰,崩潰在delegate或者datasource的方法裏。
解決方法也很簡單,在dealloc的時候把dataSource和delegate設爲nil便可。
- (void)dealloc
{
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
複製代碼
崩潰堆棧也是最多見的objc_msgSend,這裏能夠看到是工程中hook的某個方法崩潰了
libobjc.A.dylib objc_msgSend (pv_gestureRecongizerAction:)
UIKit -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:]
UIKit ____UIGestureRecognizerUpdate_block_invoke662
UIKit __UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks
UIKit __UIGestureRecognizerUpdate
SEGV_ACCERR
複製代碼
咱們本身的代碼以下,就是在addGestureRecognizer
方法中加了一層調用,加了一層target-action。這至關因而給gestureRecognizer
加了兩個target-action
-(void)pv_addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
[gestureRecognizer addTarget:self action:@selector(pv_gestureRecongizerAction:)];
[self pv_addGestureRecognizer:gestureRecognizer];
}
複製代碼
因爲target對於gesture來講在iOS8上也是相似assign的,因此這裏就是self被釋放了,變成野指針了,可是gestureRecognizer的target依然指向了self的內存。 當self已經被釋放了,可是gestureRecognizer還沒被釋放的時候就會發生這種狀況。
野指針定位有幾個關鍵:
Mach Exception
大多數都是野指針的問題,崩潰日誌裏最常見objc_msgSend
和unrecognized selector sent to
等等。並且每每跟iOS SDK版本和iphone型號有關。 認識到野指針的問題後,就沒必要要拘泥於崩潰日誌,由於崩潰的地方離崩潰的緣由比較遠了。Zombie Object/Scribble/Aasn
均可以。我的認爲本身實現的Zombie Object最好,既能夠脫離Xcode debug的限制,使用又比較簡單。malloc stack/lzMalloc
找到野指針指向對象初始化的位置和dealloc的位置,判斷是否過早釋放等。