- 我们需要了解的是 Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
- 简单来说我们主要是使用Method Swizzling来把系统的方法交换为我们自己的方法,从而给系统方法添加一些我们想要的功能。
- Method Swizzling通用方法封装
我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别,这样我们后续调用也会方便些。
#import
#import
@interface NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
bySwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替换原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
1、有时候无网络链接、数据为空时需要占位图,可以利用分类实现。利用Method Swizzling替换reload方法实现tableView占位图。
项目写一个分类用于tableView占位图的实现。
#import
@interface UITableView (placeholder)
/* 占位图 */
@property (nonatomic, strong) UIView *placeHolderView;
@end
#import "UITableView+placeholder.h"
#import
@implementation NSObject (swizzle)
+ (void)swizzleInstanceSelector:(SEL)originalSel
WithSwizzledSelector:(SEL)swizzledSel
{
Method originMethod = class_getInstanceMethod(self, originalSel);
Method swizzedMehtod = class_getInstanceMethod(self, swizzledSel);
BOOL methodAdded = class_addMethod(self, originalSel, method_getImplementation(swizzedMehtod), method_getTypeEncoding(swizzedMehtod));
if (methodAdded) {
class_replaceMethod(self, swizzledSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzedMehtod);
}
}
@end
@implementation UITableView (placeholder)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceSelector:@selector(reloadData) WithSwizzledSelector:@selector(gy_reloadData)];
});
}
- (void)setPlaceHolderView:(UIView *)placeHolderView
{
objc_setAssociatedObject(self, @selector(placeHolderView), placeHolderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIView *)placeHolderView
{
return objc_getAssociatedObject(self, @selector(placeHolderView));
}
- (void)gy_reloadData
{
[self gy_checkEmpty];
[self gy_reloadData];
}
- (void)gy_checkEmpty
{
BOOL isEmpty = YES;
id src = self.dataSource;
NSInteger sections = 1;
if ([src respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
sections = [src numberOfSectionsInTableView:self];
}
for (int i = 0; i < sections; i++) {
NSInteger rows = [src tableView:self numberOfRowsInSection:i];
if (rows) {
isEmpty = NO;
}
}
if (isEmpty) {
[self.placeHolderView removeFromSuperview];
[self addSubview:self.placeHolderView];
}else{
[self.placeHolderView removeFromSuperview];
}
}
@end
- 先自定义一个无数据时的视图CloudBaNoDataView
#import
@interface CloudBaNoDataView : UIView
- (instancetype)initWithFrame:(CGRect)frame image:(UIImage *)img viewClick:(void(^)())clickBlock;
@end
#import "CloudBaNoDataView.h"
@interface CloudBaNoDataView ()
@property (nonatomic, copy) void(^clickBlock)();
@property (nonatomic, strong) UIImageView *imgView;
@property (nonatomic, strong) UIImage *img;
@end
@implementation CloudBaNoDataView
- (instancetype)initWithFrame:(CGRect)frame image:(UIImage *)img viewClick:(void(^)())clickBlock
{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor whiteColor];
self.clickBlock = clickBlock;
self.img = img;
[self setupSubViews];
}
return self;
}
- (void)setupSubViews
{
UIImageView *imgView = [[UIImageView alloc] initWithImage:self.img];
[self addSubview:imgView];
self.imgView = imgView;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(clickImgView:)];
imgView.userInteractionEnabled = YES;
[imgView addGestureRecognizer:tap];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.imgView.center = CGPointMake(self.center.x, self.center.y-64-64);
}
- (void)clickImgView:(UITapGestureRecognizer *)recognizer
{
self.clickBlock();
}
@end
- 在需要使用该分类的tableView中添加一下代码
_tableView.placeHolderView = [[CloudBaNoDataView alloc] initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight-64) image:[UIImage imageNamed:@"no_data"] viewClick:^{
NSLog(@"点击了没有更多数据的图片");
}];
- 编译运行,发现没有问题。
2、防止按钮重复暴力点击
程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。
#import
//默认时间间隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//点击间隔
@property (nonatomic, assign) NSTimeInterval timeInterval;
//用于设置单个按钮不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end
#import "UIButton+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIButton (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)];
});
}
- (NSTimeInterval)timeInterval{
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//当按钮点击事件sendAction 时将会执行sure_SendAction
- (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
if (self.isIgnore) {
//不需要被hook
[self sure_SendAction:action to:target forEvent:event];
return;
}
if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval;
if (self.isIgnoreEvent){
return;
}else if (self.timeInterval > 0){
[self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
}
}
//此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
self.isIgnoreEvent = YES;
[self sure_SendAction:action to:target forEvent:event];
}
//runtime 动态绑定 属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
// 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
//_cmd == @select(isIgnore); 和set方法里一致
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIsIgnore:(BOOL)isIgnore{
// 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnore{
//_cmd == @select(isIgnore); 和set方法里一致
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
[self setIsIgnoreEvent:NO];
}
@end
3、解决获取索引、添加、删除元素越界崩溃问题
对于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免会进行索引访问、添加、删除元素的操作,越界问题也是很常见,这时我们可以通过Method Swizzling解决这些问题,越界给予提示防止崩溃。
#import "NSMutableArray+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation NSMutableArray (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
});
}
- (void)safeAddObject:(id)obj {
if (obj == nil) {
NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
} else {
[self safeAddObject:obj];
}
}
- (void)safeRemoveObject:(id)obj {
if (obj == nil) {
NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
return;
}
[self safeRemoveObject:obj];
}
- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
if (anObject == nil) {
NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
} else if (index > self.count) {
NSLog(@"%s index is invalid", __FUNCTION__);
} else {
[self safeInsertObject:anObject atIndex:index];
}
}
- (id)safeObjectAtIndex:(NSUInteger)index {
if (self.count == 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return nil;
}
if (index > self.count) {
NSLog(@"%s index out of bounds in array", __FUNCTION__);
return nil;
}
return [self safeObjectAtIndex:index];
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
if (self.count <= 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return;
}
if (index >= self.count) {
NSLog(@"%s index out of bound", __FUNCTION__);
return;
}
[self safeRemoveObjectAtIndex:index];
}
@end
对应大家可以举一反三,相应的实现添加、删除等,以及NSArray、NSDictionary等操作,因代码篇幅较大,这里就不一一书写了。
这里没有使用self来调用,而是使用objc_getClass("__NSArrayM")来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。
4、App热修复
因为AppStore上线审核时间较长,且如果在线上版本出现bug修复起来也是很困难,这时App热修复就可以解决此问题。热修复即在不更改线上版本的前提下,对线上版本进行更新甚至添加模块。国内比较好的热修复技术:JSPatch。JSPatch能做到通过JS调用和改写OC方法最根本的原因是Objective-C是动态语言,OC上所有方法的调用/类的生成都通过Objective-C Runtime在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,进而替换出现bug的方法或者添加方法等。bang的博客上有详细的描述有兴趣可以参考,这里就不赘述了。
。