runtime

作者: wxhan | 来源:发表于2019-02-24 01:26 被阅读192次

    一.什么是runtime(运行时)

    Objective-C是一个动态语言,一个由Objective-C编写的程序跑起来分为编译过程和运行过程,当程序在运行的时候,才能做一些处理,比如说类的创建,方法的调用,消息的传递和转发等。也就是说runtime就是程序正在运行的时候的状态。(C是一种静态语言,它在编译的时候就完成这些处理)

    可以简单的理解为:Objective-C编写的程序在运行的过程,其实就是runtime状态下的C语言代码。

    实例说明:在.h文件声明一个对象方法,如果在.m文件中不做任何实现的话,然后在外部对这个对象方法进行调用。程序编译的时候不会报错,运行的时候再报错。

    二.runtime中的class(类)

    Objective-C中,任何类的定义都是对象。类和类的实例(对象)在本质上是没有区别的。

    1.打开Xcode,然后按下 shift + command + O,搜索 objc.h

    #if !OBJC_TYPES_DEFINED
    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    #endif
    
    /// An opaque type that represents a method selector.
    typedef struct objc_selector *SEL;
    
    /// A pointer to the function of a method implementation. 
    #if !OBJC_OLD_DISPATCH_PROTOTYPES
    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    #else
    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
    #endif
    

    可以看出 Class (类)是一个 objc_class 结构类型的指针。
    2.搜索 objc_class

    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    

    三.runtime中消息的传递

    对象方法的调用:[obj method] ,在runtime机制中,其实就是消息的发送objc_msgSend(obj, method)

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Person : NSObject
    
    - (void)setName:(NSString *)name;
    
    - (void)setAge:(NSString *)age;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    Person *p = [[Person alloc] init];
    [p setName:@"wxh"];
    

    当程序运行中,执行到[p setName:@"wxh"];这条语句时候,在runtimeC做了这样的消息传递objc_msgSend(p, setName:),具体如下:

    1.通过 p (obj)isa指针找到Person这个类(class)

    注:isa指针 是一个Class类型的指针,每个实例对象有个isa的指针,他指向对象的类。

    2.在Person这个类(class)methodLists(存放这个类对象函数的一个数组列表)中找到setName:这个函数(method),如果找不到,就到Person的父类(super_class)中去找。

    3.找到setName: 这个函数(method),通过setName:这个函数(method)IMP指针 ,调用这个函数(method)

    注:IMP指针 IMP是一个函数指针,指向函数实现的地址。

    4.另外一个重要成员, objc_cache类型的cache。它把经常调用的函数缓存起来,当再次收到setName:的消息的时候,直接从cache里面找,大大提高了效率。

    总结:Objective-C中,函数的调用,其实就是在runtimeC做了消息传递。runtimeC做消息传递的函数。
    objc_msgSend(void /* id self, SEL op, ... */ )
    说到这里,顺便看一下底层中的定义函数

    typedef struct objc_method *Method;
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp   
    }                                 
    

    SEL是指向 函数名的指针 IMP是指向函数实现地址的指针 char函数的类型
    由此可以看出,在Objective-C底层中定义一个函数的时候,并没有涉及到参数,这就是为什么Objective-C中不可以使用函数重载的原因。

    四.runtime的应用

    1. 关联对象。

    runtime给出的方法有

    //关联对象
    ①void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    //获取关联的对象
    ②id objc_getAssociatedObject(id object, const void *key)
    //移除关联的对象
    ③void objc_removeAssociatedObjects(id object)
    

    (1)给分类category添加属性
    举个例子:给NSObject添加一个分类 NSObject+wxh,记得#import "objc/runtime.h"

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSObject (wxh)
    
    @property (nonatomic,copy)NSString *name;
    
    @property (nonatomic,strong)NSArray *datas;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "NSObject+wxh.h"
    #import "objc/runtime.h"
    
    @implementation NSObject (wxh)
    // 用一个字节来存储key值,设置为静态私有变量,避免外界修改
    static char nameKey;
    - (void)setName:(NSString *)name{
        // 将某个值与某个对象关联起来,将某个值存储到某个对象中
        objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    - (NSString *)name{
        return objc_getAssociatedObject(self, &nameKey);
    }
    
    static char datasKey;
    - (void)setDatas:(NSArray *)datas{
        objc_setAssociatedObject(self, &datasKey, datas, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (NSArray *)datas{
        return objc_getAssociatedObject(self, &datasKey);
    }
    
    @end
    
     NSString *string = [[NSString alloc] init];
     string.name = @"123";
     NSLog(@"%@",string.name);
     string.datas = @[@"w",@"x",@"h"];
     NSLog(@"%@",string.datas);
    

    解释:KVC的字典转模型差不多逻辑。

    • 先通过void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy):设置关联对象的方法,相当于setValue:forKey
    • 再使用id objc_getAssociatedObject(id object, const void *key):获取关联对象。相当于valueForKey:
    • objc_AssociationPolicy policy:关联策略。
    OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
    OBJC_ASSOCIATION_RETAIN_NONATOMIC 等价于 @property(strong, nonatomic)。
    OBJC_ASSOCIATION_COPY_NONATOMIC 等价于 @property(copy, nonatomic)。
    OBJC_ASSOCIATION_RETAIN 等价于 @property(strong,atomic)。
    OBJC_ASSOCIATION_COPY 等价于 @property(copy, atomic)。
    

    (2)使分散的代码通过block的形式集中到一起。这样可读性更强,代码更简洁。
    举个例子:button添加点击事件。

    - (void)viewDidLoad {
        [super viewDidLoad];
        UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
        [btn setBackgroundColor:[UIColor redColor]];
        [self.view addSubview:btn];
        [btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
    }
    - (void)btnClick{
        NSLog(@"我被点击了");
    }
    

    也就是说我们创建按钮按钮点击事件是分开的处理,如果页面逻辑比较多,调试的时候就会比较麻烦,有时候找一个按钮的点击事件找了大半天。下面将两者关联起来。

    #import <UIKit/UIKit.h>
    
    typedef void (^event)(id sender);
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface UIButton (wxh)
    
    - (void)handelWithEvent:(event)block;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "UIButton+wxh.h"
    #import <objc/runtime.h>
    
    @implementation UIButton (wxh)
    
    - (void)handelWithEvent:(event)block {
        if (block) {
            objc_setAssociatedObject(self, @selector(btnAction:), block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        [self addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
    }
    - (void)btnAction:(id)sender{
        event block = objc_getAssociatedObject(self,@selector(btnAction:));
        if (block) {
            block(sender);
        }
    }
    
    @end
    
        UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
        [btn setBackgroundColor:[UIColor redColor]];
        [self.view addSubview:btn];
        [btn handelWithEvent:^(id sender) {
            NSLog(@"我被点击了");
        }];
    

    也就是说在开发中,我们可以利用这一点代替比较繁琐的代理事件。

    2. 交换方法

    class_getClassMethod 得到类的类方法
    class_getInstanceMethod 得到类的对象方法

    • 交换类方法
    import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface NSURL (wxh)
    + (instancetype)wxhURLWithString:(NSString *)URLString;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "NSURL+wxh.h"
    #import <objc/runtime.h>
    @implementation NSURL (wxh)
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class cls = object_getClass([NSURL class]);
            
            SEL SEL_old = @selector(URLWithString:);
            SEL SEL_new = @selector(wxhURLWithString:);
            
            Method method_old = class_getClassMethod([NSURL class], SEL_old);
            Method method_new = class_getClassMethod([NSURL class], SEL_new);
            
            IMP imp_old = method_getImplementation(method_old);
            IMP imp_new = method_getImplementation(method_new);
            
            BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
            if (beMethod_old) {
                class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
            }
            else{
                method_exchangeImplementations(method_old, method_new);
            }
        });
    }
    + (instancetype)wxhURLWithString:(NSString *)URLString{
        if (!URLString.length) {
            URLString = @"www.baidu.com";
        }
        NSURL *url = [NSURL wxhURLWithString:URLString];
        return url;
    }
    @end
    
    #import "ViewController.h"
    @interface ViewController ()
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSURL *url = [NSURL URLWithString:@""];
        NSLog(@"打印URL:%@",url);
    }
    @end
    

    打印结果:打印URL:www.baidu.com

    • 交换对象方法
    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface NSMutableArray (wxh)
    - (void)wxhAddObject:(id)object;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "NSMutableArray+wxh.h"
    #import <objc/runtime.h>
    @implementation NSMutableArray (wxh)
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSMutableArray *obj = [[NSMutableArray alloc] init];
            [obj exchangeImplementationsMethod:@selector(addObject:) WithMethod:@selector(wxhAddObject:)];
        });
    }
    - (void)wxhAddObject:(id)object{
        if (object) {
            [self wxhAddObject:object];
        }
        else{
            NSLog(@"=== object is nil ===");
        }
    }
    - (void)exchangeImplementationsMethod:(SEL)SEL_old WithMethod:(SEL)SEL_new{
        Class cls = [self class];
        Method method_old = class_getInstanceMethod(cls, SEL_old);
        Method method_new = class_getInstanceMethod(cls, SEL_new);
        
        IMP imp_old = method_getImplementation(method_old);
        IMP imp_new = method_getImplementation(method_new);
        
        BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
        if (beMethod_old) {
            class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
        }
        else{
            method_exchangeImplementations(method_old, method_new);
        }
    }
    @end
    
    #import "ViewController.h"
    @interface ViewController ()
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSString *r = nil;
        NSMutableArray *arr = [NSMutableArray array];
        [arr addObject:r];
    }
    @end
    

    打印结果:=== object is nil ===
    解释:
    (1)全局效果:不需要引入头文件,直接调用原来的方法就可。
    (2)只实现一次dispatch_once:交换方法效果是全局的,只能让它实现一次。虽然系统加载程序时+ (void)load只调用一次,但+ (void)load是一个公开的类函数,有可能被手动调用。因此加上dispatch_once是必要的。
    (3)class_addMethod:给class动态添加方法sel,如果selclass中已经存在的,返回值是NO,否则返回值为YES
    (4)class_replaceMethod:修改函数的IMP指针,使其指向新的实现地址。
    (5)原理:首先,通过dispatch_once保证方法交换有且只有一次,然后根据class_addMethod的返回值判断要被交换的方法本身是否存在。如果为NO,说明存在,通过exchangeImplementationsMethod将两个函数的IMP指针指向的函数实现地址交叉互换。如果为YES,说明要在交换的方法本身是不存在的,但是这时候已经通过class_addMethod将它添加到这个class里面,而且将他的IMP指向新的方法。那么最后只需要再通过class_replaceMethod将新的方法的IMP指向要被交换的方法即可。(其实最后这一步是可以省略的,因为如果要被交换的方法本身是不存在的,那这整个过程其实就相当于给class添加方法而已。)
    (6)object_getClass获取父类的Class

    • 应用场景 一般都是用来改变一些系统方法的实现。
      (1)例如上面NSMutableArray的例子,达到防奔溃处理。
      (2)改变viewDidLoad打印出当前控制器的名称等。

    3.动态添加函数

    class_addMethod返回BOOL值,YES表示添加成功,NO表示添加不成功。
    + (BOOL)resolveInstanceMethod:(SEL)sel对象函数动态决议方法。
    + (BOOL)resolveClassMethod:(SEL)sel类函数动态决议方法。

    • 动态添加对象方法:创建一个Person继承于NSObject,不添加任何属性和方法,直接在外部给Person添加一个对象方法。
    #import "Person.h"
    #import <objc/runtime.h>
    @interface ViewController ()
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *p = [[Person alloc] init];
        class_addMethod([Person class], @selector(say:WithName:), class_getMethodImplementation([ViewController class], @selector(personSay:WithName:)), method_getTypeEncoding(class_getInstanceMethod([ViewController class], @selector(personSay:WithName:))));
        [p performSelector:@selector(say:WithName:) withObject:@"hello" withObject:@"wxh"];
    }
    - (void)personSay:(NSString *)string WithName:(NSString *)name{
        NSLog(@"%@,%@",name,string);
    }
    

    打印结果为wxh,hello

    • 动态添加类方法
    #import "ViewController.h"
    #import "Person.h"
    #import <objc/runtime.h>
    @interface ViewController ()
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        class_addMethod(object_getClass([Person class]), @selector(drive), class_getMethodImplementation(object_getClass([ViewController class]), @selector(personDrive)), method_getTypeEncoding(class_getClassMethod(object_getClass([ViewController class]), @selector(personDrive))));
        [Person performSelector:@selector(drive)];
    }
    + (void)personDrive{
        NSLog(@"===");
    }
    

    打印结果为===
    解释:
    (1)如果是一个类class要添加对象方法,直接用类class添加。如果一个类class要添加类方法,只需要通过这个类的父类object_getClass,添加成父类的对象方法,也就是该类的类方法。
    (2)方法添加成功之后,必须要通过performSelector调用,不能直接调用。因为class_addMethod是在程序运行状态下才执行的,而编译状态下,还找不到动态添加的方法。
    (3)当一个对象cls去调用一个函数method的时候,cls通过自身的ipa指针找到该对象的类class,然后通过class中的函数列表methodLists查找method的函数名sel,如果找到sel,通过IMP指针指向的'method'的实现地址,调用该method。那如果找不到呢,这时候就会去到动态决议方法那里,并返回NO值。
    (4)class_addMethod当要添加的方法在改类或者该类的父类中已经存在时,class_addMethod返回NO

    • 应用场景
      (1)在类的外部动态给类class添加方法。
      (2)重写动态协议,防止调用不存在的方法时,程序崩溃crash
      (3)通过class_addMethod的返回值判断某个类中是否有某个方法。

    4.获取属性,修改属性和获取方法。

    • 获取属性
      class_copyIvarList
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([UITextField class], &count);
        for (int i = 0; i<count; i++) {
            // 取出成员变量
            Ivar ivar = *(ivars + i);
            // 打印成员变量名字 数据类型
            NSLog(@"%s--%s", ivar_getName(ivar),ivar_getTypeEncoding(ivar));
        }
        // 释放
        free(ivars);
    
    • 获取方法
      class_copyMethodList
        unsigned int methCount = 0;
        Method *meths = class_copyMethodList([UITextField class], &methCount);
        for(int i = 0; i < methCount; i++) {
            Method meth = meths[i];
            SEL sel = method_getName(meth);
            const char *name = sel_getName(sel);
            NSLog(@"%s", name);
        }
        free(meths);
    
    • 修改属性
      通过KVC的方式进行修改即可
    [textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];
    

    5.归档&解档

    归档&解档

    6.热更新

    JSPatch

    7.逆向开发

    相关文章

      网友评论

        本文标题:runtime

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