Objc Runtime在專案中該怎麼用

aron1992發表於2019-04-04

Objc Runtime在專案中該怎麼用

從以下四個方面講述Objc Runtime在專案中的使用場景,使用的例子來自於github上的開源專案FDFullscreenPopGestureGVUserDefaults 以及系統中KVO的底層實現例子

  • Method Swizzling
  • 動態方法新增
  • isa Swizzling
  • 訊息轉發

Method Swizzling

Method Swizzling簡單的講就是方法替換,是一種hook技術,一個典型的Method Swizzling例子如下,註釋部分說明了為什麼這麼做。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 在進行Swizzling的時候,我們需要用class_addMethod先進行判斷一下原有類中是否有要替換的方法的實現。
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            // 如果class_addMethod返回YES,說明當前類中沒有要替換方法的實現,我們需要在父類中去尋找。這個時候就需要用到method_getImplementation去獲取class_getInstanceMethod裡面的方法實現。然後再進行class_replaceMethod來實現Swizzling。
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 如果class_addMethod返回NO,說明當前類中有要替換方法的實現,所以可以直接進行替換,呼叫method_exchangeImplementations即可實現Swizzling。
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
    // 由於我們進行了Swizzling,所以其實在原來的- (void)viewWillAppear:(BOOL)animated方法中,呼叫的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實現。所以不會造成死迴圈。相反的,如果這裡把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會造成死迴圈。因為外面呼叫[self viewWillAppear:animated];的時候,會交換方法走到[self xxx_viewWillAppear:animated];這個方法實現中來,然後這裡又去呼叫[self viewWillAppear:animated],就會造成死迴圈了。
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
複製程式碼

FDFullscreenPopGesture 這個庫使用的就是Method Swizzling技術實現的全屏手勢返回的效果

UINavigationController (FDFullscreenPopGesture)分類的load方法中替換了系統的pushViewController:animated:方法

+ (void)load
{
    // Inject "-pushViewController:animated:"
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(pushViewController:animated:);
        SEL swizzledSelector = @selector(fd_pushViewController:animated:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
複製程式碼

在替換的方法中禁用了系統的邊緣返回手勢,新增了自定義的手勢來處理

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        
        // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
        
        // Forward the gesture events to the private handler of the onboard gesture recognizer.
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
        
        // Disable the onboard gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    // Handle perferred navigation bar appearance.
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    // Forward to primary implementation.
    if (![self.viewControllers containsObject:viewController]) {
        [self fd_pushViewController:viewController animated:animated];
    }
}
複製程式碼

然後在自定義的手勢的回撥方法gestureRecognizerShouldBegin中處理手勢的返回

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    // Ignore when no view controller is pushed into the navigation stack.
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }
    
    // Ignore when the active view controller doesn't allow interactive pop.
    UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
    if (topViewController.fd_interactivePopDisabled) {
        return NO;
    }
    
    // Ignore when the beginning location is beyond max allowed initial distance to left edge.
    CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
    if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
        return NO;
    }
    
    // Ignore pan gesture when the navigation controller is currently in transition.
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }
    
    // Prevent calling the handler when the gesture begins in an opposite direction.
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    BOOL isLeftToRight = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight;
    CGFloat multiplier = isLeftToRight ? 1 : - 1;
    if ((translation.x * multiplier) <= 0) {
        return NO;
    }
    
    return YES;
}
複製程式碼

動態方法新增

GVUserDefaults 是一個屬性和NSUserDefaults之間實現自動寫入和讀取的開源庫,該庫(當前最新版本 1.0.2)使用到的技術就是動態方法新增,專案中使用到的主要技術點列舉如下:

  • class_copyPropertyList獲取類的property列表
  • property_getName獲取property名稱
  • property_getAttributes獲取property的屬性,可以參考Property Attribute Description Examples
  • sel_registerName註冊SEL
  • class_addMethod新增方法

主要看generateAccessorMethods方法的實現,該方法自動生成屬性對應的Getter和Setter方法,關鍵的地方有新增了註釋

- (void)generateAccessorMethods {
    unsigned int count = 0;
    // 獲取類的屬性列表
    objc_property_t *properties = class_copyPropertyList([self class], &count);

    self.mapping = [NSMutableDictionary dictionary];

    for (int i = 0; i < count; ++i) {
        objc_property_t property = properties[i];
        // 獲取property名稱
        const char *name = property_getName(property);
        // 獲取property的屬性
        const char *attributes = property_getAttributes(property);

        char *getter = strstr(attributes, ",G");
        if (getter) {
            // getter修飾的屬性:@property (nonatomic, getter=zytCustomerGetter) float customerGetter; -> "Tf,N,GzytCustomerGetter"
            getter = strdup(getter + 2);
            getter = strsep(&getter, ",");
        } else {
            getter = strdup(name);
        }
        SEL getterSel = sel_registerName(getter);
        free(getter);

        char *setter = strstr(attributes, ",S");
        if (setter) {
            // setter 修飾的屬性:@property (nonatomic, setter=zytCustomerSetter:) float customerSetter; -> Tf,N,SzytCustomerSetter:
            setter = strdup(setter + 2);
            setter = strsep(&setter, ",");
        } else {
            asprintf(&setter, "set%c%s:", toupper(name[0]), name + 1);
        }
        // 註冊SEL
        SEL setterSel = sel_registerName(setter);
        free(setter);

	// 同一個屬性的`Getter`或者`Setter`方法在`self.mapping`對應的值是一樣的,
        NSString *key = [self defaultsKeyForPropertyNamed:name];
        [self.mapping setValue:key forKey:NSStringFromSelector(getterSel)];
        [self.mapping setValue:key forKey:NSStringFromSelector(setterSel)];

        IMP getterImp = NULL;
        IMP setterImp = NULL;
        char type = attributes[1];
        switch (type) {
            case Short:
            case Long:
            case LongLong:
            case UnsignedChar:
            case UnsignedShort:
            case UnsignedInt:
            case UnsignedLong:
            case UnsignedLongLong:
                getterImp = (IMP)longLongGetter;
                setterImp = (IMP)longLongSetter;
                break;

            case Bool:
            case Char:
                getterImp = (IMP)boolGetter;
                setterImp = (IMP)boolSetter;
                break;

            case Int:
                getterImp = (IMP)integerGetter;
                setterImp = (IMP)integerSetter;
                break;

            case Float:
                getterImp = (IMP)floatGetter;
                setterImp = (IMP)floatSetter;
                break;

            case Double:
                getterImp = (IMP)doubleGetter;
                setterImp = (IMP)doubleSetter;
                break;

            case Object:
                getterImp = (IMP)objectGetter;
                setterImp = (IMP)objectSetter;
                break;

            default:
                free(properties);
                [NSException raise:NSInternalInconsistencyException format:@"Unsupported type of property \"%s\" in class %@", name, self];
                break;
        }

        char types[5];

        snprintf(types, 4, "%c@:", type);
        // 新增方法
        class_addMethod([self class], getterSel, getterImp, types);
        
        snprintf(types, 5, "v@:%c", type);
        // 新增方法
        class_addMethod([self class], setterSel, setterImp, types);
    }

    free(properties);
}
複製程式碼

其中property_getAttributes(property)獲取到的property屬性字串的第二位的型別資訊可以參考Apple官方文件

Type Encodings

對應的程式中定義了TypeEncodings列舉

enum TypeEncodings {
    Char                = 'c',
    Bool                = 'B',
    Short               = 's',
    Int                 = 'i',
    Long                = 'l',
    LongLong            = 'q',
    UnsignedChar        = 'C',
    UnsignedShort       = 'S',
    UnsignedInt         = 'I',
    UnsignedLong        = 'L',
    UnsignedLongLong    = 'Q',
    Float               = 'f',
    Double              = 'd',
    Object              = '@'
};
複製程式碼

比如物件型別的屬性,經過如下的處理

getterImp = (IMP)objectGetter;
setterImp = (IMP)objectSetter;

//...

class_addMethod([self class], getterSel, getterImp, types);
class_addMethod([self class], setterSel, setterImp, types);
複製程式碼

使用屬性Getter或者Setter最終會呼叫以下的方法

static id objectGetter(GVUserDefaults *self, SEL _cmd) {
    NSString *key = [self defaultsKeyForSelector:_cmd];
    return [self.userDefaults objectForKey:key];
}

static void objectSetter(GVUserDefaults *self, SEL _cmd, id object) {
    NSString *key = [self defaultsKeyForSelector:_cmd];
    if (object) {
        [self.userDefaults setObject:object forKey:key];
    } else {
        [self.userDefaults removeObjectForKey:key];
    }
}
複製程式碼

這裡用到的defaultsKeyForSelector方法定義如下,把屬性Getter或者Setter方法對映為對應的屬性的儲存的Key的字串,同一個屬性的Getter或者Setter方法在self.mapping對應的值是一樣的,詳細的儲存對映資訊到self.mapping中可以檢視generateAccessorMethods方法。

- (NSString *)defaultsKeyForSelector:(SEL)selector {
    return [self.mapping objectForKey:NSStringFromSelector(selector)];
}
複製程式碼

isa Swizzling

系統的KVO的實現是基於isa swizzling實現的,建立一個NSObject的分類模擬KVO的實現,主要用到技術點

  • objc_allocateClassPair新增一個類
  • objc_registerClassPair註冊新增的類
  • object_setClass修改當前類的class,也就是isa指標
  • class_addMethod類動態新增方法
  • objc_msgSend使用底層C的方法執行方法呼叫
  • objc_setAssociatedObjectobjc_getAssociatedObject設定和獲取關聯物件

主要的思路如下:

  • - (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context 方法是類似系統新增KVO監聽的方法,該方法中
    • 儲存 observerkeyPath 引數;動態的建立和註冊KVO類;
    • 使用object_setClass修改當前物件的isa指標;
    • class_addMethod動態新增一個監聽的keyPath屬性對應的Setter方法
  • keyPath屬性對應的Setter方法的實現:
    • 使用object_setClass修改物件的isa指標為原始的類,後面使用Setter方法設定新值呼叫的objc_msgSend方法才能正確執行;
    • 使用objc_getAssociatedObject獲取繫結的關聯物件中keyPath,還原出Getter方法和Setter方法,使用Getter方法獲取舊的值,使用Setter方法設定新值;
    • 使用objc_getAssociatedObject獲取繫結的關聯物件中observer,向 observer 物件傳送屬性變換的訊息;
    • 最後重置物件的isa指標為動態建立的KVO類,下一次的流程才能正常執行
@implementation NSObject (YTT_KVO)

- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    // 儲存keypath
    objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 獲取當前類
    Class selfClass = self.class;
    
    // 動態建立KVO類
    const char * className = NSStringFromClass(selfClass).UTF8String;
    char kvoClassName[1000];
    sprintf(kvoClassName, "%s%s", "YTT_KVO_", className);
    Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
    if (!kvoClass) {
        // Nil if the class could not be created (for example, the desired name is already in use).
        kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
    }
    objc_registerClassPair(kvoClass);
    
    // 修改當前類指向為動態建立的KVO子類,kvoClass繼承自selfClass
    object_setClass(self, kvoClass);
    
    // 動態新增一個方法:setXxx()
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
    class_addMethod(kvoClass, sel, (IMP)setValue, NULL);
}

void setValue(id self, SEL _cmd, id value) {
    
    // 儲存當前的Class,重置Class使用
    Class selfClass = [self class];
    // 設定Class為原始Class
    object_setClass(self, [self superclass]);
    // 獲取keyPath
    NSString* keyPath = objc_getAssociatedObject(self, "keyPath");

    // KVO 回撥引數
    NSMutableDictionary* change = [NSMutableDictionary dictionary];
    change[NSKeyValueChangeNewKey] = value;
    
    // 獲取舊的值
    SEL getSel = NSSelectorFromString([NSString stringWithFormat:@"%@", keyPath]);
    if ([self respondsToSelector:getSel]) {
        id ret = ((id(*)(id, SEL, id))objc_msgSend)(self, getSel, value);
        if (ret) {
            change[NSKeyValueChangeOldKey] = ret;
        }
    }
    
    // 給原始類設定資料
    SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
    if ([self respondsToSelector:setSel]) {
        ((void(*)(id, SEL, id))objc_msgSend)(self, setSel, value);
    }
    
    // 傳送通知
    id observer = objc_getAssociatedObject(self, "observer");
    SEL observerSel = @selector(ytt_observeValueForKeyPath:ofObject:change:context:);
    if ([observer respondsToSelector:observerSel]) {
        ((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
    }
    
    // 重置class指標,這樣再次呼叫物件方法會走到這裡面
    object_setClass(self, selfClass);
}

@end
複製程式碼

訊息轉發

訊息轉發的步驟有以下四個:

  • 動態方法解析:resolveClassMethod:(SEL)selresolveInstanceMethod:(SEL)sel處理類方法和例項方法,可以在該方法中使用class_addMethod動態新增方法處理,返回YES表示該步驟可處理,否則表示不可處理,繼續執行下一步
  • 備援接收者:forwardingTargetForSelector:(SEL)aSelector方法返回一個可以處理該訊息的物件完成訊息的轉發處理
  • 完整的訊息轉發:forwardInvocation:(NSInvocation *)anInvocation,如果以上兩個步驟都不能處理,最終會執行到這一步,anInvocation物件包含了訊息詳細資訊,包括id targetSEL selector、引數和返回值資訊等等。該方法需要和methodSignatureForSelector:(SEL)aSelector方法一起使用,可以使用NSInvocation的例項方法invokeWithTarget:(id)target完成訊息的轉發
  • 如果以上的步驟都不能處理,最終會由NSObject的方法doesNotRecognizeSelector:(SEL)aSelector,丟擲一個unrecognized selector sent to instance xxx的異常

訊息轉發流程
訊息轉發流程

GVUserDefaults 是一個屬性和NSUserDefaults之間實現自動寫入和讀取的開源庫,該庫(0.4.1之前的舊版本)使用到的技術就是訊息轉發

GVUserDefaults庫中使用的是訊息轉發中的動態方法解析resolveInstanceMethod方法,Setter方法的訊息轉發給accessorSetterC函式處理,Getter方法的訊息轉發給accessorGetterC函式處理,accessorSetteraccessorGetter最終還是通過NSUserDefaults實現屬性對應的Key的設定和讀取,關鍵的程式碼如下:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    NSString *method = NSStringFromSelector(aSEL);

    if ([method isEqualToString:@"transformKey:"] || [method isEqualToString:@"setupDefaults"]) {
        // Prevent endless loop for optional (and missing) category methods
        return [super resolveInstanceMethod:aSEL];
    }

    if ([method hasPrefix:@"set"]) {
        class_addMethod([self class], aSEL, (IMP) accessorSetter, "v@:@");
        return YES;
    } else {
        class_addMethod([self class], aSEL, (IMP) accessorGetter, "@@:");
        return YES;
    }
}

- (NSString *)_transformKey:(NSString *)key {
    if ([self respondsToSelector:@selector(transformKey:)]) {
        return [self performSelector:@selector(transformKey:) withObject:key];
    }

    return key;
}

id accessorGetter(GVUserDefaults *self, SEL _cmd) {
    NSString *key = NSStringFromSelector(_cmd);
    key = [self _transformKey:key];
    return [[NSUserDefaults standardUserDefaults] objectForKey:key];
}

void accessorSetter(GVUserDefaults *self, SEL _cmd, id newValue) {
    NSString *method = NSStringFromSelector(_cmd);
    NSString *key = [[method stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@""] stringByReplacingOccurrencesOfString:@":" withString:@""];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] lowercaseString]];
    key = [self _transformKey:key];

    // Set value of the key anID to newValue
    [[NSUserDefaults standardUserDefaults] setObject:newValue forKey:key];
}
複製程式碼

相關文章