Edited at
RealmDay 7

[iOS]Realmがモデルクラスからどのようにしてスキーマを作成しているのか覗いてみる

More than 3 years have passed since last update.

この記事はRealm Advent Calendarの12月7日分です。iOSでのRealmが、どのようにしてモデルクラスからスキーマを作成してるのか、について調べてみました!


Realmでは、下記の様に通常のクラスと同じようにモデルクラスを定義するだけでそれがスキーマとなり、データを格納したり永続化したりすることが出来ます。


dog.h

// Dogというモデルクラスを定義

@interface Dog : RLMObject
@property NSString *name;
@end


RLMObjectクラスを継承したモデルクラスを作るだけで、それがスキーマにもなり、モデルクラスにもなる、ということです。圧倒的なお手軽さ&学習コストの低さ!

しかし、どうやってこの魔法の様な処理を行っているのか気になったので、今回はRealmの公開されているソースを覗いて調べてみました。(全ての実装を見たわけではないので、もしも間違ってる箇所や勘違いしている箇所あればご指摘いただければ嬉しいです!

なお、今回は案件でも普段使いしてるRealm Objective-Cについて調べてみました。


定義されたクラスの一覧を取得する

まず、RealmがどうやってRLMObjectを継承したクラスたちを取得しているのかを見てみます。コードを追っていくとRLMSchemaというクラスのsharedSchemaというメソッドに、プロジェクト内の全てのクラスを取得しているらしき箇所があります。


RLMSchema.mm

        unsigned int numClasses;

std::unique_ptr<__unsafe_unretained Class[], decltype(&free)> classes(objc_copyClassList(&numClasses), &free);
[self registerClasses:classes.get() count:numClasses];

objc_copyClassListというランタイム関数を呼び出している点に注目です。このobjc_copyClassListは定義された全てのClassのClass型のポインタ(ややこしい)が格納されたlistを返すメソッドで、例えば下記の様な処理を書くとプロジェクトで利用されている全てのクラスの名前をNSLogに出力することが出来ます。

    unsigned int count;

Class *classes = objc_copyClassList(&count);
for(Class *cursor = classes; *cursor != nil; cursor++) {
NSLog(@"%s", class_getName(*cursor));
}
free(classes);


RLMObjectを継承したクラスを判別する

これで全てのクラスが取得できるわけですが、必要なのはRLMObjectを継承したクラスのみです。下記の様なUtil関数を利用して、無関係なクラスをフィルタしていることがわかります。


RLMUtil.mm

BOOL RLMIsObjectSubclass(Class klass) {

return RLMIsSubclass(class_getSuperclass(klass), RLMObjectBase.class);
}


クラスのプロパティ情報を取得する

次に、スキーマを作成するためにはRLMObjectのプロパティを解析する必要があります。これについてもどのように実現しているのか探してみたところRLMObjectSchemaというクラスの下記のメソッドにて、クラスのプロパティを取得する処理が実装されていました。


RLMObjectSchema.mm

 + (NSArray *)propertiesForClass:(Class)objectClass isSwift:(bool)isSwiftClass


このメソッドでは、class_copyPropertyListというランタイム関数を呼び出し、プロパティの情報を取得しています。class_copyPropertyListの返り値はobjc_propertyというプロパティ情報が格納された構造体です。このclass_copyPropertyListを利用すれば、例えば下記の様に特定のクラスの全てのプロパティの名前を取得することが出来ます。


RLMObjectSchema.mm

    unsigned int count;

objc_property_t *props = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
NSLog(@(property_getName(props[i])));
}

property_getNameでプロパティ名が取得できるのと同様に、property_copyAttributeListというランタイム関数を呼び出すことでプロパティの型などを含めた各種情報を取得を取得することが出来ます。


まとめ

この様な操作でクラス名、プロパティ、その型と名前を取得し、Realmがスキーマを生成していることがわかります。Realmを導入する以前プロダクトでSQLLiteを利用していた時には、こうしたスキーマ情報をテーブル作成時に定義して、モデルクラスにも同様にプロパティを定義する、というある意味同じようなことを2重に行っていました。やはりモデルクラスを定義するだけでそこから自動的にスキーマ(テーブル)を作成してくれるRealmは便利だし、もう元には戻れないなー、という感じです。

また、このように普段お世話になっているライブラリの内部実装を見てみるのは、面白くもあるしお勉強にもなります。

普段の開発では基本的に上記で説明したようなランタイム機能を使うことはあまりなさそうですが、知っておくとデバッグのちょっとしたツールなどを使う時には便利かもしれません。

参考: Objective-C Runtime Reference