Effective Object C 2.0 『物件、訊息、執行期』

碼碼碼碼上有錢發表於2018-02-04

第6條:屬性(property)

@interface SomeClass : NSObject

@property NSString *instanceObj;

@end
複製程式碼

上面程式碼寫出來的類與下面是等價的:

 @interface SomeClass : NSObject 

- (NSString*)instanceObj;
- (void)setInstanceObj:(NSString *)instanceObj;

@end
複製程式碼

因為宣告瞭instanceObje屬性

@property NSString *instanceObj;

相當於在標頭檔案生成getter、setter方法頭,如下:

- (NSString*)instanceObj;
- (void)setInstanceObj:(NSString *)instanceObj;
複製程式碼

以及在.m實現檔案新增了例項變數_instanceObj,及getter、setter方法的具體實現,如下:

@synthesize instanceObj = _instanceObj; 

- (NSString*)instanceObj {
    return _instanceObj;
}

- (void)setInstanceObj:(NSString *)instanceObj {
    _instanceObj = instanceObj;
}
複製程式碼

@dynamic,意譯動態的,@dynamic可使instanceObj編譯不生成getter,setter方法,不生成例項變數。

@dynamic instanceObj;

所以@dynamic宣告屬性後,如直接呼叫getter、setter是會造成執行時的crash的。

屬性特質

@property (nonatomic,readonly ,copy) NSString *someObject;

  • 原子性
    • 原子性(atomic):編譯器所合成的方法會通過鎖定機制確保其原子性,意思就是訪問時開同步鎖保證其多執行緒同時設定/訪問時的資料安全。
    • 非原子性(nonatomic)非執行緒安全,一般情況下非特殊要求安全性的,都建義使用此修飾,因為沒有同步鎖,性讀取能高。
  • 讀寫許可權
    • readwirte可讀寫,意思是同時擁有getter,setter方法,其實就是@synthesize實現的。
    • readonly只讀,意思是隻有getter方法,沒有實現setter方法。可以能過分類(category)的方式實現屬性的改寫。
  • 記憶體管理語義
    • assign(賦值引用)常用於基本數值型別的賦值操作,也可用於物件Object,不會使retainCount + 1,但物件釋放時不會清空指標,容易造成野指標。
    • strong(強引用)表明該屬性定義了一種『擁有關係』,該屬性在設定新值時,會先保留新值,釋放舊值,再賦值新值上去。在OC裡面只要一個物件被一個強型別的指標引用,該物件就不會被釋放,反之沒有強型別的指標引用,該物件會被釋放掉。
    • weak(弱引用/歸零引用)表明該屬性定義了一種『非擁有關係』,該屬性在設定新值時,既不保留新值也不釋放舊值,跟assign一樣是簡單的賦值,但weak是用於物件型別的,nt,屬性值也會置nil,但assign修飾物件時不會置nil。
    • unsafe_unreatin 與assign相同,一般情況下assign是用於基本數值型別,unsafe_unreatin用於物件型別『非擁有關係』,當屬性所屬的物件釋放時,不會清空指標,容易造成野指標,所以它是unsafe。
    • copy 與strong類似,該屬性在賦值時,不會保留新值,而是copy一份,再賦值copy的內容給新值。好比我們在修飾NSString的時候一般都會用copy而不用strong,原因是當設定新值的時候,傳過來的值是一個可變的字串NSMutableString的時候使用strong強引用的話,那麼傳過來的這個新值NSMutableString有變動依然會改動屬性的值,這不是安全的做法。所以使用copy,拷貝一份不可變的值是安全的做法。
    • setter = < name > 或者 getter = < name > 改變getter,setter名字的做法,一般情況不推薦使用。

第7條:物件內部儘量直接訪問例項變數

  • 例項變數訪問方式
    • 直接訪問,如:_object
      • 不需要呼叫setter/getter,繞過其語義,速度快;
    • 通過屬性訪問,如:self.object,[self setObject:value]
      • 需要呼叫getter/setter方法,不繞過記憶體管理語;
      • 可觸發KVO;
      • 可通過屬性排查相關的錯誤,如傳值的問題,打斷點除錯。 作者推薦的做法是寫入變數時,使用設定方法setter,訪問變數時使用例項變數直接訪問,如此一來既可提高讀取效率,亦可慣徹setter帶來的相關好處。 不過有兩個特例:
  1. 初始化方法和dealloc方法中,需要直接訪問例項變數來進行設定屬性操作。因為如果在這裡沒有繞過set方法,就有可能觸發其他不必要的操作。
  2. 惰性初始化(lazy initialization)的屬性,必須通過屬性來讀取資料。因為惰性初始化是通過重寫get方法來初始化例項變數的,如果不通過屬性來讀取該例項變數,那麼這個例項變數就永遠不會被初始化。

第8條:物件的等同性

平時的編碼中,要比較兩相同數值型別的變數,我們直接用==來判斷這變數是否相等,但==用在兩物件型別的時候,比較的卻是指標,也就是物件所在的記憶體地址。==對比物件出來的結果有時候未必是我們想要的。應該使用NSObject協議裡面的『isEqual』方法來判斷兩物件的等同性。

OC中的字串有EqualToString:方法來比較。 陣列有isEqualToArray:方法和字典也有isEqualToDictionary:方法來比較。

深度等同性判定,例如比較兩個陣列,除了使用isEqualToArray方法外,還有深度的判斷方式:先判斷陣列的元素數量是否等同,對等位置物件是否相同,如果都相同,則兩陣列為等同。

第9條:類簇模式,隱藏實現細節

平時的開發中,如遇要繪製多種形狀的需求,程式設計師立馬腦暴一番,提手碼一個Shape基類,然後再碼Circle、Triangle、Square…等等的一些繼承Shape的子類。這是正常的做法,沒問題。但有可能需求中要求要繪製的圖形很多,幾十個甚至上百,該如何?按照邏輯我們依然要建立N個形狀的子類。這時候,如果要繪製其中的一個形狀了,我們需要在幾十個甚至上百個子類中找到我們所需的子類,無疑這是比較低效率的做法。

那麼『類簇模式』就是為了靈活地解決這個問題而存在的一種設計模式。把基類作為『抽象基類』以應對多個類,隱藏其背後的實現細節。什麼意思呢?以剛才的繪製圖形為例,在『抽象基類』中建立一個『工廠方法』,通此方法可以實現所有的子類的建立,而你要做的只需要做的是在呼叫工廠方法的時候,傳入你需要建立子類的type,上程式碼:

  • 基類實現
typedef NS_ENUM(NSInteger,ShapeType) {
    ShapeTypeCircle,            //圓
    ShapeTypeTriangle,          //三角形
    ShapeTypeSquare             //四方形
};

@interface Shape : NSObject

//實現建立所有形狀子類的工廠方法
+ (Shape *)shapeWithType:(ShapeType)type;
- (void)draw;

@end
複製程式碼
@implementation Shape

+ (Shape *)shapeWithType:(ShapeType)type {
    switch (type) {
        case ShapeTypeCircle:
            {
                return [Circle new];
            }
            break;
        case ShapeTypeTriangle:
        {
            return [Triangle new];
        }
            break;
        case ShapeTypeSquare:
        {
            return [Square new];
        }
            break;
        default:
            break;
    }
}

- (void)draw {
    //no do anything
}

@end
複製程式碼
  • 子類實現
@interface Circle : Shape
@end

@implementation Circle

-(void)draw {
    //具體畫圓的邏輯程式碼
}

@end
複製程式碼

其實cocoa系統框架裡面存在許多類簇,如UIButton,NSArray…,所以,我們平時要建立一個UIButton的時候,直接呼叫[UIButton buttonWithType:UIButtonTypeContactAdd]就可以了,很是方便。 當然,使用類簇還是有幾點要注意的,書中作者提到:

  1. 型別的判斷

  2. 子類應該繼承自類簇中的抽象基類

  3. 子類應該定義自己的資料儲存方法,因為抽象基類只是一個殼,不作任何的資料儲存。

  4. 子類應該覆寫超類指定要覆寫的方法,意思新增子類的時候,要看超類的開發文件。

    關於第1點的型別判斷,平時我們判斷一物件的型別的做法是

    id person = [[Person alloc] init];
    
    if ([Person class] == [person class]) {
        NSLog(@"is equal");
    }
    複製程式碼

    但是在類簇中使用型別判斷則需要注意,不能像上面這樣寫,如

    id shape = [Shape shapeWithType:ShapeTypeCircle];
    if ([shape class] == [Shape class]) {
        //here never run
    }
    複製程式碼

    應該這樣判斷:

    if ([shape isKindOfClass:[Shape class]]) {
        NSLog(@"here we go");
    }
    複製程式碼

第10條:關聯物件

到這裡就屬於runtime的內容了。什麼是關聯物件,顧名思義即物件之間的關聯。打個比方Cocoa Foundation框架裡的NSString類,現在你想為這個類增加一個屬性要怎麼做?用Category?但類別只能新增方法,是不能新增屬性的。這時候關聯物件就派上用場了,使用執行時方法,動態關聯物件以達到新增屬性的效果。

先看看關聯物件的3個使用方法:

   //為某個物件設定關聯物件的值:
   objc_setAssociatedObject(id  _Nonnull object, const void * _Nonnull key, id  _Nullable value, objc_AssociationPolicy policy)
   /*object 關聯的主物件,const void *key 常量指標作為key,value 被關聯的物件,policy 儲存策略,定義記憶體管理語義。*/
   2、根據key獲取關聯物件的值:
   objc_getAssociatedObject(id  _Nonnull object, const void * _Nonnull key)
   3、移除關聯的值:
   objc_removeAssociatedObjects(id  _Nonnull object)
複製程式碼

具體的用法,用個例子來說明。平時的開發中,總離不開UIButton的使用,使用者點選Button,Button事件觸發執行事件方法。正常的程式碼實現是先建立UIButton的例項,設定button的相關屬性,然後為button新增一個點選事件,並在@selector上指定方法名。當有點選行為發生時,則會跳到selector指定的方法去執行。但這過程是建立button與點選事件後的方法是分離開的,可讀性有那麼點不大直觀。現在用關聯物件可使程式碼看上去整體性會更強一些。

先建一個UIButton的類別(Category)

@interface UIButton (ClickRespone)

typedef void (^ClickRespone)(UIButton *sender);
//點選回撥的方法
- (void)buttonClickRespone:(ClickRespone)respone;

@end

//.m檔案
@implementation UIButton (ClickRespone)

- (void)buttonClickRespone:(ClickRespone)respone {
   //utt
   objc_setAssociatedObject(self, @selector(buttonClick), respone, OBJC_ASSOCIATION_COPY);
   //新增事件
   [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}

//點選事件
- (void)buttonClick {
  ClickRespone clickRespone =  objc_getAssociatedObject(self, @selector(buttonClick));
   if (clickRespone) { //如果實現了block則迴應點選事件
       clickRespone(self);
   }
}
@end
複製程式碼

具體使用:

 UIButton *someBtn = [UIButton buttonWithType:UIButtonTypeCustom];
   /*
    .
    */
 [someBtn buttonClickRespone:^(UIButton *sender) {
    NSLog(@"someBtn click");
 }];
複製程式碼

看上去,好像是直觀了一些。

第11條 objc_msgSend的作用

在iOS開發中,使用物件呼叫一個方法用術語來說就是『傳遞訊息』。訊息有名稱『name』,有選擇子『selector』,可接受引數,也可能有返回值,其實就是平時我們所說的方法啦。

但OC中,對某個物件傳遞訊息時,就會使用動態繫結機制來決定具體要呼叫的方法。什麼是動態繫結機制?OC的物件在收到訊息之後,要呼叫哪個方法完全由執行時決定的,甚至可以在程式執行時改變。 給物件發訊息:

id returnValue = [someObject messageName:parameter];
複製程式碼

someObject為接收者,messageName為選擇子selector,編譯器遇到類似的訊息會將它轉換成C語言的函式呼叫(在底層所有的OC的方法都是C語言函式),如下:

void objc_msgSend(id self, SEL cmd,…)
複製程式碼

這是引數個數可變的函式,能接兩個及兩個以上的引數,第一個為訊息的接收者,第二個為選擇子,後面那些就是訊息中的引數了。最終編譯器會將訊息轉化為OC的方法:

id returnValue = objc_msgSend(someObject,
                              @selector(messageName),
                              parameter);
複製程式碼

這個方法執行之後,先會去訊息接收者所屬類中搜尋『方法列表』list of methods,如果找到與選擇子名稱相同的方法就跳轉到其實現程式碼。如果找不到就順著繼承鏈向上一層一層地找到跳轉為止。如果還是不能找到的話,那就執行『訊息轉發』。

這裡有個小疑問是:是不是每次傳遞訊息得了這麼費勁,先找類中的list of methods,找不到再找繼承鏈。。之類的。其實不是,每個類都有一塊快取,裡面放著一張『快速對映表』。它的作用是objc_msgSend如果匹配到方法就會將其快取到裡面,那下次如果再次遇到同樣的訊息,執行就會快很多。

第12條 訊息的轉發

OC的訊息傳遞的最長鏈:向物件傳遞訊息—>『快速對映表』—>list of methods(當前類找不到順著繼承鏈向上)—>訊息轉發。那是訊息轉發是怎麼的一回事?看圖

Effective Object C 2.0 『物件、訊息、執行期』

其實訊息轉發分為兩大步驟:

1、動態方法解析(dynamic method resolution)

訊息轉發時,會先詢問接收者是否有動態新增方法以處理當前這個未知的選擇子(seletor)。會根據情況執行以下兩個方法:

//當選擇子方法為物件方法時執行
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//當選擇子方法為類方法時執行
+ (BOOL)resolveClassMethod:(SEL)sel;
複製程式碼

用一個例子來解釋『動態方法解析』是怎麼一回事,如下:

@interface MessageTransmit : NSObject

@property (nonatomic ,copy)NSString *someMessage;

@end

//.m檔案
@interface MessageTransmit()

//用來放屬性值的資料區
@property (nonatomic ,strong) NSMutableDictionary *propertyStore;

@end

@implementation MessageTransmit

- (instancetype)init
{
    self = [super init];
    if (self) {
        _propertyStore = [NSMutableDictionary dictionary];
    }
    return self;
}

//設定@dynamic之後,編譯不會生成someMessage的getter,setter方法
@dynamic someMessage;

//當執行時未能從cache、及類及類的繼承鏈中的list of methods匹配到someMessage的getter,setter方法的時候,先會呼叫此方法進行動態方法解析。
+ (BOOL)resolveInstanceMethod:(SEL)sel { 
    //獲取選擇子方法名
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {        //如果選擇子執行的方法裡開始是set,那就證明這是個setter方法
        //新增setter方法
        class_addMethod(self, sel,(IMP)autoSomeMessageSetter,"v@:@");
    }else {
        //新增getter方法
        class_addMethod(self, sel, (IMP)autoSomeMessageGetter,"@@:");
    }
    return YES;
}

//Getter C方法的實現
id autoSomeMessageGetter(id self,SEL _cmd) {
    MessageTransmit *typeSelf = (MessageTransmit *)self;
    NSMutableDictionary *store = typeSelf.propertyStore;
    //key
    NSString *key = NSStringFromSelector(_cmd);
    return [store valueForKey:key];
}

//setter C方法的實現
void autoSomeMessageSetter(id self,SEL _cmd,id value) {
    MessageTransmit *typeSelf = (MessageTransmit *)self;
    NSMutableDictionary *store = typeSelf.propertyStore;
    
    //因為上面取key的是getter的方法名,所以set的時候key也要跟getter的key保持一致。
    NSString *keyString = NSStringFromSelector(_cmd);
    NSMutableString *key = [keyString mutableCopy];
    // 去掉:
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // 去掉set
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // 取出與getter key相同的串
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [store setObject:value forKey:key];
    } else {
        [store removeObjectForKey:key];
    }
} 
@end
複製程式碼

呼叫

MessageTransmit *msgTransmit = [[MessageTransmit alloc] init];
[msgTransmit setSomeMessage:@"set the someMessage succed."];
NSLog(@"someString is %@",msgTransmit.someMessage); 
//最終結果為列印:set the someMessage succed.
複製程式碼

2、完整的訊息轉發(full forwarding mechanism),而完整的訊息轉發又分兩小點:

  • 2.1 備援接收者

    當『動態方法解析』resolveInstanceMethod或resolveClassMethod返回NO的時候,訊息轉發將繼續向後進行,runtime會請接收者再看看還有其它物件能處理這個未知選擇子,方法如下:

    //指定Target接收未知的選擇子,進行訊息轉發
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    複製程式碼

    如果還是沒有物件可以處理當前的訊息,forwardingTargetForSelector返回nil的話,接下來就會進行完整的訊息轉發。

  • 2.2 完成的訊息轉發

    在 forwardInvocation: 訊息傳送前,Runtime 系統會向物件傳送methodSignatureForSelector: 訊息,並取到返回的方法簽名用於生成 NSInvocation 物件。所以重寫 forwardInvocation: 的同時也要重寫 methodSignatureForSelector: 方法,否則會拋異常。

    //為當前選擇子的方法進行簽名,只有簽名的選擇子才能進行後續的轉發
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector;
    //完成的訊息轉發
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    複製程式碼

    由上面的描述可看出,forwardingTargetForSelector以及forwardInvocation都可以進行訊息的轉發,它們的區別在於forwardingTargetForSelector只能指定一個物件進行轉發,而forwardInvocation可以是多個物件的轉發。 還有就是class_addMethod的引數“v@:@”是型別編碼,具體解釋請參考:型別編碼

第13條:用『方法調配技術』除錯『黑盒方法』

眾所周知,Object C物件接收到訊息之後,究竟要呼叫何種方法只能在執行時才能確定。即使指定了選擇子(selector)的方法名,在執行時也可以改變其具體的呼叫方法,這功能聽上去就相當的粗大了。但具體這樣做有什麼卵用?意思是我們要改變一個類的功能就不需要拿到類的原始碼然後改寫功能的本身,亦不需要繼承其為子類重寫功能了。而且這種改寫後的新功能在類的所有例項用都有效,這種方案,專業上稱之為『方法調配method swizzling』,江湖也有種叫法,叫什麼『黑魔法』。其中到底是怎麼的一回事? 類的方法列表會把選擇子的名稱對映到方法實現之上,這樣一來『動態訊息派發系統』就可以根據選擇子的名字,找到要呼叫的方法了。這些方法都是以函式指標的形式來表示,而且這類的指標有個專業名字就『IMP』,其原型:

id (*IMP)(id,SEL,...)

說白了,IMP即方法的記憶體地址,能過IMP就能呼叫方法。

作者以NSString為例,NSString類可以響應lowercaseString、uppercaseString、capitalizedString等選擇子。這張對映表中的每個選擇子都對映到了不同的IMP之上,如下圖:

Effective Object C 2.0 『物件、訊息、執行期』

Object C的執行時提供了一些方法,用來操作上面這個表。如新增選擇子,改變選擇子方法的實現,還可以交換兩個選擇子所對映到的指標(IMP),比如這樣:

Effective Object C 2.0 『物件、訊息、執行期』

互動方法具體的實現,涉及到兩個方法:

method_exchangeImplementations(Method method1, Method method2);
複製程式碼

上述方法,不是太傻應該能看出來,這是用來交換兩方法的實現的。

class_getInstanceMethod(Class class, SEL selector);
複製程式碼

而上面這個是獲取例項的方法的,也就是返回method_exchangeImplementations的引數Method.

具體實現程式碼:

@interface NSString (NCYAddition)

- (NSString *)ncy_lowercaseStrig

@end

@implementation NSString (NCYAddition)

- (NSString *)ncy_lowercaseStrig {
    NSString *lowercase = [self ncy_lowercaseStrig];//執行期會將ncy_lowercaseStrig對應於lowercaseString的方法的實現,所以這裡是不會產生遞迴的死迴圈
    NSLog(@"%@ => %@", self, lowercase);//輸出語句,便於除錯
    return lowercase;
}

@end

//具體呼叫
Method lowercaseMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method ncyLowercaseMethod = class_getInstanceMethod([NSString class], @selector(ncy_lowercaseStrig));
method_exchangeImplementations(lowercaseMethod, ncyLowercaseMethod);
    
NSString *testString = @"Hello, You Little shiT."; 
複製程式碼

列印的結果為:『Hello, You Little shiT. => hello, you little shit』

最後作者有提到,此技術應該用於除錯程式的時候使用,濫用之,會使程式碼可讀性差且難以維護。

第14條:理解『類物件』的用意

Object C的物件本質是指向某塊記憶體資料的指標。例如說:

NSString *someStr = @"some string";
複製程式碼

someStr前面有個*號,說明它是個指定,而且這個指標指向記憶體中存放some string字串的區域。someStr本質是一個記憶體地址,它標誌記憶體中某塊資料。Object C中所有的物件都是如此。 id是能用的物件型別,但由於它自己本身就已經是指標,所以在使用它接收物件的時候,能夠:

id someStringObj = @"some string";

對比上下兩種方式宣告字串語法意義上是相同的,區別在於使用具體型別時,物件在呼叫其型別沒有的方法時,編譯是會報錯的。而使用id則不會,它會在執行時報錯。

id 在執行時庫裡面的定義是:

typedef struct objc_object { 
    Class isa
} *id
複製程式碼

id是一個isa指標,什麼是isa指標,稍後講到。再看看Class物件在執行期庫裡的定義:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
複製程式碼

Class中的首個變數也是isa指標,isa指標它其實是指定『元類meta class』的,意思是當前物件所屬的型別。前面的id someStringObj = @"some string";是一個(is a)NSString,所以isa指標就指向了NSString。那Class的定義裡面也有isa指標,也表明Class其實也是一個Object C的物件,它也有自己的所屬型別。 結構體中的super class是定義超類的,確定繼承關係。name為型別的名稱const不可變。ivars類的成員列表。methodslist方法列表等等。

假如你定義了一個類名為SomeClass,SomeClass的子類從NSObject中繼承而來,那其繼承體系圖如下:

Effective Object C 2.0 『物件、訊息、執行期』

在繼承體系中查詢型別資訊

isMemberOfClass :判斷是否是某個類的例項; isMemberOfClass :判斷物件是否為某個類或派生類的例項;

NSMutableDictionary *dict = [NSMutableDictionary new];  
[dict isMemberOfClass:[NSDictionary class]]; ///< NO 
[dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES 
[dict isKindOfClass:[NSDictionary class]]; ///< YES 
[dict isKindOfClass:[NSArray class]]; ///< NO 
複製程式碼

相關文章