【瞎搞iOS开发07】Runtime Method Swizzling 方法转换实践小结

简书的Markdown内部跳转太别扭,建议看原始版本

#import <objc/runtime.h>


  • Method Swizzling 的实现
    • 交换实例方法
    • 交换类方法
  • Method Swizzling 的应用
    • 打印当前显示的UIViewController
    • 打印字典中的中文
    • 打印数组中的中文 (按需使用)
    • 防止MutableArray插入nil、越界导致崩溃(慎用)
    • 防止MutableDictionary 传入nil导致崩溃(慎用)
    • 防止重复点击按钮Button、监听某些系统自带按钮的点击事件。
    • 拦截系统自带的导航栏'返回'按钮的Pop事件
    • 防止重复点击按钮Button、拦截某些系统自带按钮的点击事件


Method Swizzling 的实现

对于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:)
进行以下操作:

  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:)。<span id="mark_one">标记点1</span>

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


交换类方法

/**
 【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);
}


Method Swizzling 的应用

  • 如果子类的[+ load]中调用[super load],父类的[+ load]就会被调用2次,所以加上dispatch_once以防重复Swizzling,安全性更高。
  • 有些类使用Method Swizzling是为了方便DEBUG调试,对于发布版本是多余的操作,所以加上#ifdef DEBUG进行判断。
  • 可能会存在多个UIViewCOntroller分类针对viewWillAppear进行Method Swizzling,不过不用担心,经测试转换后的ObjectSEL都会被调用,当然也要注意特殊情况:标记点1

打印当前显示的控制器ViewController

@implementation UIViewController (Swizzling)

+ (void)load {
#ifdef DEBUG
    static 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优先使用无缓存加载

加载UIImage有2种方式:

  • 方式1:[UIImage imageNamed...]
  • 方式2:[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中的中文

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

@implementation NSDictionary (Swizzling)

+ (void)load {
#ifdef DEBUG
        static 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中的中文(按需使用)

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

@implementation NSArray (Swizzling)

+ (void)load {
#ifdef DEBUG
        static 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

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

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

@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

防止MutableDictionary 传入nil导致崩溃

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

@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

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

参考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

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

代码就不贴了,搜一下有一堆,随便推荐一篇文章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;
    }

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,312评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,578评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,337评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,134评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,161评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,303评论 1 280
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,761评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,421评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,609评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,450评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,504评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,194评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,760评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,836评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,066评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,612评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,178评论 2 341

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,162评论 0 7
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 737评论 0 3
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 742评论 0 1
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,930评论 1 3