iOS 运行状态和生命周期

盛年不重来,一日难再晨。及时宜自勉,岁月不待人。

iOS 系统资源是有限的,合理的去配置应用程序的运行状态以及了解 App 的生命周期对于大多数开发者来说是很重要的一件事情!在后台进行运行时 App 会受到系统的很多限制,但是也可以提高电池的使用和用户的体验。

1. 应用程序的运行状态 4 种

3. 运行状态

Not running:未运行状态,程序并没有进行启动时的状态,应用在运行过程中崩溃以及出现内存警告时 iOS 系统会首先把挂起的程序清除出内存都会回到未运行状态;

Foreground:前台状态;

  1. Inactive:未激活状态 ,应用程序在前台运行,但是不能接收事件。一般仅在应用状态之间切换时短暂停留在此状态(不活跃状态)。例如:在 App 显示启动页时,是从 Not Running 到 Active 的过程,中间会存在 Inactive 状态。唯一在此状态停留时间比较长的情况是:用户锁屏时,或者系统提示用户去响应某些(诸如电话来电、有未读短信 SMS 提示等)事件的时候;也就是手机界面还是显示着你当前的应用程序的窗口,只不过被别的任务强制占用了;
  2. Active:激活状态,程序在前台运行而且接受到事件,并对事件进行响应和处理。这也是前台的一个重要的状态;

Background:后台状态,处于运行中的应用程序切换到后台时的状态,在此状态,应用获得了一定的时间来执行一些代码,但它无法直接访问屏幕或获取任何用户输入。此状态保留一定时间,时间超过时就会进入应用程序的挂起状态,有的应用程序也可以根据需求使其长期进入后台状态;

Suspended:挂起状态,此状态也是一种后台状态,与 Background 的不同在于挂起状态的应用程序是不可以执行代码的,其特点是程序还是停留在内存中(即活跃时使用的所有内存将原封不动地得以保留),如果用户将应用切换回活跃状态,它将恢复到之前的状态。而当出现内存警告时 iOS 系统会首先把挂起的程序清除出内存并为处于前台运行状态的应用程序提供内存;

2. 应用程序的生命周期

3. 应用程序生命周期

3. 启动周期

2.1 具体执行流程

  • 程序入口:
    进入 main 函数,内部调用 UIApplicationMain 函数,创建 UIAplication 对象,并为其设置代码对象 AppDelegate。

  • 程序完成加载:程序首次已经完成启动时执行,一般在这个函数里创建 Window 对象,将程序内容通过 Window 呈现给用户。

    [AppDelegate application:didFinishLaunchingWithOptions:]
    
  • 创建 Window 窗口:

  • 程序进入非激活状态:程序将要失去 Active 状态时调用,比如有电话进来或者按下 Home 键,之后程序进入后台状态。 该函数里面主要执行操作:暂停正在执行的任务、禁止计时器、减少 OpenGL ES 帧率、若为游戏应暂停游戏;

    [AppDelegate applicationWillResignActive:];
    
  • 程序进入激活状态:程序已经变为 Active 时调用,若程序之前在后台,在此方法内刷新用户界面;

    [AppDelegate applicationDidBecomeActive:]
    
  • 程序进入后台:该函数里面主要执行操作:释放共享资源、保存用户数据(写到硬盘)、作废计时器、保存足够的程序状态以便下次修复;

    [AppDelegate applicationDidEnterBackground:];
    
  • 点击进入工程:

    程序即将进入前台时调用,对应 applicationWillResignActive(即将进入后台)。这个方法用来:撤销 applicationWillResignActive 中做的改变。

    [AppDelegate applicationWillEnterForeground:];
    

    程序被激活:

    [AppDelegate applicationDidBecomeActive:];
    
  • 程序退出:程序即将退出时调用,记得保存数据;

    [AppDelegate applicationWillTerminate:];
    

2.2 加载进入前台的生命周期

3. Foreground

应用程序进入前台的生命周期从图中可以看出包括启动时间,应用程序进入运行的时间,以及切换不同 App 时的时间。

应用程序的启动时间:应用程序的启动时间从我们触发 App 开始,到 main 函数执行,到加载 main UI 文件,直到第一次加载完成以及加载其他 UI 完成的时间并进入未激活状态所用的时间就是启动 App 时的启动时间,其代理方法的调用时间:

// 在第一次完成加载 mainUI 时进行加载
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions

// 在完全加载成功 UI 时进入此方法通知 App 完成启动进入未激活状态
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 

// 在应用程序处于运行状态时接受到事件时通知 App 进入激活状态
- (void)applicationDidBecomeActive:(UIApplication *)application  

2.3 加载进入后台的生命周期

3. Background

从上图可以看出,当应用程序启动后,进入后台时会有一个判断是否可以在后台运行,允许后台运行时会一直处于后台运行状态并执行某些代码操作,当不允许运行时短暂停留后会进入到挂起状态不可执行任何代码操作。当我们在返回前台运行时会通过代理方法通知 App 程序的运行状态从后台变为前台的未激活状态和激活状态。

2.4 AppDelegate 中生命周期代理函数

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"--- %s ---",__func__);//__func__打印方法名
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
     NSLog(@"--- %s ---",__func__);
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
   NSLog(@"--- %s ---",__func__);
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
   NSLog(@"--- %s ---",__func__);
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
  NSLog(@"--- %s ---",__func__);
}

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
     NSLog(@"--- %s ---",__func__);
}

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"--- %s ---",__func__);
}

执行顺序:

// 操作 - 启动程序
--- -[AppDelegate application:didFinishLaunchingWithOptions:] ---
--- -[AppDelegate applicationDidBecomeActive:] ---

// 操作 - 进入后台
--- -[AppDelegate applicationWillResignActive:] ---
--- -[AppDelegate applicationDidEnterBackground:] ---
  
// 操作 - 重新点击,进入程序
--- -[AppDelegate applicationWillEnterForeground:] ---
--- -[AppDelegate applicationDidBecomeActive:] ---
  
// 操作 - 选择模拟器的 Simulate Memory Warning
--- -[AppDelegate applicationDidReceiveMemoryWarning:] ---

3. UIViewController

3.1 构造函数

指定初始化器:

1)initWithNibName:bundle:用于通过指定 XIB 文件初始化视图控制器。若 nibNamenil,系统会自动查找与类名匹配的 XIB 文件(如 MyViewController.xib)。可以在这里执行关键数据初始化操作,非 StoryBoard(即 init 或initWithNibName) 创建 UIViewController 都会调用这个方法。不要在这里做 View 相关操作,View 在 loadView 方法中才初始化。

- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)

2)initWithCoder:

用于从 Storyboard 或 XIB 反序列化时初始化,如通过 instantiateViewControllerWithIdentifier: 创建。

便利初始化器(Convenience Initializer):

3)init() 默认调用 initWithNibName:bundle: 并传递 nil 参数,是代码中直接初始化时的常用方法。查找 <类名>.xib,未找到则创建空视图控制器。首次访问 view 时自动创建默认 UIView

3.2 控制器生命周期

UIViewController 的生命周期划分为初始化、视图加载、视图显示、视图隐藏、内存管理、销毁六大阶段,每个阶段对应特定的回调方法。

1)初始化阶段

视图控制器 init 初始化创建过程:init(nibName:bundle:)init(coder:)awakeFromNib

2)视图加载阶段

loadView:

loadView 方法在 UIViewController 对象的 view 属性被访问且为空(nil)的时候调用。这时候 UIViewController 会自动调用 loadView 方法来初始化一个 UIView 并赋值给 UIViewController 的 view 属性。需要注意的是 loadView 方法不应该直接被调用,而是由系统调用。

假设我们在处理内存警告时释放 view 属性: self.view = nil。因此 loadView 方法在视图控制器的生命周期内可能被调用多次。

当执行到 loadView 方法时,如果视图控制器是通过 nib 创建,那么视图控制器已经从 nib 文件中被解档并创建好了,接下来任务就是对 view 进行初始化。

在创建 view 的过程中,首先会根据 nibName 去找对应的 nib 文件然后加载。如果 nibName 为空或找不到对应的 nib 文件,则会创建一个空视图(这种情况一般是纯代码)。需要注意的是若重写 loadView 手动创建视图(如纯代码场景),应直接创建并赋值 self.view = customView不可调用 super.loadView()(否则会导致父类默认行为覆盖手动创建的视图)。若通过 XIB/Storyboard 加载,系统会自动调用 loadView,无需重写。

viewDidLoad:

当 loadView 将 UIViewController 的 view 载入内存中,会进一步调用 viewDidLoad 方法来进行进一步设置。此时,视图层次已经放到内存中,通常,我们对于各种初始化数据的载入,初始设定、修改约束、移除视图等很多操作都可以这个方法中实现。

视图层次(view hierachy):因为每个视图都有自己的子视图,这个视图层次其实也可以理解为一颗树状的数据结构。而树的根节点,也就是根视图(root view),在 UIViewController 中以 view 属性。它可以看做是其他所有子视图的容器,也就是根节点。

3)视图显示阶段

viewWillAppear:

系统在载入所有的数据后,将会在屏幕上显示视图,这时会先调用这个方法,通常我们会在这个方法对即将显示的视图做进一步的设置。比如,设置设备不同方向时该如何显示;设置状态栏方向、设置视图显示样式等。

另一方面,当 App 有多个视图时,上下级视图切换是也会调用这个方法,如果在调入视图时,需要对数据做更新,就只能在这个方法内实现。

viewWillLayoutSubviews:

view 即将布局其 Subviews。 比如 view 的 bounds 改变了(例如:状态栏从不显示到显示,视图方向变化),要调整 Subviews 的位置,在调整之前要做的工作可以放在该方法中实现。

viewDidLayoutSubviews:

view 已经布局其 Subviews。比如 view 的 bounds 改变了(例如状态栏从不显示到显示,视图方向变化),已经调整 Subviews 的位置,在调整完成之后要做的一些工作就可以在该方法中实现。

调用顺序而言,当视图尺寸变化时,系统会先调用视图控制器的 viewWillLayoutSubviews → 视图的 layoutSubviews(自动调整子视图位置)→ 视图控制器的 viewDidLayoutSubviews

viewDidAppear:

视图已完全过渡到屏幕上时调用。在这个方法中执行视图显示相关附件任务,如果重载了这个方法,必须在方法中调用 [supper viewDidAppear]

4)视图隐藏阶段

viewWillDisappear:

在视图切换时,当前视图在即将被移除、或被覆盖时会调用该方法,此时还没有调用 removeFromSuperview。

viewDidDisappear:

view 已经消失或被覆盖,此时已经调用 removeFromSuperView。

5)内存管理阶段

didReceiveMemoryWarning:

在内存足够的情况下,App 的视图通常会一直保存在内存中,但是如果内存不够,一些没有正在显示的 viewController 就会收到内存不足的警告,然后就会释放自己拥有的视图,以达到释放内存的目的。但是系统只会释放内存,并不会释放对象的所有权,所以通常我们需要在这里将不需要显示在内存中保留的对象释放它的所有权,将其指针置 nil。

6)销毁阶段

dealloc:

视图被销毁,UIViewController 最后的清理工作,此次需要对你在 init 和viewDidLoad 中创建的对象进行释放。

3.3 UIViewController 总结

只有 init 系列的方法,如 initWithNibName 需要自己调用,其他方法如 loadView 和 awakeFromNib 则是系统自动调用。而 viewWill/Did 系列的方法则类似于回调和通知,也会被自动调用。

纯代码写视图布局时需要注意,要手动调用 loadView 方法,而且不要调用父类的 loadView 方法。纯代码和用 IB 的区别仅存在于 loadView 方法及其之前,编程时需要注意的也就是 loadView 方法。

除了 initWithNibName 和 awakeFromNib 方法是处理视图控制器外,其他方法都是处理视图。这两个方法在视图控制器的生命周期里只会调用一次。

#pragma mark --- life circle

// 非storyBoard(xib或非xib)都走这个方法
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    NSLog(@"%s", __FUNCTION__);
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
    
    }
    return self;
}

// 如果连接了串联图storyBoard 走这个方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
     NSLog(@"%s", __FUNCTION__);
    if (self = [super initWithCoder:aDecoder]) {
        
    }
    return self;
}

// xib 加载 完成
- (void)awakeFromNib {
    [super awakeFromNib];
     NSLog(@"%s", __FUNCTION__);
}

// 加载视图(默认从nib)
- (void)loadView {
    NSLog(@"%s", __FUNCTION__);
    self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.view.backgroundColor = [UIColor redColor];
}

//视图控制器中的视图加载完成,viewController自带的view加载完成
- (void)viewDidLoad {
    NSLog(@"%s", __FUNCTION__);
    [super viewDidLoad];
}

//视图将要出现
- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"%s", __FUNCTION__);
    [super viewWillAppear:animated];
}

// view 即将布局其 Subviews
- (void)viewWillLayoutSubviews {
    NSLog(@"%s", __FUNCTION__);
    [super viewWillLayoutSubviews];
}

// view 已经布局其 Subviews
- (void)viewDidLayoutSubviews {
    NSLog(@"%s", __FUNCTION__);
    [super viewDidLayoutSubviews];
}

//视图已经出现
- (void)viewDidAppear:(BOOL)animated {
    NSLog(@"%s", __FUNCTION__);
    [super viewDidAppear:animated];
}

//视图将要消失
- (void)viewWillDisappear:(BOOL)animated {
    NSLog(@"%s", __FUNCTION__);
    [super viewWillDisappear:animated];
}

//视图已经消失
- (void)viewDidDisappear:(BOOL)animated {
    NSLog(@"%s", __FUNCTION__);
    [super viewDidDisappear:animated];
}

//出现内存警告  //模拟内存警告:点击模拟器->hardware-> Simulate Memory Warning
- (void)didReceiveMemoryWarning {
    NSLog(@"%s", __FUNCTION__);
    [super didReceiveMemoryWarning];
}

// 视图被销毁
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}

4. UIView

首先为什么要说这个类呢?当然是最基础的了,最基础的往往都是很重要的。所有你能够用眼睛看到的界面、视图、控件肯定都是属于 UIView 或是其子类。即所有 UI 控件都继承自 UIView。

4.1 UIView 的生命周期

UIView 的生命周期包括初始化、布局、渲染、交互、销毁五大阶段,其核心方法在特定阶段触发,形成闭环管理。

1)初始化阶段:视图的“诞生”

视图 init 初始化创建过程:init(frame:)init(coder:)awakeFromNib

调用时机:代码创建视图(init(frame:))或从 XIB/Storyboard 加载(init(coder:)awakeFromNib)。

注意awakeFromNib 仅在 XIB/Storyboard 加载时调用,代码创建的视图不会触发此方法。

2)布局阶段:视图的“空间分配”

layoutSubviews:对子视图重新进行布局,调整子视图的尺寸和位置(如通过 Auto Layout 更新约束,或手动设置子视图的 frame)。官方文档已经强调,这个方法只能被系统触发调用,程序员不能手动直接调用该方法。那么有那些系统方法会触发此方法的调用呢?开发者想重新布局在必要的时候要如何引起该方法的调用呢?

layoutSubviews 在以下情况下会被触发调用:

  • 初始化(init)视图时设置了不为 CGRectZero 的 frame 的时候就会触发。
  • 子视图添加/移除:addSubview 会触发 layoutSubviews。
  • 设置 view 的 frame 时候(改变 view frame)会触发 layoutSubviews,当然前提是 frame 的值设置前后发生了变化。
  • 滚动 UIScrollVIew 会触发 layoutSubviews。
  • 旋转 screen 会触发父 UIView 上的 layoutSubviews。
  • 直接调用 setLayoutSubviews 会调用(这个在上面苹果官方文档里有说明)。

程序员在必要的时候可以通过以下操作去触发 layoutSubviews:

  • 调用 UIView 的 setNeedsLayout 方法来标记一个 UIView。这样一来,在 UI 线程的下次绘制循环中,系统便会调用该 UIView 的 layoutSubviews 方法。
  • 在使用约束的时候调用 layoutIfNeeded 方法,会立即触发layoutSubviews 对 subviews 重新布局。

其实 layoutSubviews 是个让我们容易忽视的方法,可能会因为在里面写代码麻烦,可能因为你不用手写代码,可能因为觉得没必要,总之这个方法其实还是蛮重要的。

我们一般习惯在 initWithFrame 里初始化布局代码。其实这样很不好,在initWithFrame 写的代码没有复用性,当父视图更改布局的时候,initWithFrame里布局的子视图布局还是原来的样子。所以苹果提供了专门写布局的layoutSubviews 方法。初始化视图不要放在这里,因为 layoutSubviews 会触发多次,会造成视图重复创建。

总结下: 一个 view 是不能够自己调用 layoutSubviews,如果要调用,需要调用 setNeedsLayout 或者 layoutIfNeeded。如果 view 的 frame 值为0,即使被添加了也不会调用 layoutSubviews。如果一个 view 的 frame 值改变了,那么它的父类的 layoutSubviews 也会被执行。

setNeedsLayout:标记视图需要重新布局,但是不立即刷新。在 UI 线程的下次绘制循环中调用该 UIView 的 layoutSubviews 方法。

layoutIfNeeded:如果视图已标记为需要布局,立即调用 layoutSubviews 进行布局。所以经常会看见以下两个方法连续调用的情况。setNeedsLayout 是做标记,layoutIfNeeded 是立即刷新视图 layout,即调用 layoutSubviews 方法:

[self setNeedsLayout];
[self layoutIfNeeded];

但是有时候你会发现只单独调用 [self layoutIfNeeded]。了也立即刷新了,比如修改约束的 constant 属性, 那是因为当这样的约束被更新时,它会自动执行相当于setNeedsLayout 的操作。

sizeToFit:调整视图尺寸以适应其内容(如 UILabel 自动调整高度)。本质是调用 sizeThatFits(_:) 计算合适尺寸,并更新视图的 frame。sizeToFit 不应该在子类中被重写,应该重写 sizeThatFits。sizeThatFits 传入的参数是 receiver 当前的 size,返回一个适合的size。sizeToFit 可以被手动直接调用 sizeToFit 和 sizeThatFits 方法都没有递归,对 subviews 也不负责,只负责自己。

3)渲染阶段:视图的“可视化呈现”

drawRect:在绘图上下文(CGContext)中绘制自定义内容(如图形、文字、图片)。视图首次显示或 setNeedsDisplay 标记后会调用绘制方法。

在以下情况下会调用这个方法:

  • 如果在 UIView 初始化时没有设置 rect 大小,将直接导致 drawRect 不被自动调用。drawRect调用是在 Controller->loadView, Controller->viewDidLoad 两方法之后调用的。所以不用担心在控制器中,这些View 的 drawRect 就开始画了。这样可以在控制器中设置一些值给View(如果这些 View draw 的时候需要用到某些变量值)。

  • 该方法在调用 sizeToFit 后被调用,所以可以先调用 sizeToFit 计算出size。然后系统自动调用 drawRect: 方法。

  • 通过设 置contentMode 属性值为 UIViewContentModeRedraw。那么将在每次设置或更改 frame 的时候自动调用 drawRect:。

  • 直接调用 setNeedsDisplay,或者 setNeedsDisplayInRect: 触发drawRect:,但是有个前提条件是 rect不能为0。以上1,2推荐;而3,4不提倡。

drawRect 方法使用注意点:

  • 若使用 UIView 绘图,只能在 drawRect:方法中获取相应的 contextRef并绘图。如果在其他方法中获取将获取到一个 invalidate 的 ref 并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。

  • 若使用 CALayer 绘图,只能在 drawInContext: 中(类似于 drawRect)绘制,或者在 delegate 中的相应方法绘制。同样也是调用setNeedDisplay 等间接调用以上方法3、若要实时画图,不能使用gestureRecognizer,只能使用 touchbegan 等方法来掉用setNeedsDisplay 实时刷新屏幕。

setNeedsDisplay/setNeedsDisplayInRect:与 setNeedsLayout 方法相似的方法是 setNeedsDisplay 方法。标记视图需要重绘(系统在下一个运行循环中调用 drawRect),可局部重绘(通过 setNeedsDisplayInRect 指定区域)。所以,当需要刷新布局时,用 setNeedsLayout方法;当需要重新绘画时,调用 setNeedsDisplay 方法。

4)交互与状态变更阶段:视图的“动态响应”

5)销毁阶段:视图的“回收”

removeFromSuperview:视图从父视图中移除时触发,解除引用避免内存泄漏。

dealloc:视图被释放时调用(ARC 下自动处理),用于释放资源(如移除通知、取消观察者、释放缓存)。

4.2 性能优化关键

避免在 layoutSubviews 中修改 bounds,防止死循环。

频繁重绘使用 Core Animation 或 GPU 加速,减少 drawRect 的调用次数。

removeFromSuperviewdealloc 中清理资源,避免内存泄漏。

与 UIViewController 的关联:UIView 的生命周期嵌入在 UIViewController 的视图管理流程中(如 viewDidLoad 初始化根视图,viewWillAppear 触发布局和渲染)。

4.3 自定义 UIView

自定义UIView步骤:

  • 重写 - (instancetype)initWithFrame:(CGRect)frame 方法,在该方法中添加子控件,但是不设置 frame。

  • 重写 - (void)layoutSubviews 方法,在该方法中设置控件的 frame,主要必须调用 [super layoutSubviews]。

  • 提供模型属性,并且重写该模型的 set 方法,在该 set 方法中取出相关的属性并赋值给子控件。

https://www.jianshu.com/p/e81034e2780a

思考点:

为什么自定义 View 时重写 - (instancetype)initWithFrame:(CGRect)frame 而不是 - (instancetype)init;
当我们使用纯代码创建自定义 View,在之后使用的过程中,可能会使用 init 创建,也可能使用 initWithFrame 创建。无论使用哪种方式,在代码执行过程中最终一定会调用 initWithFrame,所以重写 initWithFrame 方法可以保证无论使用哪种方式创建都可以保证子控件创建成功。

4.4 Layer 和 View 的关系

在 iOS 开发中,UIView(视图)CALayer(图层) 是 UIKit 与 Core Animation 框架中的核心概念,二者紧密关联但职责明确区分。以下从关系本质、职责划分、协作机制三个维度清晰阐述:

本质关系:视图是图层的“管理者”,图层是视图的“可视化载体”

  • UIView 是 UIKit 中的抽象层,负责用户交互、事件响应、布局约束、视图层级管理等高层逻辑。它本身不直接绘制内容,而是通过关联的 CALayer 实现视觉呈现。
  • CALayer 是 Core Animation 中的底层类,是实际渲染到屏幕的“画布”,负责存储位图数据、处理动画、合成特效(如圆角、阴影、渐变)、管理图层树等。每个 UIView 实例都有一个对应的 CALayer 实例(通过 view.layer 访问),称为 “backing layer”。

职责划分:各司其职,互补协作

  • UIView 的核心职责:
    • 交互与事件:处理触摸事件(如点击、滑动)、响应链传递、手势识别。
    • 布局管理:通过 frame/bounds、Auto Layout 约束确定视图位置和尺寸,并驱动子视图的布局。
    • 视图层级:维护 UIView 的父子关系(addSubview:),构建视图树(View Hierarchy)。
    • 内容管理:决定是否需要重绘(通过 setNeedsDisplay触发 drawRect:),但实际绘制由 CALayer 或其子类(如CAShapeLayer)完成。
  • CALayer 的核心职责:
    • 视觉呈现:存储位图内容(通过contents属性设置图片/视频)、绘制边框(border)、圆角(cornerRadius)、阴影(shadow)、渐变(通过CAGradientLayer)等。
    • 动画引擎:Core Animation通过隐式/显式动画操作CALayer的属性(如positiontransformopacity)实现平滑动画,无需阻塞主线程。
    • 合成与性能:管理图层树(Layer Tree),支持离屏渲染优化、图层混合(如透明度)、硬件加速(GPU渲染)。
    • 3D与特效:通过CATransform3D实现3D变换,支持CAShapeLayer绘制矢量图形、CATextLayer渲染文本等。

协作机制:视图驱动图层,图层反馈视图

  • 数据同步:当UIView的frameboundsalpha等属性变化时,会自动同步到其CALayer的对应属性(如layer.framelayer.boundslayer.opacity),反之亦然(但通常不建议直接操作CALayer的尺寸属性,应通过UIView控制)。
  • 绘制流程:UIView的drawRect:方法(如果重写)会通过CALayer的display方法触发,最终将绘制结果(如Core Graphics绘制的路径)缓存到CALayer的位图中,由GPU合成渲染。
  • 动画联动:UIView的动画方法(如UIView.animate(withDuration:))本质是操作CALayer的隐式动画;直接操作CALayer的positiontransform等属性可触发显式动画,实现更复杂的动画效果。
  • 事件传递:用户交互事件(如点击)由UIView接收,通过hitTest:方法判断是否命中其CALayer的可见区域(考虑透明度、圆角等),再决定是否响应。

总结回答框架:当被问到“Layer 和 View 的关系”时,可按以下逻辑清晰回答:

在iOS中,UIView 是负责用户事件交互、布局和视图层级管理的高层视图,而CALayer 是其底层的可视化载体,负责实际的渲染、动画和视觉特效。每个 UIView 都关联一个 CALayer 实例(通过 layer 属性访问),二者是‘管理者与执行者’的关系,它们通过属性同步(如 frame/bounds)、绘制流程、动画引擎紧密协作,共同实现屏幕内容的显示与交互。

CALayer 是“渲染管线”的优化器:

你的理解“渲染最终由CPU/GPU处理”完全正确,但 CALayer 的参与是为了让这一过程更高效、更灵活。它通过以下方式实现:

  • 分层管理:将布局(UIView)与渲染(CALayer)分离,让开发者可以专注于业务逻辑,而无需处理底层的渲染细节。
  • 属性驱动:通过CALayer的属性(如cornerRadiusshadow)直接控制GPU的渲染行为,减少CPU的计算负担。
  • 动画优化:利用GPU硬件加速动画,提升流畅度。
  • 合成与特效:通过图层树和预定义属性实现复杂的视觉效果,避免CPU的重复计算。

因此,CALayer不是“多余的一层”,而是iOS渲染管线的核心优化器,它让CPU和GPU的协作更高效,同时为开发者提供了更简洁、更强大的视觉编程接口。

posted @ 2021-10-14 19:54  背包の技术  阅读(218)  评论(0)    收藏  举报