iOS開發筆記(九):UIViewController的生命週期

卡布達巨人發表於2019-03-03

經常會用到 ViewController,但是對它的生命週期一直沒有一個比較完整地理解,最近看了幾篇部落格,在這裡對 ViewConroller 的生命週期做一個總結,一是為了自己學習,二是為了給大家一個參考,如有錯誤,歡迎指正。

ViewController 總體生命週期:

ViewController 總體生命週期

1.ViewController 多種例項化方法

1.1 程式碼

  • 通過 xib 載入

    先看一下 Demo 的檔案結構,ViewController 為 A 控制器,TestViewController 為 B 控制器。

    Demo檔案結構

    當控制器 view 通過 xib 載入的時候,可能會出現三種情況:

    • 指定xib名稱(OtherViewController.xib)

        TestViewController *testVC = [[TestViewController alloc] initWithNibName:@"OtherViewController" bundle:nil];
      複製程式碼

      當我們指定了xib的名稱,loadView方 法就會去載入對應的 xib (OtherViewController.xib),最終是這個樣子的。

      指定xib名稱

    • 不指定 xib 名稱1

        TestViewController *testVC = [[TestViewController alloc] initWithNibName:nil bundle:nil];
      複製程式碼

      如果我們不指定 xib 名稱,loadView 就會載入與控制器同名的 xib (TestViewController.xib),最終是這個樣子的。

      不指定xib名稱1

    • 不指定 xib 名稱2

      我們先將 TestViewController.xib 這個檔案刪除掉,這個時候,我們再來執行程式,結果是這樣的。

      不指定xib名稱2

      根據上圖我們可以得知,當沒有指定 xib 名稱,且沒有與控制器同名的 xib 時,會載入字首與控制器名相同而不帶 Controller 的 xib (TestView.xib)。

  • init

我們經常會用程式碼通過 init 手動建立一個 ViewController,如下:

TestViewController *testVC = [[TestViewController alloc] init];
複製程式碼

其實本質還是呼叫了 initWithNibName:bundle: 並且都傳入了 nil,只不過以上三種情況都沒有滿足,最終是這個樣子的。

init

1.2 storyboard 間接例項化(initWithCoder)

當你從 storyboard 初始化 ViewController 時,iOS 會使用 initWithCoder,而不是 initWithNibName 來初始化這個 ViewController,然後那個 storyboard 會在自己內部生成一個 nib (storyboard 例項化 view / ViewController 時,會把 nib 的資訊放在 Coder 中,呼叫 initWithCoder)。

注意

storyboard 載入的是控制器及控制器 view,而 xib 載入的僅僅只是控制器的 view。之所以這麼說,我們結合控制器的 awakeFromNib 方法解釋一下,顧名思義,當控制器從 nib 載入的時候就會呼叫這個方法,這個方法本身只是個訊號、訊息,是一個空方法 (即其預設實現為空)。

先來看看通過 storyboard 載入的情況:

//A控制器中程式碼
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"TestViewController" bundle:nil];
	TestViewController *testVC = [storyboard instantiateInitialViewController];
	[self.navigationController pushViewController:testVC animated:YES];
}
//B控制器中程式碼
- (void)awakeFromNib {
	NSLog(@"B通過nib載入");
}
複製程式碼

通過storyboard載入

呼叫了 B 控制器的 awakeFromNib 方法。

將之前刪除的 TestViewController.xib 檔案重寫新增進去,再來看通過xib載入的情況:

//A控制器中程式碼改為如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	TestViewController *testVC =[[TestViewController alloc] init];
	[self.navigationController pushViewController:testVC animated:YES];
}
//B控制器中程式碼不變
複製程式碼

通過xib載入

B 控制器的 awakeFromNib 方法並沒有被呼叫。

所以,storyboard 載入的是控制器及控制器 view,而 xib 載入的僅僅只是控制器的 view。

2. loadView

這個方法中,要正式載入View了。首先我們得知道,控制器 view 是通過懶載入的方式進行載入的,即用到的時候再載入。永遠不要主動呼叫這個方法。當我們用到控制器 view 時,就會呼叫控制器 view 的 get 方法,在 get 方法內部,首先判斷 view 是否已經建立,如果已存在,則直接返回存在的 view,如果不存在,則呼叫控制器的 loadView 方法,在控制器沒有被銷燬的情況下,loadView 也可能會被執行多次。

當 ViewController 有以下情況時都會在此方法中從 nib 檔案載入 view :

  • ViewController 是從 storyboard 中例項化的。
  • 通過 initWithNibName:bundle: 初始化。
  • 在 App Bundle 中有一個 nib 檔名稱和本類名相同。

符合以上三點時,也就不需要重寫這個方法,否則你無法得到你想要的 nib 中的 view。

如果這個 ViewController 與 nib 無關,你可以在這裡手寫 ViewController 的 view (這一步大概也可以在 viewDidLoad 裡寫,實際上我們也更常在 viewDidLoad 裡寫)。

是否需要呼叫 [super loadView]

loadView 方法的預設實現是這樣:先尋找有關可用的 nib 檔案的資訊,根據這個資訊來載入 nib 檔案,如果沒有有關 nib 檔案的資訊,預設實現會建立一個空白的 UIView 物件,然後讓這個物件成為 controller 的主 view。

所以,重寫這個函式時,你也應該這麼做。並把子類的 view 賦給 view 屬性 (property) (你 create 的 view 必須是唯一的例項,並且不被其他任何 controller 共享)。

如果你要進行進一步初始化你的 views,你應該在 viewDidLoad 函式中去做。在iOS 3.0 以及更高版本中,你應該過載 viewDidUnload 函式來釋放任何對 view 的引用或者它裡面的內容(子 view 等等)。

回到關於 [super loadView] 的討論中,如果我們的 ViewController 與 nib 有關,也就是說我們不需要重寫 loadView 方法,也就不用關心 [super loadView]。而如果與 nib 無關,我們需要重寫 loadView 方法,而 [super loadView] 根據上面的解釋就會生成一個空白的 view,這恐怕並不能滿足我們的需求,所以呼叫也沒有多大意義。

2.1 ViewController 載入 View 過程

ViewController 載入 View 過程

從圖中可以看到,在 view 載入過程中首先會呼叫 loadView 方法,在這個方法中主要完成一些關鍵 view 的初始化工作,比如 UINavigationViewController 和 UITabBarController 等容器類的 ViewController;接下來就是載入 view,載入成功後,會接著呼叫 viewDidLoad 方法,這裡要記住的一點是,在 loadView 之前,是沒有 view 的,也就是說,在這之前,view 還沒有被初始化。完成 viewDidLoad 方法後,ViewController 裡面就成功的載入 view了,如上圖右下角所示。

死迴圈

若 loadView 沒有載入 view,即為 nil,viewDidLoad 會一直呼叫 loadView 載入 view,因此構成了死迴圈,程式即卡死,所以我們常在 ViewDidLoad 裡建立 view。

2.2 ViewController 解除安裝 View 過程

ViewController 解除安裝 View 過程

從圖中可以看到,當系統發出記憶體警告時,會呼叫 didReceiveMemoeryWarning 方法,如果當前有能被釋放的 view,系統會呼叫 viewWillUnload 方法來釋放 view,完成後呼叫 viewDidUnload方法,至此,view 就被解除安裝了。此時原本指向 view 的變數要被置為 nil,具體操作是在 viewDidUnload 方法中呼叫 self.myButton = nil。

3.viewDidLoad

當控制器的 loadView 方法執行完畢,view 被建立成功後,就會執行 viewDidLoad 方法,該方法與loadView 方法一樣,也有可能被執行多次。在開發中,我們可能從未遇到過執行多次的情況,那什麼時候會執行多次呢?

比如 A 控制器 push 出 B 控制器,此時,視窗顯示的是 B 控制器的 view,此時如果收到記憶體警告,我們一般會將 A 控制器中沒用的變數及 view 銷燬掉,之後當我們從 B 控制器 pop 到 A 控制器時,就會再次執行A控制器的 loadView 方法與 viewDidLoad 方法。

4.viewWillAppear && viewDidAppear

4.1 viewWillAppear

viewWillAppear 總是在 viewDidLoad 之後被呼叫,但不是立即,當你只是引用了屬性 view,卻沒有立即把 view 新增到任何已經展示的檢視上時,viewWillAppear 不會被呼叫,這在 view 被外部引用時,就會發生。當然,隨著 ViewController 的多次推入,多次進入子頁面後返回,該方法會被多次呼叫。與 viewDidLoad 不同,呼叫該方法就說明控制器一定會顯示。

鎖屏之後會被呼叫嗎?

不會。viewWillAppear 關注的是 view 在層次中的顯示與消失,鎖屏並沒有改變 App 本身的層次。

Window疊加後,會被呼叫嗎?

不會。同鎖屏時的原因類似,疊加 Window 並沒有改變 ViewController 所在 Window 的檢視層次,換句話說,view 並沒有被覆蓋或刪除 (相對於自己所在 Window)。

注意

如果控制器 A 被展示在另一個控制器 B 的 popover 中,那麼控制器 B 不會呼叫該方法,直到控制器 A 清除。

4.2 viewDidAppear

檢視已在螢幕上渲染完成。子檢視有自定義動畫時,建議在 Did 方法中啟動,在 Will 中啟動動畫時,動畫效果將不會很理想。

5.viewWillAppear 與 viewDidAppear 之間發生了什麼

以下兩個方法將會被呼叫:

- viewWillLayoutSubviews
- viewDidLayoutSubviews
複製程式碼
  • viewWillLayoutSubviews

    該方法在通知控制器將要佈局 view 的子控制元件時呼叫。每當檢視的 bounds 改變,view 將調整其子控制元件位置。預設實現為空,可重寫以在 view 佈局子控制元件前做出改變。該方法呼叫時,AutoLayout 未起作用。

  • viewDidLayoutSubviews

    該方法在通知控制器已經佈局 view 的子控制元件時呼叫。預設實現為空,可重寫以在 view 佈局子控制元件後做出改變。該方法呼叫時,AutoLayout 未起作用。

注意

使用 Autolayout 時,子檢視大小隻有在 viewDidLayoutSubviews 才真正被設定好,所以這裡才是獲取子檢視大小的正確位置,常見的錯誤是,在 viewDidLoad 中讀取了某個 view.frame,用來給其它子檢視賦值,結果得到一堆大小“不定”的檢視,甚至可能為零,在檢視中看不見!

6.viewWillDisappear && viewDidDisappear

  • viewWillDisappear

    該方法在控制器 view 將要從檢視層次移除時被呼叫,可重寫以提交變更,取消檢視第一響應者狀態。

  • viewDidDisappear

    該方法在控制器 view 已經從檢視層次移除時被呼叫,可重寫以清除或隱藏控制元件。

兩者配套呼叫,具體指子檢視控制器是以 push 和 present 方法顯示的,父檢視控制器的以上兩個方法會被觸發。

特別的,addSubview 會呼叫子控制器 Appear 系列方法,但不會呼叫父檢視 viewWillDisappear 方法。

如下新增子檢視:

XSDViewController *subVC = [[XSDViewController alloc] init];
[self addChildViewController:subVC];
[subVC.view setFrame:self.view.frame];
[self.view addSubview:subVC.view];
[subVC didMoveToParentViewController:self];
複製程式碼

得到結果是,只有 XSDViewController 的 Appear 系列方法被呼叫,這樣的呼叫與 push / present 方法根本不同是父檢視的 view 沒有“隱藏”,只是被覆蓋了。

7.didReceiveMemoryWarning && viewDidUnload (iOS6廢除)

當系統記憶體不足時,首先 ViewController 的 didReceiveMemoryWarining 方法會被呼叫,而 didReceiveMemoryWarining 會判斷當前 ViewController 的 view 是否顯示在 window 上,如果沒有顯示在 window 上,則 didReceiveMemoryWarining 會自動將 ViewController 的 view 以及其所有子 view 全部銷燬,然後呼叫 viewcontroller 的 viewdidunload 方法。如果當前 ViewController 的 view 顯示在 window 上,則不銷燬該 ViewController 的 view,當然,viewDidunload 也不會被呼叫了。

iOS 升級到 6.0 以後,不再支援 viewDidUnload 了。官方文件的解釋是系統會自動控制大的 view 所佔用的記憶體,其他小的 view 所佔用的記憶體是極其微小的,不值得為了省記憶體而去清理然後在重新建立。如果你需要在記憶體警告的時候釋放業務資料或者做些其他的特定處理,你可以實現 didReceiveMemoryWarning 這個函式。

iOS 6.0 及以上版本的記憶體警告處理方法:

-(void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning];//即使沒有顯示在window上,也不會自動的將self.view釋放。
 	// Dispose of any resources that can be recreated.
	// 此處做相容處理需要加上ios6.0的巨集開關,保證是在6.0下使用的,6.0以前遮蔽以下程式碼,否則會在下面使用self.view時自動載入viewDidUnLoad
	if ([[UIDevice currentDevice].systemVersion floatValue] >= 6.0) {
		//需要注意的是self.isViewLoaded是必不可少的,其他方式訪問檢視會導致它載入 ,在WWDC視訊也忽視這一點。
		if (self.isViewLoaded && !self.view.window) {// 是否是正在使用的檢視
			//code
			self.view = nil;// 目的是再次進入時能夠重新載入呼叫viewDidLoad函式。
		}
	}
}
複製程式碼

8.dealloc

當發出記憶體警告呼叫 viewDidUnload 方法時,只是釋放了 view,並沒有釋放 ViewController,所以並不會呼叫 dealloc 方法。即 viewDidUnload 和 dealloc 方法並沒有任何關係,dealloc 方法只會在 ViewController 被釋放的時候呼叫。

9.其他相關方法

awakeFromNib

當 .nib 檔案被載入的時候,會傳送一個 awakeFromNib 的訊息到 .nib 檔案中的每個物件,每個物件都可以定義自己的 awakeFromNib 方法來響應這個訊息,執行一些必要的操作。也就是說通過 nib 檔案建立 view 物件時執行 awakeFromNib。

看完文件繼續補充。

10.多個 ViewControllers 跳轉時的生命週期

10.1 Push / Present

當我們點選 push 的時候首先會載入下一個介面然後才會呼叫介面的消失方法。

  • init:ViewController2
  • loadView:ViewController2
  • viewDidLoad:ViewController2
  • viewWillDisappear:ViewController1 將要消失
  • viewWillAppear:ViewController2 將要出現
  • viewWillLayoutSubviews ViewController2
  • viewDidLayoutSubviews ViewController2
  • viewWillLayoutSubviews:ViewController1
  • viewDidLayoutSubviews:ViewController1
  • viewDidDisappear:ViewController1 完全消失
  • viewDidAppear:ViewController2 完全出現

當在一個控制器內 Push / Present 新的控制器,原先的控制器並不會銷燬,但會消失,因此呼叫了 viewWillDisappear 和 viewDidDisappear 方法。

10.2 Pop / Dismiss

如果控制器 A 被展示在另一個控制器 B 的 popover 中,那麼控制器 B 不會呼叫 viewWillAppear 方法,直到控制器 A 清除。這時,控制器 B 會再一次出現,因此呼叫了其中的 viewWillAppear 和 viewDidAppear 方法。

11.參考

相關文章