UIViewController的生命周期
- UIViewController生命周期相关函数:
// 类的初始化方法
+ (void)initialize;
//通过xib来初始化控制器
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ;
// 对象初始化方法
- (instancetype)init;
// 从归档初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//归档初始化后唤醒nib
-(void)awakeFromNib;
// 加载视图:当访问UIViewController的view属性时,view如果此时是nil,那么VC会自动调用loadView方法来初始化一个UIView并赋值给view属性。此方法用在初始化关键view,需要注意的是,在view初始化之前,不能先调用view的getter方法,否则将导致死循环(除非先调用了[super loadView];)如果没有重载loadView方法,则UIViewController会从nib或StoryBoard中查找默认的loadView,默认的loadView会返回一个空白的UIView对象。
-(void)loadView;
// 视图加载完成
- (void)viewDidLoad;
// 将要展示:,在view即将添加到视图层级中(显示给用户)且任意显示动画切换之前调用,此时self.view.superview为nil.这个方法中完成任何与试图显示相关的任务,例如改变视图方向、状态栏方向、视图显示样式等。
-(void)viewWillAppear:(BOOL)animated;
// 将要布局子视图,self.view.superview为_UIParallaxDimmingView
-(void)viewWillLayoutSubviews;
// 已经布局子视图
-(void)viewDidLayoutSubviews;
// 已经展示:在view被添加到视图层级中,显示动画切换之后调用(这时view已经添加到supperView中)。在这个方法中执行视图显示相关附件任务,如果重载了这个方法,必须在方法中调用[supper viewDidAppear];,此时self.view.superview为UIViewControllerWrapperView。
-(void)viewDidAppear:(BOOL)animated;
// 将要消失:view即将从supperView中移除,移除动画切换之后调用,此时已调用removeFromSuperview。此时self.view.superview还是superview为UIViewControllerWrapperView.
-(void)viewWillDisappear:(BOOL)animated;
// 已经消失:view从superView中移除,移除动画切换之后调用,此时已调用removeFromSuperview。此时self.view.superview还是superview为nil.
-(void)viewDidDisappear:(BOOL)animated;
// 内存警告
- (void)didReceiveMemoryWarning;
// 销毁释放
-(void)dealloc;
-
如果是通过Storyboard的Segue方式
addChildViewController
或者push将要展示的ViewController,UIViewController中生命周期相关函数调用顺序如下:
-
pop 弹出,消失销毁时,,UIViewController中生命周期相关函数调用顺序如下:
-
init,push展示时调用顺序:
-
pop时调用顺序:
-
initWithNibName
,通过xib初始化控制器并push展示时调用顺序:
pop时调用顺序:
-
由打印结果可以看出,当加载展示ViewController时,三种方式调用是生命周期方法顺序大致相同,但是有些方法调用的还是有些不同的:
- 通过Storyboard创建控制器并通过Segue方式展示控制器时,会调用
- (instancetype)initWithCoder:(NSCoder *)coder;
和-(void)awakeFromNib;
方法,不会调用- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ;
和- (instancetype)init;
方法; -
init
方法初始控制器并push展示控制器,会调用- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ;
和- (instancetype)init;
方法;不会调用- (instancetype)initWithCoder:(NSCoder *)coder;
和-(void)awakeFromNib;
方法 -
initWithNibName
初始化控制器并push展示,会调用- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
,不会调用- (instancetype)init;
,- (instancetype)initWithCoder:(NSCoder *)coder;
和-(void)awakeFromNib;
方法. - 除此以外其他生命周期方法都会调用,并且调用顺序相同.
- 通过Storyboard创建控制器并通过Segue方式展示控制器时,会调用
pop弹出返回上个控制器,当前控制器消失销毁时,三种方式调用的生命周期方法是一样的.
值得注意的是当A控制器的导航控制器
push
B控制器进来显示时,调用的顺序为:B的viewDidLoad
,A的viewWillDisappear
,B的viewWillAppear
,A的viewDidDisappear
,B的viewDidAppear
;当B的导航控制器pop当前控制器,返回展示A控制器时,调用顺序:B的viewWillDisappear
,A的viewWillAppear
,B的viewDidDisappear
,A的viewDidAppear
,B的dealloc
.viewWillLayoutSubviews
和viewDidLayoutSubviews
方法会在viewWillAppear
和viewDidAppear
之间调用,并且可能会被调用多次.上面打印顺序都是基于不去变动self.view的情况下,如果是
initWithCoder
归档的方式初始化控制器,在方法内访问self.view
的getter
方法,会调用[self loadview]
方法,此时会坏内存访问报错Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
,但是可以在次创建初始化self.view
,设置frame并不影响最终展示的位置和大小,但可以设置其他属性如背景颜色,并且该创建的view会将归档中的视图替换掉.一点创建了self.view
,会优先调用viewDidLoad
方法,然后调用awakeFromNib
,并且不再调用loadView
方法.awakeFromNib
方法中默认也并没有创建self.view
,如果在awakeFromNib
方法中访问self.view
的getter
方法也会调用[self loadview]
方法,但不会报错,说明在此起价已经初始化了self.view
,然后在loadview
中去加载self.view
;
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
NSLog(@"%@: %s",NSStringFromClass([self class]) ,__func__);
self.view = [UIView new];
self.view.frame = CGRectMake(0, 88, 100, 200);
self.view.backgroundColor = UIColor.greenColor;
}
return self;
}
-
init
方法初始化控制器时,如果在initWithNibName
中访问self.view
的gettter方法,会先调用loadView
和viewDidLoad
方法,然后才调用init
方法;如果是在init
方法或者initWithNibName
方法手动初始化了self.view
,则不再调用loadView
和viewDidLoad
方法;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
NSLog(@"%@: %s",NSStringFromClass([self class]) ,__func__);
self.view.backgroundColor = UIColor.greenColor;
}
return self;
}
- 总的来说,默认会在
loadView
中通过懒加载的方式加载self.view
,如果在之前的方法中访问self.view
的getter方法会触发调用loadView
方法,如果重写初始化self.view
的方法(getter方法).则不会再调用loadView
方法;如果是加载系统的self.view
,重写时需要调用[super loadView]
,如果是自定义的self.view
(自己创建的),则不需要调用[super loadView]
. - 控制器的
self.view
的superview是在viewWillAppear
添加的,window
是在viewWillLayoutSubviews
添加的;如下图:
UIView的生命周期
- UIView生命周期相关函数:
//构造方法,初始化时调用,不会调用init方法
- (instancetype)initWithFrame:(CGRect)frame;
//添加子控件时调用
- (void)didAddSubview:(UIView *)subview ;
//构造方法,内部会调用initWithFrame方法
- (instancetype)init;
//xib归档初始化视图后调用,如果xib中添加了子控件会在didAddSubview方法调用后调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
//唤醒xib,可以布局子控件
- (void)awakeFromNib;
//父视图将要更改为指定的父视图,当前视图被添加到父视图时调用
- (void)willMoveToSuperview:(UIView *)newSuperview;
//父视图已更改
- (void)didMoveToSuperview;
//其窗口对象将要更改
- (void)willMoveToWindow:(UIWindow *)newWindow;
//窗口对象已经更改
- (void)didMoveToWindow;
//布局子控件
- (void)layoutSubviews;
//绘制视图
- (void)drawRect:(CGRect)rect;
//从父控件中移除
- (void)removeFromSuperview;
//销毁
- (void)dealloc;
//将要移除子控件
- (void)willRemoveSubview:(UIView *)subview;
-
init
方式创建视图,并添加到父视图中展示,不添加子控件,其生命周期调用顺序为:
-
initWithFrame
方式创建视图,并添加到父视图中展示,不添加子控件,其生命周期调用顺序为:
-
移除销毁是调用顺序:
-
添加子控件展示时调用顺序:
-
添加子控件移除销毁时调用顺序:
-
xib创建初始化视图,xib中不添加子控件时调用顺序:
- xib创建初始化视图,xib中添加子控件时调用顺序:
-
移除销毁时调用顺序,有子控件:
- UIView生命周期总结:
-
init
方法初始化视图,内部会调用initWithFrame
方法,不会调用initWithCoder
和awakeFromNib
方法; - xib归档创建视图会触发
initWithCoder
和awakeFromNib
方法,不再调用init
和initWithFrame
方法; - 添加视图调用
addSubview
方法会触发didAddSubview
犯法. -
willMoveToSuperview
和didMoveToSuperview
方法,父类变化时,无论是添加到父视图还是移除父视图都会调用;在添加时willMoveToSuperview
的newSuperview
即为要将要添加的父视图,此时视图的superview
为nil,当执行didMoveToSuperview
时,视图的父视图不再为nil,为刚添加的newSuperview
;移除时willMoveToSuperview
的newSuperview
为nil
,didMoveToSuperview
时,视图的父视图也为nil;
-
willMoveToWindow
和didMoveToWindow
方法,是持有的window
变化时调用,无论是添加 到窗口显示还是移除窗口消失销毁都会调用;当添加时willMoveToWindow
的newWindow
参数不为nil
,self.window
却为nil,didMoveToWindow
的self.window
即为newWindow
;移除时willMoveToWindow
的newWindow
参数为nil
,didMoveToWindow
的self.window
也为nil
,具体见下图:
- 也就是说在添加到父视图时会在
willMoveToSuperview
方法中将视图添加到父视图中,在willMoveToWindow
添加到window
中才能展示,而在移除时在willMoveToWindow
将当前持有的window
置nil
,再在willMoveToSuperview
中将父视图置nil
; - 在调用
removeFromSuperview
方法时,其superview
已经设置为nil
,willRemoveSubview
方法在dealloc
方法之后调用,但是此时self并没有销毁,并不为nil
,应该是runloop
还没有切换导致的(还未验证).
-
UIViewController和UIView进入展示时整体生命周期调用顺序
-
self.view
是控制器的默认视图,NoXibView
是添加到self.view
的子视图,UIViewController和UIView加载展示时整体生命周期调用顺序如下(红色为打印的生命周期方法,蓝色的为superview和window):
- vc(控制器)的
viewDidLoad
方法; -
NoXibView
的willMoveToSuperview
和didMoveToSuperview
,此时已将NoXibView
添加到父视图上; - vc的
viewWillDisappear
方法,促使vc的view的superview和window还是为nil; -
NoXibView
的willMoveToWindow
和didMoveToWindow
,此时已将NoXibView
添加到widonw
; - vc的
viewWillLayoutSubviews
,此时vc
的view
的superview
和window
不再为nil; - vc的
viewDidLayoutSubviews
; -
NoXibView
的layoutSubviews
; - vc的
viewDidAppear
.
- UIViewController和UIView消失销毁时整体生命周期调用顺序如下:
- vc的viewWillDisappear;
-
NoXibView
的willMoveToWindow
,didMoveToWindow
,此时NoXibView
的window置nil; - vc的
viewDidDisappear
;此时vc的view的window和superview都置为nil; - vc的'dealloc';
-
NoXibView
的willMoveToSuperview
和didMoveToSuperview
,此时NoXibView
的superview
置nil; -
NoXibView
的removeFromSuperview
; -
NoXibView
的'dealloc'; -
NoXibView
的'willRemoveSubview'.
- 总而言之加载展示时:就是先将子控件添加到当前控件中,再将子控件添加到
window
,然后将当前控件添加到父控件中,再将当前控件添加到window
中;移除销毁时:将当前空前的window置nil,再将父控件置nil,将当前控件移除销毁,再将子控件的window置nil,子控件的父控件置nil;(展示时先添加子控件再添加当前控件再添加父控件,移除时先移除父控件,再移除当前控件,再移除子控件).
开发技巧总结
- 在开发中很多方法是用不到的,所以也没必要都记顺序,上面之所以要打印superview和window,是因为不管是autoresize还是autolayout布局都是基于父视图的,wiondw是最终的显示载体;
- 对于
UIViewController
,viewDidLoad:
方法是视图加载完成,我们可以在里面添加子视图,设置相关属性和布局等,所有和self.view相关的操作在该方法中添加比较合适;viewWillAppear:
视图即将显示时调用;viewWillLayoutSubviews:
视图将要布局其子视图时被调用;viewDidLaySubviews:
视图布局完成其子视图时被调用;viewDidAppear
视图显示后被调用;viewWillDisappear:
视图将要消失时调用;viewDidDisappear:
视图已经消失时调用; -
注意
:viewWillAppear:
和viewWillDisappear:
有时要成对使用;viewDidAppear
和viewDidDisappear:
有时要成对使用,比如显示该页面定位,消失时结束定位;还比如该页面显示时影藏导航,去其他页面时恢复导航; -
UIView
的layoutSubviews
会被经常调用,下面说下layoutsubview的调用情况:
1、addSubview会触发layoutSubviews,如果addSubview 如果连续2个 只会执行一次,具体原因下面说。
2、设置view的Frame会触发layoutSubviews,必须是frame的值设置前后发生了变化。
3、滚动一个UIScrollView会触发layoutSubviews。
4、旋转Screen会触发父UIView上的layoutSubviews事件。
5、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。 - 如果要立即执行
layoutsubview
,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.其中的原理是:执行setNeedsLayout后会在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews。这样刷新会产生延迟,所以我们需要马上执行layoutIfNeeded。就会开始遍历subviews的链,判断该receiver是否需要layout。如果需要立即执行layoutsubview
. - 每一个视图只能有唯一的一个父视图。如果当前操作视图已经有另外的一个父视图,则addsubview的操作会把它先从上一个父视图中移除(包括响应者链),再加到新的父视图上面。
- 并且连续2次的addSubview,只会执行一次layoutsubview。因为一次的runLoop结束后,如果有需要刷新,执行一次即可
适用于iOS的View Controller编程指南
UIView - 生命周期