ViewController的关键流程

本文转载自:http://www.cocoachina.com/ios/20151216/14705.html.若有侵权请联系.html

在最近解决某个问题的时候,发如今ViewDidDisappear中去获取self.navigationController为空。猛然间意识到,原来在VC的生命周期中存在一些细节问题须要注意。并且,最近一段时间,对基于流程(生命周期是特殊的流程)建模的编程思想也开始有些反思。因此就总结了一下VC生命周期的一些问题。ios

先说点比较抽象的东西,关于流程建模的。对于同一个对象而言,每每在不一样的业务场景中其有不同的流程。换句话说,对于一个对象而言其可能出在多个流程中。好比咱们拿一个VC来讲:数据库

  1. 每个OC的实例都有其自己的生命周期——建立、使用、销毁编程

  2. 而对于VC来说在处理内存问题的时候,还有其特有的ViewDidLoad,等过程设计模式

  3. 在处理页面展现的时候,也有ViewWillAppear等过程数组

  4. 网络

而在一个流程当中,每个过程(通常会以函数表示)都有其特殊的职责。好比alloc用于非配内存,init用于初始化内存。而咱们在这些函数中作的事情,也必须尽量的和该函数的职责所匹配。一个被设计好的流程(一般会以一组函数的形式呈现),就像是一个插排。上面的每一个插口都有本身适配的类型,若是你乱插,可能会有烧掉保险丝的危险。好比你在alloc中硬要作dealloc的事情。从设计模式的角度来讲,这种思想叫作『控制反转』,是设计框架的时候经常使用的技巧,经过约束使用者的使用方式,来完成功能。而咱们在使用UIKit等框架的时候,咱们做为使用方,天然要接受这种『控制反转』。且可以在正确的地方作正确的事情。一句话说就是:恰如其分。app

同时,我但愿经过阐释VC的一些生命流程和其使用细节的事情。也能激发读者对于基于流程建模的编程思想的反思。经过这种思想去反思在平常编程中,其余库中一些流程的使用。甚至是在本身进行程序设计的时候,可以也注意使用一下这种方式。框架

好了下面咱们就开始看看一个VC都有哪些流程须要注意的.btw,穷举全部的流程是一个费时费力的事情,因此会只摘几个比较关键的流程来描述和讲解。最重要的目的仍是在于可以启发各位用流程建模这个视角来思考编程的一些问题:),偷懒了。编程语言

内存使用流程

VC的实例在内存使用上面,打的流程和其余对象实例的使用相似,都要通过下述的一些过程:

建立->初始化->使用->销毁

后面的阐述也是相似,咱们先说流程。而后再具体到函数的使用。由于咱们在使用一个库或者框架的时候,首先要关注的是他的模型。尤为是流程模型。而具体的函数每每是在该模型基础上,实践下来的产物。

(1)建立

苹果在内存处理上使用的是两段式构造的思想:将建立和初始化分两步走

建立的核心关注点在于内存分配。从堆栈上批出一块内存给对象使用。至于该对象,如何使用该内存(初始化)则是另外的函数的事情。通过建立和初始化两步以后,才可以给出一个干净可使用的对象实例。

在建立的时候,通常涉及到的函数为: ~~~ + (instancetype)alloc + (instancetype)allocWithZone:(struct _NSZone *)zone ~~~ 这两个函数为系统函数,咱们不能重载该函数。这点是苹果在文档中格外强调的。于是,对于建立咱们也只是调用一下系统函数的事情,没有太多自定义的工做须要咱们去作。

(2)初始化 (RAII)

初始化是两段式构造的第二步,对象实例只有通过该步骤以后,才是一个干净可使用的对象。这种思想在不少编程语言中咱们能够看到,好比C++。固然也有不少一段式构造的例子好比C语言。

而在OC中,初始化使咱们进行对象自定义操做的开始。这里咱们须要初始化一些当前类特有的属性的值,以保证后续业务逻辑可以够正常。好比当咱们从xib文件中加载VC的使用咱们会使用到函数:

1
- (instancetype _Nonnull)initWithNibName:(NSString * _Nullable)nibName bundle:(NSBundle * _Nullable)nibBundle

该函数将会经过传入的xib文件名和bundle来加载界面,而且初始化相关的数据。固然这是系统的函数。而咱们更关注的是咱们在这里应该作什么和能够作什么。

说句废话:要作对象实例的初始化。主要是变量的赋值操做。

For Exmaple:

1
2
3
4
5
6
7
8
9
10
11
- (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变量。

1
2
3
@interface B : UIViewController
@property (strong) NSString* name;
@end

而后使用的时候这个样子:

1
2
3
B* vc = [B  new ];
vc.name = @ "xx" ;
[self.navigationController push:vc];

这种作法,封装性不好,任何持有VC2实例的地方都可以修改这个name值,致使一些很奇怪的逻辑。并且每每是那种不可预期的变更。一旦出现bug查找起来极其困难。

其实这种状况应当属于外部初始化的典型应用。更好的方式就是咱们就把name当成对象初始化必须的一个变量,须要对其进行初始化,那么就应当提供相应的函数来进行初始化。这样能够保持比较好的封装性。

建议之后采起这样的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// .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等概念不是很清晰,从初始化这个事情上可见一斑。然,这些概念对于程序的健壮性又是多么的相当重要。仍是应该拾起来的。

经常使用的函数

1
2
3
- init;
- (instancetype _Nonnull)initWithNibName:(NSString * _Nullable)nibName bundle:(NSBundle * _Nullable)nibBundle
- (instancetype _Nonnull) initWith****

其中init函数为全部OC对象都有的。

(3)使用

关于使用这个实际上是最重要的部分,而对象一旦建立并初始化完成以后,就能够嵌入到除了内存使用流程以外的流程之中。而在内存流程中咱们所谓的使用,就是在其余流程中,对该内存对象进行的一系列的操做,包括且不止于:增删改查。

对于使用的细节,可参考其余流程的介绍。

(4)销毁

对象在完成使命以后,天然要被销毁,来释放其持有的资源。所谓有借有还再借不难,在建立过程当中占用的内存,在初始化过程当中持有的其余系统资源,在这个时候要作统一的释放。并且这是最后的释放时机,否则这个对象就成了小偷,会永久性的把资源偷走,好比在传统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等函数)。

(5)异常

这个没有罗列在最初的那么内存流程模型当中,由于这样的,在建模的时候,首先要作的是让整个模型Work起来,然后再去处理各类边界问题。若是一上来就把精力集中在边界问题的处理上,就会无限制的放大问题的复杂度,增长处理的麻烦。

而咱们在看了基础的内存使用流程模型以后,在看在异常状况下apple是怎样处理的。

  • 初始化内存不足

  • 直接返回nil

  • 使用期间内存不足

咱们这里之说iOS6.0以上的状况,6.0以后viewDidUnload等被废弃,并且目前市面上6.0如下的机器也快成古董了。

当系统遭遇内存警告的时候,会调用VC的下述函数,在该函数内存,咱们能够释放一些可以再次被建立的资源,好比维持的从网络或者数据库来的数据等等。 ~~~

(void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } ~~~

视图管理流程

7df22103jw1ez0b3hn5jmj207o0egwf8.jpg

 

 

先来看一张比较大的图,这是apple目前提供的和View控制相关的一些函数的摘录(UIViewController中的函数).而这也是一个调用的时序关系图。VC的view还有其子View的建立使用,都在这个流程之中。

流程解释

  • 建立根视图

当VC.view为空的时候,而且第一次调用vc.view的时候,会调用loadView函数来加载跟视图。

1
2
3
4
- (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}。也就是说,你在这里进行布局的话,很是有多是乱的。

1
2
3
4
5
6
- (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发生变化的时候会接受到通知

1
- layoutSublayersOfLayer:

系统的视图控制器会在这里面调用这两个函数来通知其当前的子VC去作布局的工做:

1
2
- viewWillLayoutSubviews
- viewDidLayoutSubviews

而这个子VC通常是咱们建立的。在这两个函数里面咱们去作布局的操做。这两个函数一个是在view自己的布局作完以前调用,一个是以后。不管哪一个函数,这里面渠道的根视图的frame或者bounds信息都是准确的。

并且,若是在这两个函数里面进行相对布局操做的话,将会让VC的根视图拥有适配不一样屏幕的能力,同时当调整根视图的frame的时候,整个视图的布局也可以做出相应的变化。

  • 显示流程

1
2
3
4
- 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.

而上述显示流程可以被触发是依赖系统的这套机制的:

1
2
3
4
5
     [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的关键的流程,先谈内存和视图管理这两个。固然其还有其余的一些流程,要说完有点太复杂了。但愿经过上述的两个例子,可以展现一下流程建模在理解框架和使用框架上的一些的裨益。可以使用这种思想来思考平常的编程问题。

相关文章
相关标签/搜索