必备知识架构-①语言-消息转发

CJBaseHelper/HookCJHelper

pod search CJBaseHelper

image-20240822165215629

库的使用方法,见我简书中的以下文章:

CJHook

pod search CJHook

image-20240822165517533

在调用原始方法之前插入额外的执行逻辑

1、使用场景

  1. 日志记录与监控: 在方法执行前后添加日志记录,可以帮助开发者监控应用的行为,特别是在调试和性能分析时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    - (void)swizzleMethod:(Class)class withSelector:(SEL)selector {
    Method originalMethod = class_getInstanceMethod(class, selector);
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP newIMP = imp_implementationWithBlock(^(id self) {
    NSLog(@"Before method execution");
    ((void(*)(id, SEL))originalIMP)(self, selector);
    NSLog(@"After method execution");
    });
    method_setImplementation(originalMethod, newIMP);
    }

2、调用

以在进入个人主页前,需要先判断是否登录为例

常规:使用Util式、使用宏

1
2
3
4
5
6
7
8
- (void)goMineHomePage {
// 在调用原始方法之前执行的代码(进入个人主页前,需要先判断是否登录)
bool canPush = [LoginCheckerUtil checkLogin];
if (!canPush) return;

MineHomeViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
}

改进(改进后发现,其实可读性不高,所以还不如不这么改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LoginCheckUtil.m
#define NeedLoginAnnotation(Class originalSelOwnerClass, SEL originalSelector) \
HookCJHelper_addPreExecutionToClassMethod(originalSelOwnerClass, originalSelector, [LoginCheckUtil class], @selector(loginCheckMethod));
class LoginCheckUtil: NSObject {
+ (bool)loginCheckMethod {
if (UserManager.isLogin) {
return YES;
}
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return NO;
}
}

// HomeViewController.m
+ (void) initialize {
@NeedLoginAnnotation([self class], @selector(goMineHomePage)) // 在 + (void) initialize 中调用
}

- (void)goMineHomePage {

}

3、内部核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 在调用原始方法之前插入额外的执行逻辑
bool HookCJHelper_addPreExecutionToClassMethod(Class originalSelOwnerClass, SEL originalSelector, Class preExecSelOwnerClass, SEL preExecutionSelector) {
// 原始方法检查
if (!originalSelOwnerClass || !class_getClassMethod(originalSelOwnerClass, originalSelector)) {
return false;
}
Method originalMethod = class_getClassMethod(originalSelOwnerClass, originalSelector);
IMP originalIMP = method_getImplementation(originalMethod);
if (!originalIMP) {
return false;
}

// 要插入的方法
if (!preExecSelOwnerClass || !class_getClassMethod(preExecSelOwnerClass, preExecutionSelector)) {
return false;
}

// 创建新的方法实现
IMP newIMP = imp_implementationWithBlock(^(Class self) {
// 调用额外的逻辑(预执行类方法)
((void(*)(Class, SEL))objc_msgSend)(preExecSelOwnerClass, preExecutionSelector);
// 调用原始的类方法
((void(*)(Class, SEL))originalIMP)(self, originalSelector);
});

// 替换原始方法的实现为新的实现
method_setImplementation(originalMethod, newIMP);

return true;
}
  1. 什么时候会报unrecognized selector的异常
  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候。可以通过消息转发进行解决,流程见下图

    img

    image

  • OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
    但是在这之前,OC的运行时会给出三次拯救程序崩溃的机会

  • Method resolution(消息动态解析
    OC运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发(Message Forwarding

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 重写 resolveInstanceMethod: 添加对象方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel {
    // 如果是执行 run 函数,就动态解析,指定新的 IMP
    if (sel == NSSelectorFromString(@"run:")) {
    // class: 给哪个类添加方法
    // SEL: 添加哪个方法
    // IMP: 方法实现 => 函数 => 函数入口 => 函数名
    // type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
    class_addMethod(self, sel, (IMP)runMethod, "v@:@");
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }

    //新的 run 函数
    void runMethod(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@", meter);
    }
  • Fast forwarding(消息接受者重定向
    如果目标对象实现了-forwardingTargetForSelector:Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nilself,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点

    1
    2
    3
    4
    5
    6
    7
    8
    // 消息接受者重定向
    - (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run:)) {
    return [[Person alloc] init];
    // 返回 Person 对象,让 Person 对象接收这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
    }
    • Normal forwarding(消息重定向
      这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nilRuntime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 获取函数的参数和返回值类型,返回签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
    }

    // 消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 从 anInvocation 中获取消息
    SEL sel = anInvocation.selector;
    if (sel == NSSelectorFromString(@"run:")) {
    // 1\. 指定当前类的一个方法作为IMP
    // anInvocation.selector = @selector(readBook:);
    // [anInvocation invoke];

    // 2\. 指定其他类来执行这个IMP
    Person *p = [[Person alloc] init];
    // 判断 Person 对象方法是否可以响应 sel
    if([p respondsToSelector:sel]) {
    // 若可以响应,则将消息转发给其他对象处理
    [anInvocation invokeWithTarget:p];
    } else {
    // 若仍然无法响应,则报错:找不到响应方法
    [self doesNotRecognizeSelector:sel];
    }
    }else{
    [super forwardInvocation:anInvocation];
    }
    }

    - (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
    }

    既然-forwardingTargetForSelector:-forwardInvocation:都可以将消息转发给其他对象处理,那么两者的区别在哪?
    区别就在于-forwardingTargetForSelector:只能将消息转发给一个对象。而-forwardInvocation:可以把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。修改消息的target,selector,参数等。将消息转发给多个对象