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