[Titanium勉強日記 5] Viewを作るJavaScriptが実行される時の流れを追ってみた

モジュールの作り方が何となく分かったので、さっそくカスタムViewを書き始めましたが、幾つか分からない点があったので記録しておきます。

先ほどの記事にも書きましたが、欲しいのは同一View内に2つTableView A,Bがあって、AからBにドラッグドロップできるというものです。なのでTableViewを作って張りたいのですが、どこにその処理を書けば良いのか分かりません。Objective-CでiPhoneアプリを作る時はIBでGUIを作って、awakeFromNibとかViewControllerのviewDidLoadあたりで調整すると思うのですが、IBを使わないTitaniumモジュールの場合どうすれば良いのか分からないのです。

処理の流れを把握しておいた方が良さそうなのでソースコードを追ってみることにしました。まず出発点のJSのコードは以下のようになるかと思います。このFooViewが作られる過程を把握して、適切な場所でFooViewのサイズに合わせてTableViewを張りたいという事です。

var test = require('com.test');
var foo = test.createFooView({
	width: 'auto',
	height: 'auto'
});
win.add(foo);

まずJSのコードが評価されるまでのフローはコチラの記事が分かりやすかったです。合わせて参照ください。

今は早い話、上の記事の続きが知りたいのですが、記事内でも書かれている通りJSのエンジン部がプリコンパイルされていて、追うのが面倒です。そこでモジュールのテストを走らせる時に生成されるXcodeプロジェクトを利用しましょう。以下のようにtitanium runコマンドが出力するログからプロジェクトのパスを確認できます。/var/folder下のパスです。下記の場合だと、/var/folders/8c/8lmltdzx4tbd4w_mwhy_j6b00000gn/T/mfxZqOlti/firstmod/build/iphoneにxcodeprojが生成されています。

$ titanium run
...
[DEBUG] processing /Library/Application Support/Titanium/mobilesdk/osx/2.1.2.GA/iphone/Classes/AccelerometerModule.h => /var/folders/8c/8lmltdzx4tbd4w_mwhy_j6b00000gn/T/mfxZqOlti/firstmod/build/iphone/Classes/AccelerometerModule.h
...

適当にブレークポイントを張りながら調べてみるとKrollMethod::callというメソッドが見つかりました。JS側に公開されているAPIを叩くとこのメソッドが呼ばれて、対応するコードが実行されます。例えばcreateFooViewだと下記部分が実行されます。selectorからIMPを取得して実行しています。なおcreateから始まるメソッドはTiModuleのcreateProxy:forName:context:にマッピングされています。

// KrollMethod.m
-(id)call:(NSArray*)args
{
    ...
	
	// special generic factory for creating proxy objects for modules
	if (type == KrollMethodFactory)
	{
		//TODO: This likely could be further optimized later
		//
		NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector];
		bool useResult = [methodSignature methodReturnLength] == sizeof(id);
		id result = nil;
		id delegate = context.delegate;
		IMP methodFunction = [target methodForSelector:selector];
		if (useResult) {
			result = methodFunction(target,selector,args,name,delegate);
		}
		else
		{
			methodFunction(target,selector,args,name,delegate);
		}
		return result;
	}
	...
}

そして以下がcreateProxy:forName:context:です。何をしているかというとforNameで与えられたクラス名から、プレフィックスにモジュール名、サフィックスに”Proxy”をつけて、実際のクラス名を取得、続いて_initWithPagecontextを呼び出しています。モジュールを作る際の命名規則はココで使われています。ハードコードされてるのがわかりまうす。どうりでモジュール中で使うクラス名が間違っていると動かないわけですね。

// TiModule.m
-(id)createProxy:(NSArray*)args forName:(NSString*)name context:(id<TiEvaluator>)context
{
	Class resultClass = (Class)CFDictionaryGetValue(classNameLookup, name);
	if (resultClass == NULL)
	{
		NSRange range = [name rangeOfString:@"create"];
		if (range.location==NSNotFound)
		{
			return nil;
		}
		
		// this is a magic check to see if we're inside a compiled code or not
		// and if so, drop the prefix
		NSString *prefix = @"Ti";
		NSString *moduleName_ = nil;
		if (moduleName==nil)
		{
			moduleName_ = [NSString stringWithCString:class_getName([self class]) encoding:NSUTF8StringEncoding];
		}
		else 
		{
			// this is only set in the case of a Plus Module
			moduleName_ = moduleName;
			prefix = @"";
		}
		moduleName_ = [moduleName_ stringByReplacingOccurrencesOfString:@"Module" withString:@""];
		NSString *className = [NSString stringWithFormat:@"%@%@%@Proxy",prefix,moduleName_,[name substringFromIndex:range.location+6]];	
		resultClass = NSClassFromString(className);
		if (resultClass==nil)
		{
			DebugLog(@"[WARN] Attempted to load %@: Could not find class definition.",className);
			@throw [NSException exceptionWithName:@"org.test.module" 
										   reason:[NSString stringWithFormat:@"invalid method (%@) passed to %@",name,[self class]] 
										 userInfo:nil];
		}
		CFDictionarySetValue(classNameLookup, name, resultClass);		
	}

	return [[[resultClass alloc] _initWithPageContext:context args:args] autorelease];
}

続いて_initWithPageContext:context:argsです。_initWithPropertiesもついでに見ておきましょう。createFooView等で渡されたハッシュはここでPropertyに変換されます。ココで分かるのはcreateFooViewされた時に最初に作られるのはViewでは無くてViewProxyだと言う事です。ではViewはいつ作られるのでしょうか。

// TiProxy.m
-(id)_initWithPageContext:(id<TiEvaluator>)context_ args:(NSArray*)args
{
	if (self = [self _initWithPageContext:context_])
	{
		// If we are being created with a page context, assume that this is also
		// the execution context during creation so that recursively-made
		// proxies have the same page context.
		executionContext = context_;
		id a = nil;
		int count = [args count];
		
		if (count > 0 && [[args objectAtIndex:0] isKindOfClass:[NSDictionary class]])
		{
			a = [args objectAtIndex:0];
		}
		
		if (count > 1 && [[args objectAtIndex:1] isKindOfClass:[KrollCallback class]])
		{
			[self _initWithCallback:[args objectAtIndex:1]];
		}
		
		[self _initWithProperties:a];
		executionContext = nil;
	}
	return self;
}

// TiViewProxy.m
-(void)_initWithProperties:(NSDictionary*)properties
{
    [self startLayout:nil];
	// Set horizontal layout wrap:true as default 
	layoutProperties.layoutFlags.horizontalWrap = YES;
	[self initializeProperty:@"horizontalWrap" defaultValue:NUMBOOL(YES)];
	
	if (properties!=nil)
	{
        ...
	}
	[super _initWithProperties:properties];
    [self finishLayout:nil];
}

_initWithPropertiesでstartLayout、finishLayoutというそれっぽいメソッドが呼ばれています。ところが中を見てみるとフラグを上げ下ろししているだけで、Viewの作成等は行っていません。面倒なのでprocessTempProperties以下は省略します。まだViewが作られていませんが処理が返ってしまいます。

// TiViewProxy.m
-(void)startLayout:(id)arg
{
    updateStarted = YES;
    allowLayoutUpdate = NO;
}
-(void)finishLayout:(id)arg
{
    updateStarted = NO;
    allowLayoutUpdate = YES;
    [self processTempProperties:nil];
    allowLayoutUpdate = NO;
}

というわけでcreateFooViewでは実際のViewが作られない事が分かったわけですが、Viewの初期化フローが知りたいので引き続き追っていきます。作成したFooViewのinitにブレークポイントを張ってみると(最初からそうしろよという話はあります。)、コールスタックからTiViewProxyのaddからlayoutChildを経由して呼ばれている事がわかりした。下記にlayoutChildを適当に抜粋してコメントを追加しています。

// TiViewProxy.m
-(void)layoutChild:(TiViewProxy*)child optimize:(BOOL)optimize withMeasuredBounds:(CGRect)bounds
{
	...
	if (optimize==NO)
	{
		TiUIView *childView = [child view];      // このviewのgetter内でviewのインスタンスが作られる
		...                                      // でもこの時点のchildViewのフレームはまだおかしい
		[ourView insertSubview:childView atIndex:insertPosition];  // 色々してからinsertSubview
	}
	[child setSandboxBounds:bounds];
	if ([[child view] animating])
	{
        // changing the layout while animating is bad, ignore for now
        DebugLog(@"[WARN] New layout set while view %@ animating: Will relayout after animation.", child);
	}
	else
	{
		[child relayout];                        // この処理を抜けるとフレームの値が正しく設定されている
	}

	// tell our children to also layout
	[child layoutChildren:optimize];
}

さらにrelayoutの中を見てみます。ようやく見つかりました。sizeとcenterを設定しています。ここでようやくviewのframeが取得できる状態になったわけですね。さらにpostlayoutイベントが発火しているのがわかります。なので一番早いタイミングとしてはpostlayoutイベント受けてframeを使う処理ができます。また先ほどのlayoutChild中にinsertSubviewが呼ばれているので、この処理が抜けた後にlayoutSubviewsが走ります。なのでTiUIViewのサブクラス内でlayoutSubviewsをオーバーライドして処理するのが普通っぽい気がしますね。

// TiViewProxy.m
-(void)relayout
{
	if (!repositioning)
	{
		ENSURE_UI_THREAD_0_ARGS

		repositioning = YES;

        UIView *parentView = [parent parentViewForChild:self];
        CGSize referenceSize = (parentView != nil) ? parentView.bounds.size : sandboxBounds.size;
        if (parent != nil && (!TiLayoutRuleIsAbsolute([parent layoutProperties]->layoutStyle)) ) {
            sizeCache.size = SizeConstraintViewWithSizeAddingResizing(&layoutProperties,self, sandboxBounds.size, &autoresizeCache);
        }
        else {
            sizeCache.size = SizeConstraintViewWithSizeAddingResizing(&layoutProperties,self, referenceSize, &autoresizeCache);
        }
       
		positionCache = PositionConstraintGivenSizeBoundsAddingResizing(&layoutProperties, self, sizeCache.size,
		[[view layer] anchorPoint], referenceSize, sandboxBounds.size, &autoresizeCache);

		positionCache.x += sizeCache.origin.x + sandboxBounds.origin.x;
		positionCache.y += sizeCache.origin.y + sandboxBounds.origin.y;

		[view setAutoresizingMask:autoresizeCache];
		[view setCenter:positionCache];
		[view setBounds:sizeCache];

		[parent insertSubview:view forProxy:self];


		repositioning = NO;
        
        if ([observer respondsToSelector:@selector(proxyDidRelayout:)]) {
            [observer proxyDidRelayout:self];
        }

        if ([self _hasListeners:@"postlayout"]) {
            [self fireEvent:@"postlayout" withObject:nil];
        }
	}
#ifdef VERBOSE
	else
	{
		DeveloperLog(@"[INFO] %@ Calling Relayout from within relayout.",self);
	}
#endif

}

まとめ

  • 処理を追いたい場合はKrollMethod::callで張っとけばいいよ
  • TiUIViewのサブクラス内でレイアウトする場合はlayoutSubviews
  • TiUIViewのサブクラスの準備が整ったらpostlayoutイベントが呼ばれる

以上、長々と書きましたが別に何でも無い結果になりましたね。

2 thoughts on “[Titanium勉強日記 5] Viewを作るJavaScriptが実行される時の流れを追ってみた”

  1. はじめまして、コチラの記事を書いた人です。
    その記事を書いた時は、続きを書いてもらえる人がいるなんて思ってもいなかったので大変嬉しいです。
    ありがとうございます。

  2. ダニーさん

    コチラこそありがとうございます。
    ダニーさんの記事のおかげで処理を追って見ようと言う気になりました。
    勉強になりました。

Leave a Reply

Your email address will not be published. Required fields are marked *