觀“編寫高質量iOS與OC 代碼的52個有效方法”有感(二)· 對象、消息、運行時

MacBook Pro.jpg

6、理解“屬性”這一概念

  • Objective-C面向對象語言編程。
    對象就是“基本構造單元”,開發者用對象存儲并傳遞數據。
    對象之間傳遞數據并且執行任務的過程就叫做“消息傳遞”
    程序運行起來。相關支持的代碼就叫做“Objective-C runtime”。

  • “屬性”是Objective-C的一項特性,用于封裝對象中的數據。
    Objective-C對象會把數據保存為各種實例變量。
    實例變量通過“存取方法”(accessmethod)訪問。
    getter 用于讀取變量值
    setter用于寫入變量值

  • @property
    對象接口的定義中,可以使用屬性。 -> 標準的寫法
    編譯器會自動寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量。

@property (nonatomic ,copy) NSString *nameString;

等同于下面的這種寫法

- (void)setNameString:(NSString *)nameString;

- (NSString *)nameString;
  • 屬性優勢:

    • 編譯器自動編寫與訪問這些屬性所需的方法 =“自動合成”(autosynthesis)
      這個過程是由編譯器在編譯時期執行的,所以編輯器看不到這些“合成方法”的源代碼。
    • 除了生成方法代碼之外,編譯器還會自動向類中添加適當類型的實例變量,并且添加下劃線作為實例變量的名字。
      比如上面的名稱就為_nameString。
  • 自己實現存取方法

    • 使用@dynamic關鍵字 。告訴編譯器不讓自動創建實例變量、存取方法!
  • 屬性特質

    • 原子性
      默認情況下。編譯器合成的方法通過通過鎖定機制確實其原子性(atomicity)。
      如果屬性具備nonatomic特質,則不使用同步鎖。
    • 讀/寫權限
      readwrite(讀寫):具有setter和getter方法!
      readonly(只讀):僅有getter方法。
    • 內存管理
      assign:針對CGFloat或者NSInteger等“純量類型”(基礎數據類型 和C數據類型)簡單賦值,不更改索引計算。
      strong:為屬性設置新值時,保留新值,釋放舊值,再將新值設置上去。
      weak:既不保留新值,也不釋放舊值,同assign。
      copy:與strong類似,但是并不保留新值。而是將其“拷貝”。
      經典案列:NSString 確保對象中的字符串值不會無意間改動。

7、在對象內部盡量直接訪問實例變量

  • 使_XXX直接訪問實例變量。
    • 速度快。不經過Objective-C的“方法派發”(method dispatch),編譯器直接訪問保存對象實例變量的那塊內存
    • 直接訪問實例變量,不調取“設置方法”(setter)這就繞過了內存管理語義。
    • 直接訪問實例變量,也不會觸發“鍵值觀測”(Key-Value Observing, KVO)
      這樣做是否有問題還是取決于具體的對象行為。
  • 使用self.XXX來訪問
    • 有助于排查與之相關的錯誤,因為可以給“setter”和“getter”設置斷點
    • 懶加載也是需要通過“獲取方法”來訪問屬性。否則。實例變量永遠不會初始化。
  • 建議:
    除特殊情況。
    在讀取實例變量的時候采用直接訪問的形式,
    在設置實例變量的時候通過屬性來做。

8、理解“對象等同性”這一概念

  • 等同性(equality)
    一般情況下相比較我們都是用的 “==” 但是比較出來的結果未必是我們想要的。
    因為“==”比較是指針本身,而不是其對象。

  • NSObject中的“isEqual”

    • 判斷兩個對象的等同性。
    • NSObject協議中2個判斷等同性的關鍵方法
    - (BOOL)isEqual:(id)object;
    @property (readonly) NSUInteger hash;
    

定義:如果“isEqual:”方法判斷兩個對象相等,那么其hash方法也必須返回同一個值。
如果兩個對象的hash方法返回同一個值,“isEqual”未必會認為兩者相等。

  • NSString實現了一個獨有的等同性判斷方法:isEqualToString
    該方法比“isEqual”快,因為該方法快遞對象規定為NSString。而“isEqual”還要執行額外步驟,因為“isEqual”不知道受測對象類型。

  • NSArray與NSDictionary也有類似的特殊的等同性方法。
    “isEqualToArray”與“isEqualToDictionary”
    如果檢測到受測對象不是數組或者字典就會拋出異常。

  • 自己實現等同性方法原理:

    • 首先,判斷兩個指針是否相等。相等則說明指向同一對象!
    • 其次,比較兩個對象所屬的類(考慮到父類與子類的判斷)
    • 然后,檢測每個屬性是否相等(不要盲目逐步檢查每條屬性,而是根據需求來定制)
    • 最后,實現hash方法:
      等同性約定:若兩個對象相等,則哈希碼相等,但是兩個哈希碼相同的對象卻未必相等。(應使用計算速度快而且哈希碼碰撞幾率低的算法,否則會影響性能)
  • 小技巧:

    • 如果要重寫“isEqual”方法。
      如果受測參數與接收消息對象屬于同一個類,就調用自己寫的判定方法。否則就交給超類來判斷。
    • 等同性的判斷深度。
      如果判斷兩個對象的所有屬性是否相等,這樣的叫“深度等同性判定”。
      不過更多時候是根據其中部分數據即可判斷二者是否等同。

9、以“類族模式”隱藏實現細節

  • 類族:把實現細節隱藏在一套簡單的公共接口后面。
  • 例子:
  • 系統框架中有很多類族。
    比如UIButton創建的時候的類方法
    UIButton *button = [UIButton buttonWithType:<#(UIButtonType)#>]
    這樣該方法返回的對象,決定傳入的按鈕類型。
  • NSArray與可變類型NSMutableArray。
    有兩個抽象基類,一個不可變數組,一個可變數組。
  • 動手創建一個類族:
    • 需求:一個公司分為2種人:1、管理者。2、工人。分別干不同的事!
    • 創建一個基于NSObject的類
#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, SCUserType) {
    SCUserTypeManager,
    SCUserTypeWorker,
};

@interface SCUser : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;


//創建
+ (SCUser *)scUserWithType:(SCUserType)type;

//做事情
- (void)doWork;

@end
  • .m的實現:
#import "SCUser.h"
#import "SCUserWorker.h"
#import "SCUserManager.h"

@implementation SCUser

+ (SCUser *)scUserWithType:(SCUserType)type {
    switch (type) {
        case SCUserTypeManager:
            return [SCUserManager new];
            break;
            
        case SCUserTypeWorker:
            return [SCUserWorker new];
            break;
    }
}

- (void)doWork {
    
}

@end

每個“實體子類” 都是從基類繼承來的。比如:

#import "SCUser.h"

@interface SCUserManager : SCUser

@end

//.m的實現
#import "SCUserManager.h"

@implementation SCUserManager

- (void)doWork {
    NSLog(@"管理者巡邏");
}

@end

10、在既有類中使用關聯對象存放自定義數據

  • 關聯對象(Associated Object)
    為了解決某些情況(無法從對象所屬的類中繼承一個子類,然后用子類對象存放相關信息)
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    此方法可以給定的鍵和策略為某對象設置關聯對象值

    objc_getAssociatedObject(id object, const void *key)
    

    此方法根據給定的鍵從某個對象中獲取相關對象值
    objc_removeAssociatedObjects(id object)

    此方法移除指定對象的全部關聯對象。

  • 項目中遇到的問題(runtime)
    當時是為了解決NSURL中有個URLWithString的方法會默認把帶中文的鏈接給轉碼。因為程序中很多地方都用到了這個方法,很明顯,一個一個的修改是很費力氣的。用到了runtime中的一個交換方法:
    自己實現一個函數,然后與系統的函數交換一下。完美解決問題。
    主要代碼:

#import <objc/runtime.h>

@implementation NSURL (Unicode)

+ (void)load {
    /*
     self:UIImage
     誰的事情,誰開頭 1.發送消息(對象:objc) 2.注冊方法(方法編號:sel) 3.交互方法(方法:method) 4.獲取方法(類:class)
     Method:方法名
     
     獲取方法,方法保存到類
     Class:獲取哪個類方法
     SEL:獲取哪個方法
     imageName
     */
    // 獲取imageName:方法的地址
    Method URLWithStringMethod = class_getClassMethod(self, @selector(URLWithString:));
    
    // 獲取wg_imageWithName:方法的地址
    Method sc_URLWithStringMethod = class_getClassMethod(self, @selector(sc_URLWithString:));
    
    // 交換方法地址,相當于交換實現方式2
    method_exchangeImplementations(URLWithStringMethod, sc_URLWithStringMethod);
    
}


+ (NSURL *)sc_URLWithString:(NSString *)URLString {
    
    NSString *newURLString = [self IsChinese:URLString];
    return [NSURL sc_URLWithString:newURLString];
}

11、理解 objc_msgSend 的作用

  • Objective-C中給對象發消息

    [object message:parameter];
    

    原理:
    objc_msgSend(id self, SEL cmd,...)
    核心函數,這個是“參數個數可變的函數”,能接收兩個或者兩個以上的參數。
    第一個參數代表接收者。第二個參數代表方法的名字。
    上面的OC代碼換成函數就為:
    objc_msgSend(object, @selector(message:),parameter);
    objc_msgSend函數根據接收者和方法名來調用適當的方法。
    過程:
    1.先到所屬的類尋找“方法列表”,找到就跳轉
    2.找不到,就會沿著繼承體系繼續向上查找,找到再跳轉。
    3.實在找不到就執行“消息轉發”
    看起來調用一個方法需要很多步驟。但是objc_msgSend會將匹配結果緩存在“快速映射表”里。這樣子執行就快了。

  • 其他的方法:

    // 待發送消息返回結構體
    objc_msgSend_stret
    
    //消息返回的是浮點數
    objc_msgSend_fpret
    
    //給超類發消息  例如[super XXX];
    objc_msgSendSuper

大家碼代碼時期能更多的了解一些底層的工作原理。在調試的時候會幫助你很多。

12、理解消息轉發機制

  • 消息轉發:
    因為Objective-C中,在編譯期向類發送無法解讀的消息并不會報錯,因為在運行期還可以繼續向類添加方法。因此,編譯器在編譯時無法確定類中到底會不會有某個方法實現。
    當對象接受到無法解讀的消息后,就會啟動“消息轉發”機制,程序員可以由此告訴對象如何處理未知的消息。
    大家在開發期間肯定見過這樣的錯誤:
unrecognized selector sent to instance 0x610000026560
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SCUserWorker sss]: unrecognized selector sent to instance 0x610000026560'

錯誤原因:是因為接收者無法理解sss的這個方法名,因此致使程序崩潰。

  • 對象在接收到無法解讀的消息后,會依次調用下列方法。
    + (BOOL)resolveInstanceMethod:(SEL)sel
    在這個方法中,參數就是未知的方法名稱。在這里你可以解決問題。
    - (id)forwardingTargetForSelector:(SEL)aSelector
    這個是備援接收者,也就是給接收者第二次處理的機會,如果可以找到備援對象則將其返回,若找不到就返回nil。
  • 完整的消息轉發
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    啟動完整的消息轉發機制,首先要創建 NSInvocation 對象,把尚未處理的消息相關的細節全部封與其中(方法名,目標以及參數)。
    使用:只需改變調用目標,使消息在新目標上得以調用就好了,和第二種方法“備援接收者”等效。
消息轉發.png
  • 案列:使用class_addMethod動態添加方法
    假設我故意一個類只在.h聲明了方法 沒有在.m中實現該方法
    結果就會報上面的錯,接收者無法解讀消息。讓你用runtime動態添加方法你會怎么辦呢?

    • 考慮
      原因是因為沒有實現該方法,所以無法解讀,那么我們要為其添加方法。
      那么這個方法添加到哪呢?該如何添加?
    • 動手
      首先找到沒有實現方法的那個類,在其.m添加
      + (BOOL)resolveInstanceMethod:(SEL)sel
      這個方法。上述講到過,無法解讀消息時會第一時間調用這個方法,我們可以在這來解決問題。
      接下來要用到runtime中的 class_addMethod 方法
      class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
      1.Class cls : 參數表示添加新方法的類
      2.SEL name : 表示方法名稱
      3.IMP imp : 表示由編譯器生成的、指向實現方法的指針。也就是說,這個指針指向的方法就是我們要添加的方法。
      4.const char **types :最后一個參數 *types 表示我們要添加的方法的返回值和參數。
      主要代碼在下面:
      eat:只聲明沒有實現的方法。
      SCUser:創建的類。
  • C語言函數的實現
    記得導入
    #import <objc/runtime.h>

void sayHello (id self,SEL _cmd) {
    NSLog(@"Hello");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(eat)) {
        class_addMethod([SCUser class], sel, (IMP)sayHello, "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
};
  • OC形式的實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(eat)) {
        class_addMethod([SCUser class], sel, class_getMethodImplementation(self, @selector(sayHello)), "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
};



- (void)sayHello {
    NSLog(@"hahaha");
}

13、用“方法調配技術”調試“黑盒方法”

  • 在第10條中有介紹過交換方法的案列。
  • 交換方法:
  • 在運行期,向類中新增或者替換方法。
  • 使用另一個方法替換一個方法可以向其添加新功能。
  • 只有調試程序中才會用的運行期修改方法,不宜濫用。

14、理解“類對象”的用意

  • 看下面的代碼
    NSString *nameStirng = @"小伙子";
    可以理解:nameString 為存放內存地址的變量。而NSString自身的數據就存在地址中。所有的Objective-C對象都是如此。
  • 還可以這樣寫:
    id nicknameStirng = @"牛";
    對于通用的對象類型id,因為其自身已經是指針了。所以可以這樣寫。
  • 比較
    兩者語法意義相同,
    唯一區別:如果聲明的時候指定了具體類型,那么在該類實例上調用沒有的方法時,編譯器會發出警告信息。
  • id的定義
typedef struct object {
    Class isa;
} *id;

說明每個對象結構體的首個成員是Class類的變量。通常稱為“isa”指針。

  • metaclass
    metaclass就是isa指向的一個結構體。
  • 舉例
    用一個例子說明:
    大學期間,小明的輔導員要調查小明家里有沒有黨員。
    首先,輔導員通過身份證號找到小明的檔案,發現小明不是黨員,從小明的檔案中發現小明父母的身份證號,通過小明父母的身份證號找到小明父母的檔案,發現小明父母都是黨員。
    身份證號 = isa ,檔案 = metaclass。
    (這是作者自己的粗淺理解,如果不對,歡迎指出)
  • 檢測繼承體系
    • isKindOfClass
      isKindOfClass來確定一個對象是否是一個類的成員,或者是派生自該類的成員。
    • isMemberOfClass
      isMemberOfClass只能確定一個對象是否是當前類的成員。

接下來也將會繼續整理。如果覺得有用請點個喜歡!

您的支持將是我繼續寫作的動力!謝謝。

觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(一)· 熟悉Objective-C
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(二)· 對象、消息、運行時
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(三)· 接口與API設計
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(四)· 協議與分類

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容