@xifenglang-33250
2017-01-06T16:50:42.000000Z
字数 11824
阅读 2372
iOS_Runtime
#import <objc/runtime.h>
对于Runtime Swizzling,这篇文章值得一看:《Objective-C的方法替换》,俺目前只实现了对一个类的2个“方法”进行Method Swizzling,对不同的类进行Method Swizzling出现了奇怪的问题,已放弃研究。
Method Swizzling尽量少用,这是把利剑,用好了能省很多事,用不好就可能成为猪队友。
/**【Method Swizzling-慎用】用于替换同一类的2个[实例]方法。建议放在+(void)load方法配合DispatchOnce一起使用@param originalSEL 被替换的SEL@param objectSEL 用于替换的自定义SEL@param objectClass 进行Method Swizzling的Class*/void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass);void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {Method originalMethod = class_getInstanceMethod(objectClass, originalSEL);Method replaceMethod = class_getInstanceMethod(objectClass, objectSEL);// 判断是否实现方法if (originalMethod == NULL || replaceMethod == NULL) {NSLog(@"\n.\tWarning! JK_ExchangeInstanceMethod 失败! [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));return;}// 将replaceMethod实现添加到objectClass中,并且将originalSEL指向新添加的replaceMethod的IMP。BOOL add = class_addMethod(objectClass, originalSEL, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));if (add) {// 添加成功,再将objectSEL指向原有的originalMethod的IMP,实现交换// 当前类或者父类没有实现originalSEL会执行这一步class_replaceMethod(objectClass, objectSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {// 已经实现customMethod,对systemMethod和customMethod的实现指针IMP进行交换method_exchangeImplementations(originalMethod, replaceMethod);}}
用作测试的类继承关系如:ViewController -> BaseViewController -> UIViewController
用作测试的originalSEL:@selector(viewWillAppear:)
进行以下操作:
对ViewController进行ExchangeInstanceMethod,objectSEL:@selector(edf_viewWillAppear:),而且ViewController和父类BaseViewController都【没有实现viewWillAppear】,
add = YES;
对UIViewController基类进行ExchangeInstanceMethod,objectSEL:@selector(jk_viewWillAppear:),
add = NO。
在操作2的基础上,如果在任何手动创建的类.m的(+ load)方法中执行操作1【不实现viewWillAppear:】,
add = YES,对于ViewController只会执行@selector(edf_viewWillAppear:),不会执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)。标记点1
在操作2的基础上,如果在任何手动创建的类.m除【(+ load)以外】的方法中执行操作1【不实现viewWillAppear:】,
add = YES,对于ViewController既执行@selector(edf_viewWillAppear:)又执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)。
在操作2的基础上,如果在任何手动创建的类.m的(+ load)方法中执行操作1,但是ViewController或者父类BaseViewController正常【实现了viewWillAppear:方法】,
add = NO,对于ViewController既执行@selector(edf_viewWillAppear:)又执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:),和操作4结果差不多。
初步得出的结论是 当前类或者父类没有实现originalSEL时(UIViewController排除在外),add = YES,并且执行class_addMethod前后的class_getInstanceMethod(objectClass, originalSEL)的指针地址不一样。
add = NO的时候,class_addMethod前后的class_getInstanceMethod(objectClass, originalSEL)的指针地址一样。
个人对Swizzling结果的理解是对2个SEL所关联的2个IMP进行了调换。
交换前:调用SEL1,会执行IMP_1对应的代码
SEL1 ---> IMP_1
SEL2 ---> IMP_2
交换后:调用SEL2,会执行IMP_1对应的代码
SEL1 ---> IMP_2
SEL2 ---> IMP_1
建议进行Swizzling的objectClass是拥有originalSEL的最顶层父类,先从顶层父类开始选,比如UIViewController,而不是底层的子类,比如普通的控制器类。
/**【Method Swizzling-慎用】用于替换同一类的2个[类]方法。建议放在+(void)load方法使用@param originalSEL 被替换的SEL@param objectSEL 用于替换的自定义SEL@param objectClass 进行Method Swizzling的Class*/void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass);void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {Method originalMethod = class_getClassMethod(objectClass, originalSEL);Method replaceMethod = class_getClassMethod(objectClass, objectSEL);if (originalMethod == NULL || replaceMethod == NULL) {NSLog(@"\n.\tWarning! JK_ExchangeClassMethod 失败! [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));return;}/// 交换实例方法的写法在这失效了,所以直接进行了method_exchangeImplementations,待研究method_exchangeImplementations(originalMethod, replaceMethod);}
[+ load]中调用[super load],父类的[+ load]就会被调用2次,所以加上dispatch_once以防重复Swizzling,安全性更高。#ifdef DEBUG进行判断。
@implementation UIViewController (Swizzling)+ (void)load {#ifdef DEBUGstatic dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(viewWillAppear:), @selector(jk_viewWillAppear:), self);});#endif}- (void)jk_viewWillAppear:(BOOL)animated{[self jk_viewWillAppear:animated];NSString * className = NSStringFromClass([self class]);if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_"]) {NSLog(@"即将显示:%@ 备注:%@",self.class,self.view.accessibilityIdentifier);}}@end
加载UIImage有2种方式:
[UIImage imageNamed...][UIImage imageWithContentsOfFile...]方式1会不断增加缓存,直到APP进程被杀才会释放,适用于频繁使用的图片,存放在Assets.xcassets中图片必须[UIImage imageNamed...]。
方式2不会被缓存,Image对象释放即可释放内存,适用于不怎么用的图片,而且存在于[NSBundle mainBundle]中,不能存放在Assets.xcassets中。
@implementation UIImage (Swizzling)+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeClassMethod(@selector(imageNamed:), @selector(jk_imageNamed:), self);});}/**优先无缓存加载图片,imageWithContentsOfFile*/+ (UIImage *)jk_imageNamed:(NSString *)name{UIImage * image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:[name hasSuffix:@"jpg"] ? nil : @"png"]];if (!image) {image = [self jk_imageNamed:name];}if (image == nil) {NSLog(@"\nWarning! 图片加载失败! imageName:%@",name);}return image;}@end
打印NSDictionary中的中文,针对网络请求到的数据
@implementation NSDictionary (Swizzling)+ (void)load {#ifdef DEBUGstatic dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);});#endif}- (NSString *)jk_descriptionWithLocale:(id)locale {if (self == nil || self.allKeys.count == 0) {return [self jk_descriptionWithLocale:locale];} else {@try {NSError * error = nil;NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];if (error) {return [self jk_descriptionWithLocale:locale];} else {return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];}} @catch (NSException *exception) {return [self jk_descriptionWithLocale:locale];}}}@end
按需使用)打印NSArray中的中文,针对网络请求到的数据,一般Json都会用字典包裹,所以可以不用或者注释下面的代码,毕竟数组一般都是存Model,Json转换不了,加了@try之后发生异常时会被断点捕获。(正常情况下转换NSJSONSerialization不支持的类型,会直接Crash)
@implementation NSArray (Swizzling)+ (void)load {#ifdef DEBUGstatic dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);});#endif}- (NSString *)jk_descriptionWithLocale:(id)locale {if (self == nil || self.count == 0) {return [self jk_descriptionWithLocale:locale];} else {@try {NSError * error = nil;NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];if (error) {return [self jk_descriptionWithLocale:locale];} else {return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];}} @catch (NSException *exception) {return [self jk_descriptionWithLocale:locale];}}}@end
慎用, 可能会变相的造成数据异常
@implementation NSArray (SafeSwizzling)static const char * kArrayClass = "__NSArrayI";+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexI:), objc_getClass(kArrayClass));});}- (id)jk_objectAtIndexI:(NSUInteger)index {if (index < self.count) {return [self jk_objectAtIndexI:index];} else {NSLog(@"数组查询越界,return <null>。 --[NSArray objectAtIndex:]-- index=%zd array.count=%zd",index,self.count);return [NSNull null];}}@end@implementation NSMutableArray (SafeSwizzling)static const char * kMutArrayClass = "__NSArrayM";+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(insertObject:atIndex:), @selector(jk_insertObject:atIndex:), objc_getClass(kMutArrayClass));JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexM:), objc_getClass(kMutArrayClass));});}- (void)jk_insertObject:(id)anObject atIndex:(NSUInteger)index {if (index > self.count) {NSLog(@"数组插值越界 --[NSMutableArray insertObject: atIndex:]-- object=%@ index=%zd array.count=%zd",anObject,index,self.count);} else if (anObject != nil) {[self jk_insertObject:anObject atIndex:index];} else {NSLog(@"传入空值Nil --[NSMutableArray insertObject: atIndex:]-- object=%@ index=%zd",anObject,index);}}- (id)jk_objectAtIndexM:(NSUInteger)index {if (index < self.count) {return [self jk_objectAtIndexM:index];} else {NSLog(@"数组查询越界,return <null>。 --[NSMutableArray objectAtIndex:]-- index=%zd array.count=%zd",index,self.count);return [NSNull null];}}@end
慎用,可能会变相的造成数据异常
@implementation NSMutableDictionary (SafeSwizzling)static const char * kMutDictClass = "__NSDictionaryM";+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(setObject:forKey:), @selector(jk_setObject:forKey:), objc_getClass(kMutDictClass));});}- (void)jk_setObject:(id)anObject forKey:(id<NSCopying>)aKey {if (anObject && aKey) {[self jk_setObject:anObject forKey:aKey];} else {NSLog(@"传入空值Nil --[NSMutableDictionary setObject: forKey:]-- object=%@ key=%@",anObject,aKey);}}@end
参考了UIViewController-BackButtonHandler
分类.h
/**需要拦截导航栏上系统自带的‘返回’按钮事件,就实现此协议方法*/@protocol JKViewControllerPopActionHandler <NSObject>@optional- (BOOL)jk_navigationControllerShouldPopOnBackButton;@end/**所有控制器都遵守JKViewControllerPopActionHandler协议*/@interface UIViewController (PopActionHandler)<JKViewControllerPopActionHandler>@end/**用Runtime Method Swizzling拦截@selector(navigationBar:shouldPopItem:)*/@interface UINavigationController (PopActionHandler)@end
分类.m
@implementation UIViewController (PopActionHandler)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(viewDidAppear:), @selector(jk_viewDidAppear:), self);JK_ExchangeInstanceMethod(@selector(viewDidDisappear:), @selector(jk_viewDidDisappear:), self);});}- (void)jk_viewDidAppear:(BOOL)animated {[self jk_viewDidAppear:animated];if ([self respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {/// 拦截Pop事件就关闭侧滑返回self.navigationController.interactivePopGestureRecognizer.enabled = NO;}}- (void)jk_viewDidDisappear:(BOOL)animated {[self jk_viewDidDisappear:animated];self.navigationController.interactivePopGestureRecognizer.enabled = YES;}@end@implementation UINavigationController (PopActionHandler)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{JK_ExchangeInstanceMethod(@selector(navigationBar:shouldPopItem:), @selector(jk_navigationBar:shouldPopItem:), self);});}- (BOOL)jk_navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {if([self.viewControllers count] < [navigationBar.items count]) {return YES;}UIViewController* topVC = [self topViewController];BOOL enablePop = YES;if ([topVC respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {enablePop = [topVC jk_navigationControllerShouldPopOnBackButton];}if (enablePop == YES) {dispatch_async(dispatch_get_main_queue(), ^{[self popViewControllerAnimated:YES];});}return NO;}@end
代码就不贴了,搜一下有一堆,随便推荐一篇文章iOS 解决button重复点击问题。
我的建议是有需要才做处理重复点击,创建的Button默认不处理重复点击,文章中是拦截处理了所有UIControl的点击事件,会对某些系统按钮也会拦截点,所以要对某些类做特殊处理,下面是我碰到过的。
// 拍照的控制器if ([target isKindOfClass:NSClassFromString(@"PLImagePickerCameraView")]// 网页视频播放器|| [target isKindOfClass:NSClassFromString(@"AVFullScreenPlaybackControlsViewController")]// iOS10 拍照|| [target isKindOfClass:NSClassFromString(@"CAMViewfinderViewController")]|| [target isKindOfClass:[UIBarButtonItem class]]) {// 系统拍照按钮/视频播放器按钮单独处理,其他需要快速点击的设置quickTapEnable = YES[self jk_sendAction:action to:target forEvent:event];return;}