iOS-有效編寫高質量Objective-C方法-三

weixin_34370347發表於2016-11-09

歡迎大家關注我的公眾號,我會定期分享一些我在專案中遇到問題的解決辦法和一些iOS實用的技巧,現階段主要是整理出一些基礎的知識記錄下來

文章也會同步更新到我的部落格:
ppsheep.com

本篇文章,主要是對OC中的一些原理的講解,可能會有一些枯燥,但是真正當你理解時,會有一種豁然開朗的感覺。這裡會涉及到 物件,屬性,訊息以及執行期的一些介紹,只有我們真正理解了這些原理之後,我們的開發水平才會進一步提升,而不是止步於view的簡單編寫,view的編寫想要寫得好,也需要了解這一些原理。

理解屬性(property)這一概念

在開始講屬性之前,我們先來理解一下幾個概念:

  • 物件:物件是"基本的構造單元",在OC中,我們經常使用物件來儲存和傳遞資料
  • 訊息傳遞:在物件之間傳遞資料並執行任務的過程就叫做訊息傳遞
  • OC Runtime:當應用程式執行起來,為其提供相關支援的程式碼叫做"執行環境(Runtime)",它提供了一些使得物件之間能夠傳遞資料的函式,並且包含了建立類例項需要的所有邏輯

上面三個概念,在OC程式設計中尤其重要,雖然現在可能你沒有很深刻的理解,但是隨著學習深入,你肯定能夠體會到。

屬性

"屬性(property)"相信大家都很熟悉,是OC中用來儲存物件的資料的例項變數。例項變數一般是通過"存取方法"來訪問,"設定方法"來設定例項變數。在之前我們已經講過,對於例項變數,如果是本身訪問,那麼讀取最好是直接讀取(採用下劃線方式直接訪問),而設定最好使用屬性來設定。具體的,可以參見上一篇(iOS-有效編寫高質量Objective-C方法-二)。

對於一些簡單的概念性的東西我就不講了,給出結論就行:

  • 不一定要在介面中或者說是宣告檔案中定義好所有的例項變數,可以在實現檔案中定義,以保護與類實現相關的內部資訊
  • 屬性按照標準的命名方式,在編譯器編譯期間會自動加上存取方法

接下來,我們來說幾個關鍵字:

@synthesize

我們可以在程式碼中通過這個關鍵字來指定我們想要的例項變數

例如:在標頭檔案中

@interface : NSObject

@property(nonatomic, copy) NSString *name;

@end複製程式碼

這個屬性,在我們執行期環境下,生成的例項變數為_name,但是我們在.m中並不想使用這個名稱,那麼我們在實現檔案裡就可以這樣寫:

@implementation

@synthesize name =  _myName

@end複製程式碼

那麼我們在.m實現檔案中,都可以直接使用_myName來操作屬性name

不過為了書寫的規範,和團隊之間協作,我還是建議按照規範的OC程式碼風格來編寫程式碼,團隊成員之間,一看就能夠看清楚程式碼

@dynamic

這個關鍵字 是用來阻止編譯器自動合成存取方法,不過這個關鍵字我都用的很少,上面的關鍵字同樣的,也使用較少。

這個關鍵字的意思是:阻止編譯器合成屬性所對應的例項變數,也不要合成例項變數的存取方法

這裡講一下例項變數就是帶下劃線的_name而屬性是通過property宣告的name

這兩個需要區分開來

屬性特質

nonatomic

所謂的屬性特質,就是指我們在申明屬性的時候,property括號中跟的一些關鍵字

@property(nonatomic, readwrite, copy);複製程式碼

其中的nonatomic,readwrite,copy這些都是屬性特質,我們先來說nonatomic

這個關鍵字叫做屬性的原子性,通俗來說,這個關鍵字主要是來控制屬性的同步鎖
同步鎖:不同的執行緒在讀取屬性的時候,如果屬性是通過atomic來宣告的,那麼這兩個執行緒總是能夠讀到屬性的有效值(注意這裡是有效的屬性值,並沒有說是正確的屬性值),如果屬性是通過nonatomic宣告的,那麼不同的執行緒讀取屬性值時,如果有執行緒正在修改該屬性的值,另外的執行緒正在讀取屬性值,那麼就可能將還未修改完成的屬性值讀取出來(這裡是尚未修改完成的屬性值,有可能讀出一個完全沒有任何意義的屬性值)

那麼又要來說一說,為什麼我們總是看到在編寫iOS程式時,屬性總是使用nonatomic來宣告的呢,這是因為在iOS中使用同步鎖的開銷太大,這會帶來效能問題。在一般情況下,我們並不要求屬性必須具有原子性,因為這個原子性並不是說就是"執行緒安全了",如果我們需要實現執行緒安全,那麼還需要使用更為底層的同步鎖定機制才行,即便是使用atomic來宣告,不同的執行緒還是可能讀取到不同的屬性值,只是說這個屬性值是有效的,有意義的。

所以我們在開發iOS程式時,還是使用nonaomic來宣告,但是在macOS程式開發中,卻不會遇到這種效能瓶頸,效能配置不一樣嘛

readwrite/readonly

這個屬性特質,我們根據字面意思就能看出來,就是宣告屬性許可權的,這個也沒什麼好說的了。

strong/copy/assign/weak

這個可能是我們平時用的最多的,也是思考最多的,其實平時我們怎麼用,都是知道的,但是為什麼這麼用呢?

assign:"設定方法"只會針對"純量型別"進行賦值,例如CGFloat、NSInteger這種

strong:此特質象徵了一種擁有關係,在"設定方法"中,這種屬性是先保留新值,並且釋放舊值,然後將新值設定上去

copy:這種方法和strong型別有點相似,但是它並不是保留新值,而是直接就想新值拷貝,當屬性為NSString型別時,我們經常使用這種,那麼為什麼我們在NSString經常使用拷貝呢,以為我們在設定時,可能會傳進來一個NSMutableString物件,這個物件是NSString的子類,是可以賦值給NSString的,如果我們不使用拷貝,那麼當外部改變NSMultableString值時,我們的屬性值也會直接被修改掉,所以這時,我們就需要拷貝一份不可變的

weak:這個是一種弱引用,為這種方法設定時,既不會保留新值,也不會釋放舊值,當屬性所指的物件銷燬時,屬性值也會被清空,在ViewController中定義view時我們經常會使用到weak,但是我們經常還是將view宣告為strong,當然這使用起來不會有很大的影響,但是我們的應用在執行過程中,就會產生很多的沒用view屬性值沒有被釋放掉,佔用無效記憶體。所以建議大家在使用view時,還是宣告為weak

@property(nonatomic,weak) UILable *lable;

//初始化lable時

UILable * lable = [[UILable alloc] init];
[self.view addSubview: lable];
self.lable = lable;複製程式碼

方法名

在我們定義的屬性為Boolean值時,我們的習慣是獲取方法,一般是"is"
開頭,那麼我們就可以在宣告時,這樣書寫

@property(nonatomic,getter=isOn) Bool on;複製程式碼

屬性的獲取方法,就成了isOn;

我們在屬性中定義了屬性特質,那麼我們在書寫賦值時,就應該嚴格按照屬性特質賦值,例如,我們有一個初始方法,需要對我們的屬性NSString name賦值

- (instancetype)initWithName:(NSString *)name{
    if(self = [super init]){
    //此處就應該使用copy來對name賦值
        _name = [name copy];
    }
}複製程式碼

以"類族模式"隱藏實現細節

類族是一種隱藏抽象基類背後實現細節的很有用的模式。。那麼什麼叫做類族呢?

舉個例子:

在UIKit中有一個名叫UIButton的類,如果想要建立按鈕,我們可以呼叫一個類方法

+ (UIButton *)buttonWithType:(UIButtonType)type;複製程式碼

該方法返回的物件,取決於傳入按鈕的型別,然而,不管傳入的是什麼型別,返回的類都是繼承自同一個基類:UIButton。 這樣,所有繼承自UIButton的類組成了一個類族。

在系統框架中,使用到了很多的類族。。

那麼為什麼要這樣做呢?
UIButton這個例子,在實現時,使用者無需關心建立出來的按鈕是屬於哪一個類,也不用考慮按鈕的繪製細節,我只需要知道,我怎麼建立按鈕,如何設定標題。如何增加點選操作等。

建立類族

我們現在來假設一個處理僱員的類,每個僱員都擁有自己的名字和薪水這兩個屬性,管理者可以命令器執行日常工作。但是,各種僱員的工作內容卻不同,經理在帶領僱員做專案的時候,無需關心每個人怎樣完成自己的工作,只需要指示其開工即可。

首先我們定義一個基類:

typedef NS_ENUM(NSUinteger, PPSEmployeeType){
    PPSEmployeeTypeDeveloper,
    PPSEmployeeTypeDesigner,
    PPSEmployeeTypeFinance,
}

@interface PPSEmployee    :    NSObject

@property (nonatomic, copy) NSStirng *name;
@property (nonamotic, assign) NSUInteger salary;

//建立方法
+ (PPSEmployee *)employeeWithType:(PPSEmployeeType) type;

//指示開工
- (void)doADaysWork;

@end複製程式碼
@implementation PPSEmployee

+ (PPSEmployee *)employeeWithType:(PPSEmployeeType) type{
    switch (type){
        case PPSEmployeeTypeDeveloper:
            return [PPSEmployeeDeveloper new];
            break;
        case PPSEmployeeTypeDesigner:
            return [PPSEmployeeDesigner new];
            break;
        case PPSEmployeeTypeFinance:
            return [PPSEmployeeFinance new];
            break;
    }
}

- (void)doADaysWork{
    // 子類覆寫
}

@end複製程式碼

每個實體子類都是從基類繼承而來

@interface PPSEmployeeDeveloper    :    PPSEmployee
@end複製程式碼
@implementation PPSEmployeeDeveloper

- (void)doADaysWork{
    [self coding];
}

@end複製程式碼

我們上面實現的方式,是根據僱員的類別,建立僱員類的例項,這其實是一種工廠模式。在Java中,我們知道這種方式一般是通過抽象類來實現,但是OC中沒有抽象類這一說,於是開發者通常會在文件中寫明使用方法。

在Cocoa中 有很多的類族 大部分的集合型別都是類族 我們在一個物件是否是屬於某一個類時,如果我們採用下面的方式,往往得不到我們想要的效果:

id myArr = @[@"a",@"b"];
if([myArr class] == [NSArray class]){
    //這裡永遠不會跑到 因為 我們知道這裡[myArr class]返回的永遠是NSArr的一個子類,NSArray只是一個基類
}複製程式碼

當然,我們要做這個判斷的時候,應該都知道是使用isKindOfClass 這個方法,這個方法其實是用來判斷是否是同一類族,而不是某個類

在既有類中使用關聯物件存放自定義資料

有時候我們需要在物件中存放相關的資訊,這時候我們能想到的方法就是,建立一個子類,然後我們使用的時候 直接使用子類。 但是並不是所有情況都能夠這樣做,有時候類的例項可能是由於某種機制所建立的,我們開發者是沒有辦法建立出自己建立的子類的例項。幸好,我們可以通過OC的一項強大的特性"關聯物件"來解決這個問題。

那麼什麼事關聯物件呢?

我們可以給某個物件關聯許多其他物件,這些物件通過"鍵"來區分。儲存值的時候,可以通過指明儲存策略來維護記憶體,儲存策略就是你儲存的是一個NSString啊,那你就該把從儲存策略改為copy,類似於這種

下面是物件關聯型別:

關聯型別 等效的@property屬性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY copy
OBJC_ASSOCIATION_RETAIN retain

管理關聯物件的相關方法:

  • void objc _ setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 使用此方法 以給定的鍵和策略為某物件設定關聯物件
  • void objc _ getAssociatedObject(id object, void *key) 使用此方法根據給定的鍵從某物件中獲取相關的關聯物件的值
  • void objc_removeAssociatedObject(id object)使用此方法移除指定物件的全部關聯物件

使用這種方法時,我們可以把某物件想象成一個NSDictionary,把關聯到該物件的值理解為字典中的條目,那麼這些關聯物件,就相當於設定字典裡的值和獲取字典裡的值了

使用舉例

在iOS中,我們如果想要使用UIAlertView 我們需要這樣定義

 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"你確定嗎?" 
                                     message:@"可能沒那麼確定吧" 
                                     delegate:self
                                     cancelButtonTitle:@"取消" 
                                     otherButtonTitles:@"繼續", nil];
 [alert show];複製程式碼

然後我們需要實現UIAlertView的代理,來進行操作的識別判斷


-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    if (buttonIndex == 0) {
        [self doCancle];
    }else{
        [self doContinue];
    }
}複製程式碼

這樣寫,現在看來是沒有什麼問題的,但是如果我們需要在當前的一個類中,處理多個警告資訊,那麼程式碼將會變得複雜,我們需要在delegate中判斷當前的UIAlertView的資訊,根據不同的資訊實行不同的操作。

如果 我們能在建立UIAlertView的時候 就將這些操作做好,那麼,在delegate中我們就不需要判斷UIAlertView了。事實上這種方法是可行的。

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"你確定嗎?" 
                                 message:@"可能沒那麼確定吧" 
                                 delegate:self 
                                 cancelButtonTitle:@"取消" 
                                 otherButtonTitles:@"繼續", nil];
void (^block) (NSInteger) = ^(NSInteger buttonIndex){
    if (buttonIndex == 0) {
        NSLog(@"cancle");
    }else{
        NSLog(@"continue");
    }
};

//將block設定為UIAlertView的關聯物件
objc_setAssociatedObject(alert, PPSMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];複製程式碼

我們只需要在delegate中拿到block就行

 void (^block)(NSInteger) =  objc_getAssociatedObject(alertView, PPSMyAlertViewKey);
 block(buttonIndex);複製程式碼

相關文章