Objective-Cにはweakという識別子があるが、内容を理解していなかったので少し調べた。
ここではiOSのARM64環境を前提としている。また、weakまたは__weakを付与して宣言した変数をweak変数とする。
Appleが公開しているObjective-Cのランタイムライブラリのソースコードを読んだが、実機のライブラリがこの通りなのかどうかは不明である。
参考1: https://opensource.apple.com/source/objc4/objc4-709/runtime/ のNSObject.mmなど
参考2: https://github.com/opensource-apple/objc4/blob/master/runtime/ こちらは少し古い?
参考3: https://github.com/gnustep/libobjc2/blob/master/arc.m
参考4: Objective-C Automatic Reference Counting (ARC)
参考5: Objective-CのランタイムAPIをReactiveCocoaで解説する
参考6: ダイナミックObjective-C
weakの実現方法の概要
書籍では「エキスパートObjective-Cプログラミング」に載っている。
大まかには、あるオブジェクトobjについて、それを参照するweak変数wvarを記憶しておく専用の領域を用意することにより実現されている。
代入時の処理
weak変数 wvar を宣言すると、その格納領域としてidのサイズ(ARM64では8バイト)が確保される。
wvar にオブジェクト obj を代入する文は、関数 objc_storeWeak(wvarのポインタ,obj) への呼び出しとしてコンパイルされる。
objc_storeWeak の内部処理では、 weak_unregister_no_lock と weak_register_no_lock の呼び出しが行われる。
Objective-Cのランタイム関数には、weak変数を管理するためのテーブルが用意されている。
wvar に値が入っているときは、 weak_unregister_no_lock(テーブル,obj,wvarのポインタ) を呼び出し、テーブルの obj に対応する場所から wvar を削除する。
続いて、 obj がnilでなければ、 weak_register_no_lock(テーブル,obj,wvarのポインタ) を呼び出す。これによりテーブルの obj に対応する場所に、参照先として wvar (のポインタ)が追加される。(テーブルのキーは obj である。 obj に対するweak変数は複数存在できる)
また、 wvar の値は obj となる。
メソッド呼び出し時の処理
weak変数にてメソッドを呼び出す時、単純に値 obj により objc_msgSend を呼び出すと、メソッドの処理の途中で obj が解放される危険性がある。
そのため、atomicにretainする関数が用意されている。(普通のretainは、retainを呼び出した直後に obj が解放される可能性があるので使えない)
wvar のメソッド hoge の呼び出し [wvar hoge] は、
- objc_loadWeakRetained
- objc_msgSend
- objc_autorelease
にコンパイルされる。テーブルに wvar の情報がある場合( obj のライフサイクルが終了していない場合)にのみ、 objc_loadWeakRetained によりretainされたオブジェクトを取得することができる。利用後は(普通のオブジェクトと同様に)releaseで参照カウンタを1減らす。
参照先のオブジェクトが解放される時の処理
obj の参照カウンタがゼロになり、オブジェクトが破棄されるとき、最後の後片付けの処理として clearDeallocating が呼び出される。
clearDeallocating の内部では、 obj を参照しているすべてのweak変数の値がnilに書き換えられ、 obj の登録情報が削除される。つまり、 obj が解放されるとただちに wvar が保持する値がnilになる。
tagged pointer
idがtagged pointerの場合は、参照カウンタの管理を行わないようになっている(retain時に参照カウンタが変化しない)。tagged pointerとは、idの一部に値が入っているもので、このときはヒープを使わないので、参照カウンタが不要になる。この場合でもテーブルへの登録はしていて、ライフサイクルには従うようである。
ARM64環境の場合は、最上位bitが1ならtagged pointerの模様。
参考1: 64bit環境におけるObjective-Cのポインタ
参考2: Tagged pointers and fast-pathed CFNumber integers in Lion
objc_loadWeakRetainedの処理
wvar が obj を参照していて、retainする処理は以下のような流れ。
-
wvarが保持するidを取得する。もしnilなら終了。(ここではobjが取得されたものとする) -
objに関連するweak変数用のテーブルのポインタを取得する - テーブルをロックする
- ロック後、
wvarの値が変わっていれば、1へ戻る。(テーブルの取得からロックまでの間にオブジェクトが解放された場合など) -
objのクラス(isa)を取得する - hasCustomRR()が偽のとき(独自のretainやreleaseを持たない場合)、tryRetainによりretainする
- そうでない場合、retainWeakReferenceを呼び出す。(retainWeakReferenceが未実装であれば失敗する)
- テーブルをアンロックする
※ getMethodImplementationは、メソッドが未実装の場合に_objc_msgForwardを返す模様 (objc-class.mm)
余談
なぜこれを調べようと思ったかというと、アプリからUIActivityViewControllerで他のアプリにシェアすると、投稿完了時に(どちら側のバグがわからないが)objc_loadWeakRetainedの内部で死んで、謎だったからである。
ちなみにだいたいhasCustomRR()のところで死ぬ。(同じではないが、このへん) どうやらviewのクラスが解放されたあとにアクセスしているようである。
NSObjectの先頭に入っている値はisaなのだが、下位3bitにはタグなどの情報が入っているらしく、ビットマスクしたものをポインタとして使用している。(objc-object.h)
hasCustomRR()はクラスオブジェクトのオフセット0x20(64bit環境の場合)のflagsのbit1に入っている。