Objective-C 2.0のオブジェクトとivarが謎だったので少し調べた。(この前は弱参照について調べた)
iOSのARM64環境を前提としている。
Objective-Cのオブジェクトは以下のようなものである。
- NSObjectを継承している
- メソッドに応答する
- インスタンス変数を持つ
objc_object
NSObjectの元となるobjc_objectは、isaという要素を持った構造体である。isaはクラスオブジェクトのポインタで、そのオブジェクトのクラスを示す。
idは基本的にはobjc_objectのポインタだが、タグなどの情報がミックスされている。(最上位ビットが0のとき、下位3ビットを0にしたものがアドレス)
https://github.com/opensource-apple/objc4/blob/master/runtime/objc-private.h#L127
struct objc_class;
struct objc_object;
typedef struct objc_class *Class;
typedef struct objc_object *id;
union isa_t {
Class cls;
};
struct objc_object {
isa_t isa;
};
objc_class
クラスオブジェクトもまたオブジェクトだが、もう少し情報を持っているobjc_classという構造体である。objc_objectを継承していて、先頭のメンバは(objc_objectの)isaである。
Objective-C 1.0と異なり、ivarのリストなどの情報は持っていない。
typedef uint32_t mask_t;
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct class_data_bits_t {
uintptr_t bits;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
継承関係
継承関係を調べるため、Cのポインタに変換して表示してみる。同名だと怒られるので別名で構造体を定義する。
※ ARC下で生ポインタを得るにはbridge castが必要である。
typedef struct objc_klass *Klass;
typedef uint32_t mask_t;
struct objc_objekt {
Klass isa;
};
struct objc_klass {
Klass isa;
Klass superclass;
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
} cache;
struct class_data_bits_t {
uintptr_t bits;
} bits;
};
...
NSObject *obj = [NSObject new];
struct objc_objekt *p = (__bridge struct objc_objekt *) obj;
NSLog(@"obj: %p", p);
NSLog(@"class of obj (NSObject): %p, %p, %p", p->isa, [obj class], objc_getClass("NSObject"));
NSLog(@"class of NSObject: %p, super class of NSObject: %p", p->isa->isa, p->isa->superclass);
NSLog(@"class of class of NSObject: %p, super class of class of NSObject: %p", p->isa->isa->isa, p->isa->isa->superclass);
上の実行結果は以下のような感じになる。(アドレスは毎回変わる)
obj: 0x60000000a050
class of obj (NSObject): 0x103adbe88, 0x103adbe88, 0x103adbe88
class of NSObject: 0x103adbe38, super class of NSObject: 0x0
class of class of NSObject: 0x103adbe38, super class of class of NSObject: 0x103adbe88
先頭に入っている値isaが[obj class]やobjc_getClass("NSObject")と同じもの(NSObjectのクラスオブジェクト)を指していることがわかる。
NSObjectを継承したオブジェクトHogeについて試してみると、
@interface Hoge : NSObject
@property NSString *x;
@property NSString *y;
@end
@implementation Hoge
@end
...
hoge = [Hoge new];
struct objc_objekt *p = (__bridge struct objc_objekt *) hoge;
NSLog(@"hoge: %p", p);
NSLog(@"class of hoge (Hoge): %p, %p, %p", p->isa, [hoge class], objc_getClass("Hoge"));
NSLog(@"class of Hoge: %p, super class of Hoge: %p", p->isa->isa, p->isa->superclass);
NSLog(@"class of class of Hoge: %p, super class of class of Hoge: %p", p->isa->isa->isa, p->isa->isa->superclass);
以下のようなものが得られる。
hoge: 0x608000257d90
class of hoge (Hoge): 0x1031407e8, 0x1031407e8, 0x1031407e8
class of Hoge: 0x1031407c0, super class of Hoge: 0x103adbe88
class of class of Hoge: 0x103adbe38, super class of class of Hoge: 0x103adbe38
継承関係図
継承関係を図にすると以下のようになる。Objective-C 1.0と同様の結果となった。
(参考: http://news.mynavi.jp/column/objc/016/)
なお上図はMarkDownDiagramで生成した。ソースは以下の通り。
[b1] <5,5>
#obj 0x60000000a050
isa 0x103adbe88
==>[b2]
[b2] <20,5>
#NSObject 0x103adbe88
isa 0x103adbe38
==>[b3]
---
superclass 0x0
[b3] <37,5>
#meta NSObject 0x103adbe38
---
isa 0x103adbe38
l==>l1[b3]
---
superclass 0x103adbe88
l==>l1[b2]
[b4] <5,15>
#hoge 0x608000257d90
isa 0x1031407e8
==>[b5]
[b5] <20,15>
#Hoge 0x1031407e8
isa 0x1031407c0
==>[b6]
---
superclass 0x103adbe88
l==>l1[b2]
[b6] <37,15>
#meta Hoge 0x1031407c0
---
isa 0x103adbe38
l==>l1[b3]
---
superclass 0x103adbe38
l==>l1[b3]
ivar
オブジェクトが構造体のようなものであれば、isaに続けて格納されているのではと予想できる。
Objective-Cのインスタンス変数はivarと呼ばれている。ivarは以下のような定義で、名前と型と、オブジェクトのどこに格納されるかというオフセットを持つ。
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
};
前掲のHogeについて試してみると、
@interface Hoge : NSObject
@property NSString *x;
@property NSString *y;
@end
@implementation Hoge
@end
...
unsigned int count;
Ivar *ivars = class_copyIvarList([Hoge class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSLog(@"name: %s, offset: %lu", ivar_getName(ivar), ivar_getOffset(ivar));
}
free(ivars);
以下のような結果が得られた。
name: _x, offset: 8
name: _y, offset: 16
64bit環境ではisaのサイズは8なので、連続していそうである。
試しに構造体にキャストして内容を表示してみると、isaの次に格納されているようである。
@interface Hoge : NSObject
@property NSString *x;
@property NSString *y;
@end
@implementation Hoge
@end
struct HogeHoge {
void *isa;
void *x;
void *y;
};
...
hoge = [Hoge new];
hoge.x = @"hoge";
hoge.y = @"piyo";
struct HogeHoge *hogehoge = (__bridge struct HogeHoge *) hoge;
NSLog(@"x: %@, y: %@", hogehoge->x, hogehoge->y);
実行結果
x: hoge, y: piyo
クラスの変更
どうやらObjective-Cは動的型付け言語らしく、実行時にクラスを定義したり、オブジェクトのクラスを変更したりできるらしいので、クラスを変更してみる。
クラスはobject_setClassで変更できる。クラスFugaを定義して、HogeクラスのインスタンスhogeをFugaクラスにしてみる。
Fugaは、メンバxとyの順番と型をHogeとは変えてみた。
@interface Hoge : NSObject
@property NSString *x;
@property NSString *y;
@end
@implementation Hoge
@end
@interface Piyo : NSObject
@property NSString *piyopiyo;
@end
@implementation Piyo
@end
@interface Fuga : NSObject
@property NSString *y;
@property Piyo *x;
@property NSString *v;
@property NSString *w;
@end
@implementation Fuga
@end
...
NSLog(@"size of %@: %zd", NSStringFromClass([Hoge class]), malloc_size((__bridge const void *) [Hoge new]));
NSLog(@"size of %@: %zd", NSStringFromClass([Fuga class]), malloc_size((__bridge const void *) [Fuga new]));
// dump ivars
unsigned int count;
Ivar *ivars = class_copyIvarList([Fuga class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSLog(@"name: %s, offset: %lu", ivar_getName(ivar), ivar_getOffset(ivar));
}
free(ivars);
// generate hoge
Hoge *hoge = [Hoge new];
hoge.x = @"hoge";
hoge.y = @"piyo";
NSLog(@"hoge: %zd, %@, %@, %@", malloc_size((__bridge const void *) hoge), hoge, hoge.x, hoge.y);
// change class
object_setClass(hoge, [Fuga class]);
Fuga *fuga = (Fuga *)hoge;
fuga.y = @"fuga";
NSLog(@"fuga: %zd, %@, %@, %@", malloc_size((__bridge const void *) fuga), fuga, fuga.x, fuga.y);
以下のような出力が得られた。
size of Hoge: 32
size of Fuga: 48
name: _y, offset: 8
name: _x, offset: 16
name: _v, offset: 24
name: _w, offset: 32
hoge: 32, <Hoge: 0x608000228d60>, hoge, piyo
fuga: 32, <Fuga: 0x608000228d60>, piyo, fuga
hogeはFugaクラスに変わったが(isaがFugaを指す)、それ以外の中身は変わっていない。Fugaクラスのオブジェクトは通常48バイトだが、クラスを変更しても32バイトのままである。
Fugaのivarはy,xの順番のため、Fugaクラスに変更後にyに "fuga"
を代入すると、先頭が "fuga"
に書き換わる。
fuga.xとfuga.yを表示すると、Hogeにおける2番目、1番目を表示することになるので、 piyo, fuga
になる。
というわけでオブジェクトには余分な情報が含まれておらず、割と効率的に格納されていることがわかった。