어제 Adobe AIR에서 CPU 사용을 줄이는 방법에 대해서 소개했다. 이 글에 Hika님과 찬익님이 그에 대해 댓글을 달아주시며 다른 정보를 공유해주셨다. 정말 감사드린다. 이런 것이 블로그의 매력일 것이다. 블로그를 내 완벽한 지식을 전달하기 위해 쓴다는 것은 다소 어폐가 있다. 완벽한 것은 없다. 블로그는 지식 소통의 도구로 잘 활용하면 좋은 정보 공유를 통한 더욱 완전해지는 지식습득이 가능하게 한다. 난 지난 몇년간 블로그를 하면서 정말 몸소 체험했다. 지금 이글을 보고 있는 분들도 꼭 블로그를 하길 권장한다.

각설하고. 찬익님이 Flex 4 기반에서 AIR 애플리케이션을 만들때 FrameRate를 조절하는 방법으로 backgroundFrameRate를 소개해주었다. 이것은 Event.ACTIVATE와 Event.DEACTIVATE를 이용해서 이미 Flex 4의
WindowedApplication 클래스에 적용되어 있다. 아래 코드는 WindowedApplication 클래스의 일부이다.

/**
*  Constructor.
*  
*  @langversion 3.0
*  @playerversion AIR 1.5
*  @productversion Flex 4
*/
public function WindowedApplication()
{
	super();

	addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
	addEventListener(FlexEvent.PREINITIALIZE, preinitializeHandler);
	addEventListener(FlexEvent.UPDATE_COMPLETE, updateComplete_handler);
	addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);

	var nativeApplication:NativeApplication = NativeApplication.nativeApplication;
	nativeApplication.addEventListener(Event.ACTIVATE, nativeApplication_activateHandler);
	nativeApplication.addEventListener(Event.DEACTIVATE, nativeApplication_deactivateHandler);
	nativeApplication.addEventListener(Event.NETWORK_CHANGE, dispatchEvent);

	nativeApplication.addEventListener(InvokeEvent.INVOKE, nativeApplication_invokeHandler);
	initialInvokes = new Array();

	//Force DragManager to instantiate so that it can handle drags from
	//outside the app.
	DragManager.isDragging;
}

/**
*  @private
*  Storage for the backgroundFrameRate property.
*/
private var _backgroundFrameRate:Number = 1;

/**
*  Specifies the frame rate to use when the application is inactive.
*  When set to -1, no background frame rate throttling occurs.
*
*  @default 1
*  
*  @langversion 3.0
*  @playerversion AIR 1.5
*  @productversion Flex 4
*/
public function get backgroundFrameRate():Number
{
	return _backgroundFrameRate;
}

/**
*  @private
*/ 
public function set backgroundFrameRate(frameRate:Number):void
{
	_backgroundFrameRate = frameRate;
}

/**
*  @private
*/
private function nativeApplication_activateHandler(event:Event):void
{
	dispatchEvent(new AIREvent(AIREvent.APPLICATION_ACTIVATE));

	// Restore throttled framerate if appropriate when application is activated.
	if (prevActiveFrameRate >= 0 && stage)
	{
	    stage.frameRate = prevActiveFrameRate;  
	    prevActiveFrameRate = -1;
	}
}

/**
*  @private
*/
private function nativeApplication_deactivateHandler(event:Event):void
{
	dispatchEvent(new AIREvent(AIREvent.APPLICATION_DEACTIVATE));

	// Throttle framerate if appropriate when application is deactivated.
	// Ensure we've received an updateComplete on the chance our layout
	// manager is using phased instantiation (we don't wish to store a
	// maxed out (1000fps) framerate).
	if ((_backgroundFrameRate >= 0) && (ucCount > 0) && stage)
	{
	    prevActiveFrameRate = stage.frameRate;
	    stage.frameRate = _backgroundFrameRate; 
	}
}


이는 Flex 4를 이용해 AIR 애플리케이션을 만들때 별도의 조치없이 backgroundFrameRate 속성 설정만으로 frameRate를 조절할 수 있다. 이 값은 기본으로 1이다. 더 좋은 값은 0.01이라고 한다. 이는 거의 멈추게 하는 수준이다. 이 속성은 Flex 4에서 AIR를 위해 WindowedApplication 에만 적용되어 있다. Flash기반인 Application 클래스에는 이 속성이 없다.
참고글
Adobe AIR에서 CPU 사용을 줄이는 방법

글쓴이 : 지돌스타(http://blog.jidolstar.com/624)
Adobe AIR는 다양한 OS(Windows, Mac, Linux)에 구동될 수 있는 애플리케이션을 만드는데 사용하는 일종의 Flash Platfom기술이다. Flash ActionScript 3.0이나 Ajax만으로도 AIR 애플리케이션을 만들 수 있기 때문에 기존에 웹개발자들이 다른 언어를 배우지 않고 일반 데스크탑용 애플리케이션을 만드는데 있어서 접근성이 좋다. 앞으로 AIR는 데스크탑뿐 아니라 모바일등과 같은 다양한 기기에도 적용될 예정이다.

하지만 AIR외에 다른 데스크탑용 애플리케이션과 비교해 그 적용범위가 아직까지 많은 부분 부족하고 복잡한 UI를 다루는데 있어서 성능문제가 걸릴 수 있다. 하지만 AIR 2.0 Beta 버전이 오픈된 것을 볼 수 있었듯이 계속 발전해나갈 것이다. 물론 성능면에서도 마찬가지 일 것이다. 그래도 약간은 부족하다. 기능은 그렇다 쳐도 지금의 성능을  개발자가 조금만 신경을 쓴다면  획기적으로 향상시킬 수 있다.

일전에 Reducing CPU usage in Adobe AIR라는 글을 보았다. 여기서는 매우 단순하고 쉬운 방법으로 framerate를 줄임으로서 CPU 사용율을 현격히 줄이는 방법을 소개하고 있다.

이 방법이 퍼포먼스를 올리기 위한 방법의 하나의 예시는 될 수 있지만 그 전부는 아니라는 점은 밝혀둔다.

(이 글을 완벽히 이해하기 위해서는 ActionScript 3.0과 AIR에 대한 전반적 이해가 요구됩니다.)

FrameRate를 줄임
Framerate를 줄임(Framerate throttling)은 애플리케이션의 휴면(idle)이 있을때 자원 사용을 줄여 퍼포먼스를 증가시키는 기술을 의미한다. 이를 구현하기 위해 ActionScript 3.0에서 매우 유용한 속성으로 Stage.frameRate가 있다. 이것을 이용하면 런타임시에 애플리케이션의 framerate를 변경시킬 수 있다. 

여기서 보여지는 예제는 Reducing CPU usage in Adobe AIR에서 소개한 글에 나온 예제를 조금더 실용적으로 만들었다.(실제로 실행해 볼 수 있도록) Flash Builder 4와 AIR 2.0 SDK를 설치한 사람들이라면 이 예제를 실제로 테스트 해볼 수 있다. 아래 순서대로 개발/테스트 환경을 구축하면 된다.

  • 만약 Flash Builder를 설치 안했다면 다음 링크를 통해 받으세요.
  • SDK와 Runtime을 다운로드 받습니다.
  • 다운받은 Runtime을 실행해 설치합니다.
  • (MS Windows의 경우)SDK는 압축을 풀고 그안에 있는 내용을 Flash builder가 설치된 sdks/4.0.0과 sdks/3.4.1 폴더에 각각 덮어씌웁니다. 제 경우는 C:\Program Files\Adobe\Adobe Flash Builder Plug-in Beta 2\sdks\4.0.0
  • Flash Builder를 실행합니다.
  • 메뉴에서 File > New > Flex Project를 선택합니다.
  • 프로젝트 이름을 적고 Application Type은 AIR를 선택합니다.
  • Next버튼을 두번 클릭후 Main Application file이름이 프로젝트명.mxml로 되어 있다면 Novice.as로 바꾸세요. 그리고 Finish 버튼을 누릅니다.
  • 아래 첫번째 초급 코드를 복사해서 Novice클래스를 열어 붙힙니다.
  • 디버그 모드로 테스트합니다.
  • 두번째, 세번째의 경우 만들어진 프로젝트의 소스폴더에 해당 클래스 이름으로 Class를 만듭니다. File > New > ActionScript Class를 선택한뒤 아래 소스를 복사해 붙여넣으면 되겠죠?
  • Package Explorer에 프로젝트명을 선택후 마우스 우클릭해서 컨텍스트 메뉴에서 Properties를 선택합니다.
  • 창이 뜨면 좌측 Flex Applications를 선택하고 오른쪽에 Add를 눌러 추가한 클래스(Intermediate.as, Expert.as)를 추가한뒤 OK를 합니다. Intermediate-app.xml과 Expert-app.xml이 자동 생성됩니다.
  • 이제 디버그 모드로 실행하면서 동작하는 형태를 학습하세요.
  • 아래 소스는 다음 링크에서 다운로드 받으셔도 됩니다.



  • 초급
    Framerate를 줄일때의 시점을 선택하는데 가장 쉽고 유용한 방법은 NativeApplication에서 Event.ACTIVATE 와 Event.DEACTIVATE 이벤트를 사용하는 것이다. AIR로 만들어진 빈윈도우를 선택해서 사용할때 CPU사용율이 1.8%라면 다른 윈도우를 선택해 그 윈도우의 CPU 사용율이 0.4%까지 떨여졌다고 한다. 또한 framerate를 0.01로 지정하면 0.2%까지 떨어진다고 한다. 이에 대한 예제는 다음과 같다. 예제가 이해하기 쉬우므로 따로 설명하지 않겠다.


    package {
    	import flash.desktop.NativeApplication;
    	import flash.display.NativeWindow;
    	import flash.display.NativeWindowInitOptions;
    	import flash.display.NativeWindowType;
    	import flash.display.Sprite;
    	import flash.display.StageAlign;
    	import flash.display.StageScaleMode;
    	import flash.events.Event;
    	import flash.text.TextField;
    	import flash.text.TextFieldAutoSize;
    	import flash.utils.getTimer;
    	
    	/**
    	 * CPU 사용 줄이기 예제 1 
    	 * @author Yongho, Ji
    	 * @since 2009.12.1
    	 * @see http://blog.jidolstar.com/622
    	 */ 
    	public class Novice extends Sprite {
    		private var __isActive:Boolean = false;
    		private var __window:NativeWindow;
    		private var __textField:TextField;
    		public function Novice() {
    			__init();
    			var options:NativeWindowInitOptions = new NativeWindowInitOptions();
    			options.type = NativeWindowType.UTILITY;
    			
    			__window = new NativeWindow(options);
    			__window.width = 200;
    			__window.height = 200;
    			__window.title = "Novice";
    			
    			__textField = new TextField();
    			__textField.autoSize = TextFieldAutoSize.LEFT;
    			__textField.text = "";
    			
    			__window.stage.scaleMode = StageScaleMode.NO_SCALE;
    			__window.stage.align = StageAlign.TOP_LEFT;
    			
    			__window.stage.addChild(__textField);
    			__window.activate();
    			__window.addEventListener(Event.CLOSING,__onClosing);
    		}
    		private function __init():void {
    			NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, __onActive );
    			NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, __onDeactive );
    			stage.addEventListener(Event.ENTER_FRAME,__onEnterFrame);
    		}
    		private function __onActive($event:Event):void {
    			stage.frameRate = 50;
    			__isActive = true;
    		}
    		private function __onDeactive($event:Event):void {
    			stage.frameRate = 1;
    			__isActive = false;
    		}
    		private function __onEnterFrame($event:Event):void {
    			__textField.text =  "active:" + __isActive + " " + getTimer();
    		}
    		private function __onClosing($event:Event):void {
    			NativeApplication.nativeApplication.exit();
    		}
    	}
    }
    


    중급

    위의 예제보다 조금더 고급적으로 framerate를 조절할 필요가 있다. 가령, 마우스 휠 이벤트에 따라 스크롤이 되는 컨텐츠가 있는 경우가 그것인데 평소에는 작은 framerate를 유지하다가 스크롤시에 빠른 렌더링이 필요하므로 framerate를 올려주는 것이다. 구체적으로 MouseEvent.MOUSE_WHEEL이 발생시 framerate를 올려주고 Event.ENTER_FRAME 이벤트에서 스크롤링후 500ms이 지난 다음 다시 framerate를 줄이는 것이다. 이도 휴면(idle)상태에서 쓸데없이 렌더링되는 것을 방지하고 필요한 동작할 때만 빠른 렌더링을 요구하도록 함으로써 CPU 사용율을 줄여주는 것이다.

    package {
    	import flash.desktop.NativeApplication;
    	import flash.display.NativeWindow;
    	import flash.display.NativeWindowInitOptions;
    	import flash.display.NativeWindowType;
    	import flash.display.Sprite;
    	import flash.display.StageAlign;
    	import flash.display.StageScaleMode;
    	import flash.events.Event;
    	import flash.events.MouseEvent;
    	import flash.text.TextField;
    	import flash.text.TextFieldAutoSize;
    	import flash.utils.getTimer;
    	/**
    	 * CPU 사용 줄이기 예제 2 
    	 * @author Yongho, Ji
    	 * @since 2009.12.1
    	 * @see http://blog.jidolstar.com/622
    	 */ 
    	public class Intermediate extends Sprite {
    		private static const ACTIVE:int = 50;
    		private static const INACTIVE:int = 1;
    		private var __isActive:Boolean = false;
    		private var __isScrolling:Boolean = false;
    		private var __buffer:int;
    
    		private var __window:NativeWindow;
    		public function Intermediate() {
    			__init();
    			var options:NativeWindowInitOptions = new NativeWindowInitOptions();
    			options.type = NativeWindowType.UTILITY;
    			
    			__window = new NativeWindow(options);
    			__window.width = 200;
    			__window.height = 200;
    			__window.title = "Intermediate";
    			
    			var textField:TextField = new TextField();
    			textField.y = 20;
    			textField.width = 195;
    			textField.height = 175;
    			textField.multiline = true;
    			textField.wordWrap = true;
    			textField.border = true;
    			textField.borderColor = 0xff0000;
    			textField.text = "Adobe AIR는 다양한 OS(Windows, Mac, Linux)에 구동될 수 있는 애플리케이션을 만드는데 사용하는 일종의 Flash Platfom기술이다. Flash ActionScript 3.0이나 Ajax만으로도 AIR 애플리케이션을 만들 수 있기 때문에 기존에 웹개발자들이 다른 언어를 배우지 않고 일반 데스크탑용 애플리케이션을 만드는데 있어서 접근성이 좋다. 앞으로 AIR는 데스크탑뿐 아니라 모바일등과 같은 다양한 기기에도 적용될 예정이다.하지만 원천적으로 데스크탑용 애플리케이션과 비교해 그 적용범위가 아직까지 많은 부분 부족하고 복잡한 UI를 다루는데 있어서 성능문제가 걸릴 수 있다. 적용되는 범위는 AIR 2.0 Beta 버전이 오픈된 것을 볼 수 있었듯이 계속 발전해나갈 것이다. 또한 성능면에서도 마찬가지 일 것이다. 그래도 성능면에 있어서 개발자가 조금만 신경을 쓴다면 어떤 부분에 있어서 획기적으로 AIR 애플리케이션의 성능을 향상시킬 수 있다. 일전에 Reducing CPU usage in Adobe AIR라는 글을 보았다. 여기서는 매우 단순하고 쉬운 방법으로 framerate를 줄임으로서 CPU 사용율을 현격히 줄이는 방법을 소개하고 있다. ";
    
    			__window.stage.scaleMode = StageScaleMode.NO_SCALE;
    			__window.stage.align = StageAlign.TOP_LEFT;
    			
    			__window.stage.addChild(textField);
    			__window.activate();
    			__window.addEventListener(Event.CLOSING,__onClosing);
    		}
    		private function __init():void {
    			NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, __onActive );
    			NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, __onDeactive );
    			stage.addEventListener(MouseEvent.MOUSE_WHEEL, __onMouseWheel, true);
    		}
    		private function __onActive($event:Event):void {
    			stage.frameRate = ACTIVE;
    			__isActive = true;
    			trace( "active" );
    		}
    		private function __onDeactive($event:Event):void {
    			stage.frameRate = INACTIVE;
    			__isActive = false;
    			trace( "deactive" );
    		}
    		private function __onMouseWheel($event:MouseEvent):void {
    			if( !__isActive ) {
    				if ( !__isScrolling ) {
    					stage.addEventListener(Event.ENTER_FRAME, __onEnterFrame, true);
    				}
    				stage.frameRate = ACTIVE;
    				__isScrolling = true;
    				__buffer = getTimer()+500;
    			}
    		}
    		private function __onEnterFrame($event:Event):void {
    			if( __buffer < getTimer() ) {
    				stage.frameRate = INACTIVE;
    				__isScrolling = false;
    			}
    		}
    		private function __onClosing($event:Event):void {
    			NativeApplication.nativeApplication.exit();
    		}
    	}
    }
    


    고급

    위에서 소개한 경우보다 약간더 어려운 주제로 넘어가보자. 먼저 창이 보이지 않을때와 보일때에 어떻게 처리할 것인가이다. 보이는 경우라면 일단 최소한의 framerate를 5로 주고 안보인다면 1로 준다. 이 경우는 MS Windows에서는 Tray Icon으로 바뀌며 창이 안보여질 때나 Mac에서 Dock으로만 표시될 필요가 있을때 사용될 수 있을 것이다. 굳이 보여지지 않는데 지나친 framerate를 줄 필요가 없기 때문이다. 평상시 Active한 상태에서는 24 framerate를 유지하지면 때에 따라서 부드럽게 운동하는 모습을 렌더링할 필요가 있을 때가 있다. 상태변화에 따라 Tweener 기능을 사용하는 경우가 그것인데 이때는 평소보다 framerate를 올려줄 필요가 있을 수 있다.

    아래 AIR 애플리케이션 소스코드는 두개의 버튼이 있다. 한개는 창의 visible을 false로 지정했다가 1초후 다시 true로 해주는 것이고 또 하나는 가상의 애니메이션이 있다고 가정하고 1초정도 여분을 둔다. 각 상태가 변할때마다 framerate를 Dubugging 시에 콘솔창에서 상태변화를 확인할 수 있도록 짜여있다.

    package {
    	import flash.desktop.NativeApplication;
    	import flash.display.NativeWindow;
    	import flash.display.NativeWindowInitOptions;
    	import flash.display.NativeWindowType;
    	import flash.display.Shape;
    	import flash.display.SimpleButton;
    	import flash.display.Sprite;
    	import flash.display.StageAlign;
    	import flash.display.StageScaleMode;
    	import flash.events.Event;
    	import flash.events.MouseEvent;
    	import flash.filters.BevelFilter;
    	import flash.text.TextField;
    	import flash.text.TextFormat;
    	import flash.utils.getTimer;
    	import flash.utils.setTimeout;
    	/**
    	 * CPU 사용 줄이기 예제 3 
    	 * @author Yongho, Ji
    	 * @since 2009.12.1
    	 * @see http://blog.jidolstar.com/622
    	 */ 
    	public class Expert extends Sprite {
    		public static const ANIMATING:int = 50;
    		public static const ACTIVE:int = 24;
    		public static const INACTIVE_VISIBLE:int = 5;
    		public static const INACTIVE_INVISIBLE:int = 1;
    		
    		private var __isActive:Boolean = false;
    		private var __isAnimating:Boolean = false;
    		private var __window:NativeWindow;
    		private var __buffer:int;
    		
    		public function Expert() {
    			__init();
    			var options:NativeWindowInitOptions = new NativeWindowInitOptions();
    			options.type = NativeWindowType.UTILITY;
    			
    			__window = new NativeWindow(options);
    			__window.width = 200;
    			__window.height = 200;
    			__window.title = "Expert";
    			
    			__window.stage.scaleMode = StageScaleMode.NO_SCALE;
    			__window.stage.align = StageAlign.TOP_LEFT;
    			
    			//창을 감추기 버튼 
    			var buttonSkin:Sprite = new Sprite;
    			buttonSkin.graphics.beginFill( 0xff0000, 1.0 );
    			buttonSkin.graphics.drawRect(0,0,100,50);
    			buttonSkin.graphics.endFill();
    			buttonSkin.filters = [new BevelFilter()];
    			var textFormat:TextFormat = new TextFormat();
    			textFormat.color = 0xffffff;
    			textFormat.align = "center";
    			var textField:TextField = new TextField();
    			textField.defaultTextFormat = textFormat;
    			textField.text = "창을 감추기(1초후에 나타남)";
    			textField.width = 100;
    			textField.multiline = true;
    			textField.wordWrap = true;
    			textField.y = buttonSkin.height/2 - textField.textHeight/2; 
    			buttonSkin.addChild( textField );
    			var button:SimpleButton = new SimpleButton(buttonSkin,buttonSkin,buttonSkin,buttonSkin);
    			button.addEventListener(MouseEvent.CLICK, __onHide );
    			__window.stage.addChild( button );
    			
    			//Animation 동작 시키기 버튼 
    			buttonSkin = new Sprite;
    			buttonSkin.graphics.beginFill( 0x0000ff, 1.0 );
    			buttonSkin.graphics.drawRect(0,0,100,50);
    			buttonSkin.graphics.endFill();
    			buttonSkin.filters = [new BevelFilter()];
    			textFormat = new TextFormat();
    			textFormat.color = 0xffffff;
    			textFormat.align = "center";
    			textField = new TextField();
    			textField.defaultTextFormat = textFormat;
    			textField.text = "동작시키기(1 second)";
    			textField.width = 100;
    			textField.multiline = true;
    			textField.wordWrap = true;
    			textField.y = buttonSkin.height/2 - textField.textHeight/2; 
    			buttonSkin.addChild( textField );
    			button= new SimpleButton(buttonSkin,buttonSkin,buttonSkin,buttonSkin);
    			button.y = 52;
    			button.addEventListener(MouseEvent.CLICK, __onAnimate );
    			__window.stage.addChild( button );			
    						
    			__window.activate();
    			__window.addEventListener(Event.CLOSING,__onClosing);
    		}
    		private function __init():void {
    			NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, __onActive );
    			NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, __onDeactive );
    		}
    		private function setFrameRate( frameRate:Number ):void {
    			stage.frameRate = frameRate;
    			trace( frameRate );
    		}
    		private function animate($duration:int = 1000):void {
    			trace( "동작시작");
    			setFrameRate( 50 );
    			__buffer = getTimer() + $duration;
    			if(!__isAnimating) {
    				stage.addEventListener(Event.ENTER_FRAME,__onEnterFrame);
    			}
    		}
    		private function active():void {
    			if(!__isAnimating) {
    				setFrameRate( ACTIVE );	
    			} 
    			trace( "active " );
    		}
    		private function deactive():void {
    			if (!__isAnimating) {
    				setFrameRate( (__window.visible) ? INACTIVE_VISIBLE : INACTIVE_INVISIBLE );
    			}
    			trace( "deactive " );
    		}
    		private function show():void {
    			__window.visible = true;
    			active();
    		}
    		private function hide():void {
    			__window.visible = false;	
    			deactive();
    		}
    		private function __onActive($event:Event):void {
    			__isActive = true;
    			active();
    		}
    		private function __onDeactive($event:Event):void {
    			__isActive = false;
    			deactive();
    		}
    		private function __onEnterFrame($event:Event):void {
    			if( __buffer < getTimer() ) {
    				trace( "동작끝");
    				stage.removeEventListener(Event.ENTER_FRAME,__onEnterFrame);
    				__isAnimating = false;
    				if( __isActive ) {
    					active();
    				} else {
    					deactive();
    				}
    			}
    		}
    		private function __onHide($event:MouseEvent):void {
    			hide();
    			setTimeout(show,1000);
    		}
    		private function __onAnimate($event:MouseEvent):void {
    			animate( 1000 );
    		}
    		private function __onClosing($event:Event):void {
    			NativeApplication.nativeApplication.exit();
    		}
    	}
    }
    


    참고글
    Reducing CPU usage in Adobe AIR 
    [머드초보]시스템트레이아이콘(system tray icon) 예제


    글쓴이 : 지돌스타(http://blog.jidolstar.com/622)

    Adobe Flex가 아닌 Flash CS3, CS4, ActionScript 3.0로 개발해본 사람이면 Stage를 자주 접하게 된다. 하나의 ActionScript 3.0 애플리케이션은 Stage 하나를 가지고 있다. 이것은 모든 DisplayObject 계열의 객체가 addChild()를 통해 시각화 과정이 완료후에 그 객체의 stage속성을 통해 접근이 가능하다. 모든 DisplayObject 계열의 객체에서 stage속성에 접근할 수 있다는 것은 stage가 매우 중요하다는 것을 암시하고 있다. 그러므로 잘 알고 활용해야 삽질을 방지할 수 있지 않을까?



    1. Stage.invalidate()와 Event.RENDER 이벤트의 이해 


    Stage 클래스에는 invalidate() 메소드가 있다. 이것을 호출하면 다음 렌더링 시점에 Event.RENDER 이벤트를 발생시킨다. 모든 DisplayObject 객체는 stage에 접근이 가능하므로 Event.RENDER 이벤트를 청취할 수 있다. 이는 애플리케이션 퍼포먼스를 향상시키고자 하는 개발자들이 반드시 알고 있어야할 사항이라고 생각한다. Stage의 invalidate()와 Event.RENDER 이벤트를 이용해 쓸데없는 렌더링을 예방함으로써 퍼포먼스를 향상시킬 수 있기 때문이다. 그 이유를 명확하게 알기 위해 다음 예제를 보자.


    필자는 일반 ActionScript 3.0 프로젝트를 만들고 Sprite기반에서 애플리케이션을 만들고자 한다. 이때 두개의 클래스를 만드는데 하나는 Stage.invalidate()를 사용한 클래스이고 하나는 사용하지 않는것이다. 이 두개를 비교하면 Stage.invalidate()의 유용성을 확연히 알 수 있다.


    InvalidateTest.as

    package {
    	import flash.display.Sprite;
    	import flash.display.StageScaleMode;
    	import flash.display.StageAlign;
    
    	/**
    	 * Stage.invalidate() 테스트 메인 
    	 * @author Yongho, Ji (jidolstar@gmail.com)
    	 * @since 2009.6.1
    	 */ 
    	public class InvalidateTest extends Sprite
    	{
    		public function InvalidateTest()
    		{
    			super();
    			
    			stage.align = StageAlign.TOP_LEFT;
    			stage.scaleMode = StageScaleMode.NO_SCALE;
    			
    			var button1:NotUseInvalidateButton = new NotUseInvalidateButton();
    			button1.setWidth( 100 );
    			button1.setHeight( 100 );
    			button1.backColor = 0xff0000;
    			button1.lineColor = 0x00ff00;
    			button1.x = 20;
    			button1.y = 20;
    			addChild( button1 );
    			
    			var button2:UseInvalidateButton = new UseInvalidateButton();
    			button2.setWidth( 100 );
    			button2.setHeight( 100 );
    			button2.backColor = 0xff00ff;
    			button2.lineColor = 0x0000ff;
    			button2.x = 130;
    			button2.y = 20;
    			addChild( button2 );			
    		}
    
    	}
    }
    

    위처럼 두개의 버튼 클래스를 만들었다. 2개 모두 폭, 높이, 배경색, 선색을 지정하도록 되어 있다. 결과는 비슷하다. 하지만 내부적으로 NotUseInvalidateButton은 내부에 Stage.invalidate()를 사용하지 않았고 UseInvalidateButton은 사용했다. 소스를 보자.


    NotUseInvalidateButton.as

    package
    {
    	import flash.display.Sprite;
    
    	/**
    	 * Stage.invalidate()를 사용하지 않는 Button
    	 * @author Yongho, Ji (jidolstar@gmail.com)
    	 * @since 2009.6.1
    	 */ 
    	public class NotUseInvalidateButton extends Sprite
    	{
    		private var _height:Number = 50;
    		private var _width:Number = 50;
    		private var _backColor:Number = 0xffffff;
    		private var _lineColor:Number = 0x000000;
    		
    		public function NotUseInvalidateButton()
    		{
    			super();
    		}
    		
    		public function get backColor():Number
    		{
    			return _backColor;
    		}
    		
    		public function set backColor( value:Number ):void
    		{
    			_backColor = value;
    			drawNow();
    		}
    		
    		public function get lineColor():Number
    		{
    			return _lineColor;
    		}
    		
    		public function set lineColor( value:Number ):void
    		{
    			_lineColor = value;
    			drawNow();
    		}
    		
    		public function getWidth():Number
    		{
    			return _width;
    		}
    		
    		public function setWidth( value:Number ):void
    		{
    			_width = value;
    			drawNow();
    		}
    		
    		public function getHeight():Number
    		{
    			return _height;
    		}
    		
    		public function setHeight( value:Number ):void
    		{
    			_height = value;
    			drawNow();
    		}
    		
    		public function drawNow():void
    		{
    			trace( "[NotUseInvalidateButton] drawNow" );			
    			graphics.clear();
    			graphics.beginFill( backColor, 1 );
    			graphics.lineStyle( 1, lineColor, 1 );
    			graphics.drawRect( 0, 0, getWidth(), getHeight() );
    			graphics.endFill();
    		}
    		
    	}
    }
    


    UseInvalidateButton.as

    package
    {
    	import flash.display.Sprite;
    	import flash.events.Event;
    
    	/**
    	 * Stage.invalidate()를 사용하는 Button
    	 * @author Yongho, Ji (jidolstar@gmail.com)
    	 * @since 2009.6.1
    	 */ 
    	public class UseInvalidateButton extends Sprite
    	{
    		private var _height:Number = 50;
    		private var _width:Number = 50;
    		private var _backColor:Number = 0xffffff;
    		private var _lineColor:Number = 0x000000;
    		
    		private var isDrawNow:Boolean = false;
    		private var isInvalidated:Boolean = false;
    				
    		public function UseInvalidateButton()
    		{
    			super();
    			
    			this.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
    		}		
    		
    		private function onAddedToStage( event:Event ):void
    		{
    			this.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
    			
    			if( isInvalidated )
    			{
    				isInvalidated = false;
    				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
    				this.stage.invalidate();
    			}
    		}
    		
    		private function onRender(event:Event):void
    		{
    			if( this.stage )
    			{
    				this.stage.removeEventListener( Event.RENDER, onRender, false );
    			}
    			if( isDrawNow )
    			{
    				isDrawNow = false;
    				drawNow();
    			}
    		}
    		
    		private function invalidate():void
    		{
    			if( this.stage )
    			{
    				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
    				this.stage.invalidate();
    			}
    			else
    			{
    				isInvalidated = true;	
    			}
    		}
    		
    		
    		public function get backColor():Number
    		{
    			return _backColor;
    		}
    		
    		public function set backColor( value:Number ):void
    		{
    			_backColor = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function get lineColor():Number
    		{
    			return _lineColor;
    		}
    		
    		public function set lineColor( value:Number ):void
    		{
    			_lineColor = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function getWidth():Number
    		{
    			return _width;
    		}
    		
    		public function setWidth( value:Number ):void
    		{
    			_width = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function getHeight():Number
    		{
    			return _height;
    		}
    		
    		public function setHeight( value:Number ):void
    		{
    			_height = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function drawNow():void
    		{
    			trace( "[UseInvalidateButton] drawNow" );
    			graphics.clear();
    			graphics.beginFill( backColor, 1 );
    			graphics.lineStyle( 1, lineColor, 1 );
    			graphics.drawRect( 0, 0, getWidth(), getHeight() );
    			graphics.endFill();
    		}		
    		
    	}
    }
    
    

    디버깅 모드로 프로그램을 실행해보면 콘솔창에 다음과 같이 출력된다.


    [NotUseInvalidateButton] drawNow

    [NotUseInvalidateButton] drawNow

    [NotUseInvalidateButton] drawNow

    [NotUseInvalidateButton] drawNow

    [UseInvalidateButton] drawNow 


    NotUseInvalidateButton은 drawNow()메소드가 4번 호출되고 UseInvalidateButton은 1번만 호출된다. 이것만 보더라도 쓸데없는 렌더링을 줄여준 UseInvalidateButton 클래스가 더 잘 설계되었다는 것을 확인할 수 있다. 


    NotUseInvalidateButton를 잘 살펴보면 setWidth(), setHeight(), set backColor, set lineColor 를 호출할 때마다 drawNow() 메소드를 호출한다. 개발자라면 이 4가지 속성이 모두 적용된 다음에 drawNow()를 호출하여 한번만 그려주고 싶어질 것이다. 이때 유용하게 사용할 수 있는 것이 Stage.invalidate() 이다.


    UseInvalidateButton을 보자 NotUseInvalidateButton보다 약간 복잡해보이지만 잘 따라가보면 어렵지 않게 분석이 가능할 것이다. 결국 4개의 속성이 설정되면 invalidate() 메소드가 호출되고 Event.RENDER 이벤트를 받는 onRender()에서 drawNow()가 호출되도록 한다. 이렇게 되면 4개든 여러개든 할 것 없이 렌더링에 영향을 주는 다중 속성을 설정할 때마다 drawNow()를 호출하여 그림을 그려주는 부담을 줄일 수 있다. 


    그림을 그리는 행위는 일반 속성을 설정하는 것보다 더 비싼 비용을 가진다. 그러므로 그림을 그리는 것은 되도록 한번에 처리할 수 있도록 하는 것이 애플리케이션 퍼포먼스 향상의 중요한 요소가 될 수 있는 것이다. 



    2. Stage.invalidate()의 버그와 해결방법 


    하지만 이렇게 좋은 Stage.invalidate() 가 있음에도 불구하고 실제로 이 메소드를 호출해도Event.RENDER 이벤트가 발생하지 않아 당황스러운 경우가 필자에게 있었다. 그래서 어떤 것은 drawNow()가 호출되고 또 어떤 것은 호출안되는 상황이 발생한 것이다. 너무도 어이가 없지만 Flash Player 10으로 넘어오면서도 이 버그는 아직까지도 Fix가 되지 않은 모양이다. 



    위 링크는 모두 Adobe Bugs 관리 시스템에 등록된 것이다.(여러분도 가입해서 투표하길 바란다. 버그 시스템을 잘 이용하면 해결하지 못하는 문제도 쉽게 해결할 수 있을지 모른다. ^^)


    Event.RENDER가 발생한 중간에 stage.invalidate()가 호출되면 이게 무시가 되나보다. 그래서 동작상으로는 문제없지만 개발자에게는 버그처럼 느껴질 수 있을지 모르겠다. 어쨌든 필자도 이 문제로 고민을 하다가 한가지 해결방법을 찾았는데 그것은 Event.ENTER_FRAME 이벤트를 이용하는 방법이다. 


    Event.ENTER_FRAME을 이용해서 stage.invalidate()의 버그를 말끔히 해결할 수 있다. 다음 코드는NotUseInvalidateButton을 Event.ENTER_FRAME을 이용하는 것으로 바꿔본 것이다.


    NotUseInvalidateButton.as



    package
    {
    	import flash.display.Sprite;
    	import flash.events.Event;
    
    	/**
    	 * Stage.invalidate()를 사용하는 Button.
    	 * Event.ENTER_FRAME 로 Stage.invalidate() 버그 우회 
    	 * @author Yongho, Ji (jidolstar@gmail.com)
    	 * @since 2009.6.1
    	 */ 
    	public class UseInvalidateButton extends Sprite
    	{
    		private var _height:Number = 50;
    		private var _width:Number = 50;
    		private var _backColor:Number = 0xffffff;
    		private var _lineColor:Number = 0x000000;
    		
    		private var isDrawNow:Boolean = false;
    		private var isInvalidated:Boolean = false;
    				
    		public function UseInvalidateButton()
    		{
    			super();
    			
    			this.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
    		}		
    		
    		private function onAddedToStage( event:Event ):void
    		{
    			this.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
    			
    			if( isInvalidated )
    			{
    				isInvalidated = false;
    				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
    				this.stage.addEventListener(Event.ENTER_FRAME, onRender, false, 0, true );
    				this.stage.invalidate();
    			}
    		}
    		
    		private function onRender(event:Event):void
    		{
    			if( this.stage )
    			{
    				this.stage.removeEventListener( Event.RENDER, onRender, false );
    				this.stage.removeEventListener( Event.ENTER_FRAME, onRender, false );
    			}
    			if( isDrawNow )
    			{
    				isDrawNow = false;
    				drawNow();
    			}
    		}
    		
    		private function invalidate():void
    		{
    			if( this.stage )
    			{
    				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
    				this.stage.addEventListener(Event.ENTER_FRAME, onRender, false, 0, true );
    				this.stage.invalidate();
    			}
    			else
    			{
    				isInvalidated = true;	
    			}
    		}
    		
    		
    		public function get backColor():Number
    		{
    			return _backColor;
    		}
    		
    		public function set backColor( value:Number ):void
    		{
    			_backColor = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function get lineColor():Number
    		{
    			return _lineColor;
    		}
    		
    		public function set lineColor( value:Number ):void
    		{
    			_lineColor = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function getWidth():Number
    		{
    			return _width;
    		}
    		
    		public function setWidth( value:Number ):void
    		{
    			_width = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function getHeight():Number
    		{
    			return _height;
    		}
    		
    		public function setHeight( value:Number ):void
    		{
    			_height = value;
    			isDrawNow = true;
    			invalidate();
    		}
    		
    		public function drawNow():void
    		{
    			trace( "[UseInvalidateButton] drawNow" );
    			graphics.clear();
    			graphics.beginFill( backColor, 1 );
    			graphics.lineStyle( 1, lineColor, 1 );
    			graphics.drawRect( 0, 0, getWidth(), getHeight() );
    			graphics.endFill();
    		}		
    		
    	}
    }
    


    위에서 stage.addEventListener( Event.ENTER_FRAME )과 stage.removeEventListener( Event.ENTER_FRAME ) 이 추가된 것을 확인하자. 무작정 이벤트를 청취하고 있으면 안되므로 적절하게 삭제도 하고 있다. 


    이렇게 처리함으로써 stage.invalidate()가 제대로 작동안하더라도 drawNow()메소드가 호출이 안되는 경우는 없게 된다.



    3. fl.core.UIComponent 수정하기 


    Flash의 UIComponent 를 보면 이것 stage.invalidate() 때문에 렌더링이 안되는 경우가 종종 발생한다고 한다. 필자는 Flash는 해본적이 없기 때문에 확실하게 모르지만 이것도 Event.ENTER_FRAME 을 이용해 해결할  수 있다. 그 방법은 다음 링크를 참고한다.


    How to Fix the Flash CS3 Components


    UIComponent의 callLater()가 어떻게 동작하는 것인지 이것을 보고 감을 잡을 수 있을 것이다. 결국  Event.RENDER, Event.ENTER_FRAME 으로 한다!


    4. mx.core.UIComponent의 무효화 메소드들


    Flex의 UIComponent에는 invalidateDisplayList(), invalidateProperties(), invalidateSize()와 같은 invalidate() 메소드가 존재한다. 이는 앞선 메소드들의 호출에 대해 각각 updateDisplayList(), commitProperties(), measure() 메소드가 다음 렌더링 시점에 호출되도록 하고 있다. 이는 invalidate/validate 패턴이다. 


    재미있게도 Flex에서는 Stage.invalidate()에서 처럼 동작하지 않는 버그는 존재하지 않는다. 어떻게 된 것일까?


    사실 Flex 내부를 살펴보면 Stage.invalidate() 와 Event.ENTER_FRAME을 이용해 invalidate 계열의 메소드를 처리하도록 되어 있다. UIComponent 부터, LayoutManager, SystemManager 를 들춰보면 Event.RENDER와 Event.ENTER_FRAME을 가지고 invalidate/validate 패턴을 구현하고 있다. (Flex 개발자도 어쩔 수 없었나보다. ㅡㅡ;;) 


    이에 대한 자료는 아래 링크를 읽어보면 되겠다.


    Flex internals: Setting a button label



    Flex만 접해보고 ActionScript 3.0 학습에 소홀히 했다면 절대 이런 문제를 발견할 수 없었을 것이다. 




    정리하며


    Stage.invalidate()가 호출할 때 때떄로 Event.RENDER가 호출되지 않는 문제는 버그가 아닐 수 있다. 원래 그렇게 만들어졌을지도 모른다. 하지만 개발자는 이것을 버그로 여긴다. 왜냐하면 보통 개발할때는 그런 것은 생각안하고 메뉴얼만 보고 당연히 그러겠지 생각하기 때문이다. 좀 황당하지만 버그 아닌 버그로 Flex 까지 다 까보게 되었다 ㅡㅡ;


    Event.RENDER, Stage.invalidate() 모를때는 setTimeout()과 Timer를 이용해 invalidate/validate 패턴을 구현했었다. 아~ 무식은 용감하다. ㅋ



    ActionScript 3.0은 스크립트 언어이기 때문에 어떻게 코딩을 하느냐에 따라 퍼포먼스에 크게 영향을 줄 수 있다. 다음 글에서 최적화 코딩 기술에 대해 언급하고 있으니 참고하면 좋겠다.

     

    ActionScript 3.0 and Flex optimization techniques and practices

     

    글쓴이 : 지돌스타(http://jidolstar.com/blog)

    + Recent posts