美文网首页iOS开发iOS开发将来跳槽用
Objective-C Runtime 的一些基本使用

Objective-C Runtime 的一些基本使用

作者: BYQiu | 来源:发表于2016-11-24 16:17 被阅读642次

    在上一篇文章《Objective-C Runtime详解》中我们探讨了Runtime的基本原理,这篇文章我们将总结一下Runtime的一些基本使用

    目录

    • 查询方法
    • 给分类添加属性
    • 更换代码的实现方法
    • 动态添加方法
    • 字典转属性

    先创建两个类

    ClassA.h

    #import <Foundation/Foundation.h>
    
    @interface ClassA : NSObject {
        // 公有变量
        NSString *_publicVar1;
        NSString *_publicVar2;
    
    }
    // 公有属性
    @property(nonatomic,copy) NSString *publicProperty1;
    @property(nonatomic,copy) NSString *publicProperty2;
    
    /* 公有方法 */
    -(void)methodAOfClassAWithArg:(NSString *)arg;
    
    @end
    
    

    ClassA.m

    #import "ClassA.h"
    
    @interface ClassA()
    // 私有属性
    @property(nonatomic,copy) NSString *privateProperty1;
    @property(nonatomic,copy) NSString *privateProperty2;
    
    @end
    
    @implementation ClassA {
        // 私有变量
        NSString *_privateVar1;
        NSString *_privateVar2;
    }
    
    /* 公有方法 */
    -(void)methodAOfClassAWithArg:(NSString *)arg {
        NSLog(@" methodAOfClassA arg = %@", arg);
    }
    
    /* 私有方法 */
    -(void)MethodBOfClassAWithArg:(NSString *)arg {
        NSLog(@" methodBOfClassA arg = %@", arg);
    }
    @end
    

    ClassB.h

    #import <Foundation/Foundation.h>
    
    @interface ClassB : NSObject
    
    /* 公有方法 */
    -(void)methodAOfClassBWithArg:(NSString *)arg;
    
    @end
    

    ClassB.m

    #import "ClassB.h"
    
    @implementation ClassB
    - (void)methodAOfClassBWithArg:(NSString *)arg {
        NSLog(@" methodAOfClassB arg = %@", arg);
    }
    
    -(void)methodBOfClassBWithArg:(NSString *)arg {
        NSLog(@" methodBOfClassB arg = %@", arg);
    }
    
    @end
    

    查询方法


    在Objective-C Runtime下没有真正意义上的私有变量和方法,因为这些私有变量和方法都可以通过Runtime方法获取,这当然包括系统的私有API。接下来我们来一一介绍获取类中属性和方法的方法。当然不要忘了#import <objc/runtime.h>.

    获取类的名称

    方法:const char *object_getClassName(id obj),使用比较简单,传入对象即可得到对应分类名。

    ClassA *classA = [[ClassA alloc] init];
    const char *className = object_getClassName(classA);
    NSLog(@"className = %@", [NSString stringWithUTF8String:className]);
    
    //输出
    className = ClassA
    
    获取类中的方法

    方法:Method *class_copyMethodList(Class cls, unsigned int *outCount)

    上代码:

    UInt32 count;
    char dst;
    Method *methods = class_copyMethodList([classA class], &count);//获取方法列表
    for (int i = 0; i < count; i++) {
        Method method = methods[i];// 获取方法
        SEL methodName = method_getName(method);// 获取方法名
        method_getReturnType(method, &dst, sizeof(char));// 获取方法返回类型
        const char *methodType = method_getTypeEncoding(method);// 获取方法参数类型和返回类型
        NSLog(@"methodName = %@",NSStringFromSelector(methodName));
        NSLog(@"dst = %c", dst);
    }
        
     // 输出
     methodName = methodAOfClassAWithArg:
     dst = v
     methodType = v24@0:8@16
     methodName = MethodBOfClassAWithArg:
     dst = v
     methodType = v24@0:8@16
     methodName = publicProperty1
     dst = @
     methodType = @16@0:8
     methodName = setPublicProperty1:
     dst = v
     methodType = v24@0:8@16
     methodName = publicProperty2
     dst = @
     methodType = @16@0:8
     methodName = setPublicProperty2:
     dst = v
     methodType = v24@0:8@16
     methodName = privateProperty1
     dst = @
     methodType = @16@0:8
     methodName = setPrivateProperty1:
     dst = v
     methodType = v24@0:8@16
     methodName = privateProperty2
     dst = @
     methodType = @16@0:8
     methodName = setPrivateProperty2:
     dst = v
     methodType = v24@0:8@16
     methodName = .cxx_destruct
     dst = v
     methodType = v16@0:8
    
    

    class_copyMethodList([classA class], &count) 传入元类和计数器地址,返回方法列表。这里注意,返回的是Method结构体类型的C数组,Method类型我们在上篇文章中已经详细说明,

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

    但要区分Method *methodsMethod method的区别,这是比较基础C语言知识。还有Uint32是OC定义的unsigned int类型typedef unsigned int UInt32;

    这里我们来看看 method_getReturnType(method, &dst, sizeof(char)) 方法简单输出返回值类型,输出为 v@ ,参考Apple文档可知道返回类型为 voidid

    A void v
    A method selector (SEL)  :
    An object (whether statically typed or typed id) @ 
    

    method_getTypeEncoding(method)方法可以输出返回值,参数类型以及接收器类型。我们看输出的v24@0:8@16,分析上面的说明就可以知道: v24返回类型为viod,@0接收器类型为id,@16参数类型为id

    至于类型后面的值观察可以发现都是相差8,我认为是在method中的位置,分别以8bit存储不同类型的数据。

    若有两个参数返回值为 v32@0:8@16@24 ,对比可以猜测,在method中各个成员的排列是这样的: 接收器|SEl标识|参数1|参数2|...|返回值,然后由 method_getTypeEncoding(method) 输出的顺序为: 返回值类型|接收器类型|SEL标识|参数1|参数2|... 此处为个人见解,如有错误或不同意见欢迎提出探讨。

    最后发现了一个奇怪的方法 .cxx_destruct ,在中这篇文章中:

    ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

    和《Effective Objective-C 2.0》中提到的:

    When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

    可以了解到,.cxx_destruct 方法原本是为了C++对象析构的,ARC借用了这个方法插入代码实现了自动内存释放的工作

    关于 .cxx_destruct 可以参考这篇文章:ARC下dealloc过程及.cxx_destruct的探究

    获取类中的属性

    上篇文章Property 中我们也提到了获取类中的属性的方法,如下:

    id LenderClass = objc_getClass("ClassA");//获取classA 的元类,不同于[ClassA class]返回本身
    unsigned int outCount;//属性数量
    // 获取属性列表
    objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
    
    // 遍历
    for (int i = 0; i < outCount; i++) {
    
        objc_property_t property = properties[i];
        
        const char *propertyName = property_getName(property);// 获取属性名
        const char *propertyAttributes = property_getAttributes(property);// 获取属性描述
        
        printf("propertyName:%s \n", propertyName);
        printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
    }
    
    // 输出
    propertyName:privateProperty1 
    propertyAttributes:T@"NSString",C,N,V_privateProperty1
    --------
    propertyName:privateProperty2 
    propertyAttributes:T@"NSString",C,N,V_privateProperty2
    --------
    propertyName:publicProperty1 
    propertyAttributes:T@"NSString",C,N,V_publicProperty1
    --------
    propertyName:publicProperty2 
    propertyAttributes:T@"NSString",C,N,V_publicProperty2
    --------
    

    发现会输出公有属性以及私有属性。

    获取类中的成员变量

    我们可以发现获取类中的方法,属性过程基本一致:通过元类获取方法列表或属性列表,然后在进行遍历。获取成员变量也一样:

    id selfClass = [Class class];
    unsigned int numIvars = 0;
    Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar ivar = ivars[i];
        const char *ivarName = ivar_getName(ivar);
        const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型
        
        printf("ivarName:%s\n", ivarName);
        printf("ivarType:%s\n------\n", ivarType);
    }
    
    // 输出
    ivarName:_publicVar1
    ivarType:@"NSString"
    ------
    ivarName:_publicVar2
    ivarType:@"NSString"
    ------
    ivarName:_privateVar1
    ivarType:@"NSString"
    ------
    ivarName:_privateVar2
    ivarType:@"NSString"
    ------
    ivarName:_publicProperty1
    ivarType:@"NSString"
    ------
    ivarName:_publicProperty2
    ivarType:@"NSString"
    ------
    ivarName:_privateProperty1
    ivarType:@"NSString"
    ------
    ivarName:_privateProperty2
    ivarType:@
    

    可以发现输出了所有的成员变量,包括属性声明的 _+属性名 变量。

    给分类添加属性


    众所周知,分类中是不能声明属性的。

    我们创建一个 ClassA 的分类 ClassA+CategoryA ,在 ClassA+CategoryA 中添加一个属性 name

    #import "ClassA.h"
    
    @interface ClassA (CategoryA)
    
    @property (nonatomic, strong) NSString *name;
    
    @end
    
    

    若在我们调用ClassA分类的name 将会crash,原因是分类中使用 @property 声明属性并不会生成settergetter方法,但是我们会想,我们可以自己实现呀,没错,看下面的代码

    #import "ClassA+CategoryA.h"
    #import <objc/runtime.h>
    
    @implementation ClassA (CategoryA)
    
    - (NSString *)name {
        return name;
    }
    
    - (void)setName:(NSString *)name {
        _name = name;
    }
    
    @end
    

    这里会报编译错误,因为分类中使用 @property 声明属性也不会生成成员变量 _name,并且手动声明也不行

    编译错误,提示实例变量无法添加到分类中,用正常的方法确实无法在分类中添加属性。

    但是可以通过Runtim机制进行“添加”。其本质是给这个类添加属性关联,而非把这个属性添加到类中。

    #import "ClassA+CategoryA.h"
    #import <objc/runtime.h>
    
    
    @implementation ClassA (CategoryA)
    
    - (NSString *)name {
        // _cmd -> @selector(name)
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setName:(NSString *)name {
        objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    @end
    

    调用:

    classA.name = @"邱帅";
    NSLog(@"%@",classA.name);
    
    // 输出
    2016-11-21 16:18:48.084 UseRuntime[4392:1325037] 邱帅
    

    可以看出添加属性成功!

    我们来看看关联属性的这几个方法:

    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
        
    OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
        
    OBJC_EXPORT void objc_removeAssociatedObjects(id object)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
    
    

    objc_setAssociatedObject() 方法为关联属性,参数如下:

    • object:属性关联的源对象,这里使用了self,代表关联本类的对象
    • key:区分属性的唯一标识,因为关联的属性可能不止一个,我们使用了- (NSString *)name方法的SEL @selector(name)作为唯一标示,当然也可以用下面的方法来生成Key :
    //利用静态变量地址唯一不变的特性
    1、static void *strKey = &strKey;
    
    2、static NSString *strKey = @"strKey"; 
    
    3、static char strKey;
    
    • value:关联的属性值
    • policy:设置关联对象的copystorynonatomic等参数:

    这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,              
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,                                                  
        OBJC_ASSOCIATION_RETAIN = 01401,       
        OBJC_ASSOCIATION_COPY = 01403                                               
    };
    

    objc_getAssociatedObject(id object, const void *key) 方法通过 objectKey 直接获取关联的属性值

    上面代码中的第二个参数写的是 _cmd,等价于@selector(name)

    Objective-C的编译器在编译后会在每个方法中加两个隐藏的参数:
    一个是_cmd,当前方法的一个SEL指针。
    另一个就是用的比较多的self,指向当前对象的一个指针。

    objc_removeAssociatedObjects() 移除关联

    我们使用上面的获取类中属性和成员变量的方法,发现输出:

    // 有属性输出
    propertyName:name 
    propertyAttributes:T@"NSString",&,N
    

    没有成员变量 _name,进一步说明分类中不能添加成员变量!其本质是添加属性与分类之间关联。

    更换代码实现方法(Method Swizzling)


    上篇中详细介绍了Method Swizzling的原理,其本质是更换了 selectorIMP

    #import "ViewController.h"
    #import <objc/runtime.h>
    #import "ClassA.h"
    #import "ClassB.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    + (void)load {
        Method classA_method = class_getInstanceMethod([ClassA class], @selector(methodAOfClassAWithArg:));
        Method classB_method = class_getInstanceMethod([ClassB class], @selector(methodAOfClassBWithArg:));
        method_exchangeImplementations(classA_method, classB_method);
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [classA methodAOfClassAWithArg:@"classA 发出的 A方法"];
        [classB methodAOfClassBWithArg:@"classB 发出的 A方法"];
    }    
    
    // 输出
    
    2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassB arg = classA 发出的 A方法
    2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassA arg = classB 发出的 A方法
    

    首先交换方法写在 +(void)load,在程序的一开始就调用执行,你将不会碰到并发问题。

    我们可以发现两个方法的实现过程以及对换。

    当然,平时使用我们并不会这么做,当我们要在系统提供的方法上再扩充功能时(不能重写系统方法),就可以使用Method Swizzling.

    我们给NSArray添加一个分类AddLog,给 arrayByAddingObject:方法添加一个输出方法:

    #import "NSArray+AddLog.h"
    #import <objc/runtime.h>
    
    @implementation NSArray (AddLog)
    
    + (void)load {
    
        SEL ori_selector = @selector(arrayByAddingObject:);
        SEL my_selector = @selector(my_arrayByAddingObject:);
        
        Method ori_method = class_getInstanceMethod([NSArray class], ori_selector);
        Method my_method  = class_getInstanceMethod([NSArray class], my_selector);
        
        if (([NSArray class], ori_selector, method_getImplementation(my_method), method_getTypeEncoding(my_method))) {
            
            class_replaceMethod([NSArray class], my_selector, method_getImplementation(ori_method), method_getTypeEncoding(ori_method));
            
        } else {
            method_exchangeImplementations(ori_method, my_method);
        }
    
    }
    
    - (NSArray *)my_arrayByAddingObject:(id)anObject {
    
        NSArray *array = [self my_arrayByAddingObject:anObject];
        NSLog(@"添加了一个元素 %@", anObject);
        return array;
    }
    
    @end
    
    

    我们来看看这三个方法:

    • class_addMethod():给一个方法添加新的方法和实现
    • class_replaceMethod():取代了对于一个给定的类的实现方法
    • method_exchangeImplementations():交换两个类的实现方法

    这里我们先使用 class_addMethod() 在类中添加方法,若返回Yes说明类中没有该方法,然后再使用 class_replaceMethod() 方法进行取代;若返回NO,说明类中有该方法,使用method_exchangeImplementations()直接交换两者的 IMP.

    其实在这里直接使用method_exchangeImplementations()进行交换就可以了。因为类中必定有arrayByAddingObject:方法。

    我给我们自己的方法命名为my_arrayByAddingObject:,在原来的方法名上加上前缀,既可以防止命名冲突,又方便阅读,在我们my_arrayByAddingObject:方法中调用本身

    NSArray *array = [self my_arrayByAddingObject:anObject];
    

    看似会陷入递归调用,其实则不会,因为我们已经在+ (void)load方法中更换了IMP,他会调用arrayByAddingObject:方法,然后在后面添加我们需要添加的功能。

    arrayByAddingObject:方法的调用不变;

    NSArray *arr1 = @[@"one", @"two"];
    NSArray *arr2 = [arr1 arrayByAddingObject:@"three"];
    NSLog(@"arr2 = %@", arr2);
    
    // 输出
    2016-11-22 13:57:00.021 UseRuntime[1147:743449] 添加了一个元素 three
    2016-11-22 13:57:00.021 UseRuntime[1147:743449] arr2 = (
        one,
        two,
        three
    )
    

    动态添加方法

    动态添加方法就是在消息转发前在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中使用class_addMethod() 添加方法。

    下面我面添加一个名为resolveThisMethodDynamically的方法:

    void dynamicMethodIMP(id self, SEL _cmd) {
        // implementation ....
        printf("执行了dynamicMethodIMP!!!!");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        
        if (sel == @selector(resolveThisMethodDynamically)) {
            class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    

    调用:

    performSelector:@selector(resolveThisMethodDynamically)];
    
    // 输出
    执行了dynamicMethodIMP!!!!
    

    对于上面添加的的方法 resolveThisMethodDynamically ,使用 [self performSelector:@selector(resolveThisMethodDynamically)] 进行调用,不能使用[self resolveThisMethodDynamically],因为压根就没有声明 -(void)resolveThisMethodDynamically,会报编译错误。

    整个过程就是,performSelector:调用resolveThisMethodDynamically方法,然后在列表中找不到(因为类中根本就没有注册该方法),然后跳入 + (BOOL)resolveInstanceMethod: 中,我们再为resolveThisMethodDynamically方法添加具体实现。

    字典转属性

    将字典转化为模型,是在我们iOS开发中最为常用的技能。iOS的模型框架如JSONModel,MJExtension,MJExtension等皆是利用了runtime,将字典转为模型,不过兼顾的细节更多。下面我们来实现一个简易的字典转模型框架。

    先上代码:

    #import "NSObject+BYModel.h"
    #import <objc/runtime.h>
    #import <objc/message.h>
    
    @implementation NSObject (BYModel)
    
    - (void)by_modelSetDictionary:(NSDictionary *)dic {
    
        Class cls = [self class];
        
        // 遍历本类和父类的变量
        while (cls) {
            //获取所有成员变量
            unsigned int outCount = 0;
            Ivar *ivars = class_copyIvarList(cls, &outCount);
            
            for (int i = 0; i < outCount; i++) {
                Ivar ivar = ivars[i];
                
                // 获取变量名
                NSMutableString *ivar_Name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
            
                [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];// _ivar -> ivar
                
                //
                NSString *key = [ivar_Name copy];
                if ([key isEqualToString:@"dece"]) {
                    key = @"description";
                }
                if ([key isEqualToString:@"ID"]) {
                    key = @"id";
                }
                
                id value = dic[key];
                if (!value) continue;
                
                // 拼接SEL    ivar -> setIvar:
                
                NSString *cap = [ivar_Name substringToIndex:1];
                cap = cap.uppercaseString; // a->A
                [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:cap];
                [ivar_Name insertString:@"set" atIndex:0];
                [ivar_Name appendString:@":"];
                
                SEL selector = NSSelectorFromString(ivar_Name);
                
                // 判断类型并发送消息
                NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
                
                if ([type hasPrefix:@"@"]) { // 对象类型
                    objc_msgSend(self, selector, value);
                } else { // 非对象类型
                    if ([type isEqualToString:@"d"]) {
                        objc_msgSend(self, selector, [value doubleValue]);
                    } else if ([type isEqualToString:@"f"]) {
                        objc_msgSend(self, selector, [value floatValue]);
                    } else if ([type isEqualToString:@"i"]) {
                        objc_msgSend(self, selector, [value intValue]);
                    } else {
                        objc_msgSend(self, selector, [value longLongValue]);
                    }
                }
                
                
            }
            // 获取父类进行遍历变量
            cls = class_getSuperclass(cls);
        }
        
    }
    
    

    这个这个段代码可能出现编译错误:

    解决办法很简单:

    将项目 Project -> Build Settings -> Enable strct checking of objc_msgSend Calls 设置为 NO 即可

    接下来我们创建一个模型类Student

    #import <Foundation/Foundation.h>
    
    @interface Student : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) int idNumber;
    
    @end
    
    
    

    使用我们的转模型方法:

    NSDictionary *dic = @{ @"name":@"邱帅", @"age": @(23), @"idNumber":@(1234567)};
    
    Student *stu = [Student new];
    [stu by_modelSetDictionary:dic];
    
    NSLog(@"%@", [NSString stringWithFormat:@"%@, %d, %d", stu.name, stu.age, stu.idNumber]);
    
    // 输出
    2016-11-24 15:32:46.351 Demo_字典转模型(Runtime)[2131:884627] 邱帅, 23, 1234567
    

    该方法先利用我们上面介绍的class_copyIvarList()获取类中的成员变量列表,然后进行遍历,拼接字符串setIvar:,最后调用objc_msgSend()直接发送设置变量的消息,完成属性的赋值。

    while (cls) {
    
        //code..
        
     cls = class_getSuperclass(cls);
    }
    

    这个循环是则获取父类中的属性:当前类的属性遍历结束之后,指向父类,若父类存在则在继续遍历属性,否则就退出循环。

    当然,这个方法只是介绍了利用runtime进行字典转模型的原理,实际中还有很多需要考虑的细节,项目中我还是推荐使用像YYModel这些比较成熟而且安全的模型框架。

    关于快速字典转模型可以参考我写的一篇《快速完成JSON\字典转模型 For YYModel》

    相关文章

      网友评论

        本文标题:Objective-C Runtime 的一些基本使用

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