ピンチ操作による拡大縮小
例えば写真や3Dモデルなどのビューワーアプリ、あるいは地図アプリなどでは、ピンチ操作(2本の指を画面に振れさせ、摘むように指同士を近づけたり、あるいは遠ざけたりする操作)によって対象を拡大縮小する機能がよく備えられています。そこで、前の記事で作成した(クォータニオンを用いて)ドラッグ操作でモデルを回転させることが出来るアプリに、更にピンチ操作によりモデルの拡大縮小を行う機能を追加してみます。
Pinch Gesture Recognizer の利用
ピンチ操作を検出するための方法はいくつかあるようですが、XCode(この記事執筆時点のバージョンは4.6)のStoryboardのObject Libraryには、「Pinch Gesture Recognizer」というそのものズバリなオブジェクトが用意されていますので、これを用いる方法についてメモします(以下のメモは、前の記事で作成したアプリに付け足していく形とします。また、この方法はあくまで自己流であり、他にも方法があるかもしれません)。
- 最初に ViewController.h に、以下のようにデリゲート(UIGestureRecognizerDelegate)を追加します。
//ViewController.h #import <UIKit/UIKit.h> #import <GLKit/GLKit.h> @interface ViewController : GLKViewController<UIGestureRecognizerDelegate> @end
- 続いて、Storyboard 上で Object Library から Pinch Gesture Recognizer を探し、View の上にドラッグ&ドロップします。
- Pinch Gesture Recognizer のアイコンが追加されますので、このアイコンから ViewController のアイコンまで Control キーを押しながら(あるいは、右クリックをしながら)ドラッグ&ドロップします。ドラッグ時に青線が表示されるはずです。ドロップすると下図のようなポップアップが表示されますので、「Outlets」の下にある「delegate」を左クリックします。これで ViewController がピンチ操作を検知するようになります。
- あとは、ピンチ操作検出時のアクションを設定します。まず下図のように ViewController.h を表示し、Pinch Gesture Recognizer のアイコンから ViewController.h の適当な行まで、Control キーを押しながら(あるいは右クリックをしながら)ドラッグ&ドロップします。
- すると下図のようなダイアログが表示されますので、「Connection」の種類を「Action」にし、適当な名前(ここでは pinchGestureRecognized としました)を付けて、「Connect」ボタンをクリックします。
- これで、ViewController.m の中に、以下のようにピンチ操作を検知するたびに呼び出されるアクションメソッドが自動的に追加されます。
//ViewController.m の末尾に自動的に追加 - (IBAction)pinchGestureRecongized:(id)sender { }
- あとはアクションメソッドの中に、以下のようにモデルの拡大縮小を行うための処理を記述します。
- (IBAction)pinchGestureRecongized:(id)sender { GLfloat factor = [(UIPinchGestureRecognizer *)sender scale]; eyeDist += eyeDist * (1.0f - factor) * 0.05f; if(eyeDist > -5.0f) eyeDist = -5.0f; if(eyeDist < -100.0f) eyeDist = -100.0f; }
ここでは単純に、視点からモデル中心までの距離を表す変数(eyeDist 変数)の値を操作する方法をとります。以下のように scale プロパティを介して、ピンチ操作の倍率(最初に指を置いた位置に対して、指同士がどれだけ近づいたか(または離れたか))を取得することが出来ます。
GLfloat factor = [(UIPinchGestureRecognizer *)sender scale];
そして倍率(eyeDist は負の値ですので、(1.0f - factor) という計算を噛ませてあります)、および現在の eyeDist の値に比例させた補正値をeyeDist に加算します。補正値を現在の eyeDist の値に比例させるのがポイントであり、これによって常に一定の速度で拡大縮小が行われるように見せることが出来ます(これについては、もっとスマートな方法があるかもしれませんが…)。
実行してピンチ操作をしてみてください(iPhone シミュレータ上であれば、option キーを押しながらシミュレータの画面上をドラッグするとピンチ操作が入力出来ます。2つの白い円が、2本の指先の位置を表します)。ピンチインによってモデルが縮小され、ピンチアウトによってモデルが拡大されます。
クォータニオンによる視点回転操作
iPhone上で3Dグラフィックスを扱うのであれば、やはり画面タッチ操作により3Dモデルを自由に回転させたりしてみたいもの。画面がタッチされたことや、ドラッグの移動距離の検出自体は touchBegin/touchMoved/touchEnded メソッドを用いて簡単に実現出来ますが、それをモデルの回転とどう結びつけたら良いでしょうか。
単純なモデル回転方法と問題点
単純な発想としては、例えば垂直方向、水平方向へのドラッグ距離に応じた角度で、それぞれx軸、y軸回りにモデルを回転させるような方法があります。GLKitを用いるのであれば、以下のように GLKMatrix4Rotate() 関数などを用いれば実装は容易でしょう。
//angleX、angleYは角度を表す変数であり、touchMoved メソッドなどの中で //タッチ移動距離に応じた角度を計算して代入してあるとする。 baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, angleX, 1.0f, 0.0f, 0.0f); baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, angleY, 0.0f, 1.0f, 0.0f);
こんな方法でも一応は目的を実現出来ます。しかし、この方法だとモデルの向き次第では、例えば画面水平方向にタッチ移動したのにモデルが上下に回転したり、ドリルのようにz軸中心に回転したり、といったことが起こってしまい、あまり直感的なモデル回転が出来ません。
クォータニオンを用いたモデル回転
そこで「クォータニオン」という数学的概念を用いると、もっと直感的なモデル回転を実現することが可能です。クォータニオンは高校数学で習う複素数をさらに拡張したような概念です。複素数はその乗算(掛け算)によって平面における回転操作を表現することが出来ますが、クォータニオンでは3次元空間における任意の軸回りの回転操作を表現することが出来ます。
…とは言いながら、どうしてクォータニオンの演算により回転操作が実現出来るのかについての数式的な理解などはまだ出来ていません(爆)。しかし、ひとまず道具としてクォータニオンを使ってみることは可能です。ここでは床井研究室様*1のサンプルソースを参考に、GLKitを用いてドラッグ操作により3Dモデルを回転する簡単なプログラムを作成してみました。
XCode(この記事執筆時点のバージョンは4.6)で「OpenGL Game」テンプレートを選択してプロジェクトを作成し、ViewController.m の中身を以下のソースコードに書き換えれば動作します(はずです)。
#import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) #define SCALE (2.0f * M_PI) GLfloat gCubeVertexData[216] = { 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f }; @interface ViewController () { GLfloat _rotation; GLuint _vertexArray; GLuint _vertexBuffer; GLKMatrix4 modelViewMatrix1; GLKMatrix4 modelViewMatrix2; CGPoint _touchBegan; CGPoint _touchFinished; GLfloat eyeDist; //視点操作用のクオータニオンおよび行列 GLKQuaternion cquat; GLKQuaternion tquat; GLKMatrix4 rotMat; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; //クオータニオンおよび行列の初期化 cquat = GLKQuaternionIdentity; tquat = GLKQuaternionIdentity; rotMat = GLKMatrix4MakeWithQuaternion(cquat); eyeDist = -10.0f; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ _touchBegan = [[touches anyObject] locationInView:self.view]; } -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ //ドラッグの移動距離から、回転のクォータニオンを求め、更に行列に変換する。 _touchFinished = [[touches anyObject] locationInView:self.view]; GLfloat dx = (_touchFinished.x - _touchBegan.x) / self.view.bounds.size.width; GLfloat dy = (_touchFinished.y - _touchBegan.y) / self.view.bounds.size.height; GLfloat a = sqrtf(dx * dx + dy * dy); if(a != 0.0){ GLfloat ar = a * SCALE * 0.5; GLfloat as = sinf(ar) / a; GLKQuaternion dquat = GLKQuaternionMake(dy * as, dx * as, 0.0f, cosf(ar)); tquat = GLKQuaternionMultiply(dquat, cquat); rotMat = GLKMatrix4MakeWithQuaternion(tquat); } } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ cquat = tquat; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); glEnable(GL_DEPTH_TEST); glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData), gCubeVertexData, GL_STATIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribNormal); glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(12)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; } - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 300.0f); self.effect.transform.projectionMatrix = projectionMatrix; GLKMatrix4 baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, eyeDist); //回転のクォータニオンから変換された行列を、モデルビュー行列に乗じる baseModelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, rotMat); GLKMatrix4 modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix); modelViewMatrix1 = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix1 = GLKMatrix4Rotate(modelViewMatrix1, _rotation, -1.0f, -1.0f, 1.0f); modelViewMatrix1 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix1); modelViewMatrix2 = GLKMatrix4MakeTranslation(0.0f, 0.0f, 1.5f); modelViewMatrix2 = GLKMatrix4Rotate(modelViewMatrix2, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix2 = GLKMatrix4Scale(modelViewMatrix2, 2.0f, 2.0f, 2.0f); modelViewMatrix2 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix2); _rotation += self.timeSinceLastUpdate * 0.5f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.65f, 0.65f, 0.65f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArrayOES(_vertexArray); self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); self.effect.transform.modelviewMatrix = modelViewMatrix1; [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); self.effect.light0.diffuseColor = GLKVector4Make(0.4f, 0.4f, 1.0f, 1.0f); self.effect.transform.modelviewMatrix = modelViewMatrix2; [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); } @end
クォータニオン関連の操作については、ほとんどサンプルソースをGLKit向けに移植したようなモノとなっておりますが、GLKitにはあらかじめクォータニオンを扱うための構造体(GLKQuarternion)や関数群が用意されておりますので、それを活用しています。そしてtouchBegin/touchMoved/touchEnded メソッドを用い、この中でクォータニオン関連の操作を行っています。
実行すると例によって、下図のように2つの立方体が表示されますが、ドラッグ操作によってモデル全体(別の言い方をすれば視点)をグリグリ回転させることが可能です。常にドラッグした方向通りに、直感的に回転をしてくれます。
追記(2013/09/08)
ソースコードに一部誤りがあったので訂正しました。viewDidLoad の中のクオータニオンおよび行列を初期化する部分で、tquat を単位クオータニオンで初期化する1行が抜けていました。
//クオータニオンおよび行列の初期化 cquat = GLKQuaternionIdentity; tquat = GLKQuaternionIdentity; //この1行が抜けていた rotMat = GLKMatrix4MakeWithQuaternion(cquat);
この行が抜けていると、回転の位置によってはモデルが一切表示されなくなる不具合が生じてしまうようです。これまで本ソースを参考にされた方には失礼致しました。
「OpenGL Game」テンプレートを使わずGLKitを使う方法
XCode(この記事執筆時点のバージョンは4.6)で「OpenGL Game」テンプレートを使わずに、例えば「Tabbed Application」など通常のテンプレートを用いたアプリ開発でも、GLKitでOpenGL ES 2.0を使えるようにする手順についてメモします。ここでは例として「Tabbed Application」を用いていますが、それ以外のテンプレートでも同様の手順で行けるはずです。
- まずは普通に、「Tabbed Application」テンプレートを選択してプロジェクトを作成します。
- プロジェクトに「GLKit.framework」「OpenGLES.framework」という2つのフレームワークを追加します。
- Storyboardエディタを開いて、既存の ViewController に貼付けられた UIView を削除します(今回の場合はTabbed Applicationなので、どちらか片方のみ(ここではFirstViewControllerの側)を削除してみます)
- 右下のObject Libraryから「GLKit View」を選んで、先程削除した UIView の代わりに貼付けます。
- GLKit View を貼付けた ViewController のヘッダファイル(ここでは FirstViewController.h )を開いて、以下のような書き換えを行います。GLKit/GLkit.h ヘッダファイルのインポートを追加して、UIViewControllerクラスではなく、GLKViewControllerクラスを継承するように書き換えます。
//FirstViewController.h #import <UIKit/UIKit.h> #import <GLKit/GLKit.h> //追加 @interface FirstViewController : GLKViewController //GLKViewControllerクラスを継承するように書き換え @end
- 後は、GLKit View を貼付けた ViewController のソースファイル(ここでは FirstViewController.m)に、GLKitを用いたコードを記述していくだけです。ここでは前の記事で記述した、GLKitのみで2つの回転する立方体を描画するコードをそのまま(ただし、ViewController の名前は FirstViewController に修正して)用いて実行してみます。以下のように、片方のタブでは OpenGL ES 2.0 によるグラフィックを表示、もう片方のタブでは通常のUIViewによる表示、といったアプリが作成出来ます。
「OpenGL Game」テンプレートを簡略化
前の記事にも書いたように、XCode 4.6(この記事の執筆時点)に用意されている「OpenGL Game」テンプレートでは、シェーダプログラムを用いるパターンと、GLKitを用いるパターンの両方で、OpenGL ES 2.0により立方体を描画するアプリを自動生成してくれます。
しかしこのテンプレートを足掛かりに、シェーダを用いずにGLKitのみを用いて自作アプリを開発しようとするなら、シェーダに関するコードは不要となります。そこで、こちらのサイト(外部)を参考にして、テンプレートの ViewController.m からシェーダに関するコードを削除してみました。以下に削除後のコードを掲載しておきます。しかし、ただ削除するだけでは面白くないので、少しだけ手を加えてあります。
#import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) GLfloat gCubeVertexData[216] = { // Data layout for each line below is: // positionX, positionY, positionZ, normalX, normalY, normalZ, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f }; @interface ViewController () { GLKMatrix4 modelViewMatrix1; GLKMatrix4 modelViewMatrix2; float _rotation; GLuint _vertexArray; GLuint _vertexBuffer; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } // Dispose of any resources that can be recreated. } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); glEnable(GL_DEPTH_TEST); glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData), gCubeVertexData, GL_STATIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribNormal); glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; } #pragma mark - GLKView and GLKViewController delegate methods - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 100.0f); self.effect.transform.projectionMatrix = projectionMatrix; GLKMatrix4 baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -8.0f); baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, _rotation, 0.0f, 1.0f, 0.0f); modelViewMatrix1 = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix1 = GLKMatrix4Rotate(modelViewMatrix1, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix1 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix1); modelViewMatrix2 = GLKMatrix4MakeTranslation(0.0f, 0.0f, 1.5f); modelViewMatrix2 = GLKMatrix4Rotate(modelViewMatrix2, _rotation, -1.0f, 1.0f, 1.0f); modelViewMatrix2 = GLKMatrix4Scale(modelViewMatrix2, 1.5f, 1.5f, 1.5f); modelViewMatrix2 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix2); _rotation += self.timeSinceLastUpdate * 0.5f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.65f, 0.65f, 0.65f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArrayOES(_vertexArray); self.effect.transform.modelviewMatrix = modelViewMatrix1; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); self.effect.transform.modelviewMatrix = modelViewMatrix2; self.effect.light0.diffuseColor = GLKVector4Make(0.4f, 0.4f, 1.0f, 1.0f); [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); } @end
シェーダを用いて立方体を描画するコードを削除する代わりに、GLKitを用いて2つの立方体を描画するように手を加えました。インスタンス変数として2つのモデルビュー行列(GLKMatrix4 型の modelViewMatrix1, 2)を用意しておき、それぞれに異なる操作(移動、回転、スケーリング)を施しておきます。そして glkView メソッドの中で、それぞれの立方体を描画する直前に、エフェクトに対してモデルビュー行列(ついでに diffuseColorも)を設定し直しています。
これを実行すると、下図のように色と大きさの異なる2つの立方体が、くるくる回転します。
OpenGL ES 2.0 と GLKit
OpenGL ES 2.0の高い壁
iPhoneでも、OpenGLを用いて3Dグラフィックス全開なアプリが作成出来ると聞いて、前々からのOpenGL使いである私は早速学習に乗り出しました。しかし、iPhoneのようなスマホ向けに用いられているのは「OpenGL ES」という、本家とは異なるサブセットであることを知ったのです。
最初は「サブセットとは言っても、基本的な文法はきっとそんなに変わらないよね」とタカを括りながら学習を始めました。しかし、特に現在メインとなっているOpenGL ES 2.0では固定機能シェーダが廃止されており、それに伴って glBegin/glEnd とか、glPushMatrix/glPopMatrix とか、glOrtho とか glLight とか glMaterial とか、とにかく本家で慣れ親しんだ関数たちがごっそり消滅していました。その代わりにOpenGL ES 2.0 ではプログラマブルシェーダ(頂点シェーダ、フラグメントシェーダ)のみが実装されており、自前でシェーダプログラムを書くことによって、従来の固定機能シェーダよりも遥かに自由度の高いグラフィックス処理が可能であるようです。
しかし自由度が高くなった分、習得するためのハードルも高くなってしまったようです。素のOpenGL ES 2.0では、ちょっとした図形を描くだけでも、シェーダで視点、照明、座標変換といった様々な行列計算などを自前で実装しないといけずに、シェーダの文法などに関する知識は当然として、幾何学とか行列計算についても相応に高度な知識が必要とされる雰囲気でした。あらかじめ用意された関数やライブラリに頼るプログラミングにドップリ浸っていた自分には、非常に敷居が高く感じてしまいました。
便利なGLKitフレームワーク
そこで、Apple社が私のような存在を見かねてか(笑)、iOS 5.0のリリースと同時に「GLKit」というフレームワークを提供し始めたそうです。これは一言で言うと、シェーダを書くこと無しに、割と従来のOpenGLと近い感覚で、OpenGL ES 2.0によるプログラミングを可能にしてくれるフレームワークです。GLKitには、主に以下のような機能が備わっているそうです。
- 行列やベクトルなどの数学的計算機能や、更には従来の glOrtho や glFrustrum や gluLookAt といった視点変換行列の生成関数と同等の機能を持つ関数を提供するGLKMath
- 自力で画像ファイルをピクセルデータの配列に変換しなくても、画像ファイルのパスを指定するだけで、自動的にテクスチャを生成してくれるGLKTextureLoader
- UIViewクラスから派生しており、各種バッファの処理や画面のスナップショット機能なども備えたGLKView、GLKViewのビューコントローラーであり、アニメーションのFPS設定や一時停止、再開などの機能も備えたGLKViewController
- 自前でシェーダを書くこと無しに、視点変換行列、照明、マテリアル、テクスチャなどの設定や利用を、従来のOpenGLと近い感覚で行うことが可能になるGLKBaseEffect
この記事を執筆時点で私が用いている XCode 4.6 では、「OpenGL Game」というiOS Application向けのテンプレートが用意されており、シェーダプログラムを用いるパターンと、GLKitを用いるパターンの両方で、OpenGL ES 2.0により立方体を描画するアプリを自動生成してくれます。これを足掛かりとして、実際にGLKitを使って色々プログラムを書いてみましたが、実際に非常に便利なフレームワークです。シェーダプログラムを一切書かなくても、結構な範囲のグラフィックス処理が実現出来そうです(現在公開している「iPendulumWaves」というアプリは、GLKitだけで作成しました)。
まだまだGLKitについての情報はネット上にも書籍などにも少ないようですので、自分なりにGLKitを学習して理解したことがあったら、このブログにメモとして公開して行きたいと思います。もちろんGLKitだけで何もかも出来るというわけではなさそうですので、シェーダについても平行して学習して行きたいと思います。
プロパティ宣言の様々な書き方について
Objective-Cにおけるプロパティ宣言についてですが、XCode のバージョンアップなどに伴って様々な書き方が出来るようになっているようですので、簡単にまとめてメモしてみます。
最も基本的(であると思われる)な方法
まずは、一番基本的であると思われる方法でプロパティを用いたプログラムを書いてみます。
#import <Foundation/Foundation.h> @interface myClass : NSObject{ int num; } @property int number; @end @implementation myClass @synthesize number = num; -(void)showNumber{ NSLog(@"num = %i\n", num); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
最初にクラスのインタフェース部で、インスタンス変数(ここでは num)と、それにアクセスするためのプロパティ(ここでは number)を宣言します。そして実装部で、@synthesize 指示子を用いて以下のように、インスタンス変数とプロパティを対応させます。
@synthesize number = num;
これで以下のようにドット演算子を用いて、number プロパティを介して変数 num に外からアクセスすることが可能となります(ただし、変数 mc を id 型にしてしまうとドット演算子が使えないことに注意。型としてクラスを明示的に指定する必要があります)。
mc.number = 1;
変数名とプロパティ名が同一の場合
変数名とプロパティ名を同一にすれば、@synthesize 指定子を用いるときに、イコール演算子(=)を用いる必要が無くなります。
#import <Foundation/Foundation.h> @interface myClass : NSObject{ int number; } @property int number; @end @implementation myClass //イコール演算子(=)を用いる必要はなし! @synthesize number; -(void)showNumber{ NSLog(@"num = %i\n", number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
インスタンス変数の定義の省略
実は、プロパティだけを宣言して @synthesize 指定子を用いると、宣言したプロパティと同名、同型のインスタンス変数が裏側で自動的に定義されるようです。すなわち、インスタンス変数の定義を省略することが出来ます。
このインスタンス変数には、もちろんプロパティを介してアクセスすることが可能です。
#import <Foundation/Foundation.h> @interface myClass : NSObject //インスタンス変数は定義しない。プロパティのみ宣言! @property int number; @end @implementation myClass @synthesize number; -(void)showNumber{ //int 型のインスタンス変数 number が、自動的に生成されている。 NSLog(@"num = %i\n", number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
@synthesize 指定子の省略
更にXCode 4.4からは、インスタンス変数の定義に加えて、@synthezie 指定子すら省略出来るようです。このときは、宣言したプロパティと同型で、プロパティ名の頭にアンダーバー(_)が付けられた名前のインスタンス変数が自動的に定義されるようです。
もちろん、このアンダーバーが付いたインスタンス変数にも、プロパティを介してアクセスすることが可能です。@synthesize 指定子を省略した(かつ、@dynamic 指定子も用いていない)場合には、@synthesize 指定子が裏側で自動的に補完されるようです。
#import <Foundation/Foundation.h> @interface myClass : NSObject //インスタンス変数は定義しない。プロパティのみ宣言! @property int number; @end @implementation myClass //@synthesize 指定子も省略! -(void)showNumber{ //int 型のインスタンス変数 _number が、自動的に生成されている。 //アンダーバー(_)が名前の頭に付くことに注意! NSLog(@"num = %i\n", _number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
ARCについて色々実験(5)
※ARCについて色々実験(4)の続きとなります。
弱い参照について
実験5-01
これまで述べてきたように、ARCでは変数(id 型など)がオブジェクトを参照する場合、デフォルトで強参照として扱われます。また、以下のように「__strong」という修飾子を変数に付けることによって、明示的に強参照を指定することが可能です。
//変数 mc からのオブジェクトへの参照は、強参照となる。 __strong id mc = [[myClass1 alloc] init]; //__strong 修飾子は、以下のような位置に付けても可 id __strong mc = [[myClass1 alloc] init];
実は、ARCには「弱い参照(弱参照)」という概念も存在します。これは以下のように「__weak」という修飾子を変数に付けて指定します。
//変数 mc からのオブジェクトへの参照は、弱参照となる。 __weak id mc = [[myClass1 alloc] init]; //先程同様に、修飾子は以下のような位置に付けても可 id __weak mc = [[myClass1 alloc] init];
例えば、ある変数 A がオブジェクトを弱参照しても、そのオブジェクトは保持されません(A はオーナーにはなりません)。従って、弱参照をする変数に代入されるオブジェクトは、オーナーが別に存在している必要があります。そしてそのオーナーが居なくなると、A からの弱参照が存在していてもオブジェクトは解放されてしまいます。このとき、A には自動的に nil が代入されます。詳しくは後述しますが、この nil が代入されるという点は地味に重要です。
以下では強参照と弱参照について具体的に実験してみます。まず基本となるプログラム(srcARC5-01)を以下に示します。
//srcARC5-01 //当然ながら、ARCをオンにしてのコンパイルが必須です #import <Foundation/Foundation.h> @interface myClass1 : NSObject{ int num; } @end @implementation myClass1 -(void)showNumber{ NSLog(@"******** 私は myClass No.%iです。\n", num); } -(void)dealloc { NSLog(@"******** myClass No.%i 解放\n", num); } @end int main(int argc, const char * argv[]) { __strong id mc1; __strong id mc2; mc1 = [[myClass1 alloc] init]; mc2 = mc1; mc1 = nil; NSLog(@"mc1 にnilを代入"); [mc2 showNumber]; return 0; }
srcARC1-01 に少し手を加えただけの、非常に単純なプログラムです。変数 mc1、mc2 の __strong 修飾子は無くても構いません(ARCでは、変数からオブジェクトへの参照は、デフォルトで強参照となるため)。これを実行すると以下のログが出力されます。
mc1 にnilを代入
******** 私は myClass No.0です。
******** myClass No.0 解放
このプログラム自体は、何も目新しいことはしていません。mc1 が強参照している myClass1 のオブジェクトをmc2 に代入し、その後 mc1 に nil を代入しています。ここで mc1 からオブジェクトへの強参照は外れますが、代入により mc2 もオブジェクトを強参照するようになったので、ここではオブジェクトは解放されません。mc2 にメッセージを送信して、メソッドを呼び出すことも当然可能です。
実験5-02
それでは、srcARC5-01 における変数 mc2 の修飾子を __weak に変更してみます(以下の srcARC5-02)。 それ以外の変更は一切加えません。
//srcARC5-02 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ main 関数以外は、srcARC5-01 と同一 int main(int argc, const char * argv[]) { __strong id mc1; __weak id mc2; mc1 = [[myClass1 alloc] init]; mc2 = mc1; mc1 = nil; NSLog(@"mc1 にnilを代入"); [mc2 showNumber]; return 0; }
これを実行すると、以下のログが出力されます。
******** myClass No.0 解放
mc1 にnilを代入
今度は mc2 にオブジェクトを代入しても、mc2 はオブジェクトを弱参照するためオーナーにはなりません。そのため mc1 に nil を代入した時点で、オブジェクトは解放されてしまいます。このとき mc2 には前述のように、自動的に nil が代入されます。
その後、以下のように mc2 にメッセージを送信して、メソッドを呼び出そうとしているわけですが、mc2 には既に nil が代入されています。従ってここでは nil に対してメッセージを送信しているわけです。
[mc2 showNumber];
一見エラーが発生しそうですが、実は Objective-C では nil へどんなメッセージを送信をしても「何もしない」という仕様となっているそうです。従って、この行では何の処理も行われず、何事も無かったかのようにプログラムは進行します。このため、例えば以下のような書き方でエラー対策をする必要は全くありません。これが弱参照の地味ですが重要なポイントの一つです。
//こんなエラー対策は不要! if(mc2 != nil){ [mc2 showNumber]; }
循環参照について
実験5-03
ARCには弱参照という概念があるのは分かりましたが、どのようなときに必要になるのでしょうか。弱参照の主な用途として「循環参照」の回避が挙げられます。循環参照とは例えば、あるオブジェクト A のインスタンス変数がオブジェクト B を強参照しており、また B のインスタンス変数も A を強参照しているような状態です。 A と B はお互いがお互いのオーナーとなっているため、どちらも解放されることが無くなってしまいます。以下の srcARC5-03 で、循環参照を引き起こす具体的なプログラム例を示します。
//srcARC5-03 //当然ながら、ARCをオンにしてのコンパイルが必須です #import <Foundation/Foundation.h> //myClassA の定義内で、まだ定義されていない myClassB を用いるための前方宣言 @class myClassB; @interface myClassA : NSObject{ } @property(nonatomic, strong) myClassB *B; @end @implementation myClassA -(void)showMe{ NSLog(@"私は myClassA です。\n"); } -(void)dealloc { NSLog(@"myClassA 解放\n"); } @end @interface myClassB : NSObject{ } @property(nonatomic, strong) myClassA *A; @end @implementation myClassB -(void)showMe{ NSLog(@"私は myClassB です。\n"); } -(void)dealloc { NSLog(@"myClassB 解放\n"); } @end int main(int argc, const char * argv[]) { @autoreleasepool { myClassA *mca = [[myClassA alloc] init]; myClassB *mcb = [[myClassB alloc] init]; //循環参照を引き起こす! mca.B = mcb; mcb.A = mca; [mca.B showMe]; [mcb.A showMe]; } return 0; }
ここでは2つのクラス myClassA、myClassB を定義しています。これらのクラスはそれぞれ、相手方のクラスを強参照するインスタンス変数(プロパティを介して)を有しています。ここではプロパティを用いていますが、強参照、弱参照の考え方は普通の変数と何ら変わりません。以下のように strong、weak という属性を付けることによって、参照のタイプを制御することが出来ます。
//強参照を持つプロパティの宣言例 @property(strong, nonatomic) myClass1 *mc; //弱参照を持つプロパティの宣言例 @property(weak, nonatomic) myClass1 *mc;
そして main 関数では両者のオブジェクトを生成し、プロパティにお互いを代入させ合って循環参照を引き起こさせています。これを実行すると以下のログが出力されます。
私は myClassB です。
私は myClassA です。
見てのとおり、両方のオブジェクトについて dealloc メソッドが呼び出されていません。循環参照によって、両方のオブジェクトが解放されないまま残り続けてしまっているわけです。これはメモリリークの原因となります。
実験5-04
循環参照が起こるのを回避するためには、弱参照の概念が必要となります。と言っても難しいことではなく、srcARC5-03 の場合は myClassA か myClassB のどちらかについて、プロパティの属性を弱参照に変更するだけです。ここでは以下のように、myClassA の持つプロパティの方を変更してみます。
//myClassA の持つプロパティの属性を弱参照に変更 //(myClassB の持つプロパティの方を変更してもよい) @property(nonatomic, weak) myClassB *B;
プロパティ属性を変更した srcARC5-03 を実行すると、以下のログが出力されます。
私は myClassB です。
私は myClassA です。
myClassB 解放
myClassA 解放
今度はきちんと、両方のオブジェクトが解放されています。myClassA のオブジェクト(のインスタンス変数)は myClassB のオブジェクトを弱参照しているため、myClassB のオブジェクトのオーナーは変数 mcb だけとなります。これによって、循環参照は解消されます。
弱参照と自動解放プール
実験5-05
ところで srcARC5-03 では、main 関数内で自動解放プールを用いていますが、一見これは必要なさそうに見えます。実際、素の srcARC5-02 では自動解放プールを消去しても、全く同じ動作をします(循環参照が起こります)。
しかし、実験5-04 のように弱参照を用いたときは話が違います。自動解放プールを消去した状態で実験5-04 の内容を実行すると、出力されるログは以下のようになります。
私は myClassB です。
私は myClassA です。
なんと、きちんと弱参照を用いて循環参照を解消したはずなのに、両方のオブジェクトが解放されなくなってしまいました。非メソッドファミリのメソッドによるオブジェクト生成などを特に行っていないにも関わらず、自動解放プール無しではプログラムが意図した通りに動作しません。もう少し検証してみたいと思います。
実験5-06
弱参照と自動解放プールの関係を検証するために、別の実験をしてみます。まずプログラム(srcARC5-06)を以下に示します。
//srcARC5-06 //当然ながら、ARCをオンにしてのコンパイルが必須です #import <Foundation/Foundation.h> @interface myClass1 : NSObject{ int num; } @end @implementation myClass1 -(void)showNumber{ NSLog(@"******** 私は myClass No.%iです。\n", num); } -(void)dealloc { NSLog(@"******** myClass No.%i 解放\n", num); } @end int main(int argc, const char * argv[]) { NSLog(@"autoreleasepool 開始\n"); @autoreleasepool{ NSLog(@"** ローカルスコープ開始\n"); { __strong id smc = [[myClass1 alloc] init]; __weak id wmc; wmc = smc; [wmc showNumber]; } NSLog(@"** ローカルスコープ終了\n"); } NSLog(@"autoreleasepool 終了\n"); return 0; }
myClass1 クラスについては srcARC5-01 と全く同じです。そして main 関数では、まず自動解放プールを用意し、その内側にローカルスコープを用意しています。そしてローカルスコープ内で alloc/init メソッドにより myClass1 のオブジェクトを生成して強参照の変数 smc に代入しています。また直後に、弱参照の変数 wmc にも代入しています。
これを実行すると、以下のログが出力されます。
autoreleasepool 開始
** ローカルスコープ開始
******** 私は myClass No.0です。
** ローカルスコープ終了
******** myClass No.0 解放
autoreleasepool 終了
smc はローカルスコープ内のローカル変数なので、ローカルスコープを抜けるときに当然消滅します。wmc はオブジェクトを弱参照しかしていないので、このタイミングでオブジェクトのオーナーが居なくなり解放される……かと思いきや解放されていません。そして、自動解放プールが終了するタイミングでオブジェクトが解放されています。
つまり、オブジェクトはいつの間にか自動解放プールに登録されているようです。おそらくARCでは、弱参照の変数にオブジェクトを代入すると、そのとき同時にオブジェクトが自動解放プールに登録されるのではないか、と最初は考えました。
しかし……例えば srcARC5-02 では自動解放プールを用いていませんが、弱参照の変数に代入したオブジェクトは問題なく解放されています。上記の考え方が正しいならば、 srcARC5-02 ではARCについて色々実験(3)で述べたような、自動解放プールに登録されるべきオブジェクトが登録されないことによるメモリリークが起こってしまうはずです。従って、オブジェクトが自動解放プールに登録されるためには別の条件が存在するようです。
実験5-07
ここで、srcARC5-06 の main 関数内にある [wmc showNumber]; の行だけを以下のようにコメントアウトして実行してみます。それ以外の変更は一切加えません。
//[wmc showNumber];
showNumber メソッド(ここでは弱参照の変数 wmc を介して呼び出している)は、ただ NSLog 関数を用いて文章を出力するだけのメソッドです。これをコメントアウトした所で、オブジェクトの状態には何ら影響を及ぼさないように思えてしまいます。しかし、出力されるログを見ると……
autoreleasepool 開始
** ローカルスコープ開始
******** myClass No.0 解放
** ローカルスコープ終了
autoreleasepool 終了
なんと実験5-06 と異なり、ローカルスコープを抜けるタイミングでオブジェクトが解放されています。オブジェクトは自動解放プールには登録されておらず、smc からの強参照が外れてオーナーが居なくなるタイミングで素直に解放されるようです。
文章出力のみを行う showNumber メソッドをコメントアウトしただけで、オブジェクトが自動解放プールに登録されるか否かの違いが生じてしまいました。どうやら、メソッドの動作内容よりも、メソッドを呼び出すこと自体がカギとなっていそうです。
これらの結果から、ARCにおける弱参照の変数へのオブジェクトの代入は、以下のように処理されると推測されます。
- 弱参照の変数にオブジェクトを代入しても、その時点ではオブジェクトは自動解放プールに登録されない。
- 弱参照の変数を介して、オブジェクトに何らかのメッセージを送信する(メソッドを呼び出す)と、同時にそのオブジェクトが自動解放プールに登録される。
あくまでも推測であり、本当にこのような処理となっているのか断言までは出来ません。しかし、このように考えれば、srcARC5-02 で自動解放プールが無くても問題がない理由も説明が付きます(弱参照の変数を介したオブジェクトへのメッセージ送信が行われる前に、変数 mc1 への nil 代入によってオブジェクトが解放されてしまうため)。
ただし実用的なプログラムでは、弱参照の変数を用いるときに、オブジェクトを代入するだけでメッセージ送信を一切しない、などということはあまり無いでしょう。従って、弱参照の変数を利用する際は、必ず自動解放プールを用意した方が間違いが無いと思います。
(ひとまず一区切り。今後何か新しいことが分かったら、続きを書くかもしれません。)
ARCについて色々実験(4)
※ARCについて色々実験(3)の続きとなります。
メソッドファミリに属さないメソッドによるオブジェクトの生成(他クラスのメソッドの戻り値として)
実験4-01
srcARC3-01 に、新しいクラスである myClass2 を追加してみます。myClass2 はgetMyClass1: というメソッドを持ち、myClass1 のオブジェクトを alloc と initWithNumber: メソッドにより生成して返します(getMyClass1: メソッドは、いかなるメソッドファミリにも属しません)。すなわち、myClass1 のオブジェクトを、myClass2 という他クラスのメソッドによって生成してみます。これを srcARC4-01 として以下に示します。
//srcARC4-01 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ myClass2の追加、および main 関数以外は、srcARC3-01 と同一 @interface myClass2 : NSObject @end @implementation myClass2 -(id)getMyClass1:(int)n{ NSLog(@"******** myClass No.%i 他クラスのメソッドで生成\n", n); return [[myClass1 alloc] initWithNumber:n]; } @end int main(int argc, const char * argv[]) { id mc2 = [[myClass2 alloc] init]; NSLog(@"autoreleasepool 開始\n"); @autoreleasepool { NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); id mc = [mc2 getMyClass1:i]; [mc showNumber]; NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); } NSLog(@"autoreleasepool 終了\n"); return 0; }
main 関数の最初に alloc/init メソッドで myClass2 のオブジェクトを生成しておきます(変数 mc2 に代入)。そして、srcARC3-01 ではコンビニエンスコンストラクタ(myClassWithNumber: メソッド)で myClass1 のオブジェクトを生成しましたが、その代わりに myClass2 の getMyClass1: メソッドを用いています。これを実行すると、以下のログが出力されます。
autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 他クラスのメソッドで生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.2 解放
******** myClass No.1 解放
autoreleasepool 終了
ログを見ると「他クラスのメソッドで生成」という文字列の表示以外は、srcARC3-01 と全く同じ実行結果が得られています。生成された myClass1 のオブジェクトは自動解放プールに登録されています。myClass2 の getMyClass1: メソッドは、戻り値が myClass1 のコンビニエンスコンストラクタと全く同様であり、またメソッドファミリに属していないという点も同様です。そのため、srcARC3-01 と srcARC4-01 の実行結果が同じになったのです。
あるクラスのメソッドで、他クラスのオブジェクトを返すような場合であっても、ARCではあくまでメソッドファミリの規則に従って、そのオブジェクトの状態が決まるようです。
自動解放プールの記述位置について
実験4-02
srcARC3-01 でも srcARC4-01 でも、main 関数では for ループ全体を自動解放プールで囲い込むようにしていました。そこで、逆に for ループの内側で自動解放プールを用いるよう srcARC4-01 を srcARC4-02 のように書き換えてみます。
//srcARC4-02 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ main 関数以外は、srcARC4-01 と同一 int main(int argc, const char * argv[]) { id mc2 = [[myClass2 alloc] init]; NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); NSLog(@"autoreleasepool 開始\n"); @autoreleasepool { id mc = [mc2 getMyClass1:i]; [mc showNumber]; } NSLog(@"autoreleasepool 終了\n"); NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); return 0; }
srcARC4-01 とは異なり、for ループが1周するごとに、ループの先頭で自動解放プールのブロックが開始し、ループの末尾でブロックを抜けるようにしています。これを実行すると以下のログが出力されます。
** ループ開始
**** ループ1週目 先頭
autoreleasepool 開始
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
******** myClass No.1 解放
autoreleasepool 終了
**** ループ1週目 末尾
**** ループ2週目 先頭
autoreleasepool 開始
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
******** myClass No.2 解放
autoreleasepool 終了
**** ループ2週目 末尾
**** ループ3週目 先頭
autoreleasepool 開始
******** myClass No.3 他クラスのメソッドで生成
******** 私は myClass No.3です。
******** myClass No.3 解放
autoreleasepool 終了
**** ループ3週目 末尾
** ループ終了
ログを見ると、srcARC4-01(あるいはsrcARC3-01)の実行結果とは異なり、ループが1周するごとに新たな自動解放プールが開始しては終了し、それに伴って、その周のループで生成されたオブジェクトも解放されています。
getMyClass1: メソッドの戻り値として得られたオブジェクトは、その周のループで開始した自動解放プールに登録されると同時に、その自動解放プール内のローカル変数である mc に代入されます。そして、その周の自動解放プールが終了すると同時に mc も消滅する(すなわち、mc からのオブジェクトへの強参照が外れる)ため、このタイミングでオブジェクトが解放されるようです。
なお srcARC4-01 と srcARC4-02 では、表面的には両者の動作内容(ループの1周ごとに myClass1 のオブジェクトを生成し、そのメソッドを呼び出す)には違いがありません。おそらくループの回数がごく少ないときには、どちらの方式で自動解放プールを利用しても、大きな影響は生じないと思います。
しかし、ループ回数が非常に多い場合はどうでしょうか。srcARC4-02 の方式ではループの各周の末尾でオブジェクトは解放されます。しかし srcARC4-01 の方式ではループの各周ごとに、(ループの外側の)自動解放プールにオブジェクトが登録されて貯まっていきます。例えばループが一度に10000周する場合には、一時的とはいえ10000個のオブジェクトが貯まってしまいます。特にiPhoneアプリでは、限られたiPhoneのメモリを圧迫してしまい、最悪アプリが落ちる原因になりかねません。
そのため、メソッドファミリに属さないメソッドを用いて、ループの中で何度もオブジェクトを生成するような場合には、srcARC4-02 のようにループの中で自動解放プールを用いる方式のほうが無難だと思います。
自動解放プールの強制脱出
実験4-03
srcARC4-02 のようにループの内側で自動解放プールを用いる場合に、その中で break などの命令を実行し、ループを強制脱出したらどうなるか実験をしてみます。srcARC4-02 の main 関数に、if 文を追加して書き換えた srcARC4-03 を以下に示します。
//srcARC4-03 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ main 関数以外は、srcARC4-02 と同一 int main(int argc, const char * argv[]) { id mc2 = [[myClass2 alloc] init]; NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); NSLog(@"autoreleasepool 開始\n"); @autoreleasepool { id mc = [mc2 getMyClass1:i]; [mc showNumber]; //この if 文を追加 if(i == 2){ NSLog(@"********ループ強制脱出!"); break; } } NSLog(@"autoreleasepool 終了\n"); NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); return 0; }
本来は3周する for ループですが、2周目の時点で、if 文により自動解放プールの内部で break 命令が実行されます。すなわち自動解放プールごと、for ループを強制脱出してしまいます。これを実行すると以下のログが出力されます。
** ループ開始
**** ループ1週目 先頭
autoreleasepool 開始
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
******** myClass No.1 解放
autoreleasepool 終了
**** ループ1週目 末尾
**** ループ2週目 先頭
autoreleasepool 開始
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
********ループ強制脱出!
******** myClass No.2 解放
** ループ終了
「myClass No.2」が生成されてから、自動解放プールの終端に達する前に、break 命令によって自動解放プールごとループを強制脱出してしまいます。しかし、だからといって「myClass No.2」が解放されないまま残ったりすることはなく、強制脱出のタイミングできちんと解放されています。自動解放プールを強制脱出しても、それまでに登録されたオブジェクトは問題なく解放されるようです。
なお蛇足ですが、ARC導入以前は以下のように、自動解放プールをオブジェクトとして生成していたそうです。
//ARC導入以前の、旧式な自動解放プール。ARC環境では利用不可。
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
[pool release];
この旧式な自動解放プールでは、break 命令などによる強制脱出はしてはいけないそうです。今後これを使うことになる可能性は低そうですが、一応メモ。
以下、ARCについて色々実験(5) に続きます。
ARCについて色々実験(3)
※ARCについて色々実験(2)の続きとなります。
メソッドファミリに属さないメソッドによるオブジェクトの生成(コンビニエンスコンストラクタ)
実験3-01
これまでの実験では、メソッドファミリに属するメソッドの戻り値として、オブジェクトを返す場合について様々な検証をしてきました。そこで今度は、メソッドファミリに属さないメソッドの戻り値として、オブジェクトを返す場合について検証したいと思います。
srcARC2-03 の newMyClassWithNumber: メソッドを、いかなるメソッドファミリにも属さない myClassWithNumber: という名前に書き換えてみます。また、main 関数の中身を自動解放プール(@autoreleasepool)を追加したものに書き換えます。これを srcARC 3-01 として以下に示します。
//srcARC3-01 //当然ながら、ARCをオンにしてのコンパイルが必須です #import <Foundation/Foundation.h> @interface myClass1 : NSObject{ int num; } @end @implementation myClass1 -(id)initWithNumber:(int)n{ self = [super init]; if(self != nil){ num = n; //NSLog(@"******** myClass No.%i イニシャライザにより初期化\n", n); } return self; } //srcARC2-03 の newMyClassWithNumber: メソッドを書き換え +(id)myClassWithNumber:(int)n{ NSLog(@"******** myClass No.%i myClassWithNumber: で生成\n", n); return [[self alloc] initWithNumber:n]; } -(void)showNumber{ NSLog(@"******** 私は myClass No.%iです。\n", num); } -(void)dealloc { NSLog(@"******** myClass No.%i 解放\n", num); } @end int main(int argc, const char * argv[]) { //自動解放プールを追加 NSLog(@"autoreleasepool 開始\n"); @autoreleasepool { NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); id mc = [myClass1 myClassWithNumber:i]; [mc showNumber]; NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); } NSLog(@"autoreleasepool 終了\n"); return 0; }
newMyClassWithNumber: メソッドと、myClassWithNumber: メソッドは、名前(とNSLog 関数で出力する文字列)を変えただけです。動作内容には手を加えていません。両者とも以下の全く同じコードによって myClass1 のオブジェクトを生成し、戻り値として返しています。
return [[self alloc] initWithNumber:n];
両者の違いは、ただ名前がメソッドファミリに属しているか否かだけです。
これを実行すると、以下のログが出力されます。
autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.2 解放
******** myClass No.1 解放
autoreleasepool 終了
ログを見ると、ループが1周するたびに新しいオブジェクトが生成されていますが、ループの内部ではオブジェクトの解放は一切起こっておりません。ループを抜けた後に、自動解放プールを抜けるタイミングで、生成された3つのオブジェクトがまとめて解放されています。srcARC2-03 の実行結果とは明らかに様子が異なっています。
前述のようにARCでは、メソッドファミリに属さないメソッドの戻り値として返されるオブジェクトは、参照カウンタ方式で言うところの autorelease 操作が自動的に施されます。myClassWithNumber: で戻り値を返すコードは、ARCでは自動的に、参照カウンタ方式における以下のようなコードと同様に扱われるようです。
//※参照カウンタ方式(ARCオフ)におけるコード return [[[self alloc] initWithNumber:n] autorelease];
すなわち、myClassWithNumber: の戻り値として得られるオブジェクトは、自動解放プールに登録されます。
myClassWithNumber: メソッドの戻り値であるオブジェクトを、変数 mc に代入すると、これまでと同様に mc はオブジェクトを強参照しますが、同時にオブジェクトは、ループ全体を囲む自動解放プールに登録されます。mc は for ループ内のローカル変数であるため、ループが1周するたびに消滅してオブジェクトへの強参照が外れるのですが、自動解放プールが存在し続けているため、ここでオブジェクトの解放は起こりません。
そしてループが1周するたびに、新しいオブジェクトが自動解放プールに登録されていき、最終的にループを抜けた時点では、3つのオブジェクトが自動解放プールに登録されています。そして自動解放プールを抜けるタイミングで、登録されていた3つのオブジェクトがまとめて解放されます。ARCではオーナーがいなくなったオブジェクトであっても、自動解放プールに登録されている限りは保持され続けるようです。
実験3-02
それでは、もし自動解放プールを用意せずに、myClassWithNumber: を用いたらどうなるでしょうか。srcARC3-01 の main 関数にある自動解放プールをコメントアウトして、srcARC3-02 のように書き換えてみます。
//srcARC3-02 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ main 関数以外は、srcARC3-01 と同一 int main(int argc, const char * argv[]) { //自動解放プールをコメントアウト //NSLog(@"autoreleasepool 開始\n"); //@autoreleasepool { NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); id mc = [myClass1 myClassWithNumber:i]; [mc showNumber]; NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); //} //NSLog(@"autoreleasepool 終了\n"); return 0; }
これを実行すると、以下のログが出力されます。
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
ログを見ると、それぞれのオブジェクトは生成はされており、オブジェクトのメソッド(shouNumber メソッド)も利用は出来るのですが、オブジェクトの解放が全く行われていません。すなわち、メモリリークが起こってしまっています。
あくまでこれは想像なのですが、ARCでは、メソッドファミリに属さないメソッドの戻り値として得られた直後のオブジェクトは、自動解放プールに登録されることを前提として、内部的に retainCount の値が 1 になっているのだと思います。そしてこのオブジェクトが mc に代入されると、mc からの強参照によって retainCount の値が 2 に増えます。
そして、自動解放プールがきちんと用意されている場合には、mc からの強参照が外れるときにオブジェクトの retainCount が減って 1 となり、自動解放プールを抜けるときにまた retainCount が減って 0 となり、ここでオブジェクトが解放されます。
しかしながら、自動解放プールが用意されていない場合には、mc からの強参照が外れたときにオブジェクトの retainCount が減って 1 となりますが、それ以上 retainCount を減らすものが存在しなくなってしまいます。そのため、オブジェクトは解放されずにいつまでも残り続けてしまうのだと思います(繰り返しますが、あくまで内部的な動作の想像です。実際に確かめたわけではないので鵜呑みにはしないでください)。
したがってARCでは、メソッドファミリに属さないメソッドの戻り値としてオブジェクトを受け取る場合には、自動解放プールを用意することは必須となるようです。
実験3-03
それでは、srcARC3-01 を以下の srcARC3-03 のように書き換えてみます。
//srcARC3-03 //当然ながら、ARCをオンにしてのコンパイルが必須です //※ main 関数以外は、srcARC3-01 と同一 int main(int argc, const char * argv[]) { id mc2; NSLog(@"autoreleasepool 開始\n"); @autoreleasepool { NSLog(@"** ループ開始"); for(int i = 1; i <= 3; i++){ NSLog(@"**** ループ%i週目 先頭\n", i); id mc = [myClass1 myClassWithNumber:i]; [mc showNumber]; if(i == 2) mc2 = mc; NSLog(@"**** ループ%i週目 末尾\n", i); } NSLog(@"** ループ終了\n"); } NSLog(@"autoreleasepool 終了\n"); [mc2 showNumber]; return 0; }
ここでは、ループおよび自動解放プールの外側で変数 mc2 を宣言しています。そして、ループが i = 2 のときに生成されるオブジェクトを、mc2 = mc のように代入しています。そして自動解放プールを抜けた後で、mc2のメソッド(showNumber)を呼び出しています。これを実行すると以下のログが出力されます。
autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.1 解放
autoreleasepool 終了
******** 私は myClass No.2です。
******** myClass No.2 解放
ログを見ると、「myClass No.1」および「myClass No.3」は srcARC3-01 と同様に、自動解放プールを抜けるときにまとめて解放されています。一方で、ループ内の以下のコードによって「myClass No.2」は自動解放プールの外側の mc2 に代入されます。
if(i == 2) mc2 = mc;
そのため「myClass No.2」は自動解放プールを抜けても、mc2 に強参照をされているため解放されず、showNumber メソッドもきちんと利用できて「私は myClass No.2です。」というログを出力します。そして main 関数が終了し、mc2 が消滅するタイミングで「myClass No.2」は解放されるようです。
したがって、自動解放プールに登録されたオブジェクトであっても、その外側で宣言された(強参照の)変数に代入すれば、自動解放プールを抜けた後でもその変数がオーナーとなってオブジェクトを保持することが可能であるようです。
なお、この myClassWithNumber: メソッドのように、alloc + init(または init〜 という名前のイニシャライザ)でオブジェクトを生成し、それを自動解放プールに登録される状態(参照カウンタ方式で言うと autorelease メソッドが呼び出された状態)で戻り値として返してくれるクラスメソッドを「コンビニエンスコンストラクタ」と言います。
例えば、Foundationフレームワークのクラス(NSString など)にもコンビニエンスコンストラクタ(stringWithString: など)を有するものが多数ありますが、これらを利用するときには、やはり自動解放プールを用意することが必須であると考えた方が良さそうです。
以下、ARCについて色々実験(4) に続きます。