1.SEL
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
Objective-C 在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID,本质上就是一个字符串。只要方法名称相同,那么它们的ID就是相同的。
2.IMP
typedef id (*IMP)(id, SEL, ...);
实际上就是一个函数指针,指向方法实现的首地址。前两个参数是固定的,后面的参数根据函数的具体参数而定,有几个就传几个,返回值也是根据实际情况而定
通过取得 IMP,我们可以跳过 runtime 的消息传递机制,直接执行 IMP指向的函数实现,这样省去了 runtime 消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些,这里铺垫一下,后续再详细介绍这种调用方式,先看下简单的调用,当然必须说明的是,这种方式只适用于极特殊的优化场景,如效率敏感的场景下大量循环的调用某方法;
IMP imp = [self methodForSelector:sel];
((void(*)(id,SEL,id))imp)(self,sel,scrollView);
3.Method
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
Method = SEL + IMP + method_types,相当于在SEL和IMP之间建立了一个映射。调用过程无非就是去targer的class中根据Sel寻找对应的IMP,要是这种类或者父类中都找不到就会进入动态消息转发和处理过程
首先例如我们调用objectAtIndex
的时候,难免出现越界的问题,下面有三种解决方式
方案一
Category替换掉原生的方法,用自己的方法进行内部判断
// 这种写法其实更使用一点,容易理解
- (id)mkj_ObjectAtIndex:(NSUInteger)index{
if (index < self.count) {
return [self objectAtIndex:index];
}
NSLog(@"thread:%@",[NSThread callStackSymbols]);
return nil;
}
方案二
就是本文要介绍的Hook函数,交换Method的实现 可以再Class的+()load
方法中进行方法交换,也可以自己手动启用,我感觉后者好一点,能让能看得明白点,不然谁知道你替换方法了呢
void mkj_ExchangeMethod(Class aClass, SEL oldSEL, SEL newSEL)
{
Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
assert(oldMethod);
Method newMethod = class_getInstanceMethod(aClass, newSEL);
assert(newMethod);
method_exchangeImplementations(oldMethod, newMethod);
}
+ (void)avoidCrash_Open{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class __NSArrayI = NSClassFromString(@"__NSArrayI");
// 多个元素
mkj_ExchangeMethod(__NSArrayI,
@selector(objectAtIndex:),
@selector(avoidCrash_arrayI_objectAtIndex:));
});
}
- (instancetype)avoidCrash_arrayI_objectAtIndex:(NSInteger)index {
// NSLog(@"__NSArrayI-----------------------------");
NSArray *returnArray = nil;
@try {
returnArray = [self avoidCrash_arrayI_objectAtIndex:index];
} @catch (NSException *exception) {
mkj_SendErrorWithException(exception, @"数组越界");
} @finally {
return returnArray;
}
}
这种做法交换了方法的实现,因此,你在外部调用原生的方法的时候,就进入我们自定义的函数,从而hook了方法,然后处理之后,再调用自身,再回调原生的方法,不破坏原来的环境,我们就能做一些异常处理。但是如果这么做,就不会Crash,第三方就无法检测手机到崩溃日志
方案三
只是相对于方案二的另一种,这种拿到Method之后,先用一个变量存储IMP指针,然后重新给Class中的实现赋值到自己的方法实现hook,然后再根据临时存储的IMP指针调回原生的方法
// 拿到方法结构体
Method old_func_imap_object = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
// 吧原来的IMP指针存储
array_old_func_imap_object = method_getImplementation(old_func_imap_object);
// 重新给Method指定新的IMP函数指针
method_setImplementation(old_func_imap_object, [self methodForSelector:@selector(fm_objectAtIndex:)]);
// 上面重新赋值的IMP实现
- (id)fm_objectAtIndex:(NSUInteger)index {
// 判断兼容
if (index < [(NSArray*)self count]) {
// 然后用存储的IMP指针调用原生的方法
return ((id(*)(id, SEL, NSUInteger))array_old_func_imap_object)(self, @selector(objectAtIndex:), index);
}
NSLog(@"NArray objectAtIndex 失败--%@", [NSThread callStackSymbols]);
return nil;
}
这种方法看起来和第二种很类似,但是某种意义上来讲也是优化的一点,因为你直接调用IMP指针肯定比原来的自动寻找来的更高效,都不需要找了,直接定位,调用函数指针传参数,不过都是思路,我还是觉得第二种好一点。
经常能遇到unrecognize selected的崩溃,调用不属于该对象的方法,放按如下
动态消息转发
首先方法会根据sel去找对应的方法实现,但是如果本类和基类都无法找到,那么就会进行动态消息处理和转发,可以参考上面链接第六点,而且都没做处理,就进入下面的消息转发,你给NSObject实现一个Category
// A Selector for a method that the receiver does not implement.
// 当category重写类已有的方法时会出现此警告。
// Category is implementing a method which will also be implemented by its primary class
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"unrecognized selector : classe:%@ sel:%@",NSStringFromClass([self class]),NSStringFromSelector(aSelector));
// 元类 meta class 创建 重新指定Selector 防止崩溃 http://ios.jobbole.com/81657/
// 1、为”class pair”分配内存 (使用objc_allocateClassPair).
// 2、添加方法或成员变量到有需要的类里 (我已经使用class_addMethod添加了一个方法).
// 3、创建出来
// 用objc_allocateClassPair创建一个自定义名字的元类
Class class = objc_allocateClassPair(NSClassFromString(@"NSObject"), "UnrecognizedSel", 0);
// 类添加方法 Sel 和 Imp
class_addMethod(class, aSelector, class_getMethodImplementation([self class], @selector(customMethod)), "v@:");
// class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
// objc_registerClassPair(class)
// 创建
id tempObject = [[class alloc] init];
return tempObject;
}
#pragma clang diagnostic pop
- (void)customMethod{
NSLog(@"呵呵");
}
上面就是实现动态消息转发的时候会调用,如果不做任何处理,那么就会Crash,上面的方法,咱们可以自己创建一个简单的元类,然后把传进来未找到方法实现的SEL重新定义一个自定义的实现,从而避免崩溃
App数据统计我们都会用第三方,例如友盟,极光什么的,但是这些用过的都知道,很麻烦,你需要在每个触发的地方添加他的代码,虽然很简单,但是会把那些垃圾代码埋在各个地方,如果你可以Hook关键函数,就应该能更清晰一点
1.控制器页面的统计
ViewDidAppear 很简单,我们只需要添加一个Category,然后Hook原来的方法,用我们自己实现的方法,进行数据上报
@implementation UIViewController (HookViewControllerAppear)
+ (void)hook_ViewcontrollerOpen{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mkj_ExchangeMethod([self class], @selector(viewDidAppear:),@selector(mkj_viewDidAppear));
});
}
- (void)mkj_viewDidAppear{
// 在这里可以进行数据的上报
NSLog(@"hook Viewcontroller viewdidAppear---- class:%@",NSStringFromClass([self class]));
[self mkj_viewDidAppear];
}
@end
2.按钮点击事件上报
其实就是找到事件触发的统一方法,然后hook出来,添油加醋之后再调回原来的方法,那么UIControl的触发事件都会调用@selector(sendAction:to:forEvent:)
+(void)mkjhood_touchActionOpen{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mkj_ExchangeMethod([self class], @selector(sendAction:to:forEvent:), @selector(mkj_SendAction:to:forEvent:));
});
}
- (void)mkj_SendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event{
NSLog(@"control--%@,action---%@,target---%@,Point---%@",
NSStringFromClass([self class]),
NSStringFromSelector(action),
NSStringFromClass([target class]),
NSStringFromCGRect(self.frame));
[self mkj_SendAction:action to:target forEvent:event];
}
// send the action. the first method is called for the event and is a point at which you can observe or override behavior. it is called repeately by the second.
//- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
还可以针对那些代理事件的Hook,有需要的可以下载Demo看看
Objective-C Hook函数的三种方法
系统提供了该函数NSSetUncaughtExceptionHandler()
我们在Delegate的时候初始化创建即可,貌似很多第三方应该也是这么做的
@implementation AppDelegate (ColletionCrash)
- (void)collectionCrash{
struct sigaction newSignalAction;
memset(&newSignalAction, 0,sizeof(newSignalAction));
newSignalAction.sa_handler = &signalHandler;
sigaction(SIGABRT, &newSignalAction, NULL);
sigaction(SIGILL, &newSignalAction, NULL);
sigaction(SIGSEGV, &newSignalAction, NULL);
sigaction(SIGFPE, &newSignalAction, NULL);
sigaction(SIGBUS, &newSignalAction, NULL);
sigaction(SIGPIPE, &newSignalAction, NULL);
//异常时调用的函数
NSSetUncaughtExceptionHandler(&handleExceptions);
}
// 这种搜集到的崩溃,一般都会,但是我们之前写了NSArray的hook和NSObject的拦截,就不会进入Crash
void handleExceptions(NSException *exception) {
NSLog(@"exception = %@",exception);
NSLog(@"callStackSymbols = %@",[exception callStackSymbols]);
}
void signalHandler(int sig) {
}
@end
上面提到的各种例子都有验证过,Demo如下
Demo
参考链接
数组越界
Crash捕获
无埋点
Hook方法
元类
IMP