UIViewController的基本概念與生命週期

原文地址:UIViewController的基本概念與生命週期

UIViewController是iOS頂層視圖的載體及控制器,用戶與程序界面的交互都是由UIViewController來控制的,UIViewController管理UIView的生命週期及資源的加載與釋放。

UIView與UIWindow共同展示了應用程序的用戶界面。可以將UIView理解成畫布,UIWindow理解成畫框。這兩個類的繼承關係是這樣的:

NSObject — UIResponder — UIView — UIWindow

iOS中,所有顯示在界面上的對象都是從UIResponder直接或間接繼承的,UIView和UIWindow也不例外。

可以將它們之間的關係想象成這樣一個場景:首先會有一個空的畫框(UIWindow),我們在畫框上放置一塊畫布(UIView),然後可以在這個畫布(UIView)上進行繪畫,畫布上可能會被畫上各種元素,例如UILabel、UIButton等。這些元素其實也是一個又一個UIView,它們會有一個層級關係管理,有點相當於Photoshop圖層的概念,層級高的元素會覆蓋住層級低的元素,從而導致層級低的元素被部分或完全遮擋。

UIWindow

雖然UIWindow繼承自UIView,但是在模型中,它是一個首席View。UIWindow的主要作用是提供一個區域來顯示UIView,然後將事件分發給UIView。一般情況下,應用程序只有一個UIWindow對象,即使有多個UIWindow對象,也只有一個UIWindow可以接受到用戶的觸屏事件。

當新建一個最原始的Empty Application工程後,會發現系統在application:didFinishLaunchingWithOptions:方法裏已經爲我們建好了一個UIWindow,代碼如下:

複製代碼
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
複製代碼

1. 創建一個全屏的window:

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

2. 給window設置背景色:

self.window.backgroundColor = [UIColor whiteColor];

3. 給將window設置爲KeyWindow並顯示:

[self.window makeKeyAndVisible];

假如這個時候通過調試器啓動這個程序,將會收到系統輸出的一個提示:

Application windows are expected to have a root view controller at the end of application launch

這個提示表示沒有指定根view controller,我們可以新建一個帶xib文件的ViewController類作爲window的rootViewController:

MyFirstViewController *myVC = [[MyFirstViewController alloc] init];
self.window.rootViewController = myVC;

P.s. 這裏有一點需要注意,無論你怎麼修改ViewController類實例的尺寸,都會強制鋪滿整個window,文檔裏是這樣解釋的:”If a view controller is owned by a window object, it acts as the window’s root view controller. The view controller’s root view is added as a subview of the window and resized to fill the window.

UIView

UIView有下面這些基礎概念:

  • UIViewController的view屬性擁有一個UIView。
  • UIView中可以包含多個UIView(subviews)。
  • UIView可以通過superview屬性訪問父UIView。
  • 如果是UIWindow的子元素,則可以通過Window屬性訪問UIWindow。

UIView中常用的結構體:

CGPoint point = CGPointMake(x, y); //座標
CGSize size = CGSizeMake(width, height); //大小
CGRect rect = CGRectMake(x, y, width, height); //位置和大小

UIView的常用屬性:

frame — 相對父視圖的位置和大小

bounds — 相對自身的位置和大小,所以bounds的x和y永遠爲0

center — 子視圖的中點座標相對父視圖的位置

transform — 可以通過這個屬性控制視圖的放大縮小和旋轉

superview — 獲取父視圖

subviews — 獲取所有子視圖

alpha — 視圖的透明度(0 - 1)

tag — 視圖的標誌,設置了tag後,可以通過viewWithTag方法獲取這個視圖

userInteractionEnabled — 是否相應用戶交互事件

通過transform屬性來對視圖進行縮放、旋轉和平移

複製代碼
//獲取當前transform
CGAffineTransform transform = self.lblSeg.transform;

//縮放
self.lblSeg.transform = CGAffineTransformMakeScale(.5, .5);

//在一個transform的基礎上再縮放
self.lblTest.transform = CGAffineTransformScale(transform, 0.5, 0.5);

//旋轉(弧度制)
self.lblSeg.transform = CGAffineTransformMakeRotation(M_2_PI);

//在一個transform的基礎上再旋轉
self.lblTest.transform = CGAffineTransformRotate(transform,M_2_PI);

//平移
self.lblSeg.transform = CGAffineTransformMakeTranslation(100, 100);

//在一個transform的基礎上再平移
self.lblTest.transform = CGAffineTransformTranslate(transform, 100, 100);
複製代碼

UIView的常用方法:

初始化視圖

- (id)initWithFrame:(CGRect)aRect

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(10.0, 20.0, 150.0, 100.0)];

 

將視圖從父視圖中移除

- (void)removeFromSuperview

[self.lblTest removeFromSuperview];

 

插入一個視圖到指定位置

- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index

複製代碼
NSInteger myIndex = 0;

if(self.myView1.subviews.count>0)
{
    myIndex = self.myView1.subviews.count;
}

[self.myView1 insertSubview:self.lblTest atIndex:myIndex];
複製代碼

 

將index1和index2位置的兩個視圖互換位置

- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2

NSInteger index1 = [self.view.subviews indexOfObject:self.lblTest1];

NSInteger index2 = [self.view.subviews indexOfObject:self.lblTest2];

[self.view exchangeSubviewAtIndex:index1 withSubviewAtIndex:index2];

 

添加子視圖

- (void)addSubview:(UIView *)view

[self.myView1 addSubview:self.lblTest];

 

插入視圖到指定位置

- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index

[self.view insertSubview:self.lblTest atIndex:0];

 

插入視圖到指定視圖的下面

- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview

[self.view insertSubview:self.lblTest1 belowSubview:self.lblTest2];

 

插入視圖到指定視圖的上面

- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview

[self.view insertSubview:self.lblTest1 aboveSubview:self.lblTest2];

 

將指定子視圖移到最頂層

- (void)bringSubviewToFront:(UIView *)view

[self.view bringSubviewToFront:self.lblTest];

 

將指定子視圖移到最底層

- (void)sendSubviewToBack:(UIView *)view

[self.view sendSubviewToBack:self.lblTest];

 

根據視圖的tag查找視圖

- (UIView *)viewWithTag:(NSInteger)tag

[self.lblTest setTag:5];
[self.view viewWithTag:5].alpha = 0;

 

取得視圖下的所有子視圖

@property(nonatomic, readonly, copy) NSArray *subviews

NSArray *arrView = [self.view subviews];

for (UIView *view1 in arrView)
{
    NSLog(@"%@",view1);
}

 

UIScreen

UIScreen類代表了屏幕,通過這個類我們可以獲取一些關於屏幕的內容:

返回帶有狀態欄的Rect

複製代碼
CGRect bounds = [UIScreen mainScreen].bounds;

NSLog(@"%.0f,%.0f,%.0f,%.0f",bounds.origin.x,
                     bounds.origin.y,
                     bounds.size.width,
                     bounds.size.height
      );
複製代碼

在4英寸設備的屏幕下測試,將打印出0,0,320,568

 

返回不帶狀態欄的Rect

複製代碼
CGRect bounds = [[UIScreen mainScreen] applicationFrame];

NSLog(@"%.0f,%.0f,%.0f,%.0f",bounds.origin.x,
                     bounds.origin.y,
                     bounds.size.width,
                     bounds.size.height
      );
複製代碼

在4英寸設備的屏幕下測試,將打印出0,20,320,548

無論是帶狀態欄還是不帶狀態欄獲得的Rect,都是相對於設備屏幕來說的,所以當返回不帶狀態欄的Rect時,y座標爲20(狀態欄的高度爲20),而高度減少了20,爲568-20=548。

下面這個方法可以獲得狀態欄(StatusBar)的位置和大小:

複製代碼
CGRect rect = [[UIApplication sharedApplication] statusBarFrame];

NSLog(@"%.0f,%.0f,%.0f,%.0f",rect.origin.x,
                     rect.origin.y,
                     rect.size.width,
                     rect.size.height
      );
複製代碼

在4英寸設備的屏幕下測試,將打印出0,0,320,20

 

生命週期

我們建立一個簡單的模型來測試生命週期:新建兩個ViewController,一個是主視圖控制器(main ViewController,以下簡稱mainVC),一個是副視圖控制器(sub ViewController,以下簡稱subVC),在mainVC裏點擊一個Button,以modal方式切換至subVC,然後在subVC裏點擊另一個Button關閉subVC並返回mainVC。我們將這兩個控制器的每個狀態都打印出來,各個階段的執行如下:

case 1. 第一次運行app:

main loadView

main viewDidLoad

main viewWillAppear

main viewDidAppear

 

case 2. 在mainVC裏點擊Button,以modal方式切換至subVC:

sub loadView

sub viewDidLoad

main viewWillDisappear

sub viewWillAppear

sub viewDidAppear

main viewDidDisappear

 

case 3. 在subVC裏點擊Button關閉subVC並返回mainVC

sub viewWillDisappear

main viewWillAppear

main viewDidAppear

sub viewDidDisappear

sub dealloc

 

當一個視圖控制器被創建,並在屏幕上顯示的時候代碼的執行順序:

step 1:alloc 創建對象,分配空間

step 2:init (initWithNibName) 初始化對象

step 3:loadView 從nib載入視圖 ,通常這一步不需要去幹涉。除非你沒有使用xib文件創建視圖

step 4:viewDidLoad 載入完成,可以進行自定義數據以及動態創建其他控件

step 5:viewWillAppear 視圖將出現在屏幕之前,馬上這個視圖就會被展現在屏幕上了

step 6:viewDidAppear 視圖已在屏幕上渲染完成

 

當一個視圖控制器被移除屏幕並且銷燬的時候的執行順序:

step 1:viewWillDisappear 視圖將被從屏幕上移除之前執行

step 2:viewDidDisappear 視圖已經被從屏幕上移除,用戶看不到這個視圖了

step 3:dealloc 視圖被銷燬

 

這裏需要說一下loadView與viewDidLoad的區別:當loadView時,還沒有view;而viewDidLoad時,view已經創建好了。詳細的加載循環:

step 1:程序請求ViewController的view屬性

step 2:如果view在內存中,則直接加載;如果不存在,則調用loadView方法

step 3:loadView方法執行如下方法:

  • 如果重載了這個方法,則必須創建必要的UIView並且將一個非nil值傳給ViewController的view屬性。
  • 如果沒有重載這個方法,ViewController會默認使用自己的nibName和nibBundle屬性嘗試從nib文件加載view。如果沒有找到nib文件,它嘗試尋找一個與ViewController類名匹配的nib文件。
  • 如果沒有可用的nib文件,那麼它創建一個空的UIView作爲它的view。

最後還要考慮一個重要的情況:內存不足警告。當程序收到內存警告的時候,會調用每一個ViewController的didReceiveMemoryWarning方法,我們需要做出相應,釋放程序中暫時不需要的資源;通常都會重寫該方法,但記得重寫的時候要調用super的該方法。

iOS3.0 - iOS6.0期間,didReceiveMemoryWarning方法會判斷當前ViewController的view是否顯示在window上,如果沒有顯示在window上,則didReceiveMemoryWarning會自動將ViewController的view以及其所有子view全部銷燬,然後調用View Controller的viewDidUnload方法。但是從iOS6.0開始,viewDidUnload和viewWillUnload這兩個方法已被廢除,收到low-memory時系統不會釋放view,而只是釋放controller的resource。

一種常見處理內存警告的方式:

複製代碼
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    
    float ver = [[[UIDevice currentDevice] systemVersion] floatValue];
    
    if(ver >= 6.0f)
    {
        if(self.isViewLoaded && !self.view.window)
        {
            self.view = nil; //確保下次重新加載
        }
    }
}
複製代碼

上面的代碼先取得當前iOS系統的版本號,如果是iOS6.0或以上版本,進一步判斷視圖是否被裝載進內存,並且是否爲當前視圖,在這兩個條件都滿足(已經裝載進內存&&不是當前視圖)時,將self.view設置爲nil,這樣就能保證再調用該ViewController時,loadView和viewDidLoad被再次調用。

我們在xcode調試器裏模擬內存警告,監控此時切換的狀態:

case 4. 當已切換至subVC,模擬內存警告,並返回mainVC,不處理didReceiveMemoryWarning。

Received memory warning.

main didReceiveMemoryWarning

sub didReceiveMemoryWarning

sub viewWillDisappear

main viewWillAppear

main viewDidAppear

sub viewDidDisappear

sub dealloc

 

case 5. 當已切換至subVC,模擬內存警告,並返回mainVC,處理didReceiveMemoryWarning。

Received memory warning.

main didReceiveMemoryWarning

sub didReceiveMemoryWarning

main loadView

main viewDidLoad

sub viewWillDisappear

main viewWillAppear

main viewDidAppear

sub viewDidDisappear

sub dealloc

 

可以很明顯的看出,當處理了didReceiveMemoryWarning後,重新執行了非當前視圖的loadView和viewDidLoad方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章