2010年12月8日 星期三

認識 iPhone 程式架構

要從一個main function 裡的程式進而演化成一個iPhone的程式是有相當難度的一件事,那怕只是一些很基本的功能,要處理的事情可是非常之多。幸運的是Apple的工程師幫我們把這些基本又多的工作給完成,把一個好用的SDK 開放給大家使用,可以專心在自己的idea上面不需要花太多精神在這些基本建設上。這個部分就是要讓讀者快速地認識 SDK 還有讓讀者熟悉 SDK 開發的精神,這樣一來可以幫助讀者迅速了解工具,馬上可以開發自己的程式。實際開發 iOS 程式前,要先介給一個很重要的觀念 MVC Design Pattern 。

Model View Controller

不論程式大或小在規畫程式的時候有一個非常不錯的樣式或者說模版可以讓我們的程式裡各個Class 任務分明,彼此之間的錯誤不太會影響其他物件,專心負責自己的事情,這樣一來也方便開發者除錯。這個模版分成三個部分,分別是 Model,View和Controller 而彼此之間溝通的關系是這樣畫的。
View
  • 用來呈現資料給使用者看
  • 通常不會直接和管理資料的部分接觸
Model
  • 常用來管理資料和提供演算法
  • 和資料呈現的部分沒有直接關係

Controller
  • 負責程式的流程控
  • 在 View 和 Model 沒有溝通的管道時,提供這兩者間的資料交換

舉一個實際一點的例子,假設我們要寫一個程式要依照使用者點選的資料從資料庫裡把相關的資料找出來,然後適當的安排呈現資料到螢幕上供給使用者看。在這段話裡我們把動作細分成幾個部分再加上剛剛上述的三種角色重新詮釋一下。先看一個示意圖

  1. 使用者從鍵盤輸入想要資料的條件限制,按下 Enter
  2. Controller 接受到Enter這個事件
  3. 把使用者剛填入在某個 View 產生視覺元件的資料讀出送到 Model 去進行和資料庫的比對
  4. Model 收到資料後迅速地從資料庫裡找到符合使用者所設限制條件的資料傳給 Controller
  5. Controller 依 Model 給的不同的資料特色,適當地安排給要呈現資料的 View
  6. View 會依需求,動態地呈現資料或是利用原有的 View來呈現新產生的資料
只要我們記住這個觀念,不管程式大或小如此地編寫程式,權責分明,會幫助我們思考解決問題而且除錯時也比較容易到找錯誤的地方。而且Xcode 的開發套件也都是按照這個架構地底下完成的,了解這個觀念之後也可以讓我們比較快入手 iOS SDK 。

第一個 iOS 專案

不管三七,二十一我們先開啟一個 iOS 專案。
左邊欄選 iOS Application 右邊選 Window-based Application 且有個下拉選單記得選 iPhone
 選擇 Choose 之後,會有個視窗提醒我們存檔,選好位置,輸入檔名 PhoneApp

按下 Save 之後會看到一個 iPhone 的專案
接下來首先我們要確定一件事,就是要執行的程式是在模擬器上,而不是實體的 iPhone 機子,在最左上方有個下拉式選單,按下選 Simulator
接下來直接按下上方的 Build and Run 之後在下方程式列就會看到 iPhone 的模擬器

跑出來了。
結果就是這樣一張白白的圖內容什麼都沒有。接著讓筆者來解說一下這個專案的內容。

應用程式內容

首先來談談一個開發程式用的專案裡面,應該包涵那些東西。如果我們開啟一個 iOS 的專案在左邊Groups & Files 的地方應該會看到這些Group。



很明顯地這個專案的名字叫PhoneApp然後我們大致上可以這樣分類一下
Classes
  • 自己產生的程式碼,包含 .h 和 .m
Other Sources
  • 系統自行產生的程式碼有 main.m
Resources
  • 有 Xib 檔和設定檔(這個例子裡為PhoneApp-Info.plist),Xib 檔裡儲存 View 相關的設定,由 Interface Builder 來負責管理。在這個地方還可以存放一些圖片,音樂等等的資料
Frameworks
  • 基本的系統必要的Framework,在Cocoa Touch 之下除了 Foundation 之外,UIKit 也是基本的 Framework
UIApplication

一支iOS 程式就代表著有一個掌握全局的 UIApplication ,UIApplication 是 UIKit 的一員,其主要是處理事件的分配,程式最上方的狀態列,程式的icon 等等,處理一些基本的工作之後就會把任務交給開發者的 Class ,也就是每一次開啟新的專案的時候都會在 Classes 這個 Group 看到 xxxAppDelegate.h 和 xxxAppDelegate.m 。xxx 就是專案的名稱在 PhoneApp 的例子就是 PhoneAppAppDelegate.h 和 PhoneAppAppDelegate.m 。UIApplication 把一些基本的工作做完之後就會利用 delegate 把任務交給 PhoneAppAppDelegate 所以我們在 PhoneAppAppDelegate.h 會看到 <UIApplicationDelegate> 的宣告。如下方
@interface PhoneAppAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
}
想當然在 PhoneAppAppDelegate.m 就會有 <UIApplicationDelegate> 的一些實作了。不過暫時先不討論有那些 method 需要實作先來看一下程式執行時的流程
iOS 的程式還是延續自 C 程式的習慣有由 main function 當成執行的進入點,只不過因應需求有稍做修改,不過還是先看看定義在 main.m 裡的 main function ,所有iOS 專案的 main.m 都放在 Other Sources 這個 Group 裡。
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;
}
這個 main function 相當地簡單,和之前練習過的程式碼不同之處只有這列寫著
int retVal = UIApplicationMain(argc, argv, nil, nil);
就呼叫 UIApplicationMain 然後傳入 argc,argv,nil,nil 就這樣?我們直接來看這個 function 的宣告,在 iOS Reference Library 可以查得到,在 iOS Reference Library 有對每個傳入的參數,回傳值都有說明
int UIApplicationMain (
   int argc,
   char *argv[],
   NSString *principalClassName,
   NSString *delegateClassName
);
參數
argc
在 argv 裡元素的個數。通常對應 main 的參數。
argv
一串輸入值。通常對應 main 的參數。
principalClassName
UIApplication 或其子類別實體的 Class name,如果值是 nil 則認為是 UIApplication。
delegateClassName
接受 application 委任實體的  Class name 。如果 principalClassName 是 UIApplication 的子類別時,可以指定這個子類別當受委任者。其子類別的實體接受 application-delegate 訊息。如果是 nil 代表你要從 main nib 檔(在 iOS 是 xib 檔,且一開始預設為 MainWindow.xib )載入受委任者物件。
回傳值
雖然有設定回傳的型別,但是這個 function 不會回傳東西。當使用者按下 Home Button (唯一的實體按鈕 )的時候這個程式會進入背景。

討論
這個 function 從 principal class 實體化 application 物件,從給定的 (如果有的話)class name 實體化委任物件並設定委任的關系。它也會建立一個主要事件的迴圈包含程式執行的迴圈然後開始處理事件。如果程式的 Info.plist 有指定載入 main nib 檔,經由 NSMainNibFile 這個Key 相對應的合法 value 值,就會載入這個 nib 檔。
 (以上是不負責翻譯。:P)

其中特別說明一下 argc,argv 這兩個,而且如果有寫過 UNIX 程式的就知道,argc 是代表執行時傳入的參數的個數,argv 是參數的字串。比如有一個 UNIX 程式叫 a.out ,而要執行這個程式時,在命令列底下就這樣寫
./a.out
如果這個程式一開始就要傳入參數,比如
./a.out --encode utf-8
這時在 main 裡 argc 的值就是 2 ,而 argv 這個字列陣列的值就會是 {"--encode", "utf-8" } 就這樣。那後面的 principalClassName 和 delegateClassName  (兩個 nil )怎麼交互作用?那只傳這四個參數給 UIApplicationMain 就可以跑出一個簡單的 iPhone 程式?剛剛的一片白白的程式就是這樣執行出來的?不是這樣的吧?當然惡魔總是在細節中,看完這麼簡單的 main function 我們要來看這個專案的設定檔。PhoneApp-Info.plit 在 Resources 這個裡,其內容長這樣


點選左邊的 PhoneApp-Info.plist 之後右下就是其 plist 的內容,都是一些設定值,其中關鍵的地方在紅色圈起來的 Main nib file base name 的 MainWindow 。這個意思是說左邊的 Group & Files 會有一個檔案叫 MainWindow.xib 也就是筆者用紅色圈起來在 PhoneApp-Info.plist 上方的檔案。如果我們 Click 這個檔案則 Mac 程式列會有一個程式出現叫 Interface Builder
然後進入 Interface Builder 會看到這個視窗,
這個視窗叫 Document Window ,裡面有一個白白圖示的 Window,筆者用紅色下底線標示出,再 Click 一下就會出現。

這片白白的視窗就是剛剛 Build and Run 之後模擬器跑出來一片白白的畫面。不信的話,我們把 Library 叫出來。利用 Tool -> Library ,此時還是在 Interface Builder 裡面。
 
在不遠的旁邊會出現 Library 視窗
  
在其底下 Search Bar 輸入 label字樣,上方就會跑出 Label 的元件

然後,把上面的 Label 拉到 Window 畫面,拉過去之後,順手在 Label 點兩打,打上自己要 的字樣,比如 Hello IB

拉完,改完之後記得要存檔,可以按下 Command 加 S 。有沒有忘記存檔看剛剛 Document Window 左上角的紅色圈圈就知道了。
如果紅色圈圈中間有一個黑點,就是修改過且還沒存檔,存檔完就會變成。

中間黑色不見了。這樣 Interface Builder 被我們修改完了。現在回到 Xcode

 
之後再按下 Build and Run 或是快捷鍵 Command 加 Enter ,就會看到模擬器跑出來,新的畫面出現了。
剛剛在 Interface Builder 打的字樣出現了。
等等,問題來了,就這樣?那剛剛提到的 UIApplication 這個東西在什麼時候用到?別急,現在就要介紹。雖然我們很簡單地新增一個 Label ,其實 UIApplication 早就產生了。把 MainWindow.xib 這個檔再點兩下,由 Interface Builder 打開在 Document Window 裡有一個 File’s owner 如下圖
 
 Fileʼs owner 的 Type 是 UIApplication 就代表這個 xib 檔是屬於 UIApplication 的,我們就是 透過 main function (在 main.m ) 裡的 UIApplicationMain 來產生 UIApplication 實體 ,在 xib 這樣設定的話系統就會讓這個 淡黃色有點透明的立方體 (這個圖形在 Interface Builder 要代表 external object ,有寫在 Library ,也就是外部己產生的物件 )和 UIApplication 的實 體做一個連結,接著,還記得之前提過 UIApplication 這個 Class 只是處理一些基本的工作 之後會把接下來的工作透過 delegate 交給 PhoneAppAppDelegate ,於是我們在 Document Window 往下找,果然找到一個東西 Type 寫著 PhoneAppAppDelegate 。
 圖上這個橙色的立方體代表的是一個物件,右邊的Type就是其 Class 在這裡就是 PhoneAppAppDelegate 所以它相對應的程式碼就是 PhoneAppAppDelegate.h 和 PhoneAppAppDelegate.m 。所以我們點開 PhoneAppAppDelegate.h 在右邊的下方的編輯區就會看到 PhoneAppAppDelegate.h 有採用 <UIApplicationDelegate>
代表著 PhoneAppAppDelegate 會接著 UIApplication 之後去完成接下來的工作。所以接下來的工作就都定義在 <UIApplicationDelegate> 這個 protocol 裡,而 PhoneAppAppDelegate.m 就會去實作 <UIApplicationDelegate> 所定義的 method 。我們點開 PhoneAppAppDelegate.m 來看看一個目前對我們而且最重要的 method
 
叫這個 method 叫做
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
這個 method 只是把  window 這個實體變數讓呼叫 makeKeyAndVisible ,讓 window 可以被使用者看見 window ,window 這個變數的型別是 UIWindow ,一個 iOS 程式裡常通只會有一個 UIWindow,目前先認識到這樣,之後在解說 UIView 的時候會再解說 。重點是這個 method 可以視為 iOS 專案裡寫程式進入點,就好像 Console App 的 main function 一樣。可能會有讀者有疑問,這個專案也有 main function 啊?就在 main.m 為什麼我們不把這個 main function 當成寫程式進入點?因為在呼叫 main function 的時候 UIApplication 還有一些事情還沒完成,甚至每個一程式就有一個 UIWindow 可能也還沒準備好,所以和 Console  App 不一樣的是 iOS 依賴 UIApplication 和 UIWindow 的工作完成才可以使開發者接下來的工作比較方便進行。而且
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
一當程式啟動之後只會被呼叫一次,所以滿適合當寫程式的進入點的,這點要請讀者千萬要記住。說到這讀者們有頭昏了嗎?先暫停一下幫大家做一個小整理,從一個最簡單的 iOS 程式 ( window-based 專案 ) 執行的時候的流程是這樣的。

在 UIApplication 實體被產生後就會開啟一個無限的迴圈,一直等待使用者去產生一些事件,比如按按鈕,撥動等等,直到程式被關掉。
在這個例子中,很明顯有看到利用 MVC 工作分配的效果。View 比較直覺知道就是 xib 檔也就是 MainWindow.xib ,PhoneAppAppDelegate 可以視為 Controller ,當 UIApplication 把某些事情做完 (如 application:didFinishLaunchingWithOptions:)或是有事件要通知就呼叫 PhoneAppAppDelegate , PhoneAppAppDelegate 可以去更新 MainWindow.xib 定義的UI元件,如此可知 UIApplication 扮演著 Model 的角色。

2 則留言:

  1. 看完此文真是收益良多,觀念解釋得很詳細,讓我這新手都能瞭解,真的很棒!!感謝版主!

    另外有一點,關於Controller的介紹「在 View 和 Model 沒有溝通的管道時,提供這兩者間的資料交換」,這句話似乎有點怪,聽起來好像View跟Model在某些條件下能溝通。然而就我所知,View和Model彼此是不能溝通的,無論在任何條件下。此乃小弟個人淺見,如有錯誤,還請不吝指教^^。

    謝謝版主熱心分享

    回覆刪除
  2. View 和 Model 不能溝通?資料的交換呢?比如說,使用者按了 Search 然後 Controller 把使用者想要的東西從 Model 找到後,呈現在另一個 View 上。這樣算是一種溝通嗎?

    回覆刪除