読者です 読者をやめる 読者になる 読者になる

すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

【openFramewoks】oFアプリはどのようにして起動しているのか?【for iOS】

openFrameworks objective-c codeReading

こんばんは.1000chです. 実験やら研修やら,何かと年末は忙しいですね.

最近oF for iOSの記事を書いている訳ですが, 「そもそもiOS上でoFアプリはどのように動いているのだろう?」と気になりました.

そもそもiOSアプリのライフサイクルも分かっていないので,その辺りから調べてまとめようと思います!

iOSにおけるプログラムのライフサイクル

まずは,iOSにおけるobjective-cプログラムのエントリポイントから勉強します.

実際にXcodeでNew->Project->single view applicationを作り,順を追ってみましょう. Xcodeはver6.1.1です.

main関数を見る

まず,objective-cもプログラムの開始はmain関数から始まります.どこにあるのか,が一瞬分かりにくいですが,supporting filesディレクトリ配下にいるようです.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

見知ったmain関数がいらっしゃいます.ARCは置いといて,中ではUIApplicationMain関数が呼ばれているだけみたいです.docを確認しましょう.

UIKit Function Reference

This function is called in the main entry point to create the application object and the application delegate and set up the event cycle.

This function instantiates the application object from the principal class and instantiates the delegate (if any) from the given class and sets the delegate for the application. It also sets up the main event loop, including the application’s run loop, and begins processing events. If the application’s Info.plist file specifies a main nib file to be loaded, by including the NSMainNibFile key and a valid nib file name for the value, this function loads that nib file.

Despite the declared return type, this function never returns. For more information on how this function behaves, see “Core App Objects” in App Programming Guide for iOS.

ざっくり要点を拾うと,

  • main関数のエントリポイントでよばれる.
  • アプリケーションクラスのインスタンスを生成する.
  • 指定されたデリゲートクラスのインスタンスを生成する
  • イベントサイクルを初期化する.
  • info.plistでnibファイル指定して読み込める(=ストーリーボード?)

という事の様です.となれば,次に見るべきはどんなデリゲートクラスが指定されているかでしょう. この例では

[AppDelegate class]

としてデリゲートクラスのクラス名を取得していますね.

デリゲートクラスを見る

では,実際にAppDelegateクラスを見てみみます.

AppDelegate.h

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

AppDelegate.m

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
}

- (void)applicationWillTerminate:(UIApplication *)application {
}

@end

なんということでしょう. windowプロパティを持ち,didFinishLaunchingWithOptionsreturn trueする以外は特になにもやってませんね.

ここでは省いていますが,各メソッドXXXなときに呼ばれるよ!オーバーライドして使ってね!的なことが書いてあります. 現状のデリゲートクラスは特になにもやってないことがわかりました.

ストーリーボードをみる

先程UIApplicationMain関数の説明でみたように,info.plistで指定されたストーリーボードが読み込まれるはずです.開いてみましょう.

f:id:ism1000ch:20141216004332p:plain

First Responder なるものが ViewControllerに指定されています.おそらく初期ビューとして ViewControllerクラスが読み込まれるのでしょう.

ViewControllerをみる

ということでビューコントローラをみていきます.

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

ViewController.m

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

プロジェクト生成直後の現状ではほとんどなにもないですねw このあとUIButtonUILabelなど,UIViewを継承するUIパーツをこのファイルに配置していくことで,iOSアプリとして動作するのでしょう.

ここまでまとめ

というわけでiOSのアプリ起動の流れをまとめると,

  • main関数よばれる
  • UIApplicationMain関数呼ばれる
  • アプリケーションのインスタンスつくられる
  • デリゲートのインスタンスつくられる
  • storyboardで指定したViewControllerが呼ばれる

という流れのようです.

oF for iOSでは?

では本題,oF for iOSではどうなっているのかを見ていきましょう. 手元のver 0.8.3でみていきます.

main.mmをみる

サンプルとして,example/graphicsExampleをみていきます. エントリポイントは同じくmain関数でしょう.

#include "ofMain.h"
#include "ofApp.h"

int main(){
    ofSetupOpenGL(1024,768, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

この中ではofSetupOpenGLofRunAppのふたつが呼ばれています.

ofSetupOpenGLを追う

まずはofSetupOpenGLを追っていきます.

void ofSetupOpenGL(int w, int h, int screenMode){
   #ifdef TARGET_NODISPLAY
        window = ofPtr<ofAppBaseWindow>(new ofAppNoWindow());
   #elif defined(TARGET_OF_IOS)
        // iosの場合ココ
        window = ofPtr<ofAppBaseWindow>(new ofAppiOSWindow());
   #elif defined(TARGET_ANDROID)
        window = ofPtr<ofAppBaseWindow>(new ofAppAndroidWindow());
   #elif defined(TARGET_RASPBERRY_PI)
        window = ofPtr<ofAppBaseWindow>(new ofAppEGLWindow());
    #else
        window = ofPtr<ofAppBaseWindow>(new ofAppGLFWWindow());
   #endif

    ofSetupOpenGL(window,w,h,screenMode); // 1.
}

// 1.で呼ばれる
void ofSetupOpenGL(ofPtr<ofAppBaseWindow> windowPtr, int w, int h, int screenMode){
    if(!ofGetCurrentRenderer()) {
   #ifdef USE_PROGRAMMABLE_GL
        ofPtr<ofBaseRenderer> renderer(new ofGLProgrammableRenderer(false));
   #else
        ofPtr<ofBaseRenderer> renderer(new ofGLRenderer(false));
   #endif
        ofSetCurrentRenderer(renderer,false);
    }

    window = windowPtr;

    if(ofIsGLProgrammableRenderer()){
        #if defined(TARGET_RASPBERRY_PI)
        static_cast<ofAppEGLWindow*>(window.get())->setGLESVersion(2);
       #elif defined(TARGET_LINUX_ARM)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(2,0);
       #elif !defined(TARGET_OPENGLES)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(3,2);
       #endif
    }else{
       #if defined(TARGET_LINUX_ARM) && !defined(TARGET_RASPBERRY_PI)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(1,0);
       #endif
    }

    window->setupOpenGL(w, h, screenMode); // 2.
}

// 2.でよばれる
// setupOpenGLはofAppBaseWindowの抽象メソッド
// ofAppiOSWindowではこんな感じの定義
void ofAppiOSWindow::setupOpenGL(int w, int h, int screenMode) {
   // windowModeはインスタンス変数
    windowMode = screenMode; // use this as flag for displaying status bar or not
}

ごちゃごちゃ追いましたが,結局

ってだけみたいです.iOSの場合はwindowModeを指定していますね.これは最初に

ofSetupOpenGL(1024,768, OF_FULLSCREEN);

で渡しているので,OF_FULLSCREENが入る,ということでしょう.

ofRunAppを追う

では次,ofRunApp(new ofApp)を追っていきます.

そもそもnew ofAppはなんだ?って話ですが,これはいつもoF書く時に作っているクラスのことですね.いわゆるsetup, update, drawなどを定義していくアレです.

要するに,いつも作ってるofAppクラスはココでインスタンス化されてofRunAppに渡されている,ということですね.

では改めてofRunAppをみていきます.

void ofRunApp(ofBaseApp * OFSA){

    OFSAptr = ofPtr<ofBaseApp>(OFSA);
    if(OFSAptr){
        OFSAptr->mouseX = 0;
        OFSAptr->mouseY = 0;
    }

#ifndef TARGET_ANDROID
    atexit(ofExitCallback);
#endif

#if defined(TARGET_LINUX) || defined(TARGET_OSX)
    // see http://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html#Termination-Signals
    signal(SIGTERM, &sighandler);
    signal(SIGQUIT, &sighandler);
    signal(SIGINT,  &sighandler);

    signal(SIGKILL, &sighandler); // not much to be done here
    signal(SIGHUP,  &sighandler); // not much to be done here

    // http://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals.html#Program-Error-Signals
    signal(SIGABRT, &sighandler);  // abort signal
#endif


   #ifdef WIN32_HIGH_RES_TIMING
        timeBeginPeriod(1);        // ! experimental, sets high res time
                                // you need to call timeEndPeriod.
                                // if you quit the app other than "esc"
                                // (ie, close the console, kill the process, etc)
                                // at exit wont get called, and the time will
                                // remain high res, that could mess things
                                // up on your system.
                                // info here:http://www.geisswerks.com/ryan/FAQS/timing.html

   #endif

    // ここからアプリの初期化
    window->initializeWindow();

    // 初期値設定
    ofSeedRandom();
    ofResetElapsedTimeCounter();
    ofSetWorkingDirectoryToDefault();

   // イベントリスナの登録
    ofAddListener(ofEvents().setup,OFSAptr.get(),&ofBaseApp::setup,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().update,OFSAptr.get(),&ofBaseApp::update,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().draw,OFSAptr.get(),&ofBaseApp::draw,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().exit,OFSAptr.get(),&ofBaseApp::exit,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().keyPressed,OFSAptr.get(),&ofBaseApp::keyPressed,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().keyReleased,OFSAptr.get(),&ofBaseApp::keyReleased,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().mouseMoved,OFSAptr.get(),&ofBaseApp::mouseMoved,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().mouseDragged,OFSAptr.get(),&ofBaseApp::mouseDragged,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().mousePressed,OFSAptr.get(),&ofBaseApp::mousePressed,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().mouseReleased,OFSAptr.get(),&ofBaseApp::mouseReleased,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().windowEntered,OFSAptr.get(),&ofBaseApp::windowEntry,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().windowResized,OFSAptr.get(),&ofBaseApp::windowResized,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().messageEvent,OFSAptr.get(),&ofBaseApp::messageReceived,OF_EVENT_ORDER_APP);
    ofAddListener(ofEvents().fileDragEvent,OFSAptr.get(),&ofBaseApp::dragged,OF_EVENT_ORDER_APP);

    // アプリの開始
    window->runAppViaInfiniteLoop(OFSAptr.get());
}

なーんかごちゃごちゃしていますが,序盤は環境毎の差異を吸収しているだけのよう.重要なのはwindow->initializeWindow以降ですね.各値の初期化やイベントリスナの登録などをしています.基本的にはofEvents().XXXofApp::XXXが対応づけられてるみたいです.

そして最後にrunAppViaInfinitLoopでアプリ開始っぽいですね.みていきましょう.

// ofAppBaseWindowの抽象メソッド.
// ofAppiOSWindowではこんな実装
void ofAppiOSWindow::runAppViaInfiniteLoop(ofBaseApp * appPtr) {
    startAppWithDelegate("ofxiOSAppDelegate");
}

スタートwithデリゲート...どっかで聞いた感じがしてきましたね.

void ofAppiOSWindow::startAppWithDelegate(string appDelegateClassName) {
    static bool bAppCreated = false;
    if(bAppCreated == true) {
        return;
    }
    bAppCreated = true;
    
    // さっきiOSでみたのと同じだ!
    @autoreleasepool {
        cout << "trying to launch app delegate " << appDelegateClassName << endl;
        UIApplicationMain(nil, nil, nil, [NSString stringWithUTF8String:appDelegateClassName.c_str()]);
    }
}

startAppWithDelegateはデリゲートクラスの文字列を引数として,UIApplicationMain関数を実行してくれるみたいです.このあたり,先程みたiOSのライフサイクルでもでてきましたね.となれば次は,指定されているデリゲートクラスをのぞきたくなります.

こいつが文字列で指定されているせいで探しにくいのですが(笑),addons/ofxiOS/src/core配下にいらっしゃいました.

ofxiOSDelegate.h

#pragma once

#import <UIKit/UIKit.h>

@class ofxiOSViewController;

@interface ofxiOSAppDelegate : NSObject <UIApplicationDelegate> {
    NSInteger currentScreenIndex;
}

@property (nonatomic, retain) UIWindow * window;
@property (nonatomic, retain) UIWindow * externalWindow;
@property (nonatomic, retain) ofxiOSViewController * glViewController;
@property (readonly,  assign) NSInteger currentScreenIndex;

- (BOOL)application:(UIApplication*)application
      handleOpenURL:(NSURL*)url;

- (void)receivedRotate:(NSNotification*)notification;

#ifdef __IPHONE_4_3
- (BOOL)createExternalWindowWithPreferredMode;
- (BOOL)createExternalWindowWithScreenModeIndex:(NSInteger)screenModeIndex;
- (BOOL)destroyExternalWindow;
- (BOOL)displayOnScreenWithIndex:(NSInteger)screenIndex
              andScreenModeIndex:(NSInteger)screenModeIndex;
#endif

@end

#define ofxiPhoneAppDelegate ofxiOSAppDelegate

もはや完全にobjective-cになってきました. 先程のiOSの例ではほっとんど中身の無かったデリゲートクラスですが,今回はいろいろと中身がありそうです.

実装ファイルofxiOSAppDelegate.mはなかなか膨大なようで,重要なところまでを載せます.

#import "ofMain.h"
#import "ofxiOSAppDelegate.h"
#import "ofxiOSViewController.h"
#import "ofxiOSExtras.h"
#import "ofxiOSExternalDisplay.h"

@implementation ofxiOSAppDelegate

@synthesize window;
@synthesize externalWindow;
@synthesize glViewController;
@synthesize currentScreenIndex;

- (void)dealloc {
    self.window = nil;
    self.externalWindow = nil;
    self.glViewController = nil;
    [super dealloc];
}

// アプリ起動時完了時に呼ばれるメソッド
- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // windowの取得
    self.window = [[[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]] autorelease];

    // 中略. 各値の初期化,イベントリスナの設定など
 
    NSString * appDelegateClassName = [[self class] description];
    if ([appDelegateClassName isEqualToString:@"ofxiOSAppDelegate"]) {
        // 中略. デバイス向きの設定
        
        // ビューコントローラの生成
        self.glViewController = [[[ofxiOSViewController alloc] initWithFrame:frame app:(ofxiOSApp *)ofGetAppPtr()] autorelease];

        // 生成したビューをルートに設定
        self.window.rootViewController = self.glViewController;
                
        // 中略. デバイス向きの設定
        
    }
}

ビュー初期化に関わるところだけを抜くと上記のような感じになります. アプリの起動が確認されると,デリゲートのapplicationDidFinishLaunchingが呼ばれます.このなかでofxiOSViewControllerを生成し,これをwindowのルートビューに指定しています.

これは先程みたiosでいうと,ストーリーボードでのfirst responder指定に値すると言えるでしょう.

ofxiOSViewControllerをみる

では生成されるビューコントローラがどんなものかみていきます.ヘッダファイルはこんなかんじ,

#import <UIKit/UIKit.h>

class ofxiOSApp;
@class ofxiOSEAGLView;

@interface ofxiOSViewController : UIViewController

@property (nonatomic, retain) ofxiOSEAGLView * glView;

- (id)initWithFrame:(CGRect)frame app:(ofxiOSApp *)app;

- (UIInterfaceOrientation)currentInterfaceOrientation;
- (void)setCurrentInterfaceOrientation:(UIInterfaceOrientation) orient;
- (void)rotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
                            animated:(BOOL)animated;
- (BOOL)isReadyToRotate;

@end

#define ofxPhoneViewController ofxiOSViewController

ofxiOSEAGLViewがとても怪しいです.実装ファイルの重要そうな部分をみてみましょう.

- (id)initWithFrame:(CGRect)frame app:(ofxiOSApp *)app {
    currentInterfaceOrientation = pendingInterfaceOrientation = UIInterfaceOrientationPortrait;
    if((self = [super init])) {
        currentInterfaceOrientation = pendingInterfaceOrientation = self.interfaceOrientation;
        bReadyToRotate  = NO;
        bFirstUpdate    = NO;
        
        self.glView = [[[ofxiOSEAGLView alloc] initWithFrame:frame andApp:app] autorelease];
        self.glView.delegate = self;
    }
    
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // glView is added here because if it is added inside initWithFrame,
    // it automatically triggers viewDidLoad, before initWithFrame has had a chance to return.
    // so now when we call setup in our OF app, a reference to ofxiOSViewController will exists.
    
    [self.view addSubview:self.glView];
    [self.glView setup];
    [self.glView startAnimation];
}

ざっくり言うと

  • initWithFrameofxiOSEAGLViewインスタンス生成
  • viewDidLoadofxiOSEAGLViewをサブビューに追加
  • ofxiOSEAGLViewsetup,startAnimationを呼ぶ

という形みたいです. でこのofxiOSEAGLViewsetupを持ち,また継承元のEAGLViewstartAnimationを持っているよう.

setupはこんなかんじ.

- (void)setup {
    
    ofNotifySetup(); // ここでofEvents().setupが発火され,ofApp::setupが呼ばれる
    
    glClearColor(ofBgColorPtr()[0], 
                 ofBgColorPtr()[1], 
                 ofBgColorPtr()[2], 
                 ofBgColorPtr()[3]); // clear background.
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

startAnimationはこんな感じ

- (void) startAnimation {
    if(!animating) {
        if(displayLinkSupported) {
            // CADisplayLink is API new to iPhone SDK 3.1. Compiling against earlier versions will result in a warning, but can be dismissed
            // if the system version runtime check for CADisplayLink exists in -initWithCoder:. The runtime check ensures this code will
            // not be called in system versions earlier than 3.1.
            
            displayLink = [NSClassFromString(@"CADisplayLink") displayLinkWithTarget:self selector:@selector(drawView:)];
            [displayLink setFrameInterval:animationFrameInterval];
            [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        } else {
            animationTimer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)((1.0 / 60.0) * animationFrameInterval) 
                                                              target:self 
                                                            selector:@selector(drawView:) 
                                                            userInfo:nil 
                                                             repeats:TRUE];
        }
        
        animating = YES;
        
        [self notifyAnimationStarted];
    }
}

displayLinkが何を意味するのかはよくわかりませんが,ようはdrawViewを一定時間ごとに呼ぶ何かなんでしょう←

drawViewofxiOSEAGLViewにおり,

- (void)drawView {
    
    ofNotifyUpdate(); // ofEvents().uodateが発火
    
    //------------------------------------------
    
    [self lockGL];
    [self startRender];

    // 中略
    
    //------------------------------------------ draw.
    
    ofNotifyDraw(); // ofEvents().drawが発火
    
    //------------------------------------------
    

    // 中略
    
    [super notifyDraw];   // alerts delegate that a new frame has been drawn.
}

というように,このメソッドが呼ばれる毎にupdate,drawが呼ばれる形になっていることがわかります.

まとめると

当然ですが後半はがっつりobjcのコードにもぐることになりました. oF for iOSの立ち上がるまでの流れをまとめると,

  • main: OpenGL設定
  • main: ofRunAppにofAppインスタンスを与える
  • ofRunApp: UIApplicationMainにofxiOSAppDelegateを与える
  • ofxiOSAppDelegate: applicationDidFinishLaunchingでofxiOSViewControllerをルートに設定
  • ofxiOSViewController: サブビューにofxiOSEAGLViewを指定
  • ofxiOSEAGLView: setup, startAnimation呼ぶ
  • setup: ofEvents().setup発火
  • startAnimation: glDrawを定期的に呼ぶタイマーを設定
  • glDraw: ofEvents().update,ofEvents().draw発火

という形でした.長い!!!

いやほんと,こんだけ複雑ないろいろをなにも意識せずに利用できるようにしているなんて,ほんとoFつくっている方々はすごいですね...

起動までもこんだけいろいろあることに加え,ofDrawCircleなども各環境にあわせてラップしていると思うと...またコードリーディングしたくなりますねw