ObjC之RunTime(下)

scorpiozj發表於2013-11-03

之前通過學習官方文件對runtime有了初步的認識,接下來就要研究學習runtime到底能用在哪些地方,能如何改進我們的程式。

本文也可以從icocoa瀏覽。

Swizzling

Swizzling可以分為method swizzling和class(isa)swizzling兩種。顧名思義就是將方法/類在執行時替換掉。

Method Swizzling

在執行時替換/修改某個方法——可以是自己寫的方法也可以是系統的方法——當然一般是用於替換框架類中的方法。

//ZJView.m -Swizzling
+ (void)swizzleSetFrame
{
    SEL originalSel = @selector(setFrame:);
    Class myClass = [self class];
    Method originMethod = class_getInstanceMethod(myClass, originalSel);
    const char *originType = method_getTypeEncoding(originMethod);
    originalIMP = (void *)method_getImplementation(originMethod);
    class_replaceMethod(myClass, originalSel, (IMP)mySetFrame, originType);

}
static void mySetFrame(id self, SEL _cmd,CGRect frame)
{
    NSLog(@"run mySetFrame");
    if (originalIMP)
    {
        frame.origin.y += 20;
        originalIMP(self, _cmd, frame);
    }
}

如上是在自定義的View類裡替換了setFrame方法(注意這樣做在實際的編碼中沒有意義,因為完全可以通過繼承做到這一點,這裡只是從程式碼的角度來理解method swizzling)。替換的方法可以放在+load裡,或者自行顯式的呼叫。這裡需要注意的是stackoverflow上有很多是這樣進行替換的:

if(class_addMethod() )
{
    class_replaceMethod();
}
else
{
    method_exchangeImplementations();
}

之所以按這樣的流程處理是想先檢測下class下是否有需要被替換的selector。但其實runtime已經考慮了這種情形,所以直接進行class_replaceMethod即可。 通常情況下,我們可以通過繼承來過載某個方法,但對於沒有繼承關係的類的方法過載ObjC提供了Category。比如在iOS5前,要自定義NavigationBar的背景,我們就是通過建立一個category來過載drawRect。但是這樣使用Category的話有以下弊端:

  1. 方法的原先實現被完全過載了,無法呼叫原先的實現。尤其是為庫類中方法過載的時候,我們往往希望獲得原先的實現,而不是簡單的全盤替換。
  2. 如果有多個category的時候,無法保證哪一個勝出。

所以category往往用於給框架類新增方法。在這種情況下,method swizzling就是一個很好的選擇。由於現在iOS的版本也日趨變多,有時也會遇到某些類的方法在不同iOS下有不同的表現。那麼,我們就可以根據實際情況,徵對不同的iOS版本,選擇繼續用預設的實現,或者自定義的實現,或者兩者的結合。此外為了避免衝突,method swizzling最好在+load函式裡呼叫。 鑑於在實踐中使用method swizzling的場合較少,個人體會不夠深刻,暫時個人理解只能在這個層次了。我在stakoverflow上找到一篇帖子,對於使用method swizzling需要注意的地方做了詳盡的說明。

Class(isa) Swizzling

runtime通過object_setClass來動態的替換物件的class。值得注意的是新class的長度要和原先class的長度一致。此外,KVO的實現就是利用了isa swizzling,iOS6PTL中也對此進行了說明。比如物件a要觀察b的某個屬性,在新增observer的時候,系統會生成一箇中間類,並把b的isa指標指向這個新類。這也說明因為是在runtime時處理KVO,使用KVO時一定要注意遵循相應的命名規範。

關聯指標(Associative References)

關聯指標指的是在runtime給某物件新增一個變數,新增的變數不會對原有的類產生任何影響——這是優於ObjC擴充套件(Extension)的地方,主要使用以下方法:

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

對於框架類,由於沒有原始碼,可以通過這種方式新增一些變數。比如UIView,UIAlertView都可以新增tag方便以後重新獲得,我們也可以通過關聯指標使其與某個物件相關聯。

內省(Introspection)

好像也能叫Reflection(反射),不過我不確定。Introspection是OO語言都應該具備的特性,它指的是在runtime時物件通過請求可以查詢自己類的關鍵資訊的能力。首先ObjC語言本身就有這樣的介面,比如:

-isKindOfClass:; -isMemberOfClass:
-respondsToSelector: ;-conformsToProtocol:
-isEqual:

這些分別對應著:

  1. 檢視所屬類以及類的繼承關係;
  2. 檢視是否實現了某個方法或者協議
  3. 判斷物件是否相等

這些功能在NSObject類和協議裡定義的,一般情況下,iOS上的類都能使用。 接下來要介紹兩個開源庫:Mantle 和Overcoat,他們是內省的重要應用。 在實際中,我們通常會在程式中設計一個Model層,用於Json和Object之間的轉化。比較完備的Model類會考慮到:

  1. NSString屬性
  2. 通過NSString生成的如NSURL等屬性
  3. NSNumber,NSDate等屬性的格式化
  4. 實現NSCoding,NSCopying協議等等
  5. 。。。。

一般情況下,程式裡的Model不會只有一個,全部這樣實現的話,很顯然有很大一部分程式碼是“冗餘”的但卻不能通過繼承之類的方法規避。Mantle就是一個很好的選擇,它將你的注意力集中到Model的設計,實現部分只需要一些必須的方法,如:

+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
    //提供Json中key與Model中屬性的對應,如果key與屬性一致可以忽略不寫
}
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key
{
    //對一些需要進行格式化處理的key進行選擇性操作
}

Introspection在Mantle中的應用就是runtime時通過class_copyPropertyList來獲取類的屬性列表,從而簡化我們的工作。 至於Overcoat則是AFNetworking和Mantle的一個結合。AFNetworking是繼ASINetwork後,iOS和OS X上出名的網路庫,而且維護更新比較活躍。Overcoat的主要工作就是把通過AFnetworking獲取的結果轉化成物件,轉化的過程就是使用了Mantle,並且把這部分工作放在了後臺進行。Overcoat提供了一個例子ReadingList,大家可以好好研究下。注意ReadingList是需要使用到cocoapods的,不知道的朋友,out啦~

 動態屬性(方法)

之前提到過的,我們可以在runtime時新增方法,更進一步的,我們可以動態的新增屬性而不用實現宣告,下面的程式碼來自gist

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,strong) NSMutableDictionary *properties;
@end

@implementation Person

    -(id) init {
        self = [super init];
        if (self){
            _properties = [NSMutableDictionary new];
        }
        return self;
    }

    // generic getter
    static id propertyIMP(id self, SEL _cmd) {
        return [[self properties] valueForKey:NSStringFromSelector(_cmd)];
    }

    // generic setter
    static void setPropertyIMP(id self, SEL _cmd, id aValue) {

        id value = [aValue copy];
        NSMutableString *key = [NSStringFromSelector(_cmd) mutableCopy];

        // delete "set" and ":" and lowercase first letter
        [key deleteCharactersInRange:NSMakeRange(0, 3)];
        [key deleteCharactersInRange:NSMakeRange([key length] - 1, 1)];
        NSString *firstChar = [key substringToIndex:1];
        [key replaceCharactersInRange:NSMakeRange(0, 1) withString:[firstChar lowercaseString]];

        [[self properties] setValue:value forKey:key];
    }

    + (BOOL)resolveInstanceMethod:(SEL)aSEL {
        if ([NSStringFromSelector(aSEL) hasPrefix:@"set"]) {
            class_addMethod([self class], aSEL, (IMP)setPropertyIMP, "v@:@");
        } else {
            class_addMethod([self class], aSEL,(IMP)propertyIMP, "@@:");
        }
        return YES;
    }

@end

int main(int argc, char *argv[]) {
	@autoreleasepool {
		Person *p = [Person new];
		[p setName:@"Jon"];
		NSLog(@"%@",[p name]);
	}
}

以上是現階段對runtime的總結,更多內容有待進一步的探索,歡迎一起學習討論。

相關文章