iOS的MVC框架之控制層的構建(上)

歐陽大哥2013發表於2018-02-24

在我前面的兩篇文章裡面分別對MVC框架中的M層的定義和構建方法進行了深入的介紹和探討。這篇文章則是想深入的介紹一下我們應該如何去構建控制層。控制層是聯絡檢視層和模型層的紐帶。現在也有非常多的文章宣揚所謂的去控制層或者弱化控制層的作用,覺得這部分是一個雞肋,他會使得應用變得臃腫不堪。那麼他是否有存在的必要呢? 一般的應用場景裡面,我們都需要將各種介面呈現給使用者,然後使用者通過某些操作來達到某個目標。從上面的場景中可以提取出呈現、操作、目標三個關鍵字。要呈現出什麼以及要完成什麼目標我們必須要通過具體操作才能達成,也就是說是通過操作來驅動介面的不斷變化以及服務目標的不斷達成,操作是聯絡介面和目標的紐帶。為了表徵這種真實的場景,在軟體建模和設計實現中也應如此。我想這也就是MVC框架這種應用模型設計的初衷吧。在MVC框架中V負責呈現C負責操作而M則負責目標。而且這種設計還有如下更多的考量:

  • 檢視介面千變萬化,會根據使用者的體驗不停的升級和優化,甚至同一個功能的前後兩個版本都有完全不同的差異,或者某些檢視介面會分散到其他檢視介面中去,又或原來分散的檢視介面又聚合到某個新檢視介面中來。也就是說檢視呈現部分是變化最大以及永續性最短的一個部分。
  • 模型(服務)則相對穩定,他只是提供了某些具體的基礎業務服務,而且這些服務更多的是服務水平的升級而非服務的完全改變。因此模型部分是變化最小且永續性最長的一個部分。
  • 一般情況下我們對檢視介面上的操作控制需要呼叫多個服務來完成,或者不同的介面上的呈現可能會由同一個服務來支撐。因此我們不能將介面呈現和服務目標進行一對一的強行繫結,我們需要將呈現和模型進行解耦處理。

控制層的引入正是解決了上面的這些矛盾,他將檢視和模型的關聯減少到最低,同時也是將易變的和不變這種矛盾體進行了化解。控制層就是一箇中介者(參考設計模式中的中介者模式)我們應該把具體的操作交給控制層來完成,並且由控制層來驅動檢視的呈現和服務的提供。這看來好像是一種最優的解決方案。

控制器--功能的劃分邊界

那麼控制層除了具備處理操作以及實現檢視和模型之間聯絡的紐帶之外,還應該具有什麼特徵呢?

應用程式從使用者的角度來看他其實就是能夠提供某種能力的功能的集合。每個功能都具有對應的展示效果以及提供對應的服務。而且有些功能又可以細分為更多的小功能。對於開發者來說功能是一種應用縱向的切分。開發者更喜歡將他說成為模組單元或者說是功能。每個功能能夠提供一個從介面到業務邏輯的完整單元,而且功能之間一般都比較獨立,功能之間通常通過介面來進行互動。這樣設計的好處是有利於降低系統內模組之間的依賴耦合性,也有利於程式設計師之間的分工合作和任務劃分。因此無論從使用者還是開發者的角度來看功能劃分都是一種非常好的應用程式構造方式。功能的展現在設計上我們可以理解為通過檢視來完成,而業務邏輯實現則是由模型層來完成,所以必須要存在一個實體來將這兩者關聯起來,並且起到統籌和控制的能力。這個實體由控制層的控制器來實現和擔當最合適。因此在實踐中我們對功能的實現和劃分也通常是以控制器為單位來構建的,控制器是工作在控制層。也就是說我們在實現某個功能時通常是為這個功能建立一個對應的控制器來實現的,控制器負責檢視的構建和業務模型的呼叫,而思想下的框架就是經典的MVC框架!

控制層在各平臺下的實現

目前主流的iOS和Android移動開發平臺所提供的都是MVC應用框架,尤其是對於控制層的實現更是幾乎提供了相同的能力和方式。兩個平臺的控制層的實體都是由對應的控制器類來實現的(iOS叫UIViewController, Android叫Activity)。而且這兩個平臺上都提供了控制器的構建,檢視的呈現以及到控制器的銷燬的流程方法。這種實現機制是一個非常典型的模板方法設計模式,在基類中定義了一個控制器在生命週期內各環節的呼叫方法,您只需要在派生類中過載這些方法來完成控制器生命週期內各環節所要完成的動作或者處理的事情。為了處理控制器之間的互動或者呼叫,系統提供了一個導航棧的管理類。導航棧負責各功能控制器的進入和退出,同時管理著所有的控制器。

相對於iOS的UIViewController來說Android的Activity其實對功能封裝得更加徹底。Activity具有跨越程式的呼叫能力,因此作為元件化的能力更加強大,同時控制器和控制器之間的耦合度也非常得低。對於控制器之間的引數傳遞都是通過序列化和反序列化來實現的。但是這也在某方面成為了一個缺點,為了解決這個問題,Android系統中又提供了一個叫Fragment類,這是一個較Activity而言的輕量級控制器,目的是為了解決某些大功能需要拆解為多個子功能來實現的問題以及解決功能之間的引數傳遞的問題。

iOS檢視控制器生命週期的介紹。

前面大體介紹了控制層中控制器的實現以及控制器的生命週期,同時也介紹了功能和控制器之間的對應關係,控制器是檢視和業務模型之間聯絡的紐帶,因此控制器必須要在生命週期內負責檢視的構建、管理檢視的呈現、處理使用者的操作、以及進行業務模型的呼叫等工作。為了實現這些能力,控制器中採用了一種模板方法的設計模式來解決這個問題。這裡面我主要想介紹一下iOS檢視控制器為解決這些問題而所做的實現。我們知道iOS中的檢視控制器是叫UIViewController。在這個類中定義了很多的方法來描述控制器所處的狀態,而每個從檢視控制器派生的類都可以過載對應的方法以便在檢視控制器的相應狀態下進行邏輯的處理。下面列出了常見的幾種狀態下的方法以及這種狀態下我們通常應該要做的事情:

  • init 這個是控制器的構造方法

  • loadView 在這個方法中完成檢視的構造。如果你的檢視是由Storyboard或者XIB來構建那麼你不需要過載這個方法,但是如果你的檢視是通過程式碼構建的話則應該過載這個方法。控制器的預設實現將會找到關聯的Storyboard或者XIB中的檢視佈局描述資訊, 如果找到了則根據佈局描述來構建要呈現的檢視,如果沒有找到則會構建出一個預設的空檢視。

  • viewDidLoad 這個方法被呼叫時表示檢視已經構建完畢了,一般在這裡構建模型層的業務模型物件,以及一些事件的繫結,委託delegate的設定等工作。如果你是通過程式碼來構建佈局時,不建議在這裡進行檢視佈局的構建而應該將構建的程式碼寫在loadView裡面去。

  • viewWillAppear 檢視將要呈現時呼叫,只有當將一個檢視新增到一個視窗UIWindow時檢視才會呈現出來,因此這個方法是在將檢視新增到視窗前被呼叫。

  • viewDidAppear 檢視已經呈現到視窗中,這個方法會在檢視新增到視窗後被呼叫。

  • viewWillDisappear 檢視將要從視窗中刪除時被呼叫。

  • viewDidDisappear 檢視已經從視窗中刪除時呼叫。

  • dealloc 控制器被銷燬前被呼叫。

如何構建您的控制層

如何構建一個控制層是一個非常廣泛的命題,需要具體業務具體分析。雖然如此總是還能找到一些共同點和方法論,一個優秀的設計方法,將不會出現所謂的控制器程式碼膨脹的問題。MVC本身的框架思想非常的優秀,當出現問題時首先要考慮的並不是去替換掉現有的框架而是從設計的角度去優化現有的程式碼以及邏輯,讓整個系統達到一個最優的組合。

1. 功能資料夾的劃分

在前面的論述中可以看出檢視控制器是功能實現的基本單元,一個檢視控制器是一個功能的載體。一個應用中具有多個功能,而一些相似的功能通常組成一個功能集,比如一個應用的註冊流程可能會分為好幾步;比如說使用者體系的各種特性的設定;比如說一個訂單的支付部分等等。為了對功能集進行管理,可以將某些功能集下的所有功能放置到一個特定目錄中。最終的構成一個應用功能目錄樹:

功能目錄樹

通過對功能進行劃分管理,有利於功能的檢索和增強你應用系統的可理解性。作業系統以及XCODE上的資料夾就是一種非常常見的功能樹目錄構建方式。在進行功能目錄樹劃分時注意如下幾個要點。

  • 如果某些功能是一些基本的功能,可能多個其他功能都會用到那麼可以將這些功能提煉出來儲存到一個特定的資料夾中(資料夾可以命名為Common或者Base之類的)。比如你可以在系統提供的控制器的基礎上派生出你自己的控制器基類,然後把這些基類也可以單獨的儲存到一個資料夾中。
  • 最好不要以每個功能單獨建立資料夾來管理。有些案例裡面會出現每個VC的.h和.m檔案都給他建立資料夾,其實這樣不可取,因為有可能導致資料夾過多而變得查詢定位更加麻煩。我們應該以相似的功能聚集在一起來建立資料夾進行管理。
  • 有時候某個功能集可能過於龐大,這時候我們可以對功能集進行再次分類,並建立子資料夾進行管理,資料夾劃分不一定是單層樹形結構也可以是多層樹形結構。
  • 在XCODE中可以建立兩種資料夾:真實資料夾(New Group with Folder)和虛擬資料夾(New Group)。 這裡建議是最好建立虛擬的資料夾,原因是為了後續好管理,因為有時候可能出現控制器檔案從一個資料夾移動到另外一個資料夾的情況(功能轉移)。如果你建立真實的資料夾的話,那麼移動後控制器所在的真實的資料夾就有可能會和你專案工程上的所在的資料夾對應不上的情況。而用虛擬資料夾就不會出現這種情況的發生。
  • 功能資料夾的劃分方法有很多種,你可以從業務的角度來劃分資料夾,也可以從你的應用介面上的展現來劃分資料夾。比如一個應用中我們有商品展示體系、支付體系、使用者體系,而我們的介面展示可能是底部分為首頁、購物車,我的組成的四個Tab介面。這時候你可以按業務來分為商品、支付、使用者資料夾,也可以按展示介面來分為首頁、購物車、我的資料夾。因此資料夾的劃分並沒有標準就看你個人的喜好而定了。唯一的要求就是同一個資料夾內的功能要體現出聚合性強的原則,也就是在某一天甚至可以將這部分單獨抽離出來構建一個子專案時而不需要進行進行大量的改變。

2. 基本控制器以及派生類。

一個應用總是會有自己獨特的設計風格,比如標題欄的文字和字型以及背景。或者我們要對應用進行整體的監控,比如對介面進入退出進行埋點處理。因此我們需要在系統提供的基本控制器UIViewController, UITableviewController, UINavigationController, UICollectionViewController等控制器之上進行派生類的構建,也就是實現某個具體功能的控制器不要從系統的控制器之上派生而應該從派生的控制器基類之上再派生出來。這樣我們就可以在我們派生的控制器基類上增加一些具有自己特色的業務邏輯或者介面邏輯,也可以實現某些AOP方面的處理。比如我們可以構建一個UINavigationController的派生基類,這樣在進行控制器的push以及pop之前或者之後進行一些特殊處理。 但是這樣問題就來了,因為OC語言並不支援多重繼承。而我們可能要建立上面四個系統控制器的派生類,並且需要在相似的地方新增同樣的程式碼,比如都要在viewDidLoad中新增一段相似的程式碼。為了實現這一點,就需要新增四份相同的程式碼比如:

@interface XXXBaseViewController:UIViewController
@end

@implementation  XXXBaseViewController

-(void)helperfn
{
    //..實現特定的擴充套件功能。
}

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self helperfn];  //呼叫擴充套件方法
}

@end


@interface XXXBaseNavigationController: UINavigationController
@end

@implementation  XXXBaseNavigationController

-(void)helperfn
{
    //..實現特定的擴充套件功能。
}

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self helperfn];  //呼叫擴充套件方法
}

@end

//...分別實現的UITableviewController、UICollectionViewController等等都將實現重複的程式碼。

複製程式碼

很明顯這是一種低效率的解決方案,因為一旦需求變更我們就可能需要對helperfn方法改動四次。怎麼解決這種問題呢?我們分為2種場景:

一、 所有的功能擴充套件中都不需要擴充套件屬性

在這種情況下,因為擴充套件的方法中都不需要用到物件的例項屬性,所以我們可以通過建立分類(Category)的方法來實現這些共有的功能,我們可以為UIViewController建立出一個分類來,並在這個分類中實現共有的方法,然後在每個派生類的特定位置中呼叫這個共享的分類方法。具體程式碼如下:


@interface UIViewController(XXXExtend)
    
     -(void)helperfn;
@end

@implementation  UIViewController(XXXExtend)

//一份實現
 -(void)helperfn
{
    //實現特定功能。
}

@end


@interface XXXBaseViewController:UIViewController
@end

@implementation  XXXBaseViewController

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self helperfn];  //呼叫擴充套件方法
}

@end


@interface XXXBaseNavigationController: UINavigationController
@end

@implementation  XXXBaseNavigationController

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self helperfn];  //呼叫擴充套件方法
}

@end

//...分別實現的UITableviewController、UICollectionViewController的派生類。


複製程式碼

當然你也許覺得上面的方法需要在每個派生類的特定地點都新增一遍程式碼而感到麻煩,你也可以採用method swizzle的方法來解決上述的問題,你可以在分類的+load方法中實現程式碼替換。下面是具體的程式碼實現:

@interface UIViewController(XXXExtend)
@end

@implementation UIViewController(XXXExtend)

-(void)helperfn
{
    
}

-(void)viewDidLoadXXXExtend
{
    [self viewDidLoadXXXExtend];
    
    [self helperfn];
}


+(void)load
{
    Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(viewDidLoadXXXExtend));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

@end



@interface XXXBaseViewController:UIViewController
@end

@implementation  XXXBaseViewController

@end


@interface XXXBaseNavigationController: UINavigationController
@end

@implementation  XXXBaseNavigationController
@end

//...分別實現的UITableviewController、UICollectionViewController的派生類。

複製程式碼
二、有需要擴充套件屬性的情況。

如果你的基類擴充套件方法中有用到屬性的話那麼我們知道分類中是不能支援編譯時擴充套件屬性的(但是支援執行時擴充套件屬性的增加)。除了用運算時擴充套件屬性的方法外,還可以將共有的方法和屬性單獨提煉出來讓一個輔助類來實現,然後在派生基類的初始化方法中建立這個輔助類,並且後續的一些方法都委託給輔助類來實現。具體的結構設計如下:


//Helper.h

@interface Helper:NSObject

@property   id prop1;
@property   id prop2;

-(void)fn1;
-(void)fn2;

-(id)initWithViewController:(UIViewController*)vc;

@end

//Helper.m

@interface Helper()

@property(weak) UIViewController *vc;  //這裡一定要定義為弱引用

@end

@implementation Helper

-(id)initWithViewController:(UIViewController*)vc
{
     self = [self init];
     if (self != nil)
    {
           _vc = vc;
    }

   return self;
}

-(void)fn1
 {
      //...
      self.vc.xxxx;  //這裡可以訪問檢視控制器的方法。
 }


@end

..........................................

//XXXBaseViewController.h

@interface XXXBaseViewController:UIViewController

@property id prop1;

@end

//XXXBaseViewController.m

@interface XXXBaseViewController()

   @property(strong) Helper *helper;
@end

@implementation  XXXBaseViewController

-(id)init
{
     self = [super init];
    if (self != nil)
    {
            //在檢視控制器的初始化裡面初始化一個幫助物件。
           _helper = [[Helper alloc] initWithViewController:self];
    }

   return self;
}

//重寫控制器中的屬性,並把真實的實現委託給helper來完成
-(void)setProp1:(id)prop1
{
     self.helper.prop1 = prop1;
}

-(id)prop1
{
    return self.helper.prop1;
}

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self.helper fn1];  //呼叫幫助類的方法。
}

@end

//在你的其他幾個派生類中採用同樣的機制來處理。

複製程式碼

從上面可以看出來,輔助類裡面設計了一個弱引用指標來指向控制器,而控制器則是強引用輔助類,這樣做的目的是為了防止迴圈引用的發生,而且這種設計模式也是一種在實踐中非常經典的方法:有時候我們需要將類A的某些功能委託給類B實現,而B又有可能會在特定的地方訪問A的屬性,為了防止相互引用而形成死鎖導致兩個物件都無法被釋放,這時候就需要使用強弱引用來解決這個問題。上面藉助輔助類來實現的方法可以解決我們的派生類中程式碼重複的問題。上面的方法缺點就是我們的派生類中需要編寫很多重複的、程式化的程式碼。如何來精簡呢?其實我們可以藉助介面協議和OC中的forwarding機制來解決這些問題:


//Helper.h

@protocol Helper

//請將介面的屬性和方法都設定為可選
@optional

 @property   id prop1;
 @property   id prop2;

-(void)fn1;
-(void)fn2;

@end

//真實實現介面的物件
@interface Helper:NSObject<Helper>

@property   id prop1;
@property   id prop2;

-(id)initWithViewController:(UIViewController*)vc;

@end

//Helper.m
@interface Helper()

@property(weak) UIViewController *vc;  //這裡一定要定義為弱引用

@end

@implementation Helper

-(id)initWithViewController:(UIViewController*)vc
{
     self = [self init];
     if (self != nil)
    {
           _vc = vc;
    }

   return self;
}

-(void)fn1
 {
      //...
      self.vc.xxxx;  //這裡可以訪問檢視控制器的方法。
 }

-(void)fn2
{
   //...
}

@end

..........................................

//XXXBaseViewController.h

@interface XXXBaseViewController:UIViewController

@end

//XXXBaseViewController.m

//這裡實現Helper協議,如果基類的擴充套件屬性可以被外面訪問則應該在.h中的類申明裡面表明實現了Helper協議
@interface XXXBaseViewController()<Helper>
   @property(strong) Helper *helper;
@end

@implementation  XXXBaseViewController

-(id)init
{
     self = [super init];
    if (self != nil)
    {
            //在檢視控制器的初始化裡面初始化一個幫助物件。
           _helper = [[Helper alloc] initWithViewController:self];
    }

    return self;
}

-(void) viewDidLoad
{
    [super viewDidLoad];
    [self fn1];  //呼叫fn1
  
   id *p = self.prop1  //讀取屬性。

}

//這是實現的關鍵點,過載這個方法。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
   //判斷這個方法是否是協議定義的方法,如果是則將方法的實現者設定為helper物件。
    struct objc_method_description  omd = protocol_getMethodDescription(@protocol(Helper), aSelector, NO, YES);
    if (omd.name != NULL)
    {
        return self.helper;
    }
    
    return [super forwardingTargetForSelector:aSelector];
    
}

@end

//在你的其他幾個派生類中採用同樣的機制來處理。

複製程式碼

可以看出我們可以藉助OC的forwardingTargetForSelector方法來實現方法呼叫的轉發處理,而不必具體的定義方法的實現。

3.對內和對外的介面定義

物件導向程式設計的一個主旨思想就是封裝,所謂封裝就是在進行類的定義和設計時,我們儘可能的暴露出外面可以理解以及需要的介面或者方法,而在內部實現中所用到的中間特性或者私有特性則儘可能的隱藏。儘可能的隱藏複雜的實現細節,而把簡單易用的介面暴露給外部使用。比如對於汽車的封裝,我們對外暴露的就僅僅是開鎖、發動、掛擋、轉向等操作,而具體內部的組成、發動機的原理、以及發動機如何驅動行走等細節都不需要開車的人瞭解。正是物件導向這種封裝的特性就使得我們能更加從應用層面去使用某個物件的方法而不需要知道其中的細節。因此我們在類的設計中也要遵循這個設計的思想,把必要的東西暴露給外部,而把實現細節則隱藏在類的內部來完成。這一節所介紹的並不僅僅適用在控制器類的設計上,所有其他系統也是同樣適用的。 類的封裝實現在不同的語言上所提供的能力是不一樣的,這一點非常有意思。向在C/C++/OC這幾種語言中,類的宣告和類的實現需要在不同的檔案裡面完成(.h是宣告,而.m/.c/.cpp中則是實現)而像Java和Swift等語言則是申明和實現都放在同一檔案中完成。在前面的三種語言中因為宣告和實現分離,所以我們可以把一些對外暴露的方法和屬性放到標頭檔案中申明,而內部的私有屬性則放到實現檔案中申明和定義。而使用者則只需要引入共有標頭檔案即可。而後面兩種語言中因為沒有分開,所以在這些語言更傾向於通過介面定義和實現來完成這種共有屬性和私有屬性分類的機制(您可以看出在Java中大量的使用了介面來完成整個體系架構,以及Swift中也是推崇介面程式設計這種理念)。

物件導向設計中,類和類之間不可能獨立存在,他們之間總是要建立一種關聯,這種關聯有可能是單向的也有可能是雙向的。我們都推崇類和類之間的單向依賴來降低類與類之間的耦合性。這種單向依賴至少在明面上是如此的,也就是類所公開的方法和屬性是可以看出來的。但是在實際的內部實現中這種單向依賴可以就會被打破。舉一個很常見的例子我們都知道檢視控制器UIViewController中有一個view屬性來儲存控制器所管理的檢視,但是我們在檢視UIView中卻看不見任何關於控制器的屬性。這樣的表象就是表明檢視控制器依賴檢視,而檢視則不依賴檢視控制器,這也是非常符合MVC中三層設計思路的。但實際中是如此嗎? 結果並不是這樣的,因為在系統的內部如果某個檢視是控制器的根檢視的話他可能會具有一些不同的特性以及不同的處理邏輯,因此其實在UIView的內部私有屬性中是有一個檢視所歸屬的檢視控制器的屬性的,這個屬性就是:viewDelegate。 並且在UIView上他是定義為了id型別的。

上面的兩段描述中我們都提到了對公和對私的方法和屬性的申明的問題,可以看出在設計上要按照這個思路去設計我們的類,我們只需要將共有的方法和屬性暴露出來,而將私有的方法和屬性則隱藏起來。那麼怎麼來實現這種共有和私有方法的定義實現呢?我們來看下面三個具體的場景:

  • 類的某些屬性公開某些屬性不公開
/*一個類裡面某些屬性公開某些屬性不公開的實現可以很簡單的通過類的申明和類的擴充套件來實現*/

//XXXX.h

@interface XXXX
 
//對外公開的屬性
 @property  id pubp1;
 @property  id pubp2;

//對外公開的方法
-(void)pubfn1;
-(void)pubfn2;
 
@end

//XXXX.m

//只在內部使用的屬性和方法定義在擴充套件中。
@interface XXXX()

//對內私有的屬性
 @property  id prip1;
 @property  id prip2;

//對內私有的方法
-(void)prifn1;
-(void)prifn2;

@end

@implementation  XXXX
@end

複製程式碼
  • 類的某些屬性對外暴露的是隻讀的,但是內部實現確實可以被改變的。這樣做的目的是為了訪問和使用的安全。
//XXXX.h

@interface XXXX
 
//對外公開的屬性只讀
 @property(readonly)  id pubp1;
 @property(readonly)  id pubp2;
 
@end

//XXXX.m

//只在內部使用的屬性和方法定義在擴充套件中。
@interface XXXX()

//在類的內部這些屬性是可讀寫的
 @property  id pubp1;
 @property  id pubp1;

@end

@implementation  XXXX
@end

複製程式碼
  1. 類A和類B之間有關聯,A中持有B的例項並公開,而B則有可能在實現中需要用到A的內部的方法或者屬性,同時B是不向外暴露對A的持有的情況。因為我們都是通過標頭檔案引用,所以在標頭檔案中看不到這種互相依賴關係,但是內部的實現檔案則可以很清楚的看到其中的依賴了。
//A.h

@interface A

@property(strong)  B *b;   //A的外部暴露持有B
@property  id others;

-(void) pubfn1;

@end

//A.m

//在A的內部用到了B的prip1, prifn1所以要這裡申明一下。
@interface B()

@property  id prip1;
-(void)prifn1;

@end

//內部實現
@interface A()

  @property id prip1;
  -(void)prifn1;

@end

@implementation  A

-(void)pubfn1
{
    //...
    [b prifn1];    //A內部呼叫B的私有方法
    [b pubfn1];

    id temp = b.prip1;  //內部屬性
}

@end

...............................

//B.h

//B的外部並不暴露對A的持有
@interface B

-(void)pubfn1;

@end

//B.m

@interface A()

//因為B內部要用到prifn1所以這裡再申明一下。
-(void)prifn1;

@end


@interface B()

//B的內部持有A。這裡因為有相互持有所有要有一個是強持有一個是弱持有。
@property(weak) A *a;   

//內部實現的其他
@property id prip1;
-(void)prifn1;

@end

@implementation  B

-(void)pubfn1
{
    [a prifn1];
}

@end



複製程式碼

4.各種屬性的定義以及分類擴充套件

控制器用來實現對檢視物件和業務模型物件的建立以及管理和控制,在實現上控制器會擁有眾多檢視層物件的屬性,以及模型層物件的屬性,同時還會擁有自身的一些屬性。同時控制器還要在適當的時候對使用者的輸入進行處理,以及在適當的時候呼叫業務模型所提供的服務,還要在適當的時候將業務模型提供服務的結果通知給檢視進行呈現和更新。因此如何去組織一個控制器的程式碼佈局(此程式碼佈局非檢視的介面佈局而是原始碼的佈局)就非常的重要了。如何合理的定義以及放置屬性,如何合理的對控制器中的方法進行分類,以及在何時建立檢視、在何時建立業務物件,在何時新增和銷燬觀察者,在類的析構中作如何處理等等這些其實都是有一定的規則和規範的。這一節更像是一份程式碼規約方面的介紹。我將會從下面幾個點來分別闡述。

(一). 屬性的定義順序和規則 一個類的設計首要構造的就是屬性和成員變數,控制器也無外乎。前面說到控制器管理著檢視物件和模型物件,因此我們一般要將檢視物件和業務物件作為屬性定義在控制器中。這裡整理出一下幾點:

  • 如果控制器中的屬性和成員變數只在類內部使用和訪問,那麼我們應該要將屬性定義在控制器的實現檔案中的擴充套件裡面,而不要定義在控制器的標頭檔案中,除非這個屬性會被外部訪問或者設定。比如如下程式碼:
//XXX.m

@interface XXX()

//在擴充套件中定義內部使用的屬性
  @property(nonatomic, weak)  UILabel *label;
  @property(nonatomic, strong)  User *user; 
@end

@implement XXX
@end   


複製程式碼
  • 如果控制器中需要訪問某個子控制元件檢視那麼在定義子控制元件檢視時,屬性最好是weak而非strong。這樣做的目的一來iOS對於SB或者XIB上的子控制元件的屬性定義都是預設為weak的、二來最主要的原因是有可能控制器中的根檢視有可能會在執行時被重新構造(比如說我們要實現一個換膚功能,我們就有可能會重新構造檢視控制器中的根檢視來實現)這樣當控制器中的根檢視被銷燬時,根檢視裡面的子檢視也應該被銷燬,而如果你用strong來定義子檢視時就有可能導致子檢視的生命週期要長於根檢視。另外有可能我們的子控制元件會採用懶載入的模式來實現根檢視中子檢視的建立,因此如果你用strong的話就有可能導致子檢視不會被重新構建。

  • 對於NSString型別的屬性來說我們最好將他宣告為copy。原因是如果宣告為strong或者assign的話,那麼對於NSMutableString型別的字串進行賦值時就有可能會在後續的程式碼中內容被改變,從而引起異常的問題。比如:

   NSMutableString *str1 = [@"hello" mutableCopy];
    self.userName =   str1;   //如果userName被申明為strong的話則後續對str1的內容的更改同時也會導致userName的內容的改變!!
複製程式碼
  • 如果你的屬性不會涉及到任何多執行緒訪問的場景那麼最好不要在屬性定義上帶上atomic 修飾符。原因是如果帶上atomic修飾符的話所有屬性的賦值和讀取操作都會通過作業系統原子API來進行賦值和讀取。

  • 不要將狀態以及持久資料儲存到檢視物件中。

  • 如果可能最好將控制器中的檢視物件屬性和模型物件屬性分開定義,並且把檢視物件屬性放在最上面, 控制器本地的屬性放在中間,而模型物件屬性放在最下面。

下面是一個典型的控制器屬性定義的程式碼示例,僅供大家參考:

    //XXXViewController.m

@interface XXXViewController()

  //不同層次上的屬性分開定義
  
  //檢視物件屬性放在最上面。
  @property(nonatomic, weak)  UITextField *nameLabel;
  @property(nonatomic, weak)  UITextField *passwordLabel;

//中間定義控制器本身的變數
  @property(nonatomic, copy)  NSString *name;
  @property(nonatomic, assign) BOOL isAgree;

 //底部定義模型物件屬性 
@property(nonatomic, strong)  User *user;

@end

@implement XXXViewController
@end   
複製程式碼

(二). 類中各種方法的分類 當你的屬性定義完畢後,我們就要實現方法了。在一個類的方法中我們有構造和析構的方法、有需要過載的方法、有事件處理的方法、有個委託Delegate或者觀察者的方法、還有一些對外公開的方法、以及一些私有輔助的方法。為了便於管理,我們最好能將這些方法進行分類擺放,這樣也有利於查詢定位,對於一個控制器來說一般就是上面所說的幾種方法,一般情況下我們對相同性質的方法放在一塊實現,並用一些特定的關鍵字或者用分類的機制來對控制器中的所有方法進行歸類。下面我用兩種不同的方式來對方法進行歸類處理:

  • 通過語法關鍵字。 在OC中我們可以通過 #progma mark -- 名稱 來便於定位和查詢。在實踐中控制器類一般都要實現:重寫基類的方法、公有方法、事件處理方法、Delegate中的方法、私有方法這幾種型別,因此我們可以專門為這些方法定義不同的標籤。具體如下:
//  XXXViewController.m

@implement XXXViewController

//最開始放構造和析構的方法。這兩個方法放在一起就可以清楚的看出我初始化了那些東西,析構了那些東西。
-(instanceType)initXXXXX{}
-(void)dealloc{}

//接下來放過載的方法。對屬性set,get重寫的方法
-(void)viewDidLoad{}
-(void)viewWillAppear:(BOOL)animated{}
-(void)viewDidAppear:(BOOL)animated{}
-(NSString*)name{}
-(void)setName:(NSString*)name{}

//接下來放共有的方法,也就是控制器類暴露給外面呼叫的方法。
#pragma mark -- Public Methods
-(void)publicFn1{}
-(void)publicFn2{}

//接下來放事件處理的方法,這些事件處理包括控制元件的觸控事件,NSNotificationCenter、定時器事件、其他的註冊的事件。我個人比較喜歡以handle開頭。

#pragma mark -- Handle Methods

-(void)handleAction1:(id)sender{}
-(void)handleAction2:(NSNotification*)noti{}
-(void)handleAction3:(NSTimer*)timer{}


//接下來是放各種Delegate的事件,我們名稱都以Delegate協議的名稱做標誌。

#pragma mark -- UIDataSource

//..這裡新增代理的方法。。。

#pragma mark -- UITableViewDelegate


//接下來是存放各種私有方法。

#pragma mark -- Private Methods

-(void)privateFn1{}
-(void)privateFn2{}









@end

複製程式碼
  • 通過分類 如果你不想使用#pragma 帶標籤的形式,那也可以用分類的方式來實現。比如下面的例子:

//  XXXViewController.m

@implement XXXViewController

//預設的部分實現,構造和析構方法以及所有從基類過載的方法
-(instanceType)initXXXXX{}
-(void)dealloc{}

//接下來放過載的方法。對屬性set,get重寫的方法
-(void)viewDidLoad{}
-(void)viewWillAppear:(BOOL)animated{}
-(void)viewDidAppear:(BOOL)animated{}
-(NSString*)name{}
-(void)setName:(NSString*)name{}

@end

//公有方法
@implement XXXViewController(Public)
@end

//事件處理方法
@implement XXXViewController(Handle)
@end

@implement XXXViewController(UIDataSource)
@end
   
@implement XXXViewController(UITableViewDelegate)
@end

@implement XXXViewController(Private)
@end

複製程式碼

歡迎大家訪問我的github地址, 關注歐陽大哥2013

相關文章