通过ViewController的关键流程来理解流程建模

在最近解决某个问题的时候,发现在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的使用我们会使用到函数:

- (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的变量。而且细心的读者可能发现,我们用于初始化这个变量的值还不是外部传进来的,而是内部新生成的。这种方式我们称之为内部初始化。自然也会有外部初始化。

  1. 内部初始化:变量的值在内部生成
  2. 外部初始化:用于初始化成员变量的值是在外部生成,然后传给

而在实际的初始化场景中我们经常会发现这样的情况:在进行类的设计的时候,遇到传值的问题的时候,比如下述问题,我们通过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对象都有的

(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函数来加载跟视图。

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

时间: 2024-09-12 23:24:12

通过ViewController的关键流程来理解流程建模的相关文章

java-设计一个 流程执行器,流程需要任意步数,如果中间有任意要出错的,都要回滚?,如何理解设计?

问题描述 设计一个 流程执行器,流程需要任意步数,如果中间有任意要出错的,都要回滚?,如何理解设计? 面试java研发工程师时,面试馆提出来的,需要编码实现!我当时的理解是:1.按照题目中说的流程执行器是,我理解是一个抽象出来,通用的执行器,那么就需要对流程一开始方法的入参还有流程中存在调用的全局变量都需要回退,这样的理解我一时想不出来解决方案,2.如果是工作流模型的我认为建立节点,节点里是 操作对象(也就是方法),数据对象,状态,然后就想使用链表(工作流程)中存储一个操作节点,上一节点操作和下

基于开发流程的软件测试流程管理

0.引言 随着软件行业的发展.软件产品已经影响到我们社会的诸多领域,人们对软件作用的期望值也越来越高,对软件质量重要性的认识也逐渐增强. 然而,软件缺陷(bug)是伴随软件产品开发过程而产生的敷衍品,采用新的技术和方法,也不能完全消灭软件缺陷.因此,在软件开发过程中尽早地引入软件测试技术来保证软件质量,降低软件缺陷率,已经得到软件业的认可.软件开发过程中的每-个阶段都会有相应的文档和产品产生,对这些文档和产品进行严格评审和测试,可以尽早发现问题.及时找出与需求分析和项目计划中的不符合项.对软件的

公文转发流程自定义的数据建模

数据 开发比较复杂的企业多用户管理信息系统(MIS),不可能不涉及到系统内多个用户之间的数据文件的流转.审批等功能的开发.由于企业的需求总是随着时间推移不断发生变化,加之各个企业内部所设置的办公流程不尽相同,一套通用性比较好的管理信息系统应该能让系统管理员自己定义公文转发的流程. 尽管笔者没有机会在已参与开发了的MIS中实现出文件转发流程自定义的功能,但是,早在2002年初就曾深入思考过这方面的设计.当时由于某些原因不能公开自己的设计思路,现在市面上已经有不少MIS产品提供这样的功能,笔者又已离

B2C网站设计:用户注册登录流程和购买流程设计

网络购物网站根据买家和卖家的不同形态,主要分为三类商业模式:B2B  B2C  C2C ,这三种商业模式在传统的线下也有相对应的典型形态:  B2B 对应专业的批发市场  B2C 对应购物超市  C2C对应个体摊位组成的市场.所以说网络购物网站并不是完全意义上模式创新,它只是利用了新的媒介和新的平台来承载卖家和买家的商业活动,因此其网络购物购物流程和传统的是一样的,还是有借鉴意义的. 我们可以画出下面这个用户网络购物的基本流程图 这几个步骤是用户网络购物的基础环节,每个网站根据不同的需要增加相应

link环境下制作一款《订餐软件》,复单走流程的同时允许不走流程,不走流程的怎么并单?

问题描述 link环境下制作一款<订餐软件>,复单走流程的同时允许不走流程,不走流程的怎么并单? link环境下制作一款<订餐软件>,复单走流程的同时允许不走流程,不走流程的怎么并单? 解决方案 无非就是你的程序中开两个数据库连接,然后执行相同的两次插入动作.

互联网企业安全高级指南3.10 流程与“反流程”

3.10 流程与"反流程" 1. 人的问题 在传统安全领域一直是强调流程的,但是互联网行业有一点反流程,甚至像Facebook这样的公司还表示除非万不得已否则不会新建一条流程来解决问题.那安全建设到底要不要流程.首先有流程肯定能解决问题,但流程化是不是最佳实践则不确定. 于是先解决第一个问题:有没有可能没有流程,什么情况下可能很少或接近于没有流程.假如公司的人很少,从工位上站起来就能看到全公司的人,要发布版本吼一嗓子全员都能听到,这种情况下确实不需要什么流程,不只是安全流程,其他的流程

php+ajax发起流程和审核流程(以请假为例)

上一篇随笔中已经提到如何新建流程,那么现在我们就来看一下如何发起一个流程和审核流程~~~ 先说一下思路: (1)登录用session获取到用户的id (2) 用户发起一个流程 注意:需要写申请事由 (3)处于节点的审核人去依次审核 注意:每审核通过一个,对应towhere字段要加1: 审核到最后时,对应的isok字段要变为1(此处1表示结束,0表示未结束) 共用到三张表: 第一步:先做一个简单的登录页面,用session获取用户名: denglu.php页面 <!DOCTYPE html> &

分享用户研究工作流程:用户理解方法和经验

文章描述:如何了解我们的用户:无线终端用户理解工作方法分享. 一. 前言 来到腾讯以后,做过iPhone阅读器.iPad阅读器.iPad音乐播放器的用户理解,目前正在做iPhone QZone的用户理解工作.做得次数多了,方法在不断改进,也积累了一些经验,与大家一起分享,希望帮助我们更了解用户. 首先介绍一下目前北分wsd(无线研发部用户体验组)用户研究工作流程:我们的用研需求来自项目组和用研组需求池,为控制节奏和质量,对承接项目组需求的工作流程进行严格把控,我们针对项目组需求,制定了用研工作流

A到A+的关键 CMMI协助改善流程 达成目标

能力成熟度整合模型(Capability Maturity Model Integration:CMMI)是一种包含产品和服务之发展与维护的最佳执行方法,可以让企业针对不同的营运目标,进行流程及组织的改造,使企业获得更高的效益及更稳定的产品或服务品质. 业内专家表示,每个企业或部门都有不同的营运目标,包括成本的降低.获利的提升.产品上市时程的掌控一直到品质的控制等等,而CMMI可以协助企业更明确的达成这些目标.「简单的说,CMMI可以让企业从A变成A+」. 而对系统委外合作双方来说,CMMI代表