[关闭]
@xifenglang-33250 2017-01-07T00:50:42.000000Z 字数 11824 阅读 2200

Runtime Method Swizzling 实践小结

iOS_Runtime

  1. #import <objc/runtime.h>

-------------------------------------------------------

-------------------------------------------------------

Method Swizzling 的实现

对于Runtime Swizzling,这篇文章值得一看:《Objective-C的方法替换》,俺目前只实现了对一个类的2个“方法”进行Method Swizzling,对不同的类进行Method Swizzling出现了奇怪的问题,已放弃研究。
Method Swizzling尽量少用,这是把利剑,用好了能省很多事,用不好就可能成为猪队友。


交换实例方法

  1. /**
  2. 【Method Swizzling-慎用】用于替换同一类的2个[实例]方法。建议放在+(void)load方法配合DispatchOnce一起使用
  3. @param originalSEL 被替换的SEL
  4. @param objectSEL 用于替换的自定义SEL
  5. @param objectClass 进行Method Swizzling的Class
  6. */
  7. void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass);
  8. void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {
  9. Method originalMethod = class_getInstanceMethod(objectClass, originalSEL);
  10. Method replaceMethod = class_getInstanceMethod(objectClass, objectSEL);
  11. // 判断是否实现方法
  12. if (originalMethod == NULL || replaceMethod == NULL) {
  13. NSLog(@"\n.\tWarning! JK_ExchangeInstanceMethod 失败! [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));
  14. return;
  15. }
  16. // 将replaceMethod实现添加到objectClass中,并且将originalSEL指向新添加的replaceMethod的IMP。
  17. BOOL add = class_addMethod(objectClass, originalSEL, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
  18. if (add) {
  19. // 添加成功,再将objectSEL指向原有的originalMethod的IMP,实现交换
  20. // 当前类或者父类没有实现originalSEL会执行这一步
  21. class_replaceMethod(objectClass, objectSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  22. } else {
  23. // 已经实现customMethod,对systemMethod和customMethod的实现指针IMP进行交换
  24. method_exchangeImplementations(originalMethod, replaceMethod);
  25. }
  26. }

用作测试的类继承关系如:ViewController -> BaseViewController -> UIViewController
用作测试的originalSEL:@selector(viewWillAppear:)
进行以下操作:

  1. ViewController进行ExchangeInstanceMethodobjectSEL:@selector(edf_viewWillAppear:),而且ViewController和父类BaseViewController都【没有实现viewWillAppear】,
    add = YES;

  2. UIViewController基类进行ExchangeInstanceMethodobjectSEL:@selector(jk_viewWillAppear:),
    add = NO。

  3. 在操作2的基础上,如果在任何手动创建的类.m的(+ load)方法中执行操作1【不实现viewWillAppear:】,
    add = YES,对于ViewController只会执行@selector(edf_viewWillAppear:),不会执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)标记点1

  4. 在操作2的基础上,如果在任何手动创建的类.m除【(+ load)以外】的方法中执行操作1【不实现viewWillAppear:】,
    add = YES,对于ViewController既执行@selector(edf_viewWillAppear:)又执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)。

  5. 在操作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,而不是底层的子类,比如普通的控制器类。


交换类方法

  1. /**
  2. 【Method Swizzling-慎用】用于替换同一类的2个[类]方法。建议放在+(void)load方法使用
  3. @param originalSEL 被替换的SEL
  4. @param objectSEL 用于替换的自定义SEL
  5. @param objectClass 进行Method Swizzling的Class
  6. */
  7. void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass);
  8. void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {
  9. Method originalMethod = class_getClassMethod(objectClass, originalSEL);
  10. Method replaceMethod = class_getClassMethod(objectClass, objectSEL);
  11. if (originalMethod == NULL || replaceMethod == NULL) {
  12. NSLog(@"\n.\tWarning! JK_ExchangeClassMethod 失败! [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));
  13. return;
  14. }
  15. /// 交换实例方法的写法在这失效了,所以直接进行了method_exchangeImplementations,待研究
  16. method_exchangeImplementations(originalMethod, replaceMethod);
  17. }

------------------------------------------------------

Method Swizzling 的应用


打印当前显示的控制器ViewController

  1. @implementation UIViewController (Swizzling)
  2. + (void)load {
  3. #ifdef DEBUG
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. JK_ExchangeInstanceMethod(@selector(viewWillAppear:), @selector(jk_viewWillAppear:), self);
  7. });
  8. #endif
  9. }
  10. - (void)jk_viewWillAppear:(BOOL)animated{
  11. [self jk_viewWillAppear:animated];
  12. NSString * className = NSStringFromClass([self class]);
  13. if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_"]) {
  14. NSLog(@"即将显示:%@ 备注:%@",self.class,self.view.accessibilityIdentifier);
  15. }
  16. }
  17. @end

UIImage优先使用无缓存加载

加载UIImage有2种方式:

方式1会不断增加缓存,直到APP进程被杀才会释放,适用于频繁使用的图片,存放在Assets.xcassets中图片必须[UIImage imageNamed...]
方式2不会被缓存,Image对象释放即可释放内存,适用于不怎么用的图片,而且存在于[NSBundle mainBundle]中,不能存放在Assets.xcassets中。

  1. @implementation UIImage (Swizzling)
  2. + (void)load{
  3. static dispatch_once_t onceToken;
  4. dispatch_once(&onceToken, ^{
  5. JK_ExchangeClassMethod(@selector(imageNamed:), @selector(jk_imageNamed:), self);
  6. });
  7. }
  8. /**
  9. 优先无缓存加载图片,imageWithContentsOfFile
  10. */
  11. + (UIImage *)jk_imageNamed:(NSString *)name{
  12. UIImage * image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:[name hasSuffix:@"jpg"] ? nil : @"png"]];
  13. if (!image) {
  14. image = [self jk_imageNamed:name];
  15. }
  16. if (image == nil) {
  17. NSLog(@"\nWarning! 图片加载失败! imageName:%@",name);
  18. }
  19. return image;
  20. }
  21. @end

打印字典NSDictionary中的中文

打印NSDictionary中的中文,针对网络请求到的数据

  1. @implementation NSDictionary (Swizzling)
  2. + (void)load {
  3. #ifdef DEBUG
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);
  7. });
  8. #endif
  9. }
  10. - (NSString *)jk_descriptionWithLocale:(id)locale {
  11. if (self == nil || self.allKeys.count == 0) {
  12. return [self jk_descriptionWithLocale:locale];
  13. } else {
  14. @try {
  15. NSError * error = nil;
  16. NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];
  17. if (error) {
  18. return [self jk_descriptionWithLocale:locale];
  19. } else {
  20. return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  21. }
  22. } @catch (NSException *exception) {
  23. return [self jk_descriptionWithLocale:locale];
  24. }
  25. }
  26. }
  27. @end

打印数组NSArray中的中文(按需使用)

打印NSArray中的中文,针对网络请求到的数据,一般Json都会用字典包裹,所以可以不用或者注释下面的代码,毕竟数组一般都是存Model,Json转换不了,加了@try之后发生异常时会被断点捕获。(正常情况下转换NSJSONSerialization不支持的类型,会直接Crash

  1. @implementation NSArray (Swizzling)
  2. + (void)load {
  3. #ifdef DEBUG
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);
  7. });
  8. #endif
  9. }
  10. - (NSString *)jk_descriptionWithLocale:(id)locale {
  11. if (self == nil || self.count == 0) {
  12. return [self jk_descriptionWithLocale:locale];
  13. } else {
  14. @try {
  15. NSError * error = nil;
  16. NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];
  17. if (error) {
  18. return [self jk_descriptionWithLocale:locale];
  19. } else {
  20. return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  21. }
  22. } @catch (NSException *exception) {
  23. return [self jk_descriptionWithLocale:locale];
  24. }
  25. }
  26. }
  27. @end

防止NSMutableArray 插入nil、数组越界导致崩溃

慎用可能会变相的造成数据异常

  1. @implementation NSArray (SafeSwizzling)
  2. static const char * kArrayClass = "__NSArrayI";
  3. + (void)load {
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexI:), objc_getClass(kArrayClass));
  7. });
  8. }
  9. - (id)jk_objectAtIndexI:(NSUInteger)index {
  10. if (index < self.count) {
  11. return [self jk_objectAtIndexI:index];
  12. } else {
  13. NSLog(@"数组查询越界,return <null>。 --[NSArray objectAtIndex:]-- index=%zd array.count=%zd",index,self.count);
  14. return [NSNull null];
  15. }
  16. }
  17. @end
  18. @implementation NSMutableArray (SafeSwizzling)
  19. static const char * kMutArrayClass = "__NSArrayM";
  20. + (void)load {
  21. static dispatch_once_t onceToken;
  22. dispatch_once(&onceToken, ^{
  23. JK_ExchangeInstanceMethod(@selector(insertObject:atIndex:), @selector(jk_insertObject:atIndex:), objc_getClass(kMutArrayClass));
  24. JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexM:), objc_getClass(kMutArrayClass));
  25. });
  26. }
  27. - (void)jk_insertObject:(id)anObject atIndex:(NSUInteger)index {
  28. if (index > self.count) {
  29. NSLog(@"数组插值越界 --[NSMutableArray insertObject: atIndex:]-- object=%@ index=%zd array.count=%zd",anObject,index,self.count);
  30. } else if (anObject != nil) {
  31. [self jk_insertObject:anObject atIndex:index];
  32. } else {
  33. NSLog(@"传入空值Nil --[NSMutableArray insertObject: atIndex:]-- object=%@ index=%zd",anObject,index);
  34. }
  35. }
  36. - (id)jk_objectAtIndexM:(NSUInteger)index {
  37. if (index < self.count) {
  38. return [self jk_objectAtIndexM:index];
  39. } else {
  40. NSLog(@"数组查询越界,return <null>。 --[NSMutableArray objectAtIndex:]-- index=%zd array.count=%zd",index,self.count);
  41. return [NSNull null];
  42. }
  43. }
  44. @end

防止MutableDictionary 传入nil导致崩溃

慎用可能会变相的造成数据异常

  1. @implementation NSMutableDictionary (SafeSwizzling)
  2. static const char * kMutDictClass = "__NSDictionaryM";
  3. + (void)load {
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. JK_ExchangeInstanceMethod(@selector(setObject:forKey:), @selector(jk_setObject:forKey:), objc_getClass(kMutDictClass));
  7. });
  8. }
  9. - (void)jk_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
  10. if (anObject && aKey) {
  11. [self jk_setObject:anObject forKey:aKey];
  12. } else {
  13. NSLog(@"传入空值Nil --[NSMutableDictionary setObject: forKey:]-- object=%@ key=%@",anObject,aKey);
  14. }
  15. }
  16. @end

拦截系统自带的导航栏'返回'按钮的Pop事件

参考了UIViewController-BackButtonHandler
分类.h

  1. /**
  2. 需要拦截导航栏上系统自带的‘返回’按钮事件,就实现此协议方法
  3. */
  4. @protocol JKViewControllerPopActionHandler <NSObject>
  5. @optional
  6. - (BOOL)jk_navigationControllerShouldPopOnBackButton;
  7. @end
  8. /**
  9. 所有控制器都遵守JKViewControllerPopActionHandler协议
  10. */
  11. @interface UIViewController (PopActionHandler)<JKViewControllerPopActionHandler>
  12. @end
  13. /**
  14. 用Runtime Method Swizzling拦截@selector(navigationBar:shouldPopItem:)
  15. */
  16. @interface UINavigationController (PopActionHandler)
  17. @end

分类.m

  1. @implementation UIViewController (PopActionHandler)
  2. + (void)load {
  3. static dispatch_once_t onceToken;
  4. dispatch_once(&onceToken, ^{
  5. JK_ExchangeInstanceMethod(@selector(viewDidAppear:), @selector(jk_viewDidAppear:), self);
  6. JK_ExchangeInstanceMethod(@selector(viewDidDisappear:), @selector(jk_viewDidDisappear:), self);
  7. });
  8. }
  9. - (void)jk_viewDidAppear:(BOOL)animated {
  10. [self jk_viewDidAppear:animated];
  11. if ([self respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {
  12. /// 拦截Pop事件就关闭侧滑返回
  13. self.navigationController.interactivePopGestureRecognizer.enabled = NO;
  14. }
  15. }
  16. - (void)jk_viewDidDisappear:(BOOL)animated {
  17. [self jk_viewDidDisappear:animated];
  18. self.navigationController.interactivePopGestureRecognizer.enabled = YES;
  19. }
  20. @end
  21. @implementation UINavigationController (PopActionHandler)
  22. + (void)load {
  23. static dispatch_once_t onceToken;
  24. dispatch_once(&onceToken, ^{
  25. JK_ExchangeInstanceMethod(@selector(navigationBar:shouldPopItem:), @selector(jk_navigationBar:shouldPopItem:), self);
  26. });
  27. }
  28. - (BOOL)jk_navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
  29. if([self.viewControllers count] < [navigationBar.items count]) {
  30. return YES;
  31. }
  32. UIViewController* topVC = [self topViewController];
  33. BOOL enablePop = YES;
  34. if ([topVC respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {
  35. enablePop = [topVC jk_navigationControllerShouldPopOnBackButton];
  36. }
  37. if (enablePop == YES) {
  38. dispatch_async(dispatch_get_main_queue(), ^{
  39. [self popViewControllerAnimated:YES];
  40. });
  41. }
  42. return NO;
  43. }
  44. @end

防止重复点击按钮Button、拦截某些系统自带按钮的点击事件

代码就不贴了,搜一下有一堆,随便推荐一篇文章iOS 解决button重复点击问题
我的建议是有需要才做处理重复点击,创建的Button默认不处理重复点击,文章中是拦截处理了所有UIControl的点击事件,会对某些系统按钮也会拦截点,所以要对某些类做特殊处理,下面是我碰到过的。

  1. // 拍照的控制器
  2. if ([target isKindOfClass:NSClassFromString(@"PLImagePickerCameraView")]
  3. // 网页视频播放器
  4. || [target isKindOfClass:NSClassFromString(@"AVFullScreenPlaybackControlsViewController")]
  5. // iOS10 拍照
  6. || [target isKindOfClass:NSClassFromString(@"CAMViewfinderViewController")]
  7. || [target isKindOfClass:[UIBarButtonItem class]]) {
  8. // 系统拍照按钮/视频播放器按钮单独处理,其他需要快速点击的设置quickTapEnable = YES
  9. [self jk_sendAction:action to:target forEvent:event];
  10. return;
  11. }

↑↑↑ 回到顶部 ↑↑↑

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注