RunTime

作者: 曼谷第一开膛手 | 来源:发表于2019-12-05 09:48 被阅读0次
    一、数据结构:objc_object, objc_class, isa, class_data_bits_t, cache_t, method_t
    屏幕快照 2019-09-03 09.19.27.png
    • objc_object (id)
      isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关


      屏幕快照 2019-09-03 11.17.48.png
    • objc_class(class) 继承 objc_object
      Class superClass, cache_t cache,class_data_bits_bits


      屏幕快照 2019-09-03 11.18.46.png
    • isa指针,共用体 isa_t


      屏幕快照 2019-09-03 09.27.48.png
    • isa指向
      关于对象,其指向类对象。
      关于类对象,其指向元类对象。
      实例--(isa)-->class--(isa)-->MetaClass

    • cache_t
      用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用


      屏幕快照 2019-09-03 11.19.53.png
     struct cache_t {
        struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
        mask_t _mask;//分配用来缓存bucket的总数
        mask_t _occupied;//表明目前实际占用的缓存bucket的个数
    }
    struct bucket_t {
        private:
        cache_key_t _key;
        IMP _imp;
     }
    
    • class_data_bits_t:对class_rw_t的封装
    struct class_rw_t {
         uint32_t flags;
         uint32_t version;
    
         const class_ro_t *ro;
    
         method_array_t methods;
         property_array_t properties;
         protocol_array_t protocols;
    
         Class firstSubclass;
         Class nextSiblingClass;
    
         char *demangledName;
    }
    

    Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
        #ifdef __LP64__
        uint32_t reserved;
        #endif
    
        const uint8_t * ivarLayout;
        
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
      };
    
    • method_t
      函数四要素:名称,返回值,参数,函数体
    struct method_t {
      SEL name;           //名称
      const char *types;//返回值和参数
      IMP imp;              //函数体
    }
    
    二、 对象,类对象,元类对象
    • 类对象存储实例方法列表等信息。
    • 元类对象存储类方法列表等信息。
    三、消息传递
    屏幕快照 2019-09-03 11.24.04.png
    void objc_msgSend(void /* id self, SEL op, ... */ )
    
    void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    
    struct objc_super {
        /// Specifies an instance of a class.
        __unsafe_unretained _Nonnull id receiver;
    
        /// Specifies the particular superclass of the instance to message. 
    #if !defined(__cplusplus)  &&  !__OBJC2__
        /* For compatibility with old objc-runtime.h header */
        __unsafe_unretained _Nonnull Class class;
    #else
        __unsafe_unretained _Nonnull Class super_class;
    #endif
        /* super_class is the first class to search */
    };
    

    消息传递的流程:缓存查找-->当前类查找-->父类逐级查找

    • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)
    • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)
    • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)
    • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递
    四、消息转发
    + (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
    + (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
    - (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    屏幕快照 2019-09-03 10.12.42.png

    那么最后消息未能处理的时候,还会调用到

    • (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

    • 方法交换(Method-Swizzling)

     + (void)load
    {
        Method test = class_getInstanceMethod(self, @selector(test));
    
        Method otherTest = class_getInstanceMethod(self, @selector(otherTest));
    
        method_exchangeImplementations(test, otherTest);
    }
    

    应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

    • 动态添加方法
    class_addMethod(self, sel, testImp, "v@:");
    
    void testImp (void)
    {
        NSLog(@"testImp");
    }
    
    • @dynamic 动态方法解析
      动态运行时语言将函数决议推迟到运行时
      编译时语言在编译期进行函数决议
    • [obj foo]和objc_msgSend()函数之间有什么关系?
      objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。
    • runtime是如何通过selector找到对应的IMP地址的?
      缓存查找-->当前类查找-->父类逐级查找
    • 能否向编译后的类中增加实例变量?
      不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。
      但可以向 动态添加的 类中增加实例变量。
    五.应用
    1)动态的遍历一个类的所有成员变量,用于字典转模型,归档解档操作
    - (void)viewDidLoad {    
                      [super viewDidLoad];    
                      /** 利用runtime遍历一个类的全部成员变量     
                          1.导入头文件<objc/runtime.h>     */    
                      unsigned int count = 0;   
                     /** Ivar:表示成员变量类型 */    
                      Ivar *ivars = class_copyIvarList([BDPerson class], &count);//获得一个指向该类成员变量的指针   
                     for (int i =0; i < count; i ++) {        
                    //获得Ivar      
                      Ivar ivar = ivars[i];        //根据ivar获得其成员变量的名称--->C语言的字符串      
                      const char *name = ivar_getName(ivar);       
                       NSString *key = [NSString stringWithUTF8String:name];      
                      NSLog(@"%d----%@",i,key);
                    }
                }
    2)可以利用遍历类的属性,来快速的进行归档操作;将从网络上下载的json数据进行字典转模型。
    注意:归档解档需要遵守<NSCoding>协议,实现以下两个方法
                - (void)encodeWithCoder:(NSCoder *)encoder{    
                    //归档存储自定义对象    
                    unsigned int count = 0;  
                    //获得指向该类所有属性的指针   
                    objc_property_t *properties =     class_copyPropertyList([BDPerson class], &count);   
                    for (int i =0; i < count; i ++) {        
                    //获得        
                    objc_property_t property = properties[i];        //根据objc_property_t获得其属性的名称--->C语言的字符串       
                   const char *name = property_getName(property);   
                   NSString *key = [NSString   stringWithUTF8String:name];       
                   //      编码每个属性,利用kVC取出每个属性对应的数值            
                   [encoder encodeObject:[self valueForKeyPath:key] forKey:key]; 
                 }}
                
                - (instancetype)initWithCoder:(NSCoder *)decoder{    
                      //归档存储自定义对象    
                        unsigned int count = 0;   
                     //获得指向该类所有属性的指针   
                       objc_property_t *properties = class_copyPropertyList([BDPerson class], &count);   
                       for (int i =0; i < count; i ++) {       
                       objc_property_t property = properties[i];        //根据objc_property_t获得其属性的名称--->C语言的字符串       
                       const char *name = property_getName(property); 
                         NSString *key = [NSString stringWithUTF8String:name];        //解码每个属性,利用kVC取出每个属性对应的数值      
                       [self setValue:[decoder decodeObjectForKey:key] forKeyPath:key];  
                }   
                 return self;
                }
    3)交换方法
    一.例如数组越界问题,防止系统崩溃(NSMutableArray 添加空值会出现崩溃)
    ① 新建一个分类,分类中引入头文件,实现下列方法
    + (void)load{
                    Method orginalMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject:));
                    Method newMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(Mn_addObject:));
                    
                    method_exchangeImplementations(orginalMethod, newMethod);
                }
                
                - (void)Mn_addObject:(id)object{
                    if (object) {
                        [self Mn_addObject:object];
                    }
                }
    ②在项目文件中,正常使用,若添加空值,不会崩溃只会出现报警信息
    NSMutableArray *arr = [NSMutableArray array];
            [arr addObject:nil];
    二. 生命周期
    ①创建分类
    //load方法会在类第一次加载的时候被调用
                //调用的时间比较靠前,适合在这个方法里做方法交换
    + (void)load{
                    //方法交换应该被保证,在程序中只会执行一次
                    static dispatch_once_t onceToken;
                    dispatch_once(&onceToken, ^{
                        //获得viewController的生命周期方法的selector
                        SEL systemSel = @selector(viewWillAppear:);
                        //自己实现的将要被交换的方法的selector
                        SEL swizzSel = @selector(swiz_viewWillAppear:);
                        //两个方法的Method
                        Method systemMethod = class_getInstanceMethod([self class], systemSel);
                        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
                        //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
                        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
                        if (isAdd) {
                            //如果成功,说明类中不存在这个方法的实现
                            //将被交换方法的实现替换到这个并不存在的实现
                            class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
                        }else{
                            //否则,交换两个方法的实现
                            method_exchangeImplementations(systemMethod, swizzMethod);
                        }
                    });
                }
    - (void)swiz_viewWillAppear:(BOOL)animated{
                    //这时候调用自己,看起来像是死循环
                    //但是其实自己的实现已经被替换了
                    [self swiz_viewWillAppear:animated];
                    NSLog(@"swizzle");
                }
    
            ②在一个自己定义的viewController中重写viewWillAppear,Run起来看看输出吧!
                
                - (void)viewWillAppear:(BOOL)animated{
                    [super viewWillAppear:animated];
                    NSLog(@"viewWillAppear");
                }
    
    4)关联属性
    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
          OBJC_ASSOCIATION_ASSIGN = 0,  //相当于属性中的assign         
          OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,    //retain,monatomic
          OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  //copy,nonatomic
          OBJC_ASSOCIATION_RETAIN = 01401,    //retain 
          OBJC_ASSOCIATION_COPY = 01403     //copy    
      };
    //添加关联对象
            - (void)addAssociatedObject:(id)object{
                objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            }
            //获取关联对象
            - (id)getAssociatedObject{
                return objc_getAssociatedObject(self, _cmd);
            }
    
            //样例
            - (void)viewDidLoad {
                [super viewDidLoad];
                // Do any additional setup after loading the view, typically from a nib.
                UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
                btn.backgroundColor = [UIColor blackColor];
                btn.frame = CGRectMake(100, 100, 60, 30);
                [self.view addSubview:btn];
                 objc_setAssociatedObject(btn, myBtnKey, @"mybtn", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];            
            }
    
            - (void)btnClick:(id)sender {
                NSString *str = objc_getAssociatedObject(sender, myBtnKey);
                /**
                 *  CODE
                 */
            }
    5)方法拦截
    
           + (BOOL)resolveClassMethod:(SEL)sel;
            + (BOOL)resolveInstanceMethod:(SEL)sel;
    

    _objc_msgForward是 IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
    IMP msgForward = _objc_msgForward;
    如果手动调用objc_msgForward,将跳过查找IMP的过程,而是直接出发“消息转发”,进入如下流程:
    1)+ (BOOL)resolveInstanceMethodL:(SEL)sel 实现方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过class_addMethod函数动态地添加方法,消息得到处理,此流程完毕。
    2)在第一步返回的是NO时,就会进入 -(id)forwardTargetForSelector:(SEL)aSelector 方法,这是运行时给我们的第二次机会,用于指定哪个对象响应这个selector。不能指定为self。若返回nil,表示没有响应者,则会进入第三不。若返回某个对象,则会调用该对象的方法。
    3)若第二部返回的是nil,则我们首先要通过 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 指定方法签名,若返回nil,则表示不处理。若返回方法签名,则会进入下一步。
    4)当第三步放回方法签名后,就会调用 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等。
    5)若没有实现 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法,那么会进入 -(void)doesNotRecognizeSelector:(SEL)aSelector方法。若我们没有实现这个方法,那么就会crash,然后提示打不到响应的方法。到此,动态解析的流程就结束了。
    6)runtime如何实现weak变量的自动置nil?
    runtime对注册的类会进行布局,对于weak对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc。假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。
    weak修饰的指针默认值是nil(在Objective-C中向nil发送消息是安全的)

    2. 动态特性:方法解析和消息转发

    没有方法的实现,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

    • Method resolution
    • Fast forwarding
    • Normal forwarding
    2.1 动态方法解析: Method Resolution

    首先,Objective-C 运行时会调用 + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:

    void fooMethod(id obj, SEL _cmd)  
    {
        NSLog(@"Doing foo");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if(aSEL == @selector(foo:)){
            class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod];
    }
    
    

    这里第一字符v代表函数返回类型void,第二个字符@代表self的类型id,第三个字符:代表_cmd的类型SEL。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,更详细的官方文档传送门 在这里,此处不再列举了。

    2.2 快速转发: Fast Rorwarding

    与下面2.3完整转发不同,Fast Rorwarding这是一种快速消息转发:只需要在指定API方法里面返回一个新对象即可,当然其它的逻辑判断还是要的(比如该SEL是否某个指定SEL?)。
    消息转发机制执行前,runtime系统允许我们替换消息的接收者为其他对象。通过- (id)forwardingTargetForSelector:(SEL)aSelector方法。如果此方法返回的是nil 或者self,则会进入消息转发机制(- (void)forwardInvocation:(NSInvocation *)invocation),否则将会向返回的对象重新发送消息。

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if(aSelector == @selector(foo:)){
            return [[BackupClass alloc] init];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    
    2.3 完整消息转发: Normal Forwarding

    与上面不同,可以理解成完整消息转发,是可以代替快速转发做更多的事。

    - (void)forwardInvocation:(NSInvocation *)invocation {
        SEL sel = invocation.selector;
        if([alternateObject respondsToSelector:sel]) {
            [invocation invokeWithTarget:alternateObject];
        } else {
            [self doesNotRecognizeSelector:sel];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
        if (!methodSignature) {
            methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
        }
        return methodSignature;
    }
    
    

    forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。

    其中,参数invocation是从哪来的?在forwardInvocation:消息发送前,runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都继承了forwardInvocation:方法,我们可以将消息转发给其它的对象。

    2.4 区别: Fast Rorwarding 对比 Normal Forwarding?

    这两个转发都是将消息转发给其它对象,那么这两个有什么区别?

    • 需要重载的API方法的用法不同
      前者只需要重载一个API即可,后者需要重载两个API。
      前者只需在API方法里面返回一个新对象即可,后者需要对被转发的消息进行重签并手动转发给新对象(利用 invokeWithTarget:)。
    • 转发给新对象的个数不同
      前者只能转发一个对象,后者可以连续转发给多个对象。例如下面是完整转发:
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if (aSelector==@selector(run)) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector: aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
        SEL selector =[anInvocation selector];
        
        RunPerson *RP1=[RunPerson new];
        RunPerson *RP2=[RunPerson new];
        
        if ([RP1 respondsToSelector:selector]) {
            
            [anInvocation invokeWithTarget:RP1];
        }
        if ([RP2 respondsToSelector:selector]) {
            
            [anInvocation invokeWithTarget:RP2];
        }    
    }
    
    

    3. 应用实战:消息转发

    3.1 特定奔溃预防处理

    下面有一段因为没有实现方法而会导致奔溃的代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.view setBackgroundColor:[UIColor whiteColor]];
        self.title = @"Test2ViewController";
        
        //实例化一个button,未实现其方法
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.frame = CGRectMake(50, 100, 200, 100);
        button.backgroundColor = [UIColor blueColor];
        [button setTitle:@"消息转发" forState:UIControlStateNormal];
        [button addTarget:self
                   action:@selector(doSomething)
         forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    

    为解决这个问题,可以专门创建一个处理这种问题的分类

    • NSObject+CrashLogHandle
    #import "NSObject+CrashLogHandle.h"
    
    @implementation NSObject (CrashLogHandle)
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        //方法签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
    }
    
    @end
    

    因为在category中复写了父类的方法,会出现下面的警告:
    解决办法就是在Xcode的Build Phases中的资源文件里,在对应的文件后面 -w ,忽略所有警告。

    3.2 苹果系统API迭代造成API不兼容的奔溃处理
    3.2.1 兼容系统API迭代的传统方案

    随着每年iOS系统与硬件的更新迭代,部分性能更优异或者可读性更高的API将有可能对原有API进行废弃与更替。与此同时我们也需要对现有APP中的老旧API进行版本兼容,当然进行版本兼容的方法也有很多种,下面笔者会列举常用的几种:

    • 根据能否响应方法进行判断
    if ([object respondsToSelector: @selector(selectorName)]) {
        //using new API
    } else {
        //using deprecated API
    }
    
    
    • 根据当前版本SDK是否存在所需类进行判断
    if (NSClassFromString(@"ClassName")) {    
        //using new API
    }else {
        //using deprecated API
    }
    
    
    • 根据操作系统版本进行判断
    #define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) {
        majorVersion,
        minorVersion,
        patchVersion
    }]
    
    if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
        //using new API
    } else {
        //using deprecated API
    }
    
    
    3.2.2 兼容系统API迭代的新方案

    需求:假设现在有一个利用新API写好的类,如下所示,其中有一行可能因为运行在低版本系统(比如iOS9)导致奔溃的代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.view setBackgroundColor:[UIColor whiteColor]];
        self.title = @"Test3ViewController";
        
        UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain];
        tableView.delegate = self;
        tableView.dataSource = self;
        tableView.backgroundColor = [UIColor orangeColor];
        
        // May Crash Line
        tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        
        [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
        [self.view addSubview:tableView];
    }
    
    

    其中有一行会发出警告,Xcode也给出了推荐解决方案,如果你点击Fix它会自动添加检查系统版本的代码,如下图所示:


    屏幕快照 2019-09-03 17.25.31.png

    方案1:手动加入版本判断逻辑

    以前的适配处理,可根据操作系统版本进行判断

    if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
        scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    } else {
        viewController.automaticallyAdjustsScrollViewInsets = NO;
    }
    
    

    方案2:消息转发

    在iOS11 Base SDK直接采取最新的API并且配合Runtime的消息转发机制就能实现一行代码在不同版本操作系统下采取不同的消息调用方式

    • UIScrollView+Forwarding.m
    #import "UIScrollView+Forwarding.h"
    #import "NSObject+AdapterViewController.h"
    
    @implementation UIScrollView (Forwarding)
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1
        
        NSMethodSignature *signature = nil;
        if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {
            signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
        }else {
            signature = [super methodSignatureForSelector:aSelector];
        }
        return signature;
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation { // 2
        
        BOOL automaticallyAdjustsScrollViewInsets  = NO;
        UIViewController *topmostViewController = [self cm_topmostViewController];
        NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3
        [viewControllerInvocation setTarget:topmostViewController];
        [viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
        [viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4
        [viewControllerInvocation invokeWithTarget:topmostViewController]; // 5
    }
    
    @end
    
    
    • NSObject+AdapterViewController.m
    #import "NSObject+AdapterViewController.h"
    
    @implementation NSObject (AdapterViewController)
    
    - (UIViewController *)cm_topmostViewController {
        UIViewController *resultVC;
        resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
        while (resultVC.presentedViewController) {
            resultVC = [self cm_topViewController:resultVC.presentedViewController];
        }
        return resultVC;
    }
    
    - (UIViewController *)cm_topViewController:(UIViewController *)vc {
        if ([vc isKindOfClass:[UINavigationController class]]) {
            return [self cm_topViewController:[(UINavigationController *)vc topViewController]];
        } else if ([vc isKindOfClass:[UITabBarController class]]) {
            return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]];
        } else {
            return vc;
        }
    }
    
    @end
    
    

    当我们在iOS10调用新API时,由于没有具体对应API实现,我们将其原有的消息转发至当前栈顶UIViewController去调用低版本API。
    关于[self cm_topmostViewController];,执行之后得到的结果可以查看如下:


    屏幕快照 2019-09-03 17.28.03.png

    方案2的整体流程:

    1. 为即将转发的消息返回一个对应的方法签名(该签名后面用于对转发消息对象(NSInvocation *)anInvocation进行编码用)
    2. 开始消息转发((NSInvocation *)anInvocation封装了原有消息的调用,包括了方法名,方法参数等)
    3. 由于转发调用的API与原始调用的API不同,这里我们新建一个用于消息调用的NSInvocation对象viewControllerInvocation并配置好对应的target与selector
    4. 配置所需参数:由于每个方法实际是默认自带两个参数的:self和_cmd,所以我们要配置其他参数时是从第三个参数开始配置
    5. 消息转发

    4. 总结

    4.1 模拟多继承

    面试挖坑:OC是否支持多继承?好,你说不支持多继承,那你有没有模拟多继承特性的办法?

    转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
    虽然转发可以实现继承功能,但是NSObject还是必须表面上很严谨,像respondsToSelector:和isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。

    Objective-C 中给一个对象发送消息会经过以下几个步骤:

    1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
    2. 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;
    3. 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
    4. 如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

    本文Demo传送门: MessageForwardingDemo

    相关文章

      网友评论

          本文标题:RunTime

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