本文是投稿文章(點擊查看原文)
在最近解決某個問題的時候,發現在ViewDidDisappear中去獲取self.navigationController為空。猛然間意識到,原來在VC的生命周期中存在一些細節問題需要注意。而且,最近一段時間,對基於流程(生命周期是特殊的流程)建模的編程思想也開始有些反思。所以就總結了一下VC生命周期的一些問題。
先說點比較抽象的東西,關於流程建模的。對於同一個對象而言,往往在不同的業務場景中其有不一樣的流程。換句話說,對於一個對象而言其可能出在多個流程中。比如我們拿一個VC來說:
而在一個流程當中,每一個過程(一般會以函數表示)都有其特殊的職責。比如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的變量。而且細心的讀者可能發現,我們用於初始化這個變量的值還不是外部傳進來的,而是內部新生成的。這種方式我們稱之為內部初始化。自然也會有外部初始化。
而在實際的初始化場景中我們經常會發現這樣的情況:在進行類的設計的時候,遇到傳值的問題的時候,比如下述問題,我們通過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是怎樣處理的。
我們這裡之說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的時候,整個視圖的布局也能夠作出相應的變化。
- (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; }
0
從上述函數的字面意思理解:當視圖被加載之後,要在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.
而上述顯示流程能夠被觸發是依賴系統的這套機制的:
- (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; }
1
而現在系統集中默認的試圖管理器UINavitionController,UITabBarController,還有present方式,都是可以保證會使用上述機制來觸發響應的顯示邏輯的。在這些函數裡面,我們可以做一些和顯示相關的業務邏輯了。
但是當你做業務邏輯的時候,一定要考慮這個函數在整個流程中的時序關系和他所代表的涵義。尤其是在每個視圖管理器中的控制流程中,比如最開始提到的去獲取self.navigationController為空的問題。
總結
關於ViewController的關鍵的流程,先談內存和視圖管理這兩個。當然其還有其他的一些流程,要說完有點太復雜了。希望通過上述的兩個例子,能夠展示一下流程建模在理解框架和使用框架上的一些的裨益。能夠使用這種思想來思考日常的編程問題。
歡迎關注iOS開發公共賬號 iOS開發知識