Objective-C运行时Hook函数避免Crash以及无码埋点的思路
来源:互联网 发布:gentoo linux 论坛 编辑:程序博客网 时间:2024/06/07 05:14
关键字介绍 SEL IMP Method
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,要是这种类或者父类中都找不到就会进入动态消息转发和处理过程
Hook数组避免数据崩溃(三种方式)
首先例如我们调用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指针肯定比原来的自动寻找来的更高效,都不需要找了,直接定位,调用函数指针传参数,不过都是思路,我还是觉得第二种好一点。
NSObject避免Unrecognize的崩溃
经常能遇到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重新定义一个自定义的实现,从而避免崩溃
Hook函数对无码埋点的思考
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函数的三种方法
第三方Crash检测原理猜测
系统提供了该函数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的拦截,就不会进入Crashvoid handleExceptions(NSException *exception) { NSLog(@"exception = %@",exception); NSLog(@"callStackSymbols = %@",[exception callStackSymbols]);}void signalHandler(int sig) {}@end
上面提到的各种例子都有验证过,Demo如下
Demo
参考链接
数组越界
Crash捕获
无埋点
Hook方法
元类
IMP
- Objective-C运行时Hook函数避免Crash以及无码埋点的思路
- 利用Objective-C运行时hook函数的三种方法
- 利用Objective-C运行时hook函数的三种方法
- 利用Objective-C运行时hook函数的三种方法
- Hook Objective-C 的方法
- Objective-C的hook方案
- Objective-C的hook方案: Method Swizzling
- Objective-C的hook方案: Method Swizzling
- Objective-C运行时
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C 2.0的运行时编程
- Objective-C的运行时参考
- final、fianlly、finalize的区别?
- Android进阶之路
- 如何用python画多层网络--pmnet
- C++进阶—>以操作系统的角度述说线程与进程
- JavaScript入门知识
- Objective-C运行时Hook函数避免Crash以及无码埋点的思路
- Sass、LESS
- Java
- 静态代码块的使用
- MyBatis配置文件记录
- 第十四天总结
- XYNUOJ 1458 医院设置
- d3高级应用专题(一):canvas与SVG之间的转换
- Bootstrap 实例