본문 바로가기

iOS

iOS App Main Run Loop, Update Cycle

iOS App Main Run Loop

iOS Main run loop

Handling all user input events and triggers the appropriate responses in your application

앱 안에서 사용자의 입력 이벤트나 행동을 알맞은 응답으로 핸들링하는 것

 

  1. 모든 User interaction을 event queue에 추가
  2. Event queue에 추가된 모든 event들을 application 안에 다른 objects에 전달한다. (사진 속의 Application object로 전달한다?)
  3. 본질적으로 사용자 입력 event를 해석하고 그에 해당하는 handler들을 core object에서 불러오는 식으로 run loop가 실행된다.
  4. 이 handler들은 개발자가 작성한 코드들이다
  5. 한번 이러한 메서드들이 불리면(return 되면?) control은 main run loop로 되돌아가고 update cycle이 시작된다.
  6. Update cycle은 다음 section에서 묘사할  layout 과 뷰를 redrawing 하는 것을 담당한다. 
  7. 아래에서 application이 사용자 입력 이벤트와 기기간에 소통하는 것을 보여주고 있다.

 

Update Cycle

 Update cycle은 앱에서 모든 event handling code ( 개발자가 작성한 코드 )가 실행된 이후 control이 main run loop로 돌아가는 지점이다. 이 시점에서 시스템은 layout, display, 그리고 constraint들을 업데이트 하기 시작한다. 만약 당신이 event handler가 실행되는 동안 view에서 변화가 필요하다면 system은 view가 다시 그려져야 한다는 것을 마킹할 것이다. 다음 update cycle에 system은 뷰들의 변화를 실행할 것이다. 이러한 layout update와 사용자 상호작용 간의 지연은 사용자가 감지할 수 없어야 한다. iOS application은 일반적으로 60 fps로 애니메이션 되며, 이는 refresh cycle이 1/60초 밖에 걸리지 않는다는 것을 의미한다. 이것이 얼마나 빠르게 일어나는지에 따라 사용자는 장치 내에서 콘텐츠와 layout의 updqte와 application의 상호작용 간의 UI 지연을 느끼지 못한다. ( 앱을 사용할 때 사용자 편의성과 관련된 직접적인 문제점 ) 하지만, event가 처리되는 시점과 해당 view가 다시 그려지는 시점 간에 간격이 있기 때문에 view는 run loop 중 정확히 원하는 시점에 업데이트되지 않을 수도 있다. 만약 어떠한 계산이 view의 가장 최근의 콘텐츠나 layout에 의존하고 있다면 view의 오래된 정보를 조작할 수 있는 리스크가 있는 것이다. 이러한 문제를 방지하거나 디버그 하기 위해선 run loop, update cycle 그리고 특정 UIView 메서드들이 도움이 될 것이다. 

 

아래 사진은 run loop 가 끝난 후 update cycle 이 어떻기 발생하는지 에 대한 다이어그램이다.

 

Layout

view에서 layout은 화면 내에서 그것의 사이즈와 위치를 나타낸다. 모든 view는 superview에서 존재하는 좌표와 크기를 정의하는 frame이 있다. UIView는 시스템에 view의 layout 이 변경되었음을 알리는 메서드를 제공할 뿐만 아니라 view의 layout이 다시 계산된 후 수행할 작업을 정의할 수 있는 메서드 또한 제공한다.

 

layoutSubviews()

이 UIView 메서드는 모든 subview들의 위치 및 크기 재조정을 처리한다. 이것은 현재 view와 모든 subview들의 위치와 크기를 제공한다. 이 메서드는 모든 view와 subview들 그리고 그에 해당하는 layoutSubview메서드를 실행시키기 때문에 매우 비용이 많이 든다. 시스템은 view의 frame을 재계산해야 할 때마다 이 메서드를 실행하기 때문에 frame을 설정하고 위치와 크기를 조정하려는 경우 이 메서드를 재정의 (override) 해야 한다. 그러나 view의 계층적 구조에 layout의 새로고침이 필요할 때 명시적으로 이 메서드를 호출해선 안된다. 대신에 layoutSubviews를 자체 호출보다 더 저렴하게(?) run loop동안에 layoutSubviews 메서드를 유인할 수 있는 여러 메커니즘이 있다. 

 

layoutSubviews가 완료되면, view를 소유한 viewController에서 viewDidLayoutSubviews 가 불린다. layoutSubviews가 view의 layout이 업데이트되고 난 다음 실행되는 유일한 메서드 이므로 layout과 sizing에 관련된 로직은 viewDidLayoutSubViews에 입력해야 한다. ( viewDidLoad, viewDidAppear가 아님)

이것이 다른 계산을 위해 오래된 layout과 위치 변수를 사용하는 것을 피할 수 있는 유일한 방법이다.

 

자동 새로고침 방법 (layoutSubviews 메서드를 자동으로 호출하는 방법)

view가 layout의 변경을 자동으로 감지하고 layoutSubviews를 개발자가 수동으로 실행하지 않고 불리게 할 수 있는 여러 이벤트가 있다.

 

view의 layout이 변경된 것을 자동으로 시스템에 알리는 방법은 다음과 같다:

  • View를 resizing 할 때
  • Subview를 추가할 때
  • 사용자가 UIScrollView를 scroll 할 때 ( layoutSubviews 가 UIScrollView와 그것의 superview에서 호출됨)
  • 사용자가 기기를 회전할 때
  • View의 constraint를 upadte 할 때

이것들은 모든 뷰의 위치를 재계산해야 하고 자동으로 최종 layoutSubviews의 호출로 이어질 것임을 시스템에 전달한다. 

그러나, layoutSubviews 메서드를 직접 유인하는 방법도 있다.

 

setNeedsLayout()

layoutSubviews를 호출할 수 있는 가장 저렴한(?) 방법은 setNeedsLayout을 view에서 호출하는 것이다. 이것은 시스템으로 하여금 view의 layout을 재계산해야 한다는 것을 알려준다. setNeedsLayout 메서드는 실행하고 즉시 return 된다. 그리고 return 되기 전에 즉시 view를 update 하진 않는다. 대신에 해당 view에서 시스템이 layoutSubviews를 호출하고 모든 subview에서 layuoutSubviews를 후속 호출하고 난 다음 update cycle에 업데이트된다.

setNeedsLayout 메서드의 return과 view가 다시 그려지거나 layout 이 재조정되는 시점 사이에 딜레이가 있다고 하더라도 application의 지연을 유발할 만큼 길지 않아야 한다.

 

layoutSubviews를 예약하는 느낌?

 

layoutIfNeeded()

layoutIfNeeded는 layoutSubviews를 미래에 호출할 수 있는 또 다른 UIView 메서드이다. 다음 update cycle에 layoutSubviews를 실행시키는 것을 대기시키는 것 대신에 ( setNeedsLayout 메서드를 의미하는 듯), 시스템은 layoutSubviews를 view 가 layout 업데이트가 필요할 때 즉시 호출할 것이다. 

만약 layoutIfNeeded를 setNeedsLayout 메서드 혹은 위에서 설명한 자동 새로고침 방법 다음 호출하면, layoutSubviews는 view에서 호출될 것이다. 그러나, layoutIfNeeded를 호출하고 시스템에서 새로고침 해야 한다는 액션을 취하지 않으면 layoutSubviews 는 호출되지 않을 것 이다. 만약 layoutIfNeeded 메서드를 view 에서 같은 run loop동안 layout을 업데이트 하지 않고 두번 호출하면, 두번째 호출은 layoutSubviews를 호출하지 않을 것 이다. 

 

layoutIfNeeded 를 사용하면, setNeedsLayout 메서드와는 다르게 layout을 조정하거나 다시 그려지는 것들이 메서드가 return 되기 전에 바로 실행된다. ( flight animation은 제외) 이 메서드는 다음 update cycle까지 기다리지 않고 새 layout 을 표시할 때 유용하다. 그러나, 이 경우가 아니라면, setNeedsLayout을 호출하고 다음 업데이트 주기를 기다려야 run loop당 한 번만 뷰를 업데이트한다.

 

이 메서드는 constraint를 변경할 때 특히 유용하다. layoutIfNeeded를 모든 layout 업데이트가 애니메이션 시작 전에 전파되도록 애니메이션 블락 시작 전에 호출해야 한다. 애니메이션 블락 안에서 새 constraints를 구성한 다음,  layoutIfNeeded를 호출하여 새 상태로 애니메이션을 만든다.

 

결국 둘 다 layoutSubviews() 메서드를 예약하는 메서드라는 공통점이 있으나, setNeedsLayout() 다음 undateCycle에서 업데이트하는 점과 layoutIfNeeded() 즉시 layoutSubviews 호출하여 업데이트한다는 점에서 차이점이 있다.

 

Display

view의 display는 view와 subviews의 크기와 위치를 포함하지 않고 (이것은 위에 Layout에 해당), view의 색상, 텍스트, 이미지 및 Core Graphics drawing을 포함한다. Display pass 에는 업데이트를 유도하기 위한 layout pass와 비슷한 메서드가 포함되어 있다. 둘 다 system으로 하여금 그것의 변화를 감지하고, 새로고침을 유도하기 위하여 수동적으로 호출할 수 있는 메서드가 있다. 

 

draw(_: )

UIView의 draw ( Objective-C 에선 drawRect )는 view의 콘텐츠를 layuoutSubviews에서 위치와 크기를 조정하는 것 과 같은 메서드이다. 그러나, 이것은 subview에서 draw 메서드를 호출하진 않는다. layoutSubviews와 같이 직접 draw 메서드를 호출해선 안되며, run loop 동안 다른 지점에서 draw 메서드를 호출해야 한다. 

 

setNeedsDisplay()

이 메서드는 setNeedsLayout에 해당하는 메서드 이다. 이것은 콘텐츠가 뷰에서 업데이트되었으나, 실제로 뷰를 다시 그리기 전에 return 되는 내부의 플래그를 설정한다. 그런 다음다음 update cycle에서 시스템은 이 플래그로 표시된 모든 view들을 살펴보고 이들의 draw 메서드를 호출한다. 만약 다음 update cycle에서 view의 일부 내용만 다시 그리려면, 업데이트가 필요한 view에서 setNeedsDisplay 메서드를 호출하고 rect를 전달할 수 있다.

 

대부분의 경우, UI 구성 요소를 업데이트하면 자동으로 내부에 “콘텐츠 업데이트됨”이라는 플래그를 자동으로 설정하여 뷰를 “dirty”로 표시하고, 명시적인 setNeedsDisplay의 호출 없이 view의 콘텐츠를 다음 update cycle에 다시 그리도록 요청한다. 그러나, 만약 UI 구성 요소에 직접 연결되지 않은 속성이 있지만 모든 업데이트에 view가 다시 그려야 하는 경우 didSet property observer를 정의하고 setNeedsDisplay를 적절한 view update를 호출할 수 있다.

 

프로퍼티를 설정하기 위해서 custom drawing을 수행해야 하는 경우가 있는데, 이 경우 draw 메서드를 override 해야 한다. 다음 예에서 numberOfPoints를 설정하는 것은 지정된 수의 포인트 ( 꼭짓점 )가 있는 모양으로 시스템이 view를 그리게 유도해야 한다. 이런 경우, property observer ( didSet )으로 setNeedsDisplay를 호출하여 draw 메서드를 호출하여 custom drawing 한다.

 

View에서 layoutIfNeeded 메서드와 같이 즉각적으로 view의 콘텐츠를 업데이트 하는 표시 방법은 없다. 일반적으로 view를 다시 그리기 위해서 다음 update cycle을 기다리는 것으로 충분하다.

 

Constraints

Auto Layout방식으로 view를 레이 아웃하고 다시 그리는 세 가지 단계가 있다. 

  1. Updating Constrains - 시스템에서 view에 요구되는 모든 constraint를 계산하고 설정하여 constraint를 업데이트하는 것이다. 
  2. LayoutPass - layout engine이 view와 subview의 frame을 계산하고 배치한다.
  3. DisplayPass - view의 콘텐츠를 다시 그리고 필요하다면 draw 메서드를 호출한다

 

updateConstraints()

이 메서드는 Auto Layout을 사용하는 view의 Constraints를 동적으로 변경할 때 사용될 수 있다. Layout 단계에서 layoutSubviews() 나 Display 단계에서 draw 메서 도와 같이, updateConstraints()는 오직 오버 라이딩되어야 하며, 명시적으로 코드 내에서 호출해선 안된다. 일반적으로 updateConstraints에서 동적으로 변해야 하는 Constrains를 구현한다. 정적인 constrains는 interface builder 나 view의 initializer 또는 viewDidLoad() 메서드에서 정의되어야 한다.

 

  • Constraints를 활성화/비활성화하는 것
  • Constraints의 우선순위나 값을 변경하는 것
  • view의 계층에서 view를 삭제하는 것

이들은 내부의 플래그를 설정해 다음 update cycle에 updateConstraints를 호출시킨다. 그러나 updateConstraints를 명시적으로 호출하는 방법 또한 존재한다.

 

setNeedsUpdateConstraints()

setNeedsUpdateConstraints를 호출하는 것은 다음 update cycle에서 constraint를 업데이트하는 것을 보장해 준다. 이것은 한 view가 constraints가 업데이트됐을 때를 확인하면 updateConstraints() 메서드를 호출한다. 이 메서드는 setNeedsDisplay()와 setNeedsLayout() 메서드와 비슷하다.

 

updateConstraintsIfNeeded()

이 메서드는 laoutIfNeeded와 유사하다. 그러나 AutoLayout을 사용하는 view에만 유효하다. 이 메서드는 (자동으로 생성된, setNeedsUpdateConstraints에서 설정된, invalidateInstrinsicContentSize를 통해 설정된) “constraints update” 플래그를 검사한다. 만약 constraint가 업데이트되어야 한다면, run loop의 종료를 기다리지 않고 updateConstraints를 즉시 호출한다. 

 

invalidateIntrinsicContentSize()

Intrinsic Content Size : 본질적인 콘텐츠 크기

Auto Layout을 사용하는 일부 view들은 intrinsicContentSize 속성이 view의 기본 크기인 속성이 있다. view는 일반적으로 view에 포함된 intrinsicContentSize의 제약 조건에 따라 결정되지만 사용자 지정 동작을 위해 override 할 수 있다. invalidateIntrinsicContentSize() 메서드를 호출하여 다음 layout pass에 intrinsicContentSize가 바뀌었고 재 계산되어야 한다고 알리는 플래그를 설정할 수 있다.

 

How it all connects

View의 layout, display 그리고 constrains는 업데이트되는 방식과 run loop 동안 다른 지점에서 강제 실행하는 방법에서 매우 유사한 패턴을 따른다. 각각의 요소들에는 실제 업데이트를 전파하는 메서드 ( layoutSubviews, draw, updateConstraints)가 있으며, view를 수동으로 다루기 위하여 override 할 수 있지만, 어떤 상황에서도 명시적으로 호출해선 안된다. 이 메서드는 오직 run loop가 끝난 후 view가 플래그를 설정하여 일부 구성 요소를 업데이트해야 한다고 system한테 알려줄 때 호출된다. 이 플래그를 자동으로 설정하는 특정 작업이 있지만, 명시적으로 설정하여 알리는 방법 또한 있다. 

Layout Constraint 업데이트에 관련된 경우, 만약 이러한 update들이 run loop 종료까지 기다릴수 없을때 ( , 다른 작업이 view layout 종속된 경우), “layout updated” 플래그를 설정하게 하는 몇가지 명시적으로 업데이트를 호출 할 있는 메서드 들이 있다. 다음은 업데이트가 필요할 있는 UI 구성 요소와 관련하여 방법을 간략하게 설명한 차트이다.

 

 

결국 둘 다 updateConstraints() 메서드를 예약하는 메서드라는 공통점이 있으나, setNeedsNeedsUpdateConstraints() 다음 undateCycle에서 업데이트하는 점과 updateConstraintsIfNeeded() 즉시 updateConstraints 호출하여 업데이트한다는 점에서 차이점이 있다.

 

다음 차트는 update cycle과 event loop 간에 상호작용을 요약하고 위에서 설명한 일부 메서드들이 어디에서 해당하는지 나타낸 차트이다. Run loop의 어느 지점에서든 layoutIfNeeded 또는 updateConstraintsIfNeeded를 명시적으로 호출할 수 있으며, 이는 잠재적으로 비용이 많이 든다는 점을 염두에 둬야 한다. 이 루프의 끝에 update cycle이 있으며, 특정 “update constraints”, “update layout”, “needs display” 플래그가 설정된 경우 constraints, layout, display를 업데이트한다.

이러한 업데이트가 완료되면, run loop는 재실행된다.

 

참고

- https://tech.gc.com/demystifying-ios-layout/