普通视图

发现新文章,点击刷新页面。
昨天以前首页

iOS - UIViewController 生命周期

作者 齐生
2025年10月30日 19:40

核心概念

本质:一组较为稳定的事件回调。

VC的生命周期谈起,并扩展讲讲部分相关的API

UIViewController

1. 初始化阶段

  1. +initialize: 类的初始化方法 - 时机:仅 oc,且首次初始化时才会调用

  2. -init: 实例的初始化方法

    • 如果是从 xib/storyboard 来的,调用会变成:
      1. -initWithCoder: 在 nib 或 storyboard 解码时调用(对象刚被创建,未连接 IBOutlet)。
      2. -awakeFromNib: 所有子对象实例化后,并且IBOutlet都连接好后调用。
  3. -loadView: 创建 vc 的 view - 时机:访问 vc 的 view 且 view 为空时调用

    • [super loadView] 默认实现:
      1. 设置了 nibName,通过 name 查找对应 nib:
        1. 有资源,则加载对应 nib。
        2. 没资源,会按照类名匹配主 bundle 下的同名 nib。
      2. 未设置 nibName,创建一个空白 view。

2. 生命周期(相关流程)

stateDiagram-v2
    [*] --> viewDidLoad: vc 的 view 创建完成后调用
    viewDidLoad --> viewWillAppear: 视图即将可见
    viewWillAppear --> viewIsAppearing: 视图的几何尺寸、safe area、trait 等环境已确认
    viewIsAppearing --> updateViewConstraints: 约束更新,布局求解
    updateViewConstraints --> viewWillLayoutSubviews: 在本轮布局阶段开始前回调(即将布局子视图)
    viewWillLayoutSubviews --> viewDidLayoutSubviews: 在本轮布局阶段结束时回调
    viewDidLayoutSubviews --> updateViewConstraints: 循环次数系统决定,可能 0 次可能多次
    viewDidLayoutSubviews --> viewDidAppear: 过渡动画
    viewDidAppear --> viewWillDisappear: 视图即将不可见
    viewWillDisappear --> viewDidDisappear: 过渡动画
    viewDidDisappear --> [*]: 视图不可见    

⚠️:Appear 阶段的回调顺序并不是固定的,也可能是:

stateDiagram-v2
[*] --> updateViewConstraints
updateViewConstraints --> viewIsAppearing
viewIsAppearing --> viewWillLayoutSubviews
viewWillLayoutSubviews --> viewDidLayoutSubviews
viewDidLayoutSubviews --> [*]

可以看出-updateViewConstraints-viewIsAppearing的顺序不一定是固定的。

  • 原因:
    • 二者不构成先后必然关系;
    • 他们分别由“外观转场调度”与“布局引擎调度”驱动,是UIKit中两条协同的流程;
      • 外观转场调度:外观/生命周期由容器控制器(如导航)通过 begin/endAppearanceTransition 等驱动,负责“让谁消失/出现”的调度。
        • 触发外观回调viewWillAppear → viewIsAppearing → viewDidAppearviewWillDisappear → viewDidDisappear
      • 布局引擎调度:约束/布局由Auto Layout 引擎在布局阶段驱动,负责“计算 frame/安全区/约束应用”的调度。
        • 触发布局回调updateViewConstraints → viewWillLayoutSubviews → viewDidLayoutSubviews
    • 他们在主线程的同一个RunLoop上交替工作:
      • 外观转场会引发几何/安全区变化,从而“标记”需要布局。
      • 布局完成又为转场呈现提供正确的 frame。

3. 其他(不太常用)

  • 销毁
    • -dealloc
  • 内存告警
    • -didReceiveMemoryWarning:内存不足时,被 iOS 调用
    • -viewDidUnload:已弃用(iOS 3 ~ 6)
  • 容器关系
    • -willMoveToParentViewController
    • -didMoveToParentViewController
  • 环境特征/尺寸变化
    • viewWillTransition(to:with:):旋转/分屏、pageSheet 等拉动导致控制器视图 size 变化的场景。
    • traitCollectionDidChange(_:):布局方向变化(阿拉伯语 LTR -> RTL)、旋转/分屏等。

UIView(其实没有生命周期的概念,只是一些常用的事件回调)

1. 初始化

同 VC,只是没有 -loadView 而已。

2. 常用

  • 层级与窗口
    • -willMoveToSuperview -> -didMoveToSuperview
    • -willMoveToWindow -> -didMoveToWindow
  • 约束与布局
    • -setNeedsLayout:标记需要布局, 等待下次 RunLoop 执行布局
    • layoutIfNeeded:若被标记为需要布局,则“立刻在当前 RunLoop 执行一次布局”。
    • layoutSubviews:布局过程中的回调,不能手动直接调。

什么操作会标记“需要布局”呢?

  • 显示触发
    • 调用 -setNeedsLayout 方法。
    • 调用 -setNeedsUpdateConstraints修改约束
  • 几何与层级变更(UIKit 内部会标记)
    • 修改 frame/bounds/center/transform
    • 父视图的 bounds/safe area变化
    • 视图 首次加入窗口 或 窗口变化(-willMoveToWindow
  • Auto Layout 相关
    • 约束的 常量/优先级、启用/禁用
    • 组件的 抗压缩/抗拉伸 优先级
    • translatesAutoresizingMaskIntoConstraints 切换导致约束变化
  • 内在尺寸(intrinsicContentSize)变化 -(视图“基于自身内容的天然尺寸”,不依赖外部约束)
    • 调用invalidateIntrinsicContentSize
    • 改变内在尺寸的属性更新:text/font/numberOfLines等等。

3. 其他(不太常用)

  • 约束与布局
    • -setNeedsUpdateConstraints -> -updateConstraints
  • 环境变化
    • traitCollectionDidChange
    • tintColorDidChange
    • safeAreaInsetsDidChange
    • layoutMarginsDidChange
  • 渲染
    • setNeedsDisplay / setNeedsDisplay(_:)
    • draw(_:)

VC 和 View 回调的交叉(切换 vc,创建加载 view 等)

回调顺序:

1. VC 的切换

// VC(A) 切换到 VC(B)
1. B -loadView  
2. B -viewDidload  
  
3. A -viewWillDisappear  
  
4. B -viewWillAppear  
5. B -viewWillLayoutSubviews  
6. B -viewDidLayoutSubviews  
  
7. A -viewDidDisappear  
  
8. B -viewDidAppear

2. VC 与 View 的交叉

// 添加 viewB
1. VC - addSubview:viewB

2. viewB - willMoveToSuperview
3. viewB - didMoveToSuperview

// 出现 view
1. VC - viewWillAppear

2. viewB - willMoveToWindow
3. viewB - didMoveToWindow

4. VC - viewWillLayoutSubviews
5. VC - viewDidLayoutSubviews

6. viewB - layoutSubviews

7. VC - viewDidAppear

疑问:

为什么子 view 的 -layoutSubviews 打印在 -viewDidLayoutSubviews 之后?

-viewDidLayoutSubviews 的字面含义不是子 view 都做完 -layoutSubviews 了`?

  • 其实顺序是正确的,并不矛盾。-viewDidLayoutSubviews并不保证“所有子 view 的 -layoutSubviews 都已经执行完”,它只是“VC根视图这一轮布局周期结束”的回调。子视图的第一次布局可能被推迟到下一次布局循环,因此会出现在 viewDidLayoutSubviews 之后。

iOS - 从 @property 开始

作者 齐生
2025年10月29日 14:39

核心概念


本质@property 是一组访问器方法的声明 (setter/getter) ,编译器可以自动“合成”「访问器」以及「底层存储(ivar)」,并且允许用点语法调用。

  • 例如:
    @property (nonatomic) NSInteger age;
    
  • 编译器等价(自动合成):
    {
        NSInteger _age; // 可选的“底层存储” (backing ivar)
    }
    - (NSInteger)age { return _age; }              // getter
    - (void)setAge:(NSInteger)age { _age = age; }  // setter
    

好处:统一内存语义(strong/weak/copy...)、线程原子性控制(atomic/nonatomic)、可读性与 KVC/KVO 兼容。


常见属性修饰符

  • 读写性
    • readwrite:可读可写(默认)
    • readonly:只读
  • 原子性
    • atomic:保证“单次访问器调用”的原子性,速度慢。(默认)
      • 注意:atomic慢,且不保证你“对该对象做的一系列操作”是线程安全的;也不保证顺序、事务或对象内部的并发安全,实际场景还是需要显式同步。
    • nonatomic:不做同步,速度快。
  • 内存语义修饰符
    • strong:持有关系,引用计数+1,新值 retain,旧值 release.
      • 场景:一般对象所有权、父持子
    • weak:不持有,引用计数不变,对象释放时指针置空。
      • 场景:避免循环引用,如 delegate,子持父、IBOutlet
      • 注意:访问时可能已经变 nil。
    • copy:生成不可变副本,setter 执行 -(id)copy 方法。
      • 场景:阻止赋值可变对象的后续修改,block入堆。
    • assign:位拷贝,引用计数不变。
      • 场景:用于标量和结构体
      • 注意:对象指针使用 assign 会产生悬垂指针
    • unsafe_unretained:不持有,引用计数不变,对象释放不会置空。
      • 场景:以往无weak可用时使用的。
  • 其他
    • getter=isEnabled/setter=setFoo:指定自定义 setter/getter。
    • class:类属性。

copy相关延伸

strongcopy修饰的 可变/不可变 对象,对其 赋值/拷贝 会发生什么?

属性 -copy -mutableCopy 赋值 NSString 赋值 NSMutableString
(copy) NSString 浅拷贝 深拷贝 浅拷贝 深拷贝
(strong) NSString 浅拷贝 深拷贝 浅拷贝 浅拷贝
(copy) NSMutableString 浅拷贝 深拷贝 浅拷贝 深拷贝
(strong) NSMutableString 深拷贝 深拷贝 浅拷贝 浅拷贝

拷贝:

  • -copy一定返回不可变对象
    • 调用对象实际为「不可变类型」,产生浅拷贝。
    • 调用对象实际为「可变类型」,产生深拷贝,得到「不可变类型」的对象。
  • -mutableCopy一定返回可变对象(一定会深拷贝

赋值:

  • strong修饰的属性赋值,只会产生浅拷贝,属性与赋值对象「可变性」一致。
  • copy修饰的属性赋值,可能深拷贝可能浅拷贝,但是结果一定「不可变」的。
    • 赋值对象是「不可变类型」,产生浅拷贝。
      • 赋值对象是「可变类型」,产生深拷贝,得到「不可变类型」的对象。

提问:那为什么[(copy)NSMutableString copy]会是浅拷贝?

  • 答案: 基于上述结论,我们可以将答案拆分成 2 步。
    • 赋值:copy修饰的NSMutableString类型属性,在赋值时会将目标对象“深拷贝”,变为不可变的NSString。因此,我们的属性self.pStr此时实际指向的是一个NSString(不可变对象)。
    • 拷贝:在对「可变对象」进行copy操作时,返回“指针级别”的统一对象。
    • 综上,[(copy)NSMutableString copy]会是一个浅拷贝操作。

何时存储(背后存储backing ivar的规则)

  • 会有存储
    • 在类的@interface或类扩展里声明@property
    • 没有显式使用@dynamic,且没有同时手写 setter + getter 的。
  • 不会有存储
    • category里声明的@property
    • 使用@dynamic的, 承诺运行时提供访问器的。
    • 已经实现了 getter + setter 的。
    • 协议@protocol里的@property
    • 类属性。
  • 例外和细节
    • readonly 若你实现了 getter,则不会再自动合成 ivar
    • “类属性”没有ivar实例,通常用static或者其他存储来实现存储。
      @interface Config : NSObject
      @property (class, nonatomic, copy) NSString *build;
      @end
      
      @implementation Config
      static NSString *_build;
      + (NSString *)build { return _build; }
      + (void)setBuild:(NSString *)b { _build = [b copy]; }
      @end
      
    • 分类里的属性如何有“存储”?
      • 分类里的属性需要通过关联对象实现存储。
      #import <objc/runtime.h>
      @interface UIView (Badge)
      @property (nonatomic, copy) NSString *badgeText; // 分类里不会有 ivar
      @end
      
      @implementation UIView (Badge)
      static const void *kBadgeKey = &kBadgeKey;
      
      - (void)setBadgeText:(NSString *)badgeText {
          objc_setAssociatedObject(self, kBadgeKey, badgeText, OBJC_ASSOCIATION_COPY_NONATOMIC);
      }
      - (NSString *)badgeText {
          return objc_getAssociatedObject(self, kBadgeKey);
      }
      @end
      

@dynamic@synthesize计算属性

  • @dynamic

    • 作用:告诉编译器,不需要生成访问器和ivar,也不要因为找不到方法而告警。
    • 场景:Core Data 的NSManagedObject子类:
      @interface Book : NSManagedObject
      @property (nonatomic, copy) NSString *title;
      @end
      
      @implementation Book
      @dynamic title; // 访问器由运行时(Core Data)注入;编译器不生成也不报缺实现
      @end
      
  • @synthesize

    • 作用:让编译器为@property生成 getter/setter 以及背后存储 ivar,并把属性名映射到自定义 ivar 名。
  • 计算属性

    • 作用:不依赖存储,按需计算。

propertyivar 的区别

  1. ivar == 纯存储
  2. property == 访问这个存储的“方法接口”
  3. 大多数情况使用 self.age,在 init/dealloc/自定义访问器内部 常用 _age 直接访问,避免递归等问题。
❌
❌