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に入っている。