Objc Runtime在项目中该怎么用
从以下四个方面讲述Objc Runtime在项目中的使用场景,使用的例子来自于github上的开源项目FDFullscreenPopGesture、GVUserDefaults 以及系统中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_setAssociatedObject
、objc_getAssociatedObject
设置和获取关联对象
主要的思路如下:
-
- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
方法是类似系统添加KVO监听的方法,该方法中- 保存
observer
和keyPath
参数;动态的创建和注册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)sel
和resolveInstanceMethod:(SEL)sel
处理类方法和实例方法,可以在该方法中使用class_addMethod
动态添加方法处理,返回YES表示该步骤可处理,否则表示不可处理,继续执行下一步 - 备援接收者:
forwardingTargetForSelector:(SEL)aSelector
方法返回一个可以处理该消息的对象完成消息的转发处理 - 完整的消息转发:
forwardInvocation:(NSInvocation *)anInvocation
,如果以上两个步骤都不能处理,最终会执行到这一步,anInvocation对象包含了消息详细信息,包括id target
、SEL 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
方法的消息转发给accessorSetter
C函数处理,Getter
方法的消息转发给accessorGetter
C函数处理,accessorSetter
和accessorGetter
最终还是通过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];
}