[Objective-C Runtime] 类与对象

2018-05-15 17:14:29 浏览数 (1)

概述

常说Objective-C是一门动态语言,那么问题来了,这个动态表现在那些方面呢?

其实最主要的表现就是Objective-C将很多静态语言在编译和链接时做的事情放到了运行时去做。

它在运行时实现了对类、方法、成员变量、属性等信息的管理机制,同时,运行时机制为我们开发过程提供很多便利之处,比如:

  • 在运行时创建或者修改一个类;
  • 在运行时修改成员变量、属性等;
  • 在运行时进行消息分发和分发绑定; ......

与之对应实现的就是Objective-CRuntime机制。

Runtime基本是C和汇编编写的,有一系列函数和数据结构组成的,具有公共接口的动态共享库,可见苹果为了动态系统的高效而作出的努力。

你可以在这里(https://opensource.apple.com/source/objc4/)下载到苹果维护的开源代码。

同时,GNU也有一个开源的Runtime版本,他们在努力保持一致。其头文件都存放在/usr/include/objc目录下。

在Objective-C Runtime Reference(https://developer.apple.com/documentation/objectivec/objective_c_runtime?language=objc)中,有对Runtime函数使用细节的文档。

类与对象

类的数据结构

类的数据结构可以在objc/runtime.h源码中找到,如下所示:

代码语言:javascript复制
struct objc_class {
   //isa指针指向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; // 类的版本信息,默认为0
    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;

在Objective-C中是由Class表示的,Class是一个指向struct objc_class的指针。

代码语言:javascript复制
typedef struct objc_class *Class;

在这个类的数据结构中,有几个字段需要解释一下:

isa

在大多数的面向对象的语言中,都有类和对象的概念。

其中,对象是类的实例,是通过类数据结构的定义创建出来的,对象的isa指针是指向其所属类的。

同时,在Objective-C语言中,类本身也是一个对象,类作为对象时isa指针指向元类(Meta Class),后面会详解;

super_class

指向该类的父类,如果该类已经是根类(NSObject 或 NSProxy),则 其super_class 为NULL;

cache

用于缓存最近使用的方法,一个对象可响应的方法列表中通常只有一部分是经常被调用的。

cache 则是用来缓存最常调用的方法,从而避免每次方法调用时都去查找对象的整个方法列表,提升性能。

在一些结构较为复杂的类关系中,一个对象的响应方法可能来自于继承的类结构中,此情况下查找相应的响应方法时就会比较耗时,通常使用cache缓存可以减低查找时间;

version

该字段可以获取类的版本信息,在对象的序列化中可以通过类的版本信息来标识出不同版本的类定义中实例变量布局的改变。

元类(Meta Class)

上面讲到,有时候类也是一个对象,这种类对象是某一种类的实例,这种类就是元类(Meta Class)。

好比类与对应的实例描述一样,元类则是类作为对象的描述。

元类中方法列表对应的是类方法(Class Method)列表,这正是类作为一个对象所需要的。

当调用该方法[NSArray alloc]时,Runtime就会在对应的元类方法列表查找其类对应的方法,并匹配调用。

官方的解释如下所示:

Since a class is an object, it must be an instance of some other class: a metaclass. The metaclass is the description of the class object, just like the class is the description of ordinary instances. Class methods are described by the metaclass on behalf of the class object, just like instance methods are described by the class on behalf of the instance objects.

至此,又有了新的疑问:元类又是谁的实例呢?它的isa又指向谁呢?答案如下图所示:

由上图可以看出,元类的isa都指向根元类(Root Meta Class),即元类都是根元类的实例。

而根元类(Root Meta Class)的isa则指向自己,这样就不会无休止的关联下去了。图中同样展示类和元类的继承关系,非常清晰易懂。

类的实例数据结构

在 Objective-C 中类的实例的数据结构是定义在struct objc_object 中(objc/objc.h):

代码语言:javascript复制
/// Represents an instance of a class.struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以看出,这个结构体只有一个字段,即指向该实例所属类的isa指针。

这个指针跟上面介绍的类的isa不一样:类的isa指向对应的元类(Meta Class),实例的isa则是指向对应的类(Class),而这个Class里包含上述所讲的数据:父类、类名、方法列表等等。

当我们向一个类的实例发送消息时,Runtime会根据实例对象的isa找到这个实例对象所属的类,然后再在这个类的方法列表和其父类的方法列表中查找与消息相对应的selector指向的方法,进而执行目标方法。

当创建某一个类的实例时,分配的内存中会包含一个objc_object数据结构,然后是类的实例变量的相关数据。

我们常见的id是一个struct objc_object类型的指针。id 类型的对象可以转换为任何一种类型的对象,它的作用有点类似 C 语言中的 void * 指针类型。

代码语言:javascript复制
相关函数

Objective-C的Runtime我们提供了很多运行时状态跟类与对象相关的函数,如下所示:

  • const char *class_getName(Class cls),获取指定类的类名;
  • BOOL class_isMetaClass(Class cls),判断指定的类是不是一个元类;
  • Class class_getSuperclass(Class cls),获取指定类的父类;
  • int class_getVersion(Class cls),获取指定类的版本信息;
  • void class_setVersion(Class cls, int version),设定指定类的版本信息;
  • size_t class_getInstanceSize(Class cls),获取实例大小;
  • Ivar class_getInstanceVariable(Class cls, const char *name),获取指定名字的实例变量;
  • Ivar class_getClassVariable(Class cls, const char *name),获取指定名字的类变量;
  • Ivar *class_copyIvarList(Class cls, unsigned int *outCount),获取类的成员变量列表的拷贝。调用后需要自己 free();
  • Method class_getInstanceMethod(Class cls, SEL name),获取指定名字的实例方法;
  • Method class_getClassMethod(Class cls, SEL name),获取指定名字的类方法;
  • IMP class_getMethodImplementation(Class cls, SEL name),获取指定名字的方法实现;
  • BOOL class_respondsToSelector(Class cls, SEL sel),类是否响应指定的方法;
  • Method *class_copyMethodList(Class cls, unsigned int *outCount),获取方法列表的拷贝,调用后需要自己 free();
  • BOOL class_conformsToProtocol(Class cls, Protocol *protocol),类是否遵循指定的协议;
  • Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount),获取协议列表的拷贝。调用后需要自己 free();
  • objc_property_t class_getProperty(Class cls, const char *name),获取指定名字的属性;
  • objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount),获取类的属性列表。调用后需要自己 free();
  • BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types),为类添加方法;
  • BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types),给指定的类添加成员变量。这个函数只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之间调用,并且不能为一个已经存在的类添加成员变量;
  • BOOL class_addProtocol(Class cls, Protocol *protocol),为类添加协议;
  • 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),替代类的属性;
  • id class_createInstance(Class cls, size_t extraBytes),创建指定类的实例;
  • id objc_constructInstance(Class cls, void *bytes),在指定的位置创建类的实例;
  • void *objc_destructInstance(id obj),销毁实例;
  • Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes),创建类和元类;
  • void objc_registerClassPair(Class cls),注册类到 Runtime;
  • void objc_disposeClassPair(Class cls),销毁类和对应的元类;

使用上述函数时,需要特别的注意一下细节信息和使用规范,具体可以查阅 Objective-C Runtime Reference(https://developer.apple.com/reference/objectivec/1657527-objective_c_runtime)。

运行时操作操作类与对象的示例代码
  • 实例、类、父类、元类关系结构的示例代码

首先,创建继承关系为Animal->Dog->NSObject的几个类,然后使用Runtime的方法打印其中的关系,运行结果如下所示:

代码语言:javascript复制
- (void)verifyClassTypeRelation{    
    //Use `object_getClass` get Class's `isa`
    
    Dog *aDog = [[Dog alloc] init];
    Class dogCls = object_getClass(aDog); 
    NSLog(@"isa->%@ , super_class->%@", dogCls, class_getSuperclass(dogCls));   
    // print:isa->Dog, super_class->Animal

    Class dogMetaCls = objc_getMetaClass([NSStringFromClass(dogCls) UTF8String]);
    if (class_isMetaClass(dogMetaCls)) {    
       NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", dogMetaCls, class_getSuperclass(dogMetaCls), object_getClass(dogMetaCls)); 
              //print: YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
    }else{       
        NSLog(@"NO");    }
        
    Animal *animal = [[Animal alloc] init];
    Class animalCls = object_getClass(animal); 
    NSLog(@"isa->%@ , super_class->%@", animalCls, class_getSuperclass(animalCls)); 
       //print: isa->Animal , super_class->NSObject


    Class animalMetaCls = objc_getMetaClass([NSStringFromClass(animalCls) UTF8String]);
    
    if (class_isMetaClass(animalMetaCls)) {    
        NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", animalMetaCls, class_getSuperclass(animalMetaCls), object_getClass(animalMetaCls));  
         //print:YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
    }else{   
        NSLog(@"NO");
    }
    
    Class viewMetaCls = objc_getMetaClass([NSStringFromClass([UIView class]) UTF8String]); 
        
        if (class_isMetaClass(viewMetaCls)) {    
               
            NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", viewMetaCls, class_getSuperclass(viewMetaCls), object_getClass(viewMetaCls));        
           //print:YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
    }
    
    Class rootMetaCls = objc_getMetaClass([NSStringFromClass([NSObject class]) UTF8String]);    
           
           if (class_isMetaClass(rootMetaCls)) {       
            NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", rootMetaCls, class_getSuperclass(rootMetaCls), object_getClass(rootMetaCls));      
             //print:YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject
    }
    
}

打印信息如下所示:

代码语言:javascript复制
isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
isa->Animal , super_class->NSObject
YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject

需要特别注意一下,Object_getClass可以获取当前对象的isa。以Dog类打印信息为例,解释一下具体实现的原理:

代码语言:javascript复制
isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
  • 首先,通过Object_getClass获取实例aDog的Class(isa)为Dog;
  • 然后,通过class_getSuperclass获取Dog的父类为Animal类;
  • 通过objc_getMetaClass指定类名,获取对应的元类,通过class_isMetaClass方法可以判断一个类是否为指定类的元类,这里确认后,打印出YES,打印出的元类名称为Dog;打印元类父类为Animal;在通过 Object_getClass获取元类的isa,指向NSObject

同理可得,AnimalUIView打印信息解释同上。NSobject,它元类的isa指针还是指向自己的类——NSobject。打印的信息与上述的关系图保持一致。

动态操作类与实例的代码

动态创建类的源码

代码语言:javascript复制
/***********************************************************************
* _objc_allocateFutureClass
* Allocate an unresolved future class for the given class name.
* Returns any existing allocation if one was already made.
* Assumes the named class doesn't exist yet.
* Locking: acquires runtimeLock**********************************************************************/

Class _objc_allocateFutureClass(const char *name)
{    rwlock_writer_t lock(runtimeLock);

    Class cls;
    NXMapTable *map = futureNamedClasses();    
if ((cls = (Class)NXMapGet(map, name))) {        
// Already have a future class for this name.
        return cls;
    }

    cls = _calloc_class(sizeof(objc_class));
    addFutureNamedClass(name, cls);    return cls;
}
代码语言:javascript复制
- (void)dynamicAddMethod{   
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wundeclared-selector"
    
    //1、Create and register class, add method to class
    Class cls = objc_allocateClassPair([Animal class], "Lion", 0);
    // 当为`Cat`时,返回的创建类cat地址为0x0,将`Cat`作为关键字
    //method: 返回`int32_t`,type使用`i`;参数:`id self`,type使用`@`;`SEL _cmd`,type使用`:`;
    //`NSDictionary *dic`,type使用`@`.综上,type使用'i@:@'
    ///具体类型可参照 https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
    BOOL isAddSuccess = class_addMethod(cls, @selector(howlMethod), (IMP)testRuntimeMethodIMP, "i@:@"); 
    NSLog(@"%@", isAddSuccess ? @"添加方法成功" : @"添加方法失败");   
  
   //You can only register a class once.
    objc_registerClassPair(cls);   
     
    //2、Create instance of class
    id whiteCat = [[cls alloc] init];     
     
    NSLog(@"%@, %@", object_getClass(whiteCat), class_getSuperclass(object_getClass(whiteCat)));  
      // Print: Lion, Animal
    Class whiteCatCls = object_getClass(whiteCat);
    Class metaCls = objc_getMetaClass([NSStringFromClass(whiteCatCls) UTF8String]);
     
    if (class_isMetaClass(metaCls)) {      
      NSLog(@"YES, %@, %@, %@", metaCls, class_getSuperclass(metaCls), object_getClass(metaCls));   
         // Print: YES, Lion, Animal, NSObject
    }else{        
      NSLog(@"NO");
    }    
    //3、Method of class
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);  
     
     for (int32_t i = 0; i < methodCount; i   ) {
        Method aMethod = methods[i];     
        NSLog(@"%@, %s", NSStringFromSelector(method_getName(aMethod)), method_getTypeEncoding(aMethod));        
        //print:howlMethod, i@:@
    }
    free(methods);
    
    //4、call method
    int32_t result = (int)[whiteCat performSelector:@selector(howlMethod) withObject:@{@"name":@"lion", @"sex": @"male"}]; 
       NSLog(@"%d", result);//print:9
    
    //5、destory instance and class
    whiteCat = nil;    
    // Do not call this function if instances of the cls class or any subclass exist.
    objc_disposeClassPair(cls);    
     
     #pragma clang diagnostic pop

}

打印的信息如下所示:

代码语言:javascript复制
添加方法成功
 Lion, AnimalYES, Lion, Animal, NSObjecthowlMethod, i@:@
testRuntimeMethodIMP: {
    name = lion;
    sex = male;
}
9

在执行objc_allocateClassPair中,类的名称设置为Cat时,创建出的Class的地址始终指向0x0,创建类失败,猜测其中的原因可能是Cat与内部的关键字冲突了,导致类创建失败,改为cat或者其他的都可以创建成功;

  • 在上面的代码中,在运行时动态创建了Animal 的一个子类:Lion;接着为这个类添加了方法和实现;
  • 打印了 Lion 的类、父类、元类相关信息;
  • 遍历和打印了 Lion 的方法的相关信息;
  • 调用了 Lion 的方法;
  • 最后销毁了实例和类。

针对上述代码,有几点需要特殊说明一下:

  • 对于#pragma clang diagnostic...几行代码,是用于忽略编译器对于未声明@selector的警告信息的,在代码中,我们动态地为一个类添加方法,不会事先声明的;
  • class_addMethod() 函数的最后一个参数 types 是描述方法返回值和参数列表的字符串。我们的代码中的用到的 i@:@ 四个字符分别对应着:返回值 int32_t、参数id self、参数 SEL _cmd、参数 NSDictionary *dic
  • 这个其实就是类型编码(Type Encoding)的概念。在 Objective-C 中,为了协助 Runtime 系统,编译器会将每个方法的返回值和参数列表编码为一个字符串,这个字符串会与方法对应的 selector 关联。更详细的知识可以查阅 Type Encoding(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html);
  • 使用 objc_registerClassPair() 函数需要注意,不能注册已经注册过的类;
  • 使用objc_disposeClassPair() 函数时需要注意,当一个类的实例和子类还存在时,不能去销毁一个类,谨记;
isKindOf 和 isMemberOf

举个栗子:

代码语言:javascript复制
@interface TestMetaClass: NSObject

@end


@implementation TestMetaClass

@end
int main(int argc, char * argv[]) {   
 @autoreleasepool {       
  BOOL result1 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];  
  BOOL result2 = [(id)[NSObject class] isKindOfClass:[NSObject class]];        
  BOOL result3 = [(id)[TestMetaClass class] isMemberOfClass:[TestMetaClass class]];        
  BOOL result4 = [(id)[TestMetaClass class] isKindOfClass:[TestMetaClass class]];        
  NSLog(@"%d %d %d %d", result1, result2, result3, result4);        
  return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
  //log2018-02-09 16:45:54.048040 0800 RuntimeUsage[9220:5754652] 0 1 0 0

关于isMemberOfClassisKindOfClassObject.mm中的实现,具体如下:

代码语言:javascript复制
- (BOOL)isMemberOf:aClass
{     return isa == (Class)aClass;
}

- (BOOL)isKindOf:aClass
{
     Class cls;    
    for (cls = isa; cls; cls = cls->superclass)        
    if (cls == (Class)aClass)              
        return YES;    
    return NO;
}

在result1中,从isMemberOf看出NSObject classisa第一次会指向NSObjectMeta Class,因此NSObjectMeta ClassNSObject class是不相等,返回FALSE

result2中,isKindOf第一次指向NSObjectMeta Class,接着执行superclass时,根元类NSObjectMeta Class根据上面所讲的其superclass指针会闭环指向NSObject class,从而结果值为TRUE;

result3中,isa会指向TestMetaClassMeta Class,与TestMetaClass Class不相等,结果值为FALSE;

result4中,第一次是TestMetaClass Meta Class,第二次super class后就是NSObject Meta Class,结果值为FALSE;

以上再次验证了,NSObject Meta Classisa指针指向自身,其super class指向NSObject

小结

在这一章中我们介绍了Runtime运行时中与类和对象相关的数据结构,通过这些数据函数,我们可以学习Objective-C底层面向对象实现的一些信息。另外,通过丰富的操作函数,可以灵活地对这些数据进行操作。

Demo(https://github.com/123sunxiaolin/RuntimeUsage.git)

参考文献

  • Objective-C Runtime的数据类型http://www.cnblogs.com/whyandinside/archive/2013/02/26/2933552.html
  • 详解Objective-C的meta-classhttp://blog.csdn.net/windyitian/article/details/19810875
  • Objective-C Runtime Referencehttps://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/

0 人点赞