美文网首页iOS逆向
—— iOS 运行时中方法的调用流程

—— iOS 运行时中方法的调用流程

作者: 大成小栈 | 来源:发表于2021-07-06 21:12 被阅读0次

    1. 消息发送

    在iOS运行时系统中,调用方法的本质就是利用objc_msgSend进行消息发送:

    // main.m 中的方法调用
    LGPerson *person = [LGPerson alloc];
    [person sayNB];
    [person sayHello];
    
    // clang 编译后的底层实现
    LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    
    // 用objc_msgSend方法来完成[person sayNB]的功能,其打印结果一致
    LGPerson *person = [LGPerson alloc];   
    objc_msgSend(person,sel_registerName("sayNB"));
    [person sayNB];
    

    我们可以直接通过调用objc_msgSend方法,来完成[person sayNB]的功能,查看其打印是否是一致:

    1. 直接调用objc_msgSend,需要导入<objc/message.h>;
    2. 工程target --> Build Setting -->enable strict checking of obc_msgSend calls由YES 改为NO,否则objc_msgSend的参数会报warning;也可通过宏定义去掉warning。
    实例、类、元类

    iOS 中所有的类都是继承于 NSObject,一个对象所具有的方法分为实例方法和类方法,编译完成后的对象中,存在一个实例方法链表、一个缓存方法链表。当实例调用方法经objc_msgSend时:首先,在相应操做的对象中的缓存方法列表中找调用的方法,若找到,转向相应的实现并执行;若没找到,在对象的方法列表中查找,若是找到,转向相应的实现并执行;若是没找到,则递归的去父类指针所指向的类对象方法列表中查找;以此类推,若是一直到根类都没有找到,转向拦截调用,走消息转发机制;若是没有重写拦截调用方法,程序报错;

    • 调用对象方法(给实例对象发消息)
      根据实例对象的isa指针去该对象的类方法中查找,若找到则执行;
      若没找到,递归的去该类的父类类对象中查找,直到根类NSObject;
      如果都没有找到就报错(还有三次挽救的机会)

    • 调用类方法(给类对象发送消息)
      根据类对象的isa指针去元对象中查找,若找到则执行;
      若没找到,递归的去父元类对象中查找,直到根类NSOject;
      如果都没有找到就报错(也有三次挽救的机会)

    消息发送

    2. 消息转发

    消息转发也被称为拦截调用,就是在找不到调用的方法后,且在程序崩溃以前,有机会经过重写NSObject的四个方法来补救处理:

    // 有机会让类,实现并添加这个sel
    + (BOOL)resolveClassMethod:(SEL)sel;
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    
    // 让别的对象去执行这个函数
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    // (函数执行器)将目标函数以其它形式执行
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    若以上都不中,调用 NSObject 的 doesNotRecognizeSelector 方法抛出异常:

    - (void)doesNotRecognizeSelector:(SEL)aSelector;
    
    消息转发

    利用以上机制,可以对resolveInstanceMethod 和 resolveClassMethod 两个方法进行方法交换,拦截可能出现的 iOS 崩溃,然后自定义处理。

    3. 实例

    消息转发机制依次的三个过程:1)动态方法解析;2)转发给其他备用的接收对象;3)消息所有相关内容封装成一个NSInvocation对象,再做最后的尝试。

    3.1 动态方法解析

    第一阶段,先征询接收者所属的类,是否需要动态的添加方法,用来处理当前未找到的方法。对象在无法解读消息时会首先调用所属类的下列类方法,来判断是否能接收消息:

    // 如果是实例方法 (返回值表示这个类能否新增一个实例方法处理此选择子)
    + (BOOL) resolveInstanceMethod:(SEL)selector;
    // 如果是类方法(类方法的添加需要在其“元类”里面。)
    + (BOOL) resolveClassMethod:(SEL)selector;
    

    例:

    //消息转发机制的第一步 :动态方法解析
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSString *selName = NSStringFromSelector(sel);
        if ([selName hasPrefix:@"doSomeThing"]) {//判断特定无法响应的方法
            class_addMethod(self, sel, (IMP)otherOneDoSomeThing, "v@:");//动态添加响应方法
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    //动态将实现转到这个函数(或者就是单纯的添加doSomeThing方法)
    void otherOneDoSomeThing(id self ,SEL _cmd){
        NSLog(@"class:%@, sel:%s",self,sel_getName(_cmd));
        NSLog(@"原对象无法响应该消息,在动态方法解析时添加了一个方法来处理该消息");
    }
    
    3.2 备用的接收者

    第二阶段,如果动态方法解析没有发现添加的方法,那么尝试转发给其他对象来处理这个方法。该步骤调用的方法是:

    // 尝试转发给其他对象来处理这个方法
    - (id) forwardingTargetForSelector:(SEL)selector;
    

    例:

    - (id)forwardingTargetForSelector:(SEL)aSelector{
        NSString * selString = NSStringFromSelector(aSelector);
        if([@"doSomeThing" isEqualToString:selString]){
            OtherObject *someone = [[OtherObject alloc] init];//备选对象
            if ([someone respondsToSelector:aSelector]) {
                return someone;//如果可以响应该方法,则直接转交新对象处理
            }
        }
        return [super forwardingTargetForSelector:aSelector];//如果无合适的备选对象,则继续转发
    }
    
    3.3 完整的消息转发机制

    第三阶段,如果没有可用的备选者,那么系统就会把消息所有相关内容封装成一个NSInvocation对象,再做最后的尝试,启动完整的消息转发。先调用methodSignatureForSelector:获取方法签名,然后再调用forwardInvocation:进行处理,这一步的处理可以直接转发给其它对象,即和第二步的效果等效,但是很少有人这么干,因为消息处理越靠后,就表示处理消息的成本越大,性能的开销就越大。所以,在这种方式下,一般会改变消息内容,比如增加参数,改变选择子等等,具体根据实际情况而定。

    // 获取方法签名
     - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    // 完整的消息转发(消息的受主、消息体、消息参数均封装在内)
     - (void)forwardInvocation:(NSInvocation *)anInvocation
    

    例:

    //获取方法签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSString *method = NSStringFromSelector(aSelector);
        if ([@"doSomeThing" isEqualToString:method]) {
            /* 手动创建签名
             写法例子一  v@:@
             字符说明:(1)v:返回值类型void;(2)@:id类型,执行sel的对象;(3): SEL;(4)@:参数
             
             写法例子二  @@:
             字符说明:(1)@:返回值类型id;(2)@:id类型,执行sel的对象;(3):SEL
            
             */
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
            return signature;
        }
        return nil;
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        //*-----------  处理方式一:不改变sel -------------*/
        // 拿到这个消息
        SEL selector = [anInvocation selector];
        // 转发消息
        AnotherObject *otherObject = [[AnotherObject alloc] init];
        if ([otherObject respondsToSelector:selector]) {
            // 调用这个对象,进行转发
            [anInvocation invokeWithTarget:otherObject];
        } else {
            [super forwardInvocation:anInvocation];
        }
        //*---------------------------------------------*/
    
        //*-----------  处理方式二:改变sel -------------*/
        SEL selector = @selector(myAnotherMethod:);
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        anInvocation = [NSInvocation invocationWithMethodSignature:signature];
        [anInvocation setTarget:self];
        [anInvocation setSelector:@selector(myAnotherMethod:)];
        NSString *param = @"参数";
        // 消息的第一个参数是self,第二个参数是选择子,所以"参数"是第三个参数
        [anInvocation setArgument:&param atIndex:2];
        
        if ([self respondsToSelector:selector]) {//如果自己响应,就自己处理
            [anInvocation invokeWithTarget:self];
            return;
        } else {
            AnotherObject * otherObject = [[AnotherObject alloc] init];
            if ([otherObject respondsToSelector:selector]) {//交给另外的对象来处理
                [anInvocation invokeWithTarget:otherObject];
                return;
            }
        }
        [super forwardInvocation:anInvocation];
        //*---------------------------------------------*/
    }
    
    //类中的另一个方法,来处理消息
    - (void)myAnotherMethod:(NSString*)para
    {
        NSLog(@"交给我自己的另一个方法来处理:%@", para);
    }
    
    消息转发机制简图

    4. 应用场景

    1. 为@dynamic等实现方法
      使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也可用其他的方法来实现。

    2. 间接实现多继承
      Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

    3. 实现多重代理
      利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

    这里就是利用了消息转发机制的第三个阶段,将NSIvocation分发给多个代理去响应。
    https://blog.csdn.net/kingjxust/article/details/49559091

    1. iOS动态化更新(JSPatch、ReactiveCocoa等)
    • JSPatch,通过消息转发机制来进行JS和OC的交互,从而实现iOS的热更新。
    • 虽然苹果大力整改热更新让JSPatch的审核通过率在有一段时间里面无法过审,但是后面bang神对源码进行代码混淆之后,基本上是可以过审了。
    • 下面截图只摘出来用到消息转发的部分:关键点就是在第三阶段,通过invocation拿到方法参数,然后传给JS,调用JS的实现函数。

    http://blog.cnbang.net/tech/2808/
    http://blog.cnbang.net/tech/2855/

    5. 总结

    由于OC的动态特性,在编译过程向类发送了其无法理解的消息并不会报错,因为在运行时,我们可以改变对象调用的方法、向类中添加方法。只有当程序运行起来之后,才知道要真正执行哪个函数(动态绑定)。

    OC消息发送原理、方法查找过程:

    1. 调用一个方法(包括respondsToSelector),编译器将OC代码,转换成C函数,给对象发送消息 : void objc_msgSend(id self, SEL cmd,...) ,第一个参数是接收者,第二个参数是方法(名),后面是消息的参数。
    2. objc_msgSend查找方法,实例对象根据其isa指针,找到其所属的class,然后遍历其methodLists,如果找到则根据IMP函数指针去调用,并且缓存(objc_cache);如果没有找到,那么根据这个类的super_class找到其父类,再看其父类是否能相应这个方法就可以了,直到super_class为nil时,就无法响应这个方法了,此时就触发消息转发机制。
      当使用类名调用类方法(+方法)时,只需要根据class的isa指针,找到其meta-class,然后通过meta-class的methodLists找到相应的方法既可(“类”是“元类”的对象)。
    3. 如果对象接收到无法解读的消息后(未查询到该方法),就会启动“消息转发”机制,我们可在此过程告诉对象应该如何处理未知消息。如果我们不做任何处理,或处理无效,则会调用doesNotRecognizeSelector:,造成异常崩溃:unrecognized selector sent to instance 0xxx

    简单理解:

    1. 首先,若对象无法响应某个方法调用,则进入消息转发流程。
    2. 开始第一步,通过运行时的动态方法解析,可以将需要的某个方法,加入到类中。
    3. 上一步失败,开始第二步,将消息转发给其他对象处理。
    4. 上述两步失败,启动完整的消息转发机制,通过封装NSInvocation,明确指出方法的响应者(甚至改变SEL)。
    5. 上述都失败,抛出异常。

    OC、运行时初始化时机:
    https://www.jianshu.com/p/4b93b40977b5
    https://blog.csdn.net/weixin_30920513/article/details/100093380

    参考文章:
    https://www.jianshu.com/p/7e132cda35cd
    https://www.cnblogs.com/feng9exe/p/10397102.html
    https://www.shangmayuan.com/a/02d9b8b219b24d888ef93b97.html
    https://blog.csdn.net/lin1109221208/article/details/108724965

    iOS之使用NSInvocation调用方法
    https://www.jianshu.com/p/e24b3420f1b4

    相关文章

      网友评论

        本文标题:—— iOS 运行时中方法的调用流程

        本文链接:https://www.haomeiwen.com/subject/urogultx.html