ViewController的关键流程

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

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

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

  1. 每一个OC的实例都有其本身的生命周期——创建、使用、销毁
  2. 而对于VC来讲在处理内存问题的时候,还有其特有的ViewDidLoad,等过程
  3. 在处理页面展示的时候,也有ViewWillAppear等过程

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

同时,我希望通过阐释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. } ~~~

视图管理流程

先来看一张比较大的图,这是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的关键的流程,先谈内存和视图管理这两个。当然其还有其他的一些流程,要说完有点太复杂了。希望通过上述的两个例子,能够展示一下流程建模在理解框架和使用框架上的一些的裨益。能够使用这种思想来思考日常的编程问题。

时间: 2024-08-03 21:42:43

ViewController的关键流程的相关文章

VIEWCONTROLLER的启动流程

ViewController的启动流程: init/initWithCoder -> awakeFromNib -> loadView -> viewDidLoad -> viewWillApear -> viewDidApear init 用代码创建的View或者ViewController使用这个初始方法 initWithCoder 使用IB创建的View或者ViewController使用这个初始方法 loadView loadView是ViewController中的

Laravel启动执行关键流程

一. laravel结构 |– app 包含Controller.Model.路由等在内的应用目录,大部分业务将在该目录下进行 | |– Console 命令行程序目录 | | |– Commands 包含了用于命令行执行的类,可在该目录下自定义类 | | |– Kernel.php 命令调用内核文件,包含commands变量(命令清单,自定义的命令需加入到这里)和schedule方法(用于任务调度,即定时任务) | |– Events 事件目录 | |– Exceptions 包含了自定义错误

Android 4.4 Kitkat Phone工作流程浅析(九)__状态通知流程分析

本文来自http://blog.csdn.net/yihongyuelan 转载请务必注明出处 本文代码以MTK平台Android 4.4为分析对象,与Google原生AOSP有些许差异,请读者知悉. 前置文章: <Android 4.4 Kitkat Phone工作流程浅析(一)__概要和学习计划> <Android 4.4 Kitkat Phone工作流程浅析(二)__UI结构分析> <Android 4.4 Kitkat Phone工作流程浅析(三)__MO(去电)流程

测试流程?项目管理流程?

背景 工作五年了,一直是做测试.认识了很多人大牛,也接触到很多新人,从他们身上看到了很多,自己的过去,自己的未来(当然很多是自己达不到的高度). 做这测试这一行的,很多人都追求技术:自动化+性能,往往忽略测试流程,或者说是项目管理流程. 想法 流程是要结合团队来看的,换句话来说就是case by case,没有标准,适合团队/业务的流程就是好流程: Part1 待过做中国移动项目的传统行业,测试流程一套一套的,需求评审 -- 开发详细设计评审 -- 用例评审 -- 提测评审 -- 测试执行 --

HDFS文件创建流程

文件夹的创建是一个相对简单的过程,主要是通过FileSystem中的mkdirs()方法,这个方法在DFSClient实例中调用同名方法mkdirs(),通过Hadoop本身的RPC机制调用Namenode的mkdirs()方法,最终这个调用PUSH到FSNameSystem的mkdirsInternal方法,这个方法主要就是检验访问权限,最后通过FSDirectory的unprotectedMkdir()方法,构建一个INodeDirectory实例添加到文件系统的目录树中. 文件节点的创建与

企业流程管理的作用

企业流程管理的作用 --摘自<公司开了,你该这样管理>作者:张国祥 流程管理的作用 阿基米德说:给我一个支点,我可以撬动地球!企业管理是不是也有支点呢?如果有支点,它应该是什么呢?支点一定是可以让人省事省力,即使没有四两拔千斤之效,也应该有事半功倍之能.结果,我们发现流程对于企业管理而言有快速增效之功,有让组织快速蜕变之力.流程就是企业管理的支点! 要改变珍珠的价值,就必须把一组珍珠串起来变成项链.项链中的单粒珍珠价值没有什么改变,但作为珍珠团队--项链的价值却比每一粒珍珠价值之和增值了很多,

3 种关键函数调用约定

高级语言翻译成机器码后,计算机没有办法知道函数调用的参数个数.类型,也没有硬件可以保护这些参数. 另外,在C++中,因为重载的原因,所以对函数的命名方式和普通C语言并不一致,该方式称为名字改编. 函数调用者与函数之间,尤其是跨语言调用接口时,需要一个协议约定来传递参数——栈. 关键流程: 调用时,调用者依次把参数压栈,然后调用函数, 被调用函数,在堆栈中取得数据,并进行计算. 函数计算结束以后,或者调用者.或者函数本身修改堆栈,使堆栈恢复原装. 常见的函数调用约定: stdcall cdecl

什么是真正的流程管理?流程管理的是与不是?

1.什么是流程? 1.1 什么是流程? 通俗来讲,流程就是我们做事情的过程,流程是为了完成某一目标而进行的一系列相关的活动.流程客观存在于我们日常工作和生活中,不管我们是否意识到其存在.例如,工作中的流程:销售流程.设计流程.采购流程.维修流程.会议流程.决策流程.生活中的流程:我们每天从家出发到公司上班,到银行办理存取款.我们大部分业务是由一个个流程来完成的. 流程的正式定义是:流程是为了完成某一目标而进行的一系列逻辑相关的活动:一般是指重复进行的活动,接受各种投入要素,通过流程的各项活动产生

制造行业流程管理的“IPO”思维

流程管理是企业从流程角度出发,关注流程是否增值的一套管理体系.从认识流程.到建立流程.到管理流程.再到优化流程,企业管理人员要去除不增值和低价值的流程,减少员工犯错误的机会,建立一套卓越的流程体系. 我们或多或少都经历过或听说过这样的实例:某客户想查询自己公司还有多少货款余额,于是打电话给了某公司客户服务部,客户服务部说:“抱歉,您打错部门了,我帮您转到会计部.”客服给他转到了会计部,会计部说:“对不起,我帮您转到仓管部”.会计部给他转到了仓管部,仓管部又说:“抱歉,我帮不上忙,找人事部看看”.