美文网首页
iOS-Runtime6-API

iOS-Runtime6-API

作者: Imkata | 来源:发表于2019-12-11 18:25 被阅读0次

    导入#import <objc/runtime.h>头文件,我们就能使用runtime相关的API了,这里介绍一些常用的API。

    一. 类相关API

    //动态创建一个类(参数:父类,类名,额外的内存空间)
    Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
    
    //注册一个类(要在类注册之前添加成员变量)
    void objc_registerClassPair(Class cls)
    
    //销毁一个类
    void objc_disposeClassPair(Class cls)
    
    //获取对象的isa指向的Class
    Class object_getClass(id obj)
    
    //设置对象的isa指向的Class
    Class object_setClass(id obj, Class cls)
    
    //判断一个对象是否为Class
    BOOL object_isClass(id obj)
    
    //判断一个Class是否为元类
    BOOL class_isMetaClass(Class cls)
    
    //获取父类
    Class class_getSuperclass(Class cls)
    

    1. object_getClass、object_setClass、object_isClass

    MJPerson *person = [[MJPerson alloc] init];
    [person run]; //-[MJPerson run]
    
    //修改person对象isa的指向,指向MJCar类对象
    object_setClass(person, [MJCar class]);
    //person变成MJCar类型的,会去MJCar类对象里面寻找方法,最后调用-[MJCar run]
    [person run]; //-[MJCar run]
    
    NSLog(@"%d %d %d",
          object_isClass(person),// 0 person是实例对象,不是类对象
          object_isClass([MJPerson class]),// 1 是类对象
          object_isClass(object_getClass([MJPerson class]))// 1 是类对象(元类对象也是一种特殊的类对象)
          );
    
    NSLog(@"%p %p %p",object_getClass(person),object_getClass([MJPerson class]), [MJPerson class]); 
    //0x100002700 0x100002728 0x100002750
    //打印的分别是:MJCar类对象的地址,MJPerson元类对象的地址,MJPerson类对象的地址
    

    上面代码,修改person对象isa的指向,指向MJCar类对象,最后会调用MJCar类对象的方法。

    2. objc_allocateClassPair

    
    void run(id self, SEL _cmd)
    {
        NSLog(@"_____ %@ - %@", self, NSStringFromSelector(_cmd));
    }
    
    void test()
    {
        // 创建类,传入父类和类名
        Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
        // 注册类之前添加成员变量
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_weight", 4, 1, @encode(int));
        class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
        // 注册类
        objc_registerClassPair(newClass);
    
        id dog = [[newClass alloc] init];
        [dog setValue:@10 forKey:@"_age"];
        [dog setValue:@20 forKey:@"_weight"];
        [dog run];
    
        NSLog(@"%@ %@", [dog valueForKey:@"_age"], [dog valueForKey:@"_weight"]);
    
        MJPerson *person = [[MJPerson alloc] init];
        //修改person对象isa指向
        object_setClass(person, newClass);
        [person run];
         
        //打印:
        //_____ <MJDog: 0x10053a150> - run
        // 10 20
        //_____ <MJDog: 0x102008520> - run
            
        // 在不需要这个类时释放
        objc_disposeClassPair(newClass);
    }
    

    在程序运行的时候,动态添加一个类,并且添加成员变量、方法,最后使用类。

    1. 一定要在注册类之前添加成员变量,因为成员变量是在_r_o_t表里面,是只读的,所以要在类的结构确定之前添加成员变量。
    2. 不能使用class_addIvar给已经创建的类添加成员变量,因为已经创建的类的结构在代码写完就已经确定好了,程序运行中就不能给已经创建的类添加成员变量了。
    3. 方法可以在注册类之后添加,因为方法是在_r_w_t表里面,是可读可写的。

    打印如下,说明创建并使用类成功,修改对象isa指向成功。

    //_____ <MJDog: 0x10053a150> - run
    // 10 20
    //_____ <MJDog: 0x102008520> - run
    

    二. 成员变量相关API

    //获取类中指定名称实例成员变量的信息
    Ivar class_getInstanceVariable(Class cls, const char *name)
    
    //获取成员变量的相关信息
    const char *ivar_getName(Ivar v)
    const char *ivar_getTypeEncoding(Ivar v)
    
    //设置和获取成员变量的值
    void object_setIvar(id obj, Ivar ivar, id value)
    id object_getIvar(id obj, Ivar ivar)
    
    //拷贝实例变量列表(最后需要调用free释放)
    Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
    
    //动态添加成员变量(已经注册的类是不能动态添加成员变量的)
    BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
    

    1. class_getInstanceVariable、object_setIvar

    //获取类中指定名称实例成员变量的信息
    //传入的是一个类对象,所以只能获取成员变量的信息,并不能获取成员变量的值
    Ivar ageIvar = class_getInstanceVariable([MJPerson class], "_age");
    Ivar nameIvar = class_getInstanceVariable([MJPerson class], "_name");
    
    NSLog(@"%s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));
    //打印:_age i   i代表字符编码int
    
    MJPerson *person = [[MJPerson alloc] init];
    //设置成员变量的值
    //传入的是一个实例对象,所以可以设置成员变量的值
    object_setIvar(person, nameIvar, @"123");
    object_setIvar(person, ageIvar, (__bridge id)(void *)10);
    //获取成员变量的值
    id name = object_getIvar(person, nameIvar);
    
    NSLog(@"%@ %d", name, person.age);
    //打印:123 10
    

    对于这行代码:

    object_setIvar(person, ageIvar, (__bridge id)(void *)10);
    

    上面runtimeAPI内部没做转换,所以需要传什么值就传什么值,但是要做一些数据类型转换(先转成指针类型,再转成id类型)。

    如果是KVC的value值,可以传NSNumber类型的值,因为KVC内部会做转换:[@10 integerValue]。

    [person setValue:@10 forKeyPath:@"_age"]
    

    2. class_copyIvarList

    //获取成员变量数组
    unsigned int count; //成员变量数量
    //参数传入int变量的地址
    //调用完这个函数就会给count赋值
    Ivar *ivars = class_copyIvarList([MJPerson class], &count);
    
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[I];
        NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    }
    
    free(ivars); //runtime里面,调用copy、create都要释放掉
    
    打印:
    _ID I
    _weight I
    _age I
    _name @"NSString"
    

    class_copyIvarList返回值是Ivar *指针类型的,所以用Ivar *接收,C语言中指针是可以当数组来用的(C语言语法基础),所以class_copyIvarList函数的返回值可以直接当个数组来用。

    3. class_copyIvarList的使用

    如果设置UITextField占位文字的颜色,我们可以这样:

    NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
    attrs[NSForegroundColorAttributeName] = [UIColor redColor];
    self.textField.attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:@"请输入用户名" attributes:attrs];
    

    也可以:

    UILabel *placeholderLabel = [self.textField valueForKeyPath:@"_placeholderLabel"];
    placeholderLabel.textColor = [UIColor redColor];
    

    或者:

    //_placeholderLabel是懒加载的,要先设置placeholder
    self.textField.placeholder = @"请输入用户名";
    [self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
    

    后面了两种方式都用到了_placeholderLabel,但是我们怎么知道UITextField里面有_placeholderLabel呢?

    这时候就需要获取类对象的成员变量列表了:

    NSMutableArray *arr = [NSMutableArray array];
    unsigned int count;
    Ivar *ivars = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[I];
        [arr addObject:[NSString stringWithFormat:@"%s",ivar_getName(ivar)]];
        //NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    }
    NSLog(@"%@",arr);
    free(ivars);
    

    打印:

    ......
    "_placeholderLabel",
    ......
    

    使用class_copyIvarList可以获取类对象所有的成员变量,不管成员变量是不是私有的,我们知道UITextField类对象的成员变量之后就可以访问或修改成员变量了。

    MJExtension就是根据这个原理自动将json转成OC对象的,给NSObject添加分类,简单实现如下:

    + (instancetype)mj_objectWithJson:(NSDictionary *)json
    {
        id obj = [[self alloc] init];
        
        unsigned int count;
        //因为添加的是类方法,所以这个self就是方法调用者类对象
        Ivar *ivars = class_copyIvarList(self, &count);
        for (int i = 0; i < count; i++) {
            // 取出i位置的成员变量
            Ivar ivar = ivars[I];
            //将C语言字符串转成OC字符串
            NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
            [name deleteCharactersInRange:NSMakeRange(0, 1)];
            
            // 根据成员变量名获取value值
            id value = json[name];
            if ([name isEqualToString:@"ID"]) {
                value = json[@"id"];
            }
            //设值
            [obj setValue:value forKey:name];
        }
        free(ivars);
        
        return obj;
    }
    

    上面只是简单的实现,实际上一个成熟的框架还需要更多的操作,这些都可以通过runtime实现。

    三. 属性相关API

    //获取一个属性
    objc_property_t class_getProperty(Class cls, const char *name)
    
    //拷贝属性列表(最后需要调用free释放)
    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    
    //动态添加属性
    BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                           unsigned int attributeCount)
    
    //动态替换属性
    void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                               unsigned int attributeCount)
    
    //获取属性的一些信息
    const char *property_getName(objc_property_t property)
    const char *property_getAttributes(objc_property_t property)
    

    四. 方法相关API

    //获取一个实例方法、类方法
    Method class_getInstanceMethod(Class cls, SEL name)
    Method class_getClassMethod(Class cls, SEL name)
    
    //根据class和方法名获取方法的imp
    IMP class_getMethodImplementation(Class cls, SEL name)
    //设置方法的imp
    IMP method_setImplementation(Method m, IMP imp)
    //交换方法的imp
    void method_exchangeImplementations(Method m1, Method m2)
    //获取方法名
    SEL method_getName(Method m)
    //获取imp
    IMP method_getImplementation(Method m)
    //获取方法返回值类型、参数类型的编码
    const char *method_getTypeEncoding(Method m)
    //获取参数个数
    unsigned int method_getNumberOfArguments(Method m)
    //获取返回值类型
    char *method_copyReturnType(Method m)
    //根据index获取参数
    char *method_copyArgumentType(Method m, unsigned int index)
    
    //拷贝方法列表(最后需要调用free释放)
    Method *class_copyMethodList(Class cls, unsigned int *outCount)
    
    //动态添加方法
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
    
    //动态替换方法
    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
    
    //根据SEL获取名字
    const char *sel_getName(SEL sel)
    //根据字符串包装成一个SEL,和@selector("方法名字")方法等效
    SEL sel_registerName(const char *str)
    
    //根据block返回一个imp
    IMP imp_implementationWithBlock(id block)
    //根据imp返回一个block
    id imp_getBlock(IMP anImp)
    //移除imp对应的block
    BOOL imp_removeBlock(IMP anImp)
    

    1. 将block当做方法实现

    void myrun()
    {
        NSLog(@"---myrun");
    }
    
    MJPerson *person = [[MJPerson alloc] init];
    
    //将myrun函数当做方法的实现
    //class_replaceMethod([MJPerson class], @selector(run), (IMP)myrun, "v");
    
    //将block当做方法的实现
    class_replaceMethod([MJPerson class], @selector(run), imp_implementationWithBlock(^{
        NSLog(@"123123");
    }), "v");
    
    [person run]; //打印:123123
    

    2. 交换方法实现

    MJPerson *person = [[MJPerson alloc] init];
    //交换对象方法,传入类对象
    Method runMethod = class_getInstanceMethod([MJPerson class], @selector(run));
    Method testMethod = class_getInstanceMethod([MJPerson class], @selector(test));
    
    method_exchangeImplementations(runMethod, testMethod);
    
    [person run]; //打印:-[MJPerson test]
    

    五. 交换方法实现的使用

    交换方法实现在开发中经常使用,但是实际上我们使用最多的是交换系统或者第三方框架的方法。

    1. 拦截所有按钮的点击事件:

    UIButton继承于UIControl,UIControl有一个sendAction:to:forEvent:方法,每当触发一个事件就会调用这个方法,所以我们可以给UIControl添加分类,在分类中交换这个方法的实现:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // hook:钩子函数
            Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
            Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
            method_exchangeImplementations(method1, method2);
        });
    }
    
    - (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
    {
        NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    
        // 调用系统原来的实现
        // 因为方法已经交换了,所以其实是调用sendAction:to:forEvent:
        [self mj_sendAction:action to:target forEvent:event];
    
        //拦截按钮事件
        if ([self isKindOfClass:[UIButton class]]) {
            // 拦截了所有按钮的事件
    
        }
    }
    

    上面交换方法也叫钩子函数,利用钩子函数就实现了拦截所有UIButton的点击事件。

    问题1:为什么上面要加个dispatch_once?

    按理说load方法只会调用一次,万一别人主动调用了load方法那不就调用两次了吗,这样方法就交换两次了和没交换一样,所以加个dispatch_once。

    问题2:交换方法实现的原理是什么?

    method_exchangeImplementations方法是传入两个Method,以前我们讲过Method的内部结构,其实交换方法实现就是把Method里面的IMP交换了,如下图:

    交换前.png 交换后.png

    上面说的交换方法实现,交换的是方法列表(methods数组)里面的method_t(也就是Method),如果这个方法有缓存,怎么办?

    问题3:如果这个方法有缓存,怎么办?

    其实,调用method_exchangeImplementations函数会清空缓存,这样就保证了交换方法之后调用方法不会出错。

    可以在objc4里面搜索到源码:

    void method_exchangeImplementations(Method m1, Method m2)
    {
        if (!m1  ||  !m2) return;
    
        rwlock_writer_t lock(runtimeLock);
    
        IMP m1_imp = m1->imp;
        m1->imp = m2->imp;
        m2->imp = m1_imp;
    
    
        // RR/AWZ updates are slow because class is unknown
        // Cache updates are slow because class is unknown
        // fixme build list of classes whose Methods are known externally?
    
        flushCaches(nil);
    
        updateCustomRR_AWZ(nil, m1);
        updateCustomRR_AWZ(nil, m2);
    }
    

    上面源码很简单,可以发现,交换IMP之后就会清空缓存。

    2. 预防数组添加nil崩溃

    当我们给数组添加对象,如果这个对象是nil,那么就会崩溃,如下代码:

     NSString *obj = nil;
    
     NSMutableArray *array = [NSMutableArray array];
     [array addObject:@"jack"];
     [array addObject:obj]; //崩溃
     [array insertObject:obj atIndex:0]; //崩溃
    

    崩溃:

    'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
    

    如何预防?

    我们可以交换insertObject:atIndex:方法,因为无论调用addObject:还是调用insertObject:atIndex:最后都会调用insertObject:atIndex:方法。

    给NSMutableArray添加分类,实现如下代码:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型
            Class cls = NSClassFromString(@"__NSArrayM");
            Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
            Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
            method_exchangeImplementations(method1, method2);
        });
    }
    
    - (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
    {
        if (anObject == nil) return;
        
        [self mj_insertObject:anObject atIndex:index];
    }
    

    重新运行代码,发现不崩溃了。

    上面代码传入的类是__NSArrayM,这才是NSMutableArray的真实类型,我们也可打断点,po一下:

    (lldb) po array
    <__NSArrayM 0x6000004e7c00>(
    
    )
    

    发现的确是__NSArrayM。

    对于NSString、NSArray、NSDictionary它们的真实类型都是其他类型,要注意,一定要传真实类型。对于这种表面上是一种类型,实际上是另外一种类型,我们叫类簇。

    3. 预防字典key传入nil崩溃

    当可变字典的setter方法传入的key是nil,会崩溃:

    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[@"name"] = @"jack";
    dict[obj] = @"rose"; //崩溃
    dict[@"age"] = obj;
    
    NSLog(@"%@", dict);
    

    崩溃:

    'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKeyedSubscript:]: key cannot be nil'
    

    当不可变字典的getter方法传入的key是nil,实验了下,没有崩溃:

    NSDictionary *dict = @{@"name" : [[NSObject alloc] init],
                           @"age" : @"jack"};
    NSString *value =  dict[nil];
    
    NSLog(@"%@", [dict class]);
    

    为了预防以后崩溃,还是交换它的方法,给NSMutableDictionary添加分类:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class cls = NSClassFromString(@"__NSDictionaryM");
            Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
            Method method2 = class_getInstanceMethod(cls, @selector(mj_setObject:forKeyedSubscript:));
            method_exchangeImplementations(method1, method2);
            
            Class cls2 = NSClassFromString(@"__NSDictionaryI");
            Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
            Method method4 = class_getInstanceMethod(cls2, @selector(mj_objectForKeyedSubscript:));
            method_exchangeImplementations(method3, method4);
        });
    }
    
    - (void)mj_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
    {
        if (!key) return;
        
        [self mj_setObject:obj forKeyedSubscript:key];
    }
    
    - (id)mj_objectForKeyedSubscript:(id)key
    {
        if (!key) return nil;
        
        return [self mj_objectForKeyedSubscript:key];
    }
    

    上面的M猜想是Mutable的意思,I是Inmutable的意思。

    Demo地址:runtimeAPI

    六. 面试题

    1. 什么是Runtime?平时项目中有用过么?
      ① OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。
      ② OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数。
      ③ 平时编写的OC代码,底层都是转换成了RuntimeAPI进行调用。

    2. Runtime具体应用在哪里?
      ① 利用关联对象(AssociatedObject)给分类添加属性
      ② 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
      ③ 交换方法实现(交换系统的方法)
      ④ 利用消息转发机制解决方法找不到的异常问题
      ......

    相关文章

      网友评论

          本文标题:iOS-Runtime6-API

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