的Markdown内部跳转太别扭,建议看原始版本
#import
- 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:)
进行以下操作:
对
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
,对于ViewControlle
r既执行@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 。 --[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 。 --[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)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
@optional
- (BOOL)jk_navigationControllerShouldPopOnBackButton;
@end
/**
所有控制器都遵守JKViewControllerPopActionHandler协议
*/
@interface UIViewController (PopActionHandler)
@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;
}
参考文献
- UIViewController-BackButtonHandler
- iOS 解决button重复点击问题。