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的關鍵的流程,先談內存和視圖管理這兩個。固然其還有其餘的一些流程,要說完有點太複雜了。但願經過上述的兩個例子,可以展現一下流程建模在理解框架和使用框架上的一些的裨益。可以使用這種思想來思考平常的編程問題。

相關文章
相關標籤/搜索