Objective-C 的 RunTime(八):补充

NSObject 的(对象方法)与(类方法)

  • 实例对象、类对象、元类对象 之间的关系
    实例对象、类对象、元类对象 之间的关系
    关于 isa 指针:

    1. 实例对象的 isa 指针指向类对象,类对象的 isa 指针指向元类对象
    2. 所有元类对象的 isa 指针都直接指向 NSObject 的元类对象(因此,NSObject 的元类对象也被称为根元类)
    3. 根元类的 isa 指针指向自己

    关于 superclass 指针:

    1. 实例对象没有 superclass 指针
    2. 类对象的 superclass 指针指向(父类的类对象),(父类的类对象)的 superclass 指针指向(根类的类对象),(根类的类对象)的 superclass 指针指向 nil
    3. 元类对象的 superclass 指针指向(父类的元类对象),(父类的元类对象)的 superclass 指针指向(根类的元类对象)
    4. (根类的元类对象)的 superclass 指针指向(根类的类对象),(根类的类对象)的 superclass 指针最终指向 nil

    注意:

    1. 同一个类的所有实例对象,有且仅有一个与之对应的类对象
    2. 每个类对象有且仅有一个与之对应的元类对象
    3. 每一个类对象有且仅有一个父类对象
    4. 每一个元类对象有且仅有一个父元类对象
  • 对象方法与类方法的调用路径

    假设有:Person 类继承自 NSObject 类,Student 类继承自 Person

    Student 实例对象调用对象方法时:
    Student 实例对象会根据自己的 isa 指针去 Student 类对象中查找有没有对应的对象方法
    ② 如果没有的话,则 Student 类对象会根据自己的 superclass 指针去 Person 类对象中查找有没有对应的对象方法
    ③ 如果没有的话,则 Person 类对象会根据自己的 superclass 指针去 NSObject 类对象中查找有没有对应的对象方法
    ④ 如果没有的话,则会进入消息转发流程
    ⑤ 注意:在 NSObject 上没有找到对象方法时,是不会尝试调用 NSObject 上的同名类方法的。会直接进入消息转发流程

    Student 类对象调用类方法时:
    Student 类对象会根据自己的 isa 指针去 Student 元类对象中查找有没有对应的类方法
    ② 如果没有的话,则 Student 元类对象会根据自己的 superclass 指针去 Person 元类对象中查找有没有对应的类方法
    ③ 如果没有的话,则 Person 元类对象会根据自己的 superclass 指针去 NSObject 元类对象中查找有没有对应的类方法
    ④ 如果没有的话,则 NSObject 元类对象会根据自己的 superclass 指针去 NSObject 类对象中查找有没有对应的对象方法(即:NSObject 上没有找到类方法时,会尝试调用 NSObject 上的同名对象方法
    ⑤ 如果没有的话,则会进入消息转发流程

  • 代码示例

    NSObject+Addition
    Person
    Student

    -(void)superclassPointerDemo {
        [Student test];
        // 输出结果:
        // there is NSObject (Addition) instance method : test
    }
    
  • 思考与总结

    虽然在 Objective-C 的语法层面,方法前缀 - + 用于区分一个方法是对象方法还是类方法
    但是在使用 @selector(methodName) 获取方法选择器时,同名的对象方法和类方法,获取到的是同一个方法选择器
    其实在 Objective-C 底层,对象方法和类方法本质上都是 C 函数,只是调用者不同、存储位置不同:
    ① 对象方法被实例对象所调用,存储在类对象中
    ② 类方法被类对象所调用,存储在元类对象中

    更进一步地,所谓的类方法可以理解为:类对象的实例方法。以下代码获取到的都是 Person 类的 +run 方法

    Method method0 = class_getInstanceMethod(object_getClass([Person class]), @selector(run));
    IMP imp0 = method_getImplementation(method0);
    imp0();
    
    Method method1 = class_getClassMethod([Person class],  @selector(run));
    IMP imp1 = method_getImplementation(method1);
    imp1();
    
  • 补充与拓展

    -(void)supplementAndExtension {
        // 1.同一个类的所有实例对象,有且仅有一个与之对应的类对象
        NSObject* object0 = [[Person alloc] init];
        NSObject* object1 = [[Person alloc] init];
        NSLog(@"Person 类对象 %p %@", [object0 class], [object0 class]);
        NSLog(@"Person 类对象 %p %@", [object1 class], [object1 class]);
        NSLog(@"Person 类对象 %p %@", [Person class], [Person class]);
        NSLog(@"Person 类对象 %p %@", object_getClass(object0), object_getClass(object0));
        NSLog(@"Person 类对象 %p %@", object_getClass(object1), object_getClass(object1));
        // Person 类对象 0x10c0b8618 Person
        // Person 类对象 0x10c0b8618 Person
        // Person 类对象 0x10c0b8618 Person
        // Person 类对象 0x10c0b8618 Person
        // Person 类对象 0x10c0b8618 Person
    
        // 2.每个类对象有且仅有一个与之对应的元类对象
        NSLog(@"Person 元类对象 %p %@", object_getClass(Person.class), object_getClass(Person.class));
        // Person 元类对象 0x10c0b85f0 Person
    
        // 3.每一个类对象有且仅有一个父类对象
        NSLog(@"Person 类对象的父类对象 %p %@", [Person.class superclass], [Person.class superclass]);
        // Person 类对象的父类对象 0x7fff86d54660 NSObject
    
        // 4.每一个元类对象有且仅有一个父元类对象
        NSLog(@"Person 元类对象的父元类对象 %p %@", [object_getClass(Person.class) superclass], [object_getClass(Person.class) superclass]);
        // Person 元类对象的父元类对象 0x7fff86d54638 NSObject
    }
    

Objective-C 中的 “空”(nil、Nil、NULL、NSNull)

在 Objective-C 中,nilNilNULLNSNull 都可以表示(空),下面我们来一起探究一下,它们之间的区别与联系,以及各自使用的场景

  • 直观地认识:nil、Nil、NULL、NSNull

    -(void)printNull {
        NSLog(@"nil.addr = %p, nil.desc = %@", nil, nil);
        NSLog(@"Nil.addr = %p, Nil.desc = %@", Nil, Nil);
        NSLog(@"NULL.addr = %p, NULL.desc = %@", NULL, NULL);
        NSLog(@"NSNull.addr = %p, NSNull.desc = %@", [NSNull null], [NSNull null]);
        // 输出结果如下:
        // nil.addr = 0x0, nil.desc = (null)
        // Nil.addr = 0x0, Nil.desc = (null)
        // NULL.addr = 0x0, NULL.desc = (null)
        // NSNull.addr = 0x7fff8002ef10, NSNull.desc = <null>
    }
    

    从输出结果可以看出:

    1. nilNilNULL 是一样的,都是指向 0x0 的空指针
    2. [NSNull null] 是一个实例对象,与其他三个不一样
  • nil、Nil、NULL、NSNull 的本质

    在 Objective-C 环境下,nil 的本质是一个宏,它的值为 ((void *)0)

    // 在 #import <objc/objc.h> 中 nil 的定义
    #ifndef nil
    #	if __has_feature(cxx_nullptr)
    #		define nil nullptr
    #	else
    #		define nil __DARWIN_NULL
    #	endif
    #endif
    
    // 在 #include <sys/_types.h> 中 __DARWIN_NULL 的定义
    #ifdef __cplusplus
    #	ifdef __GNUG__
    #		define __DARWIN_NULL __null
    #	else /* ! __GNUG__ */
    #		ifdef __LP64__
    #			define __DARWIN_NULL (0L)
    #		else /* !__LP64__ */
    #			define __DARWIN_NULL 0
    #		endif /* __LP64__ */
    #	endif /* __GNUG__ */
    #else /* ! __cplusplus */
    #	define __DARWIN_NULL ((void *)0)
    #endif /* __cplusplus */
    

    在 Objective-C 环境下,Nil 的本质是一个宏,它的值为 ((void *)0)

    // 在 #import <objc/objc.h> 中 Nil 的定义
    #ifndef Nil
    #	if __has_feature(cxx_nullptr)
    #		define Nil nullptr
    #	else
    #		define Nil __DARWIN_NULL
    #	endif
    #endif
    
    // 在 #include <sys/_types.h> 中 __DARWIN_NULL 的定义
    #ifdef __cplusplus
    #	ifdef __GNUG__
    #		define __DARWIN_NULL __null
    #	else /* ! __GNUG__ */
    #		ifdef __LP64__
    #			define __DARWIN_NULL (0L)
    #		else /* !__LP64__ */
    #			define __DARWIN_NULL 0
    #		endif /* __LP64__ */
    #	endif /* __GNUG__ */
    #else /* ! __cplusplus */
    #	define __DARWIN_NULL ((void *)0)
    #endif /* __cplusplus */
    

    在 Objective-C 环境下,NULL 的本质是一个宏,它的值为 ((void *)0)

    // 在 #include <stddef.h> 中 NULL 的定义
    #if defined(__need_NULL)
    #undef NULL
    #	ifdef __cplusplus
    #		if !defined(__MINGW32__) && !defined(_MSC_VER)
    #			define NULL __null
    #		else
    #			define NULL 0
    #		endif
    #	else
    #		define NULL ((void*)0)
    #	endif
    #	ifdef __cplusplus
    #		...
    #	endif
    #undef __need_NULL
    #endif /* defined(__need_NULL) */
    

    在 Objective-C 环境下,NSNull 是一个继承自 NSObject 的单例对象

    // 在 #import <Foundation/NSNull.h> 中 NSNull 的定义
    #import <Foundation/NSObject.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSNull : NSObject <NSCopying, NSSecureCoding>
    
    +(NSNull *)null;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    -(void)NSNullDemo {
        NSNull* n1 = [NSNull null];
        NSNull* n2 = [NSNull null];
        NSLog(@"n1.addr = %p, n1.desc = %@",n1, n1);
        NSLog(@"n2.addr = %p, n2.desc = %@",n2, n2);
        // 输出结果如下:
        // n1.addr = 0x7fff8002ef10, n1.desc = <null>
    	// n2.addr = 0x7fff8002ef10, n2.desc = <null>
    }
    
  • nil、Nil、NULL 的使用场景

    虽然 nilNilNULL 都是指向 0x0 的空指针,在本质没有什么区别
    但是在 Objective-C 的编程规范中,它们却有着不同的使用场景:

    nil 用于置空 Objective-C 对象类型的变量(表示指向一个对象的指针为空,即该指针指向一块没有意义的内存区域)

    NSString* str = @"hcg";
    str = nil;
    
    UIView* view = [[UIView alloc] init];
    view = nil;
    

    Nil 用于置空 Objective-C 类类型的变量(表示指向一个类的指针为空,即该指针指向一块没有意义的内存区域)

    Class strCls = [NSString class];
    strCls = Nil;
    
    Class viewCls = [UIView class];
    viewCls = Nil;
    

    NULL 用于置空 C 指针类型的变量(表示指向一个 C 类型的指针为空,即该指针指向一块没有意义的内存区域)

    char * charPtr = "hcg";
    charPtr = NULL;
    
    int result = 10;
    int* intPtr = &result;
    intPtr = NULL;
    
    SEL aSelector = @selector(methodName);
    aSelector = NULL;
    
  • NSNull 的使用场景

    NSNull 用于表示值为空的对象,通常用于容器类(NSArrayNSDictionaryNSSet,…)的占位(NSNull 的实例对象拥有一个有效的内存地址,是实际存在的一个对象)

    NSArray* array = [NSArray arrayWithObjects:@"first", @"second", [NSNull null], @"fourth", nil];
    NSLog(@"array.count = %ld", array.count);
    // 输出结果如下:
    // array.count = 4
    
  • 注意

    nilNilNULL 都可以用于表示一个空指针,所谓的空指针指的是:该指针指向一块没有意义的内存区域

    ② 在 Objective-C 中:
    对空指针(nilNilNULL)发送非法消息,不会导致程序崩溃或者异常
    NSNull 发送非法消息,会导致程序崩溃或者异常

    ③ 因为 nilNSArray 中被作为结束符,所以 NSArray 无法包含 nil 作为其具体值
    如果往 NSMutableArray 中添加一个 nil 值,会导致程序异常

    // 数组初始化,以空值结束
    NSArray* arr0 = [NSArray arrayWithObjects:@"first", @"second", @"third", nil];
    NSLog(@"arr0.count = %ld", arr0.count);
    // 输出结果如下:
    // arr0.count = 3
    
    // 以下代码会导致程序奔溃
    id object = nil;
    
    NSMutableArray* mArr = [NSMutableArray array];
    [mArr addObject:object];
    
    NSMutableDictionary* mDict = [NSMutableDictionary dictionary];
    [mDict setObject:object forKey:@"aKey"];
    
    -(void)NSNullDemo00 {
        NSObject* obj0 = [[NSObject alloc] init];
        NSObject* obj1 = [NSObject new];
        NSObject* obj2 = [NSNull null];
        NSObject* obj3 = nil;
        NSObject* obj4;
        NSArray* arr = [NSArray arrayWithObjects:obj0, obj1, obj2, obj3, obj4, nil];
        NSLog(@"arr.count = %ld", arr.count);
        // arr.count = 3
    }
    
    -(void)NSNullDemo01 {
        NSObject* obj0 = [[NSObject alloc] init];
        NSObject* obj1 = nil;
        NSObject* obj2 = [NSObject new];
        NSObject* obj3 = [NSNull null];
        NSObject* obj4;
        NSArray* arr = [NSArray arrayWithObjects:obj0, obj1, obj2, obj3, obj4, nil];
        NSLog(@"arr.count = %ld", arr.count);
        // arr.count = 1
    }
    
    -(void)NSNullDemo02 {
        NSObject* obj0 = [[NSObject alloc] init];
        NSObject* obj1;
        NSObject* obj2 = [NSObject new];
        NSObject* obj3 = [NSNull null];
        NSObject* obj4 = nil;
        NSArray* arr = [NSArray arrayWithObjects:obj0, obj1, obj2, obj3, obj4, nil];
        NSLog(@"arr.count = %ld", arr.count);
        // arr.count = 1
    }
    

isKindOfClass: 和 isMemberOfClass:

  • -isKindOfClass:

    /*
    返回一个布尔值,用于标识:消息接受者(即方法调用者)是否是给定类的实例 or 消息接受者(即方法调用者)是否是给定类的任何子类的实例
    
    @param aClass 要进行测试的类对象
    @return 如果消息接受者(即方法调用者)是给定类的实例 or 给定类的任何子类的实例,则返回 YES。否则返回 NO
    @note 以下代码将返回 YES,因为在 Foundation.framework 中,NSArchiver 继承自 NSCoder
    			NSMutableData *myData = [NSMutableData dataWithCapacity:30];
    			id anArchiver = [[NSArchiver alloc] initForWritingWithMutableData:myData];
    			if ( [anArchiver isKindOfClass:[NSCoder class]] )
    				...
    @note 在由类簇表示的实例对象上使用此方法时,需要小心
    	  由于类簇的性质,你获取到的对象可能并不总是你期望的类型
    	  如果调用的方法返回一个类簇,则调用的方法返回的确切类型是如何处理该对象的最佳标识
    	  例如,如果某个方法返回了一个指向 NSArray 对象的指针,则不应使用此方法来查看该数组是否可变:
    			// 不要这么做!!!
    			if ([myArray isKindOfClass:[NSMutableArray class]])
    			{
        			// Modify the object
    			}
    	  如果你像上面这样编写代码,则你可能会误以为可以修改一个(在实际操作中不应该修改的对象)
    	  这样做可能会给(期望 myArray 保持不变的其他代码)造成问题
    @note 当消息接受者(即方法调用者)是一个类对象时
    	  如果消息接受者(即方法调用者)和参数 aClass 是相同类型的类对象,则返回 YES。否则返回 NO
    */
    -(BOOL)isKindOfClass:(Class)aClass;
    

    如上所述,对于 NSArrayNSMutableArray 要谨慎使用 isMemberOfClass:

    -(void)NSarrayAndNSMutableArray {
        NSArray* arr = [NSArray array];
        NSMutableArray* mArr = [NSMutableArray array];
        
        bool res1 = [arr isKindOfClass:[NSArray class]];
        bool res2 = [arr isMemberOfClass:[NSArray class]];
        bool res3 = [mArr isKindOfClass:[NSMutableArray class]];
        bool res4 = [mArr isMemberOfClass:[NSMutableArray class]];
        NSLog(@"res1 = %d", res1);  // res1 = 1
        NSLog(@"res2 = %d", res2);  // res2 = 0
        NSLog(@"res3 = %d", res3);  // res3 = 1
        NSLog(@"res4 = %d", res4);  // res4 = 0
        
        Class cls1 = object_getClass(arr);
        Class cls2 = object_getClass(mArr);
        Class cls3 = [NSArray class];
        Class cls4 = [NSMutableArray class];
        NSLog(@"cls1.name = %s", class_getName(cls1));  // cls1.name = __NSArray0
        NSLog(@"cls2.name = %s", class_getName(cls2));  // cls2.name = __NSArrayM
        NSLog(@"cls3.name = %s", class_getName(cls3));  // cls3.name = NSArray
        NSLog(@"cls4.name = %s", class_getName(cls4));  // cls4.name = NSMutableArray
    }
    
  • -isMemberOfClass:

    /*
    返回一个布尔值,用于标识:消息接受者(即方法调用者)是否是给定类的实例
    
    @param aClass 要进行测试的类对象
    @return 如果消息接受者(即方法调用者)是给定类的实例,则返回 YES。否则返回 NO
    @note 以下代码将返回 NO,因为在 Foundation.framework 中,NSArchiver 继承自 NSCoder
    			NSMutableData *myData = [NSMutableData dataWithCapacity:30];
    			id anArchiver = [[NSArchiver alloc] initForWritingWithMutableData:myData];
    			if ([anArchiver isMemberOfClass:[NSCoder class]])
        			...
    @note 类对象可以是由编译器创建的对象,但它们仍然支持 membership 的概念
    	  因此,您可以使用此方法来验证消息接受者(即方法调用者)是否是特定的类对象
    */
    -(BOOL)isMemberOfClass:(Class)aClass;
    
  • RunTime 源码中的 isKindOfClass: 和 isMemberOfClass:

    虽然在平时开发过程中,只会用到对象方法的 -isKindOfClass:-isMemberOfClass:
    但是在 RunTime 的源码中,同样实现了类方法的 +isKindOfClass:+isMemberOfClass:

    在 RunTime 源码(objc4-756.2/runtime/NSObject.mm)中 isKindOfClass:isMemberOfClass: 的实现如下:

    -(BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    +(BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    -(BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }
    
    +(BOOL)isMemberOfClass:(Class)cls {
        return object_getClass((id)self) == cls;
    }
    
  • 一道经典的面试题

    Question:下面代码结果如何?

    // Person 继承自 NSObject
    BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL res3 = [(id)[Person class] isKindOfClass:[Person class]];
    BOOL res4 = [(id)[Person class] isMemberOfClass:[Person class]];
    // 因为这 4 个方法的调用者都是类对象,所以执行的方法是 +isKindOfClass: 和 +isMemberOfClass:
    NSLog(@"res1 = %d", res1);  // res1 = 1
    NSLog(@"res2 = %d", res2);  // res2 = 0
    NSLog(@"res3 = %d", res3);  // res3 = 0
    NSLog(@"res4 = %d", res4);  // res4 = 0
    

class 方法、object_getClass() 函数、objc_getClass() 函数

  • class 方法在 RunTime 源码中的实现

    // path: objc4-756.2/runtime/NSObject.mm
    
    // 类方法:返回自身
    +(Class)class {
    	return self;
    }
    
    // 对象方法:返回所属的类对象
    -(Class)class {
    	return object_getClass(self);
    }
    
  • object_getClass() 函数在 RunTime 源码中的实现

    // path: objc4-756.2/runtime/objc-class.mm
    /*
    获取给定对象所属的类对象
    
    @param obj 要检视的对象
    @return 如果 obj 是实例对象,则返回类对象
    		如果 obj 是类对象,则返回元类对象
    		如果 obj 传 Nil,则返回 Nil
    */
    Class object_getClass(id obj)
    {
    	if (obj) return obj->getIsa();
    	else return Nil;
    }
    
  • objc_getClass() 函数在 RunTime 源码中的实现

    // path: objc4-756.2/runtime/objc-runtime.mm
    /*
    获取由给定的名称所标识的类对象
    
    @param name 要查找的类对象的名称
    @return 由给定的名称所标识的类对象
    		如果 Objective-C 的 RunTime 没有注册给定名称的类对象,则返回 nil
    @note objc_getClass 函数与 objc_lookUpClass 函数的不同之处在于:
    	  如果给定的类对象没有被注册到 Objective-C 的 RunTime,objc_getClass 函数会调用类处理程序的回调,然后再次检查该类对象是否已被注册
    	  objc_lookUpClass 函数不会调用类处理程序的回调
    @warning 此函数的早期实现(在 OSX10.0 之前):如果给定的类对象不存在,则会终止该程序
    */
    Class objc_getClass(const char *aClassName)
    {
        if (!aClassName) return Nil;
    
        // NO unconnected, YES class handler
        return look_up_class(aClassName, NO, YES);
    }
    
  • class 方法与 object_getClass() 函数的比较

    obj(方法调者 or 函数参数)class 方法object_getClass() 函数
    当 obj 是实例对象时返回类对象返回类对象
    当 obj 是类对象时返回类对象(即返回自身)返回元类对象
    当 obj 是元类对象时返回元类对象(即返回自身)返回根元类对象
    当 obj 是根类对象时返回根类对象(即返回自身)返回根元类对象
    当 obj 是根元类对象时返回根元类对象(即返回自身)返回根元类对象(即返回自身)
  • 关于 objc_getClass() 函数

    objc_getClass() 函数获取到的是类对象或者 nil

self、super、class、superclass

  • self 本质上是一个指针,用于标识:当前方法调用者(即消息接受者)

  • super 本质上是一个编译器指示符(仅仅是一个标志,而不是一个指针),用于告诉编译器,当前对象在查找方法的时候跳过自己的方法列表,直接从父类的方法列表开始查找。但是方法的调用者仍然是当前对象 self。使用 super 调用的方法,底层会被编译成 objc_msgSendSuper() 函数

  • class 本质上是一个方法,用于获取:当前方法调用者所属的类对象

  • superclass 本质上是一个方法,用于获取:当前方法调用者的父类对象

  • 代码示例

    Person 类如下所示:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    +(void)test;
    
    @end
    
    
    
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    +(void)test {
        NSLog(@"%@ %@ %@ %@",[self class], [self superclass], [super class], [super superclass]);   
    }
    
    @end
    

    Student 类如下所示:

    // Student.h
    #import "Person.h"
    
    @interface Student : Person
    
    +(void)test;
    +(void)superTest;
    
    @end
    
    
    
    // Student.m
    #import "Student.h"
    
    @implementation Student
    
    +(void)test {
        NSLog(@"%@ %@ %@ %@",[self class], [self superclass], [super class], [super superclass]);
    }
    
    +(void)superTest {
    	// 去调用父类的 test 方法,但是调用者还是子类对象
        [super test];
    }
    
    @end
    

    分别调用 Person 类和 Student 类的 +test+superTest,结果如下所示:

    -(void)demo {
        [Person test];          // Person NSObject Person NSObject
        [Student test];         // Student Person Student Person
        [Student superTest];    // Student Person Student Person
    }
    
  • super 的底层原理

    在 RunTime 源码中,编译器指示符 super 对应的底层数据结构为 struct objc_super,其用于标识(当前方法调用者)以及(要开始搜索方法的类对象)

    struct objc_super {
        __unsafe_unretained _Nonnull id receiver;			// 消息接受者(即当前方法调用者)
        __unsafe_unretained _Nonnull Class super_class;		// 要第一个开始搜索方法的类对象(一般指向当前方法调用者的父类对象)
    };
    

    将上面的 Student.m 通过 clang 编译器转换成 Student.cpp

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Student.m -o Student.cpp
    

    找到 +test+superTest 对应的函数实现如下所示:

    // +(void)test
    static void _C_Student_test(Class self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_92_vshp0dxd0cg90_kzw6k3mlfm0000gn_T_Student_0cfcdc_mi_0,
              ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),
              ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")),
              ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(&(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("class")),
              ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(&(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("superclass")));
    }
    
    // +(void)superTest
    static void _C_Student_superTest(Class self, SEL _cmd) {
        ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(&(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("test"));
    }
    

    去掉类型强制转换的相关代码后,如下所示:

    // +(void)test
    static void _C_Student_test(Class self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_92_vshp0dxd0cg90_kzw6k3mlfm0000gn_T_Student_0cfcdc_mi_0,
              objc_msgSend(self, sel_registerName("class")),
              objc_msgSend(self, sel_registerName("superclass")),
              objc_msgSendSuper(&(__rw_objc_super){self, class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("class")),
              objc_msgSendSuper(&(__rw_objc_super){self, class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("superclass")));
    }
    
    // +(void)superTest
    static void _C_Student_superTest(Class self, SEL _cmd) {
        objc_msgSendSuper(&(__rw_objc_super){self, class_getSuperclass(objc_getMetaClass("Student"))}, sel_registerName("test"));
    }
    

    其中 objc_msgSendobjc_msgSendSuper 的定义如下所示:

    id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
    
    id _Nullable objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...);
    

    从简化之后的代码可以得到以下 3 点:

    1. 调用 [self class] 的时候,底层是调用的 objc_msgSend(...)
    2. 调用 [super class] 的时候,底层调用的是 objc_msgSendSuper(...)
    3. 通过 super 调用的方法,从父类对象的方法列表开始查找方法实现(方法的调用者仍然是当前对象)

其他

  • Objective-C == C + 编译器 + RunTime

    苹果公司基于 RunTime,才把 C 语言封装成了我们所认识的 Objective-C 语言,总体来说苹果公司当初是这样考虑的:在现有 C 语言的基础上加个编译器和运行时环境就构成了 Objective-C 语言最基本的框架。编译器用于把 Objective-C 语言转换成 C 语言,运行时环境用于处理转换之后的 C 语言。可以将运行时环境理解为嵌入在 App 中的微型操作系统,也正是因为有了这样一个运行时环境,才让 Objective-C 语言成为了一门动态语言

    上述所说的编译器指的是 Clang 编译器,它可以把 Objective-C 语言转换成 C++ 语言。为什么不是直接转换成 C 语言呢?这是因为需要 C++ 语言的一些功能,而且 RunTime 准确来说是一套由(C、C++、汇编)实现的 API。Clang 是一个 C 语言、C++ 语言、Objective-C 语言的轻量级编译器,不同于 Windows、Linux 下常用的 C 语言编译器 GCC,Mac OS 下默认的 C 语言编译器是 Clang,它是苹果公司赞助开发的,能更好地支持苹果公司的产品

  • Objective-C 在 3 个层面上与 RunTime 系统进行交互:

    1. 通过 Objective-C 源代码(编译器会将 Objective-C 源代码转换成 RunTime 的数据结构与函数)
    2. 通过 Foundation.framework 的 NSObject 类定义的方法(让开发者自定义的类继承自 NSObject 类不仅仅是因为 NSObject 类能解决很多复杂的内存分配问题,更是因为 NSObject 类中封装了很多 RunTime 系统的功能)
    3. 通过对 RunTime 库函数的直接调用

    在这里插入图片描述

  • Selector 的定义

    在 RunTime 源码中,SEL 被定义为一个指向 struct objc_selector 的指针
    虽然在 RunTime 源码中没有找到关于 struct objc_selector 的定义
    但是通过苹果官方文档的说明与 RunTime 中关于 SEL 的注释,不难得出 SEL 本质上是一个保存方法名称的 C 字符串

    -(void)selectorDemo {
        SEL selector0 = @selector(viewDidLoad);
        SEL selector1 = @selector(undefinedMethod);
        char* cString0 = "viewDidLoad";
        char* cString1 = "undefinedMethod";
        NSLog(@"selector0 = %s", selector0);    // selector0 = viewDidLoad
    	NSLog(@"cString0 = %s", cString0);      // cString0 = viewDidLoad
        NSLog(@"selector1 = %s", selector1);    // selector1 = undefinedMethod
        NSLog(@"cString1 = %s", cString1);      // cString1 = undefinedMethod
    }
    
  • 关于 Selector 表

    1. RunTime 维护了一个巨大的全局 Selector 表
    2. 当使用 @selector()NSSelectorFromString()sel_registerName() 获取 Selector 时,会根据 Selector 的名称从 Selector 表中查找对应的 Selector。如果 Selector 表中不存在该 Selector,则会生成一个对应的 Selector 并添加到 Selector 表中
    3. 在编译期间,编译器会扫描 Objective-C 的全部头文件和实现文件,将其中的方法以及使用 @selector() 生成的 Selector 都添加到 Selector 表中
  • NSObject 中获取 IMP 的方法:methodForSelector 与 instanceMethodForSelector

    methodForSelector

    /*
    获取当前方法调用者(即消息接受者)给定方法选择器所标识的方法实现
    此方法既可以获取对象方法的实现,也可以获取类方法的实现
    
    @param aSelector 用于标识要获取方法实现的方法选择器
    				 参数 aSelector 必须有效并且不能为空			 
    @return 当前方法调用者(即消息接受者)中给定方法选择器 aSelector 所标识的方法实现
    @note 如果当前方法调用者(即消息接受者)是一个实例对象,则方法选择器 aSelector 所标识的就是对象方法
    	  如果当前方法调用者(即消息接受者)是一个类对象,则方法选择器 aSelector 所标识的就是类方法
    @note 如果不确定当前对象是否存在方法选择器 aSelector 所标识的方法,则在调用此方法前请使用 respondsToSelector: 检查当前对象是否有 aSelector 所标识的方法
    @note 如果得到了某个方法的实现,就可以绕开消息传递过程,直接执行该方法
    */
    -(IMP)methodForSelector:(SEL)aSelector;
    

    instanceMethodForSelector

    /*
    获取当前方法调用者(即消息接受者)给定方法选择器所标识的对象方法的实现
    此方法只能获取对象方法的实现,不能获取类方法的实现
    
    @param aSelector 用于标识要获取方法实现的方法选择器
    				 参数 aSelector 必须有效并且不能为空	
    @return 当前方法调用者(即消息接受者)中给定方法选择器 aSelector 所标识的对象方法的实现
    @note 此方法只能获取对象方法的实现,不能获取类方法的实现
    	  如果要获取类方法的实现,请使用方法 methodForSelector:
    @note 如果不确定当前对象是否存在方法选择器 aSelector 所标识的方法,则在调用此方法前请使用 respondsToSelector: 检查当前对象是否有 aSelector 所标识的方法
    @note 如果得到了某个方法的实现,就可以绕开消息传递过程,直接执行该方法
    */
    +(IMP)instanceMethodForSelector:(SEL)aSelector;
    
  • 如何通过 RunTime 的 API 获取和设置实例对象中基本数据类型的成员变量

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) float height;
    @property (nonatomic, strong) NSString* name;
    
    @end
    
    
    
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    @end
    
    -(void)ivarDemo {
        Person* aPerson = [[Person alloc] init];
        aPerson.age = 10;
        aPerson.height = 170.0f;
        aPerson.name = @"hcg";
        NSLog(@"aPerson = %@", aPerson);
        NSLog(@"aPerson.age = %d, aPerson.height = %f, aPerson.name = %@", aPerson.age, aPerson.height, aPerson.name);
        // 输出结果:
        // aPerson = <Person: 0x600003e18800>
        // aPerson.age = 10, aPerson.height = 170.000000, aPerson.name = hcg
        
        unsigned int count = 0;
        Ivar* ivarList = class_copyIvarList([Person class], &count);
        for (int i = 0; i < count; i++) {
            Ivar aIvar = ivarList[i];
            NSString* name = @(ivar_getName(aIvar));
            ptrdiff_t offset = ivar_getOffset(aIvar);
            NSString* type = @(ivar_getTypeEncoding(aIvar));
            NSLog(@"ivar.name = %@, ivar.offset = %zd, ivar.type = %@", name, offset, type);
        }
        free(ivarList);
        // 输出结果:
        // ivar.name = _age, ivar.offset = 8, ivar.type = i
        // ivar.name = _height, ivar.offset = 12, ivar.type = f
        // ivar.name = _name, ivar.offset = 16, ivar.type = @"NSString"
        
        // 设置成员变量:类类型
        Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
        object_setIvar(aPerson, nameIvar, @"hzp");
        NSLog(@"aPerson.name = %@", aPerson.name);
        // 输出结果:
        // aPerson.name = hzp
        
        // 设置成员变量:基本数据类型
        Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
        ptrdiff_t ageOffset = ivar_getOffset(ageIvar);
        int* ageAddr = (int *)((__bridge  void *)(aPerson) + ageOffset);
        *ageAddr = 20;
        NSLog(@"age.offset = %zd, age.addr = %p", ageOffset, ageAddr);
        NSLog(@"aPerson.age = %d", aPerson.age);
        // 输出结果:
        // age.offset = 8, age.addr = 0x600003e18808
        // aPerson.age = 20
        
        // 设置成员变量:基本数据类型
        Ivar heightIvar = class_getInstanceVariable([Person class], "_height");
        ptrdiff_t heightOffset = ivar_getOffset(heightIvar);
        float* heightAddr = (float *)((__bridge void *)(aPerson) + heightOffset);
        *heightAddr = 180.0f;
        NSLog(@"height.offset = %zd, height.addr = %p", heightOffset, heightAddr);
        NSLog(@"aPerson.height = %f", aPerson.height);
        // 输出结果:
        // height.offset = 12, height.addr = 0x600003e1880c
        // aPerson.height = 180.000000
    }
    
  • 健壮的成员变量(Non Fragile Ivars)

    相比 Legacy RunTime,Modern RunTime 具有健壮的成员变量

    当一个类被编译后,其成员变量的内存布局就固定了。成员变量的内存布局用于标识访问实例对象的成员变量时,成员变量相对于实例对象的位置。实例对象中的成员变量依次根据自己所占空间而产生位移:

    Non Fragile Ivars 0
    上图左是 NSObject 类的实例对象的成员变量的内存布局,上图右是自定义类 MyObject 的实例对象的成员变量的内存布局。这样的成员变量内存布局方式有一个很大的缺陷,那就是缺乏拓展性。如果哪天苹果更新了 NSObject 类的话,就会出现问题:

    Non Fragile Ivars 1

    (自定义类 MyObject 的实例对象存储成员变量的区域)和(父类 NSObject 类的实例对象存储成员变量的区域)重叠了
    在脆弱的成员变量(Fragile Ivars)的环境下,开发者需要重新编译继承自 Apple 的类来恢复兼容性
    在健壮的成员变量(Non Fragile Ivars)的环境下,开发者不需要重新编译继承自 Apple 的类来恢复兼容性。当 RunTime 系统检测到子类的成员变量与父类的成员变量有部分重叠时,它会调整子类新添加的成员变量的位移,那样在子类中新添加的成员变量就被保护起来了:

    Non Fragile Ivars 2
    注意,在健壮的成员变量(Non Fragile Ivars)的环境下:

    1. 获取类的实例对象的大小时,不要使用 sizeof(SomeClass),而是要使用 class_getInstanceSize([SomeClass class])
    2. 获取成员变量相对于实例对象的偏移量时,不要使用 offsetof(SomeClass, SomeIvar),而是要使用 ivar_getOffset(SomeIvar)

Objective-C RunTime 系列的参考文章

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值