개발자 아카이브 문서에 따르면, iOS에서 화면에 그리는 모든 작업(OpenGL, Quartz, UIKit 또는 Core Animation 포함 여부와 관계없이)은 UIView 클래스 또는 그 하위 클래스의 인스턴스 범위 내에서 발생한다고 합니다.
앱이 iOS 운영체제 위에 올라가 실행되고 나서 유저에게 보여지기까지 어떤 일이 일어나는지 한번 알아보죠!
요약:
1. iOS는 draw(_ :)을 이용하여 그리기 작업을 한 후 CALayer마다 가지고 있는 backing store(프레임 버퍼)에 저장하거나, 그림자, 반사 등 복잡한 작업의 경우 Offscreen Rendering을 이용하여 GPU에서 계산한 뒤 Offscreen 버퍼(임시 프레임 버퍼)에 저장합니다.
2. 이렇게 작업된 결과물들이 저장된 프레임 버퍼들은 그래픽 렌더링과 에니메이션을 도와주는 프레임워크인 Core Animation의 도움을 받아 메인 프레임 버퍼에 합성하는 과정을 거칩니다.
3. 합성된 메인 프레임 버퍼는 디스플레이 컨트롤러에 의해 디스플레이에 전송됩니다.
먼저 CALayer를 통해 backing store라는 프레임 버퍼에 저장되거나 따로 계산해서 Offscreen 임시 프레임 버퍼에 저장되었다가, 메인 프레임 버퍼에 합성된다는데...
도대체 프레임 버퍼가 뭐길래 프레임 그리는 정보들이 여기를 거쳐서 스크린에 그려지는 걸까요?
1. 프레임 버퍼(Frame Buffer)
1) 프레임 버퍼 영역 리소스
프레임 버퍼 메모리에는 스크린에 무엇을 어떻게 그릴건지에 대한 정보가 저장됩니다.
다음과 같이 세 종류의 버퍼가 들어가게 되죠. (Color, Depth, Stencil)
1 - 컬러 버퍼 (Color Buffer)
먼저 스크린은 단순히 픽셀의 집합으로 정의할 수 있습니다. 아이폰13의 스크린은 2532개 *1170개의 픽셀이 모인 직사각형 판자인 것이죠.
그리고 한 픽셀은 한개의 색을 표시합니다. 프레임 버퍼는 모든 픽셀의 색에 대한 정보를 담고 있습니다.
아이폰은 32비트 컬러(red, green, blue, alpha)를 사용하기 때문에, 한 픽셀당 4바이트의 컬러 버퍼를 가지고 있죠.
아이폰 13을 예시로 든다면, 프레임 버퍼에는 `2532 * 1170 * 4 (byte)`, 즉 11.3 MB 정도 크기의 컬러 버퍼가 저장되는 것이죠.
2 - 깊이 버퍼 (Depth Buffer)
깊이 버퍼와 스텐실 버퍼는 ARKit과 게임 등 Vision Pro가 출시된 지금 점점 더 많은 곳에서 사용하는 3D 그래픽스에서 사용하는 버퍼입니다.
객체의 픽셀이 카메라 앞에서 얼마나 멀리 떨어져 있는지를 나타내는 깊이 값을 저장하는데 사용되는 버퍼에요.
3D를 사용하는 대표적인 예시로 Apple Maps의 Flyover 기능을 예시로 들어서 설명 해드릴게요.
저기 금문교가 보이죠? 지금 사진이라서 저 친구가 그림처럼 보이는데, 사실은 3D로 만들어져 있죠.
저기 보시면 기둥에 다리가 가려져 있는 것이 보일거에요.
이건 다리보다 기둥이 더 앞에 있기 때문에, 즉 깊이값이 기둥이 다리보다 더 낮기 때문에 (더 가까이 있기 떄문에) 기둥 뒤에 있는 픽셀은 렌더링 되지 않는거죠.
예를 들어 기둥 깊이값을 5, 다리 깊이값을 10이라고 합시다. 그럼 다리 뒤에 있는 기둥 픽셀은 렌더링 되지 않는 것이죠!
3 - 스텐실 버퍼 (Stencil Buffer)
스텐실 버퍼는 3D에서 특정 영역만 렌더링하거나, 특정 조건에서만 렌더링을 하고 싶을 때 사용하는 값입니다.
마찬가지로 위의 예시를 바탕으로 설명할게요!
만약에 제가 Apple Map의 개발자여서, 저렇게 Golden Gate Bridge를 선택하면, 선택된 3D 지형이 금빛으로 변하게 하는 작업을 하려고 한다고 가정합시다.
저는 다리에 대한 3D 객체들 (다리, 기둥 등등)에 스텐실 버퍼를 임의의 N값으로 설정을 해놓을게요.
이제 렌더링 할 때, 스텐실 버퍼가 N이 아닌 부분들만 원래대로 렌더링 합니다.
그리고 스텐실 버퍼가 N인 부분들만 나중에 금색으로 렌더링하는거죠!
근데 이런 궁금증이 생기실 수 있어요.
"왜 깊이 버퍼와 스텐실 버퍼는 3D 작업에만 쓰이지?? 깊이 버퍼만 해도 여러 view가 앞뒤로 중첩되어 있는 2D 앱에 써도 좋을텐데???"
저도 이게 궁금해서 찾아봤는데 2D 작업에서는 단순히 view의 계층구조와 zPosition 속성 만으로 충분히 합성해서 렌더링 할 수 있더라구요!
깊이 버퍼와 스텐실 버퍼는 거의 3D 그래픽에서만 쓰이는걸로!! 땅땅땅!!
(이걸 다루는 작업을 아직 해본적이 없어서 나중에 한번 해보고 정리해볼게요!)
2) 프레임 버퍼의 할당 위치
먼저 전통적인 PC 등 에서는 프레임 버퍼 메모리 영역이 GPU안에 있는 VRAM에 할당되는 경우가 많았어요.
왜냐면 메인 메모리까지(DRAM)까지 왔다 갔다 하기에는 GPU와 램의 거리가 너무 멀거든요.
하지만 iOS나 M1이후의 맥과 같은 Apple Silicon 칩에서는 Frame Buffer가 램 안에 할당됩니다.
왜냐면
..
충분히 가까우니까..!!
이렇게 한 칩 위에 CPU, GPU, DRAM이 모두 있기 때문에 GPU 안에 VRAM을 따로 둘 필요가 없죠.
CPU와 GPU 모두 각각 필요한 그래픽 데이터 자원을 DRAM에 두고 서로 공유하게 됩니다.
VRAM에서 필요한 리소스를 DRAM에 복사해 가져와 사용하는 기존 방식이 아니라, CPU와 GPU 모두 통합된 메모리에서 리소스를 공유하는 방식은 사용할 때마다 가져와서 복사할 필요가 없어서 오버헤드를 줄여주지만 공유 자원에 대한 race condition 등이 발생할 수 있다는 단점은 있죠.
2. UIView와 CALayer
먼저 UIView와 CALayer가 어떤 식으로 프레임 버퍼에 렌더링 정보를 적는지를 보려고 하는데요.
먼저 UIView 부터 볼게요!
1) UIView가 도대체 뭐지?
UIView는 늘 써왓어서 익숙한데 누군가 제게 UIView에 대해서 물으면 답하기가 힘들더라고요.
"UIView가 뭐에요?"라고 질문 받았을 때 "모든 인터페이스 컴포넌트의 기초 토대이며, UIButton, UITextView 등등 모든 UIKit의 View들이 UIView를 상속받아서 만들어져있다..!!" 정도 밖에 대답하지 못했어요.
https://developer.apple.com/documentation/uikit/uiview
UIView에 관한 애플 문서 맨 앞을 이렇게 요약할 수 있을 것 같아요. 더 많은 역할들이 있겠지만, 일단은 이 세가지를 뽑네요!!
UView
모든 View에 공통적으로 적용되는 동작을 정의
view 개체는 앱이 사용자와 상호작용하는 주요 방식입니다.
1. Drawing과 Animation
UIKit이나 Core Graphics를 사용하여 직사각형 영역에 콘텐츠를 그립니다.
view의 변수들을 새로운 값으로 애니메이션할 수 있습니다.
2. Layout과 Subview 관리
view에는 0개 이상의 Subview가 포함될 수 있습니다.
view는 Subview의 크기와 위치를 조정할 수 있습니다.
3. Event 처리
view는 UIResponder의 subclass이고, 터치 및 기타 유형의 이벤트에 응답할 수 있습니다.
view는 gesture recognizers를 설치하여 일반적인 제스처를 처리할 수 있습니다.
저는 여기서 1번과 2번에 집중해보려고 해요. 3번도 UIView가 하는 중요한 역할이고 특수한 역할이지만 지금 우리는 렌더링을 집중적으로 보고 있으니까요ㅎㅎ
2) Drawing Cycle에서 레이아웃 계산
UIView에서 Layout과 Subview 관리하고 Drawing과 Animation을 할 때는 UIkit이나 Core Graphics를 사용하여 직사각형 영역에 콘텐츠를 그린다고 하는데요.
더 알아보기 전에 iOS의 Drawing Cycle부터 한번 보시죠!
!!!!!!설명할 때는 Drawing이라는 용어를 단순히 view 내부 컨텐츠를 그리는 것 뿐만 아니라 view의 layout을 계산하는 것까지도 포함할게요!!!!!!
내부 콘텐츠를 그리는 것에 한정한 draw(_:)와 다름을 주의하셔야 합니다.
이 그림을 바탕으로 설명할게요 (출처:https://tech.gc.com/demystifying-ios-layout/)
1 - iOS에서 Run Loop와 Drawing Cycle
저 그림이 정말 잘 설명하고 있다는 생각이 드는데요!
먼저 Drawing Cycle은 setNeedsDisplay, setNeedsLayout, setNeedsUpdateConstraint 메소드를 통해 트리거 됩니다.
이 메소드에 대해서는 아래에서 설명할테니, 아 항상 Drawing Cycle이 시작하는 건 아니고 RunLoop가 특정한 메소드들이 실행되면 Drawing 작업을 위한 Event들을 트리거 하는구나! 정도로 이해하시면 될 것 같아요.
RunLoop는 Drawing 조건들이 트리거 된다고 해도 이를 바로 작업하지 않고 RunLoop의 Queue에 넣고 기다렸다 화면 업데이트가 필요하다고 판단될 때 처리하는데요.**
말이 좀 어렵죠??
예시를 들어서 차근차근 설명해볼게요!
제가 사용하는 아이폰13는 60FPS 입니다. 즉, 1/60초(16.67밀리초) 마다 화면 업데이트가 되죠!
RunLoop는 이 60FPS에 최대한 맞춰주기 위해 화면 업데이트 작업을 스케쥴링합니다.
엥 FPS는 디스플레이라는 하드웨어 장치에서 깜빡깜빡 일어나는 일인데, RunLoop가 이걸 어떻게 알고 업데이트 작업을 스케쥴링 해주는거지? 하는 궁금증이 들 수 있을거에요!
여기서 쓰이는 iOS의 기술이 V-Sync 입니다.
제 아이폰은 1/60초마다 V-Sync라는 디스플레이 장치의 FPS와 동기화된 신호를 소프트웨어에서 보내게 되는데, RunLoop는 이 신호에 맞춰 최대한 1/60초 안에 화면을 그리는데 필요한 정보들을 준비하기 위해 업데이트 작업을 스케쥴링을 하게 되죠!
만약에 준비할게 너무 많아서 준비작업이 1/60초 보다 더 걸리게 되면 이때는 해당 프레임은 업데이트 되지 못하니까 사용자에게 뚝뚝 끊기는 느낌을 주게 되죠!
근데 이 반대 상황도 중요해요! 컴퓨터의 작업은 엄청나게 빠르잖아요?
예를 들어 1/600초 간격으로 랜덤으로 view의 frame값이 바뀌게 해봅시다!
class ViewController: UIViewController {
var viewToMove: UIView!
var timer: Timer?
var maxX: CGFloat!
var minX: CGFloat!
var maxY: CGFloat!
var minY: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
viewToMove = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
viewToMove.backgroundColor = .red
viewToMove.center = self.view.center
self.view.addSubview(viewToMove)
maxX = view.bounds.width - viewToMove.frame.width / 2
minX = viewToMove.frame.width / 2
maxY = view.bounds.height - viewToMove.frame.height / 2
minY = viewToMove.frame.height / 2
}
override func viewWillAppear(_ animated: Bool) {
timer = Timer.scheduledTimer(timeInterval: 1 / 600, target: self, selector: #selector(moveViewRandomly), userInfo: nil, repeats: true)
}
override func viewWillDisappear(_ animated: Bool) {
timer?.invalidate()
}
@objc func moveViewRandomly() {
let randomX = CGFloat.random(in: minX...maxX)
let randomY = CGFloat.random(in: minY...maxY)
viewToMove.center = CGPoint(x: randomX, y: randomY)
}
}
지금 저 GIF 파일 자체가 아마 프레임이 안높아서 좀 느리게 나올텐데, 사실은 1초에 600번을 랜덤으로 frame 값을 바꿔주고 있죠!!
근데... 1초에 600번 프레임 값을 바꿔준다고 해서 그걸 다 화면에 그려주는게 의미가 있을까요? 제 아이폰13은 1초에 60번밖에 화면을 못그리는데 말이에요...
이러한 작업을 다 진행하면 엄청 비효율적일거에요!
안그래도 화면 그리는 작업은 엄청나게 큰 리소스를 소모하거든요!!
실제로 런루프는 1/60초의 간격으로 업데이트 되도록 스케쥴링한 후, 이 뒤에 이야기하게 될 화면을 준비하는 과정에서 viewToMove의 그 당시 가장 최신 위치를 가지고 그리게 될거에요ㅎㅎ
아 그리고 실제로는 아무리 빨리 뭔가 바꿔야 한다고 해도 저렇게 업데이트하는건 아니고 `CADisplayLink`를 사용하여 화면 업데이트 주기에 맞춰 작업할 수 있기는 합니다ㅎㅎ 60fps면 1/60초에 맞추고 120hz면 1/120초에 맞춰서요!
그럼 쓸데없는 계산 자체를 하지 않겠죠??
이렇게요!!
var displayLink: CADisplayLink!
...
override func viewWillAppear(_ animated: Bool) {
displayLink = CADisplayLink(target: self, selector: #selector(moveViewRandomly))
displayLink.add(to: .main, forMode: .default)
}
override func viewWillDisappear(_ animated: Bool) {
displayLink.invalidate()
displayLink = nil
}
@objc func moveViewRandomly() {
let randomX = CGFloat.random(in: minX...maxX)
let randomY = CGFloat.random(in: minY...maxY)
viewToMove.center = CGPoint(x: randomX, y: randomY)
}
2- SetNeedsDisplay, SetNeedsLayout, setNeedsUpdateConstraint
iOS에서는 on-demand 드로잉 모델을 사용하고 있어요. 아까 항상 Drawing Cycle이 시작하는 건 아니고 RunLoop가 특정한 메소드들이 실행되면 Drawing을 위한 Event를 트리거 하는구나! 라고 이해하셨죠??
이는 iOS가 on-demand 드로잉 모델을 사용하고 있기 때문이에요!
on-demand 드로잉 모델은 그래픽 요소나 view의 내용이 변경되거나 업데이트가 필요할 때만 드로잉이 발생하는 것을 말해요!
만약에 어떤 화면이 100초 동안 아무 변화도 없다고 가정 해볼게요!
그럼 100초 동안 view를 새로 그리지 않습니다. 즉 CPU나 GPU에서 불필요한 그래픽 연산이나 드로잉 작업이 발생하지 않는 것이죠!!!
이걸 다르게 말하면 프레임 버퍼의 내용이 그대로 유지되고 디스플레이 하드웨어는 이 프레임 버퍼의 내용을 계속 사용하여 화면을 갱신하게 되는거죠ㅎㅎ
RunLoop가 특정한 메소드들이 실행되면 Drawing을 위한 Event를 트리거한다는 말은 결국
특정한 메소드들이 실행되면 "아 업데이트가 필요하구나!"라고 알고 Drawing Cycle에서 다시 그릴 수 있도록 스케줄링을 하는거죠!
그리고 이 특정한 메소드가 바로 SetNeedsDisplay, SetNeedsLayout, setNeedsUpdateConstraint 입니다!!
setNeedsDisplay는 View 안에 콘텐츠가 바뀌었으니까 다시 그려줘!
setNeedsLayout는 View 자체의 layout이 바뀌었으니까 다시 그려줘!!
setNeedsUpdateConstraint는 View자체의 auto layout 제약 조건이 바뀌었으니까 다시 그려줘!!!
RunLoop에 이렇게 이야기하는거죠!
그럼 RunLoop는
"오 그래 뭔가 바뀌었어? 다시 그릴 수 있게 스케줄 한 번 잡아볼게!!" 이렇게 이야기하는거죠 .
[오 근데 나는 이런 메소드들 굳이 명시해주지 않아도 알아서 바뀌던데??]
이건 UIView를 상속한 대부분의 컴포넌트들(예를 들면 UIButton, UILabel, UITextField 등등)의 프로퍼티가 수정될 때마다 내부적으로 알아서 저 메소드들을 불러주기 때문이에요.
[언제 명시해줘야 하는거지??]
만약에 예를들어 내가 직접 만든 GraphView가 있다고 해볼게요. 어떤 View 안의 그래프의 Bar 높이가 어떤 변수를 참조해서 결정이 된다면, 이 변수가 바뀌면 다시 그려져야 하기 때문에 그때는 변수가 바뀔 때 setNeedsDisplay를 명시해줘야 하는 거죠.
3- layoutIfNeeded
대표적으로 SetNeedsLayout와 같은 메소들은 RunLoop에게 "나 바뀌었으니까 적당할 때 레이아웃 업데이트 좀 잡아줘!"라고 부탁하는거라면,
layoutIfNeeded 는 "당장 레이아웃부터 업데이트 하자!" 라는 메소드에요.
물론 레이아웃 업데이트가 일어난다고 해서 바로 Drawing Cycle이 시작하는건 아니지만!! (앞서 설명한 FPS와 성능 등등의 이유로) 그래도 레이아웃 변화를 한번에 모았다가 적당한 때 빡! 업데이트 하지 않고 바로 바로 업데이트 하게 되면 실제 변경이 바로 바로 적용되기 쉽겠죠??
다음 코드는 레이아웃 변화가 바로 바로 적용되어야 하는 animation에 대한 예시에요!
@objc private func moveButtonTapped() {
viewToMove.snp.updateConstraints { make in
make.leading.equalToSuperView().offset(200)
}
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
이 코드에서는 viewToMove라는 view의 Leading Constraint 를 변경하고, 이를 0.5초 동안의 애니메이션 블록 안에서 상위 view에서의 layoutIfNeeded()을 실행했기 때문에 상위 view가 애니메이션과 함께 다시 레이아웃 계산이 되게 됩니다ㅎㅎ
3) CALayer와 Backing store
변경된 Layout이나 content(display)가 어찌어찌 계산이 되었으니 이제 적절한 때에 변경된 사항을 반영해서 다시 그려서 디스플레이에 띄워야겠죠??
즉, 이전 단계에서는 Drawing Cycle에 들어오기까지의 일들을 알아봤다면,
이 단계에서는 Drawing Cycle에 들어와서 일어나는 일들을 알아볼거에요!
어떤 방식으로 일어나는지 한번 알아봅시다!!
1- UIView와 CALayer
모든 UIView와 이를 상속받은 클래스들은 내부적으로 CALayer 인스턴스를 가지고 있어요!
class CustomView: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
let customView = CustomView()
override func viewDidLoad() {
super.viewDidLoad()
print(customView.layer)
}
}
이렇게 UIView를 상속한 어떤 customView를 만들고, 이 view의 layer를 찍어보면, 다음과 같이 나옵니다
<CALayer: 0x60000024e180>
즉, view의 layer가 CALayer라는 뜻이죠.
UIView의 모든 드로잉은 실제로 이 CALayer 인스턴스를 통해 이루어지고 있어요.
2- 그래서 CALayer가 뭐야??
CALayer는 UIView에게 컨텐츠나 애니메이션을 그리는 행위를 위임받은 객체입니다.
CA가 애초에 Core Animation의 약자라서 그래픽을 그리는 것에 최적화된 객체라는 것도 예상할 수 있으실 거에요ㅎㅎ
조금 더 쉽게 이야기하면 CALayer는 UIView에 붙어서 View를 실제로 그리는 작업(draw(_:))을 하는 친구라고 이해하시면 좋을 것 같습니다.
CALayer가 어떤 식으로 View를 그리는지를 이해해야 하는데
UIView의 draw(_:) 메소드가 호출되면, 실제로 일어나는 그리기 연산은 해당 UIView의 CALayer객체가 GPU에게 작업을 넘겨서 처리할 수 있도록 합니다. 이 작업 결과물은 해단 CALayer가 가진 Backing Store라는 frame buffer에 저장됩니다.
좀 더 자세히 설명하자면, view의 layer는 sub layer들을 가질 수 있는데, 이 구조로 layer tree 라는 것을 만들어서 어떤 layer의 프로퍼티가 변경되면, 이 변경사항을 Backing Store에 저장되는 거죠.
예시를 한번 들어볼게요!
다음과 같이 1초마다 색을 랜덤으로 바꾸는 코드를 만들어봤습니다.
좀 더 명확하게 보고싶어서 draw를 override 하는 경우로 만들어봤습니다.
한번 천천히 보시면 쉽게 이해가 가실거지만, 중요한 부분을 아래서 따로 한번 더 설명 해드릴게요.
class ViewController: UIViewController {
var circleView: CircleView!
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
circleView.center = self.view.center
self.view.addSubview(circleView)
}
override func viewWillAppear(_ animated: Bool) {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(changeColor), userInfo: nil, repeats: true)
}
@objc func changeColor() {
circleView.color = .random
}
override func viewWillDisappear(_ animated: Bool) {
timer?.invalidate()
}
}
class CircleView: UIView {
var color: UIColor = .random {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let circleRect = CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)
context.setFillColor(color.cgColor)
context.addEllipse(in: circleRect)
context.fillPath()
}
}
extension UIColor {
static var random: UIColor {
return UIColor(red: CGFloat(Int.random(in: 0...255)) / 255, green: CGFloat(Int.random(in: 0...255)) / 255, blue: CGFloat(Int.random(in: 0...255)) / 255, alpha: 1.0)
}
}
눈여겨서 보셔야 할 부분은 두 군데이고, 이해하셔야 하는건 하나인데
미리 결론부터 말씀드리자면, setNeedsDisplay 메소드가 불린 후에 override된 draw가 불리게 된다는 것입니다.
코드를 바탕으로 자세히 설명하자면, 주의깊게 보아야 하는 부분은 CircleView에서 color 프로퍼티의 didSet과 draw(_ rect: CGRect) 부분입니다.
맨 처음에 circleView가 그려질 때, 그리고 그 이후에는 color가 바뀔 때마다 didSet 안에서 "나 바꼈어!!" 라고 알려서 RunLoop보고 슬슬 적당한 때에 Drawing 할 수 있도록 하는데요.
그럼 draw 메서드가 호출되게 됩니다.
여기 draw 메서드 안에서는 UIGraphicsGetCurrentContext()를 호출해서 현재 그래픽 컨텍스트를 가져오는데, 이걸 조금 뜯어보면 Core Graphic과 연관되어 있고, 결국 Core Animation, 즉 CALayer의 그래픽 컨텍스트에 연결되어 있기 때문에, 결국 draw에서 진행되는 작업은 해당 UIView의 현재 CALayer에 그려지게 되는거죠.
그리고 저 렌더링 작업이 완료되면, CALayer의 backing store 프레임 버퍼에 저장되게 되고, 이를 이용해서 화면이 업데이트 되게 되는거죠!!
3 - 왜 draw(:_)를 직접 호출하면 안될까?
방금은 draw를 override해서 사용한거라, 직접 호출한게 아니라 draw가 호출될때 이런 이런 작업도 해줘! 라고만 했지만, 굳이 setNeedsDisplay로 기다리지 않고 그냥 개발자가 직접 draw를 호출하면 안되나? 라는 의문점이 드실 수도 있으실거에요.
하지만 이는 iOS의 on-demand 그리기 방식을 제대로 이용하지 못하는 방법으로 성능 저하를 일으킬 수 있습니다.
왜냐면 진짜 그려야 하는 부분만 그릴 수 있도록 iOS가 설계되어 있는데 이걸 무시하고 직접 그리기를 신키는 거고, draw 자체가 리소스를 많이 사용하기 때문입니다.
휴... UIView가 가진 CALayer의 Backing store에 렌더링 되는 방식은 어느정도 설명이 된 것 같아요.
추후에는 OffScreen 렌더링과 우리가 더 나은 렌더링 성능을 위해 어떤 것들을 고려해야 하는지를 한번 정리 해볼게요!!
긴 글 읽어주셔서 감사합니다.
참고
'iOS 개발 > Swift' 카테고리의 다른 글
Swift Hashable을 이해하자 (0) | 2023.12.26 |
---|---|
[Swift] Struct와 Class를 메모리 원리부터 자세하게 비교해보자 (2) | 2023.02.12 |
[ARC] 성능을 위해 unowned를 꼭 써야할까? (0) | 2023.02.03 |
[ARC] Lazy 변수 클로저에서 Unowned 캡처가 항상 안전할까? (0) | 2023.02.02 |
[Concurrency] Semaphore로 비동기적 이벤트를 동기적으로 발생시키기 (0) | 2023.01.20 |
[ARC] 약한참조(Weak, Unowned)에 대해서 (0) | 2022.11.06 |
[Swift] 지정한 For-Loop 탈출하기 (0) | 2022.10.03 |
[Swift] Stride 함수를 사용하자 (0) | 2022.10.02 |