ワイルドポインタは、削除されたオブジェクトまたは制限されたメモリ領域へのポインタです。
C ++を作成するときは、ポインターがNULLに初期化され、使い果たされた後にNULLが割り当てられることを強調します。また、ポインターを割り当てる人は、ワイルドポインターの問題を回避するために、ポインターをリサイクルします。
このポインタが指すメモリは他の場所で再利用されるのが一般的ですが、ポインタはそれを知らなくてもこのメモリを指します。
MRCの時代には、参照カウントが手動で制御されていたため、メモリは他の場所で簡単に再利用できました。ARCはこれらの問題のほとんどを解決します。、
iOS9より前、システムライブラリdelegate
、およびフォームのtarget-action
一部。assign(unsafe_unretain)
現時点では、メモリが他の場所で回復された場合、ワイルドポインタも存在します。
そのため、iOS9以降、これらの場所は弱いメモリ修飾子に変更されました。メモリがリサイクルされると、これらのポインタは弱いテーブルを介してnilに設定されます。また、ワイルドポインタの出現を大幅に減らしました。
現在でもワイルドポインタがプロジェクトに頻繁に表示される場合は、メモリが正しく使用されていないことはほぼ間違いありません。
パフォーマンス:クラッシュ
ためMach
、Unix
、NSException
クラッシュの3つの異なるレベル、言ってNSException比較、コードを直接OCを配置することができます。この問題は主に、EXC_BAD_ACCESS(SIGSEGV)
この種の例外に起因します。これは、アプリケーションコードで見つけるのが困難です。
画像
- SIGILLが不正な命令を実行しました。通常、実行可能ファイルでエラーが発生しました。
- SIGTRAPブレークポイント命令またはその他のトラップ命令の生成
- SIGABRT呼び出しの中止
- SIGBUSの不正なアドレス。間違ったメモリタイプアクセス、メモリアドレスアラインメントなど。
- SIGSEGVの不正なアドレス。未割り当てのメモリにアクセスしたり、書き込み権限なしでメモリに書き込んだりします。
- SIGFPE致命的な算術演算。数値オーバーフロー、NaN値など。
実際、私たちMach Exception
が遭遇する問題のほとんどはワイルドポインターです。SIGSEGV / SIGABRT / SIGTRAPがより一般的です。
ワイルドポインタの問題はさまざまな形で現れますが、クラッシュの場所はワイルドポインタが発生した場所ではなく、再現が難しいため、問題を特定するのが難しいことがよくあります。
画像
Tencent Buglyのこの写真は、ワイルドポインターがほとんどすべてのタイプを引き起こす可能性があることを示していますMach Exception
。
位置決めツール
Zoombieオブジェクト
これは現在、最も役立つデバッグモードです。実装の原則は、オブジェクトのdeallocメソッドをフックし、独自の__dealloc_zombie
メソッドを呼び出すことによってオブジェクトをゾンビ化することです。
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
通常のオブジェクト解放メソッドは上記のとおりですがobjc_destructInstance
、ゾンビオブジェクトが呼び出された後、直接返されなくなりfree(obj);
ます。同時に、"_NSZombie_" + clsName
クラス名が生成され、objc_setClass(self, zombieCls);
変更されたオブジェクトのisaポインターが呼び出されて、特別なゾンビクラスを指すようになります。
このオブジェクトが再びメッセージを受信した場合objc_msgsend
、abort()を呼び出すとクラッシュし、呼び出されたメソッドが出力されます。
ワイルドポインタが指すメモリが上書きされていない場合、またはアクセス可能なメモリに上書きされている場合、クラッシュが発生するとは限りません。現時点では、オブジェクトへのメッセージの送信は必ずしもクラッシュするわけではありません(このメソッドがある場合があります)、または解放されたオブジェクトにメッセージを送信します。ただし、ワイルドポインタがゾンビオブジェクトを指している場合はクラッシュします。ゾンビオブジェクトが他のメッセージによって初めてアクセスされたときにクラッシュします。
Xcodeなしのゾンビオブジェクト
Xcodeでデバッグに接続するときは、ゾンビオブジェクトを使用する必要があります。クラッシュコレクションツールと統合する場合は、ゾンビオブジェクトに似たものを実装する必要があります。
ロジックは、NSObjectのルートクラスのdeallocメソッドをフックし、新しいdeallocメソッドで、解放するオブジェクトのisaポインターを、作成した新しいゾンビクラスを指すように変更することです。
iOSはコードを使用してワイルドポインタエラーのトラブルシューティングを
行い、独自のNSZombieを開発しますこの記事では、コード内で同様のZoombieオブジェクトを実現する2つの方法について説明しますが、ZombieObjectで実現された両方を使用して大きな違いを実現することは不可能です、実際のアプリケーションには多くの誤解があります。
誤判断の主な理由は、deallocとゾンビクラスの実装がゾンビオブジェクトとは異なることです。
Appleのソースコードを参照すると、Appleがobjc_destructInstance
関数を完全に呼び出していることがわかります。他の人の実装では、この関数を呼び出さなかったか、その一部しか呼び出していませんでした。OCオブジェクトのdeallocの場合、主に2つの部分、1つのobjc_destructInstance
部分ともう1つの部分が含まれ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);
}
}
ゾンビクラスの実現のために、ゾンビオブジェクトの実現は簡単で効果的です。他の人の実装ほど肥大化していない。メソッドなしでルートクラスを宣言するだけなので、送信されたメッセージはすべてクラッシュします。
NS_ROOT_CLASS
@interface _NSZombie_ {
Class isa;
}
@end
そのため、AppleのソースコードからNSZombieの一連の実装を抽出しました。これは、Zombie Objectの実装と完全に一致し、誤判断を解決します。
落書き
Scribbleツールは、alloc時に0xAAを入力し、dealloc時に0x55を入力できます。これは、オブジェクトが解放された後、メモリがアクセスできないデータでいっぱいになることを意味します。オブジェクトに再度アクセスすると、クラッシュします。
Buglyによるこの記事Obj-Cワイルドポインターランダムクラッシュを見つける方法この方法を使用してクラッシュ率を上げ、問題の特定を容易にします。
xcodeでの使用を制限しないために、コードに同様のロジックを実装しました。フィッシュフックを介してfree
関数をフックする方法は、次のように実装されます。
void safe_free(void* p){
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
リリースされたオブジェクトには0x55が書き込まれていますが、アクセスされる前にメモリが他のユーザーによって上書きされた場合(クラッシュがトリガーされた場合)、クラッシュがトリガーされない場合があります。この状況は珍しいことではありません。そのため、Buglyは、メモリを上書きしないために、このメモリを解放するためにfreeを呼び出すことはなくなりました。この記憶を常にそこに置いてください。原理は非常に似ていZombie Object
ます。
クラッシュさせる方法は、オブジェクトがメッセージを受信したときにrsaポインター、abort()を変更することでもあります。
アドレスサニタイザー
malloc/free
関数が置き換えられます。malloc関数では、禁止領域に追加のメモリが割り当てられます。空き機能では、割り当てられたすべてのメモリ領域へのアクセスが禁止され、分離領域のキューに配置されます(一定期間内にmalloc機能によって割り当てられないようにするため)。禁止区域を訪れた場合は、直接クラッシュしてください。
CPUへの影響は2〜5⨉で、メモリ消費量は2〜3⨉増加します。
チェックできる問題:
- デアロックされたメモリへのアクセス/デアロックされたメモリの割り当て解除
- Deallocには割り当てメモリがありません(ただし、初期化されていないメモリへのアクセスをチェックアウトすることはできません)
- 関数が戻った後にスタックメモリにアクセスする/スコープ外のスタックメモリにアクセスする
- バッファオーバーフローまたはアンダーフロー、C ++コンテナオーバーフロー(ただし、整数オーバーフローはチェックできません)
メモリリークのチェックには使用できません。ASanがメモリリークをチェックできるのは間違っているとの記事もあります。GoogleのLSanはチェックできますが、XcodeのAsanはチェックできません。
Mallocスタック
以前に導入されたツールは、クラッシュのオブジェクトとメモリアドレスを取得するために、クラッシュの可能性を高めることです。クラッシュした場所は解放された場所から遠く離れているため、クラッシュしたオブジェクトを見つけることも困難です。そして、いくつかのオブジェクトは多くのプロジェクトで初期化されており、対応する問題がどこにあるのかわかりません。したがって、オブジェクトが初期化された場所を知っておくと便利です。
Malloc Stackは、mallocがすべてのオブジェクトを呼び出すときに、スタック情報を記録できます。次に、次のコマンドを実行します。
script import lldb.macosx.heap
malloc_info --stack-history 0x7fbf0dd4f5c0
オブジェクトの初期化位置のスタック情報をlldbに出力できます。
Malloc Stackには2つの大きな欠点があります。1つはシミュレーターでしか使用できないこと、もう1つはdealloc情報を出力しないことです。実際のマシンで使用したい場合は、ジェイルブレイクする必要があります。
lzMalloc
同社のGreatGodによって開発されたlldbプラグインは、Malloc Stackに基づいており、プライベート関数を呼び出すことによって、MallocStackによって記録されたデータを取得します。実際のマシンのデバッグをサポートし、deallocのスタック情報を出力できます。
Deallocを出力できる理由は、-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
これは完全にシステムライブラリの崩壊であり、エンジニアリングコードとは関係がないことがわかります。最初は混乱していました。
ここには2つの手がかりがあります。1つはNSIndexPath
、10.3.3より前のiPhone5モデルでのみ発生します。
10.3.3はiphone5でサポートされている最後のバージョンであるため、ユーザーは多くありません。
2番目のパフォーマンスは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
。
3番目の種類のパフォーマンスはかなり奇妙で、[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
以下のスタックを見ると、同じ問題がまだ見つかりますが、なぜこのような奇妙なエラーが報告されるのですか?これはワイルドポインターのパフォーマンスです。この記憶は何か他のもので覆われています。
実際、他の公演もありますが、これら3つはより代表的なものです。クラッシュログから取得できる情報は限られています。1つは、これがワイルドポインタの問題であるということです。2つ目は、このワイルドポインタオブジェクトがオブジェクトである可能性が高いNSIndexPath
ことです(完全に確実ではありません)。
それがワイルドポインターの問題であることがわからない場合は、迷って研究UICollectionView
や研究に多くの時間を費やすのは簡単UITransitionView
です。実際、ワイルドポインターが発生する場所は遠く離れているため、時間の無駄です。
Buglyの記事で述べたように、ワイルドポインターを見つけるための最も重要なことは、ワイルドポインターの可能性を高めることです。そこで今回はゾンビオブジェクトを使用し、iPhone5とiOS10.3.3での再現に限定しました。
いくつか再生した後、我々はそれがあると判断しNSIndexPath
、問題、およびすべてUICollectionView
とUITableView
影響を受けています。それで、プロジェクトのグローバルコードがフックされているのではないかと思い始めました。予想通り:
- (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がクラッシュする前のデリゲート
iOS9より前のテーブルビューのデリゲートとデータソースは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
れると、現在のものがワイルドポインタになります。テーブルビューのライフサイクルを延長するブロック呼び出しなどの一般的な状況が発生し、ワイルドポインタがクラッシュする可能性があります。通常objc_msgsend + 15
、クラッシュログのクラッシュは、デリゲートまたはデータソースの方法で発生します。
解決策も非常に簡単で、dataSourceを設定し、deallocでnilに委任するだけです。
- (void)dealloc
{
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
iOS9より前にターゲットアクションがクラッシュする
クラッシュスタックも最も一般的なobjc_msgSendです。ここでは、プロジェクトのフックのメソッドがクラッシュしたことがわかります。
libobjc.A.dylib objc_msgSend (pv_gestureRecongizerAction:)
UIKit -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:]
UIKit ____UIGestureRecognizerUpdate_block_invoke662
UIKit __UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks
UIKit __UIGestureRecognizerUpdate
SEGV_ACCERR
独自のコードは次のとおりでaddGestureRecognizer
、メソッドに呼び出しのレイヤーとターゲットアクションのレイヤーを追加します。これはgestureRecognizer
2つ追加するのと同じですtarget-action
-(void)pv_addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
[gestureRecognizer addTarget:self action:@selector(pv_gestureRecongizerAction:)];
[self pv_addGestureRecognizer:gestureRecognizer];
}
targetはiOS8でのジェスチャーの割り当てに似ているため、selfが解放されてワイルドポインターになりますが、gestureRecognizerのターゲットは依然としてselfのメモリを指します。これは、selfが解放されたが、gestureRecognizerがまだ解放されていない場合に発生します。
総括する
ワイルドポインタの配置には、いくつかの重要なポイントがあります。
- 1つ目は、これがワイルドポインターの問題であることを理解すること
Mach Exception
です。それらのほとんどはワイルドポインターの問題であり、クラッシュログobjc_msgSend
などで最も一般的unrecognized selector sent to
です。そして、それはしばしばiOSSDKバージョンとiPhoneモデルに関連しています。ワイルドポインタの問題を認識した後は、クラッシュの場所がクラッシュの原因から遠く離れているため、クラッシュログに固執する必要はありません。 - 二つ目は、可能な限り再現することです。
Zombie Object/Scribble/Aasn
あなたはそれを使うことができます。個人的には、実装したゾンビオブジェクトが最適だと思います。Xcodeデバッグの制限を打ち破ることができ、比較的簡単に使用できます。 - 3つ目は、クラッシュの方法ではなく、ワイルドポインタが指すオブジェクトに基づいてエラーの場所を特定することです。クラッシュの方法はクラッシュの原因からはほど遠いため、ワイルドポインターが指すオブジェクトのほとんどは、依然として間違ったオブジェクトです(場合によっては上書きされる可能性があります)。
- 4つ目は
malloc stack/lzMalloc
、ワイルドポインターが見つかった場所を使用して、オブジェクトの初期化を指し、deallocの場所を使用して、リリースが早すぎるかどうかを判断します。