Objective-C运行时Hook函数避免Crash以及无码埋点的思路

关键字介绍 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的拦截,就不会进入Crash
void handleExceptions(NSException *exception) {
    NSLog(@"exception = %@",exception);
    NSLog(@"callStackSymbols = %@",[exception callStackSymbols]);
}

void signalHandler(int sig) {

}

@end

上面提到的各种例子都有验证过,Demo如下
Demo

参考链接
数组越界
Crash捕获
无埋点
Hook方法
元类
IMP

你可能感兴趣的:(基础知识)