本文是投稿文章( 点击查看原文 )
在最近解决某个问题的时候,发现在ViewDidDisappear中去获取self.navigationController为空。猛然间意识到,原来在VC的生命周期中存在一些细节问题需要注意。而且,最近一段时间,对基于流程(生命周期是特殊的流程)建模的编程思想也开始有些反思。所以就总结了一下VC生命周期的一些问题。
先说点比较抽象的东西,关于流程建模的。对于同一个对象而言,往往在不同的业务场景中其有不一样的流程。换句话说,对于一个对象而言其可能出在多个流程中。比如我们拿一个VC来说:
每一个OC的实例都有其本身的生命周期——创建、使用、销毁
而对于VC来讲在处理内存问题的时候,还有其特有的ViewDidLoad,等过程
在处理页面展示的时候,也有ViewWillAppear等过程
…
而在一个流程当中,每一个过程(一般会以函数表示)都有其特殊的职责。比如alloc用于非配内存,init用于初始化内存。而我们在这些函数中做的事情,也必须尽可能的和该函数的职责所匹配。一个被设计好的流程(通常会以一组函数的形式呈现),就像是一个插排。上面的每个插口都有自己适配的类型,如果你乱插,可能会有烧掉保险丝的危险。比如你在alloc中硬要做dealloc的事情。从设计模式的角度来说,这种思想叫做『控制反转』,是设计框架的时候常用的技巧,通过约束使用者的使用方式,来完成功能。而我们在使用UIKit等框架的时候,我们作为使用方,自然要接受这种『控制反转』。且能够在正确的地方做正确的事情。一句话说就是: 恰如其分 。
同时,我希望通过阐释VC的一些生命流程和其使用细节的事情。也能激发读者对于基于流程建模的编程思想的反思。通过这种思想去反思在日常编程中,其他库中一些流程的使用。甚至是在自己进行程序设计的时候,能够也注意使用一下这种方式。
好了下面我们就开始看看一个VC都有哪些流程需要注意的.btw,穷举所有的流程是一个费时费力的事情,所以会只摘几个比较关键的流程来描述和讲解。最重要的目的还是在于能够启发各位用流程建模这个视角来思考编程的一些问题:),偷懒了。
内存使用流程
VC的实例在内存使用上面,打的流程和其他对象实例的使用类似,都要经过下述的一些过程:
创建->初始化->使用->销毁
后面的阐述也是类似,我们先说流程。然后再具体到函数的使用。因为我们在使用一个库或者框架的时候,首先要关注的是他的模型。尤其是流程模型。而具体的函数往往是在该模型基础上,实践下来的产物。
苹果在内存处理上使用的是两段式构造的思想: 将创建和初始化分两步走
创建的核心关注点在于内存分配。从堆栈上批出一块内存给对象使用。至于该对象,如何使用该内存(初始化)则是另外的函数的事情。经过创建和初始化两步之后,才能够给出一个干净可以使用的对象实例。
在创建的时候,一般涉及到的函数为: ~~~ + (instancetype)alloc + (instancetype)allocWithZone:(struct _NSZone *)zone ~~~ 这两个函数为系统函数,我们不能重载该函数。这点是苹果在文档中格外强调的。因而,对于创建我们也只是调用一下系统函数的事情,没有太多自定义的工作需要我们去做。
初始化是两段式构造的第二步,对象实例只有经过该步骤之后,才是一个干净可以使用的对象。这种思想在很多编程语言中我们可以看到,比如C++。当然也有很多一段式构造的例子比如C语言。
而在OC中,初始化使我们进行对象自定义操作的开始。这里我们需要初始化一些当前类特有的属性的值,以保证后续业务逻辑能够够正常。比如当我们从xib文件中加载VC的使用我们会使用到函数:
- (instancetype _Nonnull)initWithNibName:(NSString * _Nullable)nibName bundle:(NSBundle * _Nullable)nibBundle
该函数将会通过传入的xib文件名和bundle来加载界面,并且初始化相关的数据。当然这是系统的函数。而我们更关注的是我们在这里应该做什么和可以做什么。
说句废话:要做对象实例的初始化。主要是变量的赋值操作。
For Exmaple:
- (instancetype) initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (!self) { return self; } _payHandler = [BDWalletPayWebHandler new]; _payHandler.enviromentWebViewController = self; return self; }
上面的例子中我们在该函数中初始化了一个_payHandler的变量。而且细心的读者可能发现,我们用于初始化这个变量的值还不是外部传进来的,而是内部新生成的。这种方式我们称之为内部初始化。自然也会有外部初始化。
内部初始化:变量的值在内部生成。
外部初始化:用于初始化成员变量的值是在外部生成,然后传给。
而在实际的初始化场景中我们经常会发现这样的情况:在进行类的设计的时候,遇到传值的问题的时候,比如下述问题,我们通过VC1获取了用户的姓名,要向VC2进行传递。现在的一般做法是在定义VC2的时候,在头文件中暴漏name变量。
@interface B : UIViewController @property (strong) NSString* name; @end
然后使用的时候这个样子:
B* vc = [B new]; vc.name = @"xx"; [self.navigationController push:vc];
这种做法,封装性很差,任何持有VC2实例的地方都能够修改这个name值,导致一些很奇怪的逻辑。而且往往是那种不可预期的变动。一旦出现bug查找起来极其困难。
其实这种情况应当属于外部初始化的典型应用。更好的方式就是我们就把name当成对象初始化必须的一个变量,需要对其进行初始化,那么就应当提供相应的函数来进行初始化。这样可以保持比较好的封装性。
建议以后采取这样的方式
// .h @interface VC2 : UIViewController - (instancetype) init UNAVAILABLE; - (instancetype)initWithName:(NSString*)name; @end //.m @interface VC2 : UIViewController () { NSString* _name; } @end @implatation VC2: UIViewController - (instancetype)initWithName:(NSString*)name { self = [super init]; if(!self) return self; _name = name; return self; } @end
在.h文件中进行变量声明的时候,如果不需要外部多次修改的变量,就不要暴漏了,做成私有变量,如果该变量初始化时所需的,那么就写成初始化函数哈。因为@property这种语法的存在,削弱了OC中作用域的概念,从而导致了大家对于publick,private,protected等概念不是很清晰,从初始化这个事情上可见一斑。然,这些概念对于程序的健壮性又是多么的至关重要。还是应该拾起来的。
常用的函数
- init; - (instancetype _Nonnull)initWithNibName:(NSString * _Nullable)nibName bundle:(NSBundle * _Nullable)nibBundle - (instancetype _Nonnull) initWith****
其中init函数为所有OC对象都有的。
关于使用这个其实是最重要的部分,而对象一旦创建并初始化完成之后,就可以嵌入到除了内存使用流程之外的流程之中。而在内存流程中我们所谓的使用,就是在其他流程中,对该内存对象进行的一系列的操作,包括且不止于:增删改查。
对于使用的细节,可参考其他流程的介绍。
对象在完成使命之后,自然要被销毁,来释放其持有的资源。所谓有借有还再借不难,在创建过程中占用的内存,在初始化过程中持有的其他系统资源,在这个时候要做统一的释放。而且这是最后的释放时机,不然这个对象就成了小偷,会永久性的把资源偷走,比如在传统MRC的情境下,在init中分配是有了一个array,但是在dealloc中没有release,那么这个数组所占用的内存就写漏掉了。
这里我们重提RAII,资源获取就是初始化。因为你获取了,你得释放啊。谁污染谁治理。所以申请和释放,创建和销毁是必须成对存在的。RAII是一个广义的资源管理概念,不至于内存。
这个问题我们在Notification的使用中,经常会碰到crash的情况,一般都是因为没有正确的removeObser导致的脏内存引起的。我们可以把addObserve看成资源持有,而removeObserver看成资源释放。实际上也是如此,这对函数会对observe的引用计数进行加减操作。那么对于Notification这个事情也可以参考上述的流程来考虑。但这得和业务场景匹配才行,有些情况下接受通知可以伴随着对象的生命周期,建议在init-dealloc这对中注册取消。如果是伴随着UI显示而接收通知,则在didappear和diddisappear中进行最好(and在dealloc补充个取消,因为在navigation poptoroot的时候,中间的一些VC不会出发disappear等函数)。
这个没有罗列在最初的那么内存流程模型当中,因为这样的,在建模的时候,首先要做的是让整个模型Work起来,而后再去处理各种边界问题。如果一上来就把精力集中在边界问题的处理上,就会无限制的放大问题的复杂度,增加处理的麻烦。
而我们在看了基础的内存使用流程模型之后,在看在异常情况下apple是怎样处理的。
初始化内存不足
直接返回nil
使用期间内存不足
我们这里之说iOS6.0以上的情况,6.0之后viewDidUnload等被废弃,而且目前市面上6.0以下的机器也快成古董了。
当系统遭遇内存警告的时候,会调用VC的下述函数,在该函数内存,我们可以释放一些能够再次被创建的资源,比如维持的从网络或者数据库来的数据等等。 ~~~
(void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } ~~~
视图管理流程
先来看一张比较大的图,这是apple目前提供的和View控制相关的一些函数的摘录(UIViewController中的函数).而这也是一个调用的时序关系图。VC的view还有其子View的创建使用,都在这个流程之中。
创建根视图
当VC.view为空的时候,并且第一次调用vc.view的时候,会调用loadView函数来加载跟视图。
- (void) loadView { self.view = [UIView new]; }
在这个函数中你可以使用self.view = **来对根视图进行赋值,而且建议也是只在这里进行根视图的赋值操作。因为一旦根视图确定后,外部会对根视图进行一些布局了之类的操作,如果在使用过程中随意的更换根视图,上述的这些操作将很难重放。导致界面的一些异常。
初始化根视图上子视图
当调用了loadView加载了根视图之后,系统会触发VC的ViewDidLoad函数。这个使用self.view已经有值,可以在其上addSubView了。
在这里我们一般会做一些处理初始化子视图,并且addSubView之类的操作。注意布局的事情,就不要在这里做了,因为系统为我们提供了专门的函数来做这个事情。而且这个地方你拿到的self.view的frame信息是不准确的。比如刚才我们在loadView中没有对view进行布局初始化,给他设置一个frame。到了这个ViewDidLoad的地方的时候,你拿到的self.view.frame就是{0,0,0,0}。也就是说,你在这里进行布局的话,非常有可能是乱的。
- (void)viewDidLoad { [super viewDidLoad]; _subView = [DZView new]; _subView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:_subView]; }
布局
一般情况下,对于VC的根视图的操作是外部进行的,比如UINavigationController去push一个VC的时候,就会对vc.view.frame进行赋值,来控制VC的布局。而系统的这些试图控制器(导航了,之类的东西),都实现了CALayer的delegate,当vc的根视图的frame发生变化的时候会接受到通知
- layoutSublayersOfLayer:
系统的视图控制器会在这里面调用这两个函数来通知其当前的子VC去做布局的工作:
- viewWillLayoutSubviews - viewDidLayoutSubviews
而这个子VC一般是我们创建的。在这两个函数里面我们去做布局的操作。这两个函数一个是在view本身的布局做完之前调用,一个是之后。无论哪个函数,这里面渠道的根视图的frame或者bounds信息都是准确的。
而且,如果在这两个函数里面进行相对布局操作的话,将会让VC的根视图拥有适配不同屏幕的能力,同时当调整根视图的frame的时候,整个视图的布局也能够作出相应的变化。
显示流程
- viewWillAppear: – viewDidAppear: - viewWillDisappear: - viewDidDisappear:
从上述函数的字面意思理解:当视图被加载之后,要在window上显示出来,处于用户可见区域,或者离开用户可见区域的时候。系统将会调用VC相关函数来通知这种变化。
我们去看viewWillDisappear的文档:
This method is called in response to a view being removed from a view hierarchy. This method is called before the view is actually removed and before any animations are configured.
而上述显示流程能够被触发是依赖系统的这套机制的:
[vc willMoveToParentViewController:self]; [self addChildViewController:vc]; [self.view addSubview:vc.view]; vc.view.frame = self.view.bounds; [vc didMoveToParentViewController:self];
而现在系统集中默认的试图管理器UINavitionController,UITabBarController,还有present方式,都是可以保证会使用上述机制来触发响应的显示逻辑的。在这些函数里面,我们可以做一些和显示相关的业务逻辑了。
但是当你做业务逻辑的时候,一定要考虑这个函数在整个流程中的时序关系和他所代表的涵义。尤其是在每个视图管理器中的控制流程中,比如最开始提到的去获取self.navigationController为空的问题。
总结
关于ViewController的关键的流程,先谈内存和视图管理这两个。当然其还有其他的一些流程,要说完有点太复杂了。希望通过上述的两个例子,能够展示一下流程建模在理解框架和使用框架上的一些的裨益。能够使用这种思想来思考日常的编程问题。
欢迎关注iOS开发公共账号 iOS开发知识