2019独角兽企业重金招聘Python工程师标准>>>

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);}// 注册SELSEL 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 {// 保存keypathobjc_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继承自selfClassobject_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为原始Classobject_setClass(self, [self superclass]);// 获取keyPathNSString* 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 methodsreturn [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];
}

转载于:https://my.oschina.net/FEEDFACF/blog/1791951

Objc Runtime在项目中该怎么用相关推荐

  1. JAVA Web项目中所出现错误及解决方式合集(不断更新中)

    JAVA Web项目中所出现错误及解决方式合集 前言 一.几个或许会用到的软件下载官网 二.Eclipse的[preferences]下没有[sever]选项 三.Tomcat的安装路径找不到 四.T ...

  2. objc runtime 动态增加属性

    objective-c中,有类别可以在不修改源码的基础上增加方法 使用运行时库,必须要先引入 objc/runtime.h 可以使用的函数如下: OBJC_EXPORT void objc_setAs ...

  3. 分享.NET开发中经常使用到的代码片段 完全从实际项目中提取出来,也可被反反复复的重复借用...

    几年前,一篇<ASP.NET开发人员经常使用的三十三种代码>非常流行,它总结了一些经常在ASP.NET开发中使用到的代码,直接可以拿来使用.今天重读这篇文章,有感而发,善于总结也是进步,于 ...

  4. Android项目中创建编译期的注解

    ==注解 生命周期为RetentionPolicy.RUNTIME,可在运行时通过反射获取. 生命周期为RetentionPolicy.CLASS, 编译期处理的注解,可以使用APT(Annotati ...

  5. 在项目中使用Google Closure Compiler

    现在的Web项目总是离不开大量JavaScript,而JS文件的体积也越来越大,也越来越影响页面的感知性能(Perceived Performance).因此,我们会对JS文件进行压缩,一方面是使用G ...

  6. (转) eclipse项目中.classpath文件详解

    背景:对于java项目中.classpath文件中的相关定义一直不是很了解,有必要进行深入的学习. 1 前言 在使用eclipse或者myeclipse进行Java项目开发的时候,每个project( ...

  7. java调用项目中的文件_详解eclipse项目中.classpath文件的使用

    1 前言 在使用eclipse或者myeclipse进行java项目开发的时候,每个project(工程)下面都会有一个.classpath文件,那么这个文件究竟有什么作用? 2 作用 .classp ...

  8. 泛型委托在项目中的应用

    Action<T> 泛型委托:封装一个方法,该方法只采用一个参数并且不返回值.可以使用此委托以参数形式传递方法,而不用显式声明自定义的委托.该方法必须与此委托定义的方法签名相对应.也就是说 ...

  9. XamarinSQLite教程在Xamarin.Android项目中使用数据库

    XamarinSQLite教程在Xamarin.Android项目中使用数据库 在Xamarin.Android项目中使用预设数据库的具体操作步骤如下: (1)创建一个Xamarin.Android项 ...

最新文章

  1. linux 多路径重启,(linux多路径连接iScsi存储重启自动连接.docx
  2. SAP MM 移动类型343不开放给业务人员之思考
  3. vivo检测自启动权限_VIVO手机不如苹果?那是你不知道还有这几个功能,用了就离不开...
  4. GARFIELD@02-25-2005
  5. 简单C语言程序的执行过程
  6. 浅析Kerberos原理,及其应用和管理
  7. c# 对象json互相转换_Go语言进阶之路(六):内置JSON库和开源库gjson
  8. C#制作QQ截图的自动框选功能的个人思路(二)设置Hook
  9. STL 之 vector 的使用 (转载)
  10. vim - 之快速删除指定符号的内容
  11. android开发 写一个自定义形状的按键
  12. 信号与系统学习笔记与代码实现3-周期信号的傅里叶级数表示
  13. LOJ#6198. 谢特(SAM+01Trie树合并)
  14. C#如何遍历文件夹下的所有文件
  15. Linux - 权限管理(用户)
  16. 深度学习深度信念网络DBNs—简易详解
  17. C# 调用微软自带的语音识别
  18. 把Excel批注的“红三角”放在单元格左上角_干货!《跟王佩丰学Excel教程》笔记...
  19. 俄勒冈大学计算机科学专业,俄勒冈大学计算机
  20. 2020科目一考试口诀_2021年科目一考试口诀

热门文章

  1. mysql insert 运算_MySql insert插入操作的3个小技巧分享
  2. Redis配置优化和使用
  3. html背景视频模糊效果,怎么给竖屏视频添加模糊背景效果?
  4. java 隐藏任务栏,在Java中隐藏Windows任务栏?
  5. vue 监听浏览器页面关闭_前方高能,这是最新的一波Vue实战技巧,不用则已,一用惊人...
  6. cookie怎样存储数据?
  7. java加载jdbc驱动,加载JDBC驱动
  8. 虚拟机linux扩展磁盘容量,kvm虚拟机扩展磁盘容量
  9. redis set不可重复_Redis的使用
  10. c语言 long 用法,C语言中long long的用法