目录
NSObject 的(对象方法)与(类方法)
-
实例对象、类对象、元类对象 之间的关系
关于isa
指针:- 实例对象的
isa
指针指向类对象,类对象的isa
指针指向元类对象 - 所有元类对象的
isa
指针都直接指向NSObject
的元类对象(因此,NSObject
的元类对象也被称为根元类) - 根元类的
isa
指针指向自己
关于
superclass
指针:- 实例对象没有
superclass
指针 - 类对象的
superclass
指针指向(父类的类对象),(父类的类对象)的superclass
指针指向(根类的类对象),(根类的类对象)的superclass
指针指向nil
- 元类对象的
superclass
指针指向(父类的元类对象),(父类的元类对象)的superclass
指针指向(根类的元类对象) - (根类的元类对象)的
superclass
指针指向(根类的类对象),(根类的类对象)的superclass
指针最终指向nil
注意:
- 同一个类的所有实例对象,有且仅有一个与之对应的类对象
- 每个类对象有且仅有一个与之对应的元类对象
- 每一个类对象有且仅有一个父类对象
- 每一个元类对象有且仅有一个父元类对象
- 实例对象的
-
对象方法与类方法的调用路径
假设有:
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
上的同名对象方法)
⑤ 如果没有的话,则会进入消息转发流程 -
代码示例
-(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 中,nil
、Nil
、NULL
、NSNull
都可以表示(空),下面我们来一起探究一下,它们之间的区别与联系,以及各自使用的场景
-
直观地认识: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> }
从输出结果可以看出:
nil
、Nil
、NULL
是一样的,都是指向0x0
的空指针[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 的使用场景
虽然
nil
、Nil
、NULL
都是指向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
用于表示值为空的对象,通常用于容器类(NSArray
,NSDictionary
,NSSet
,…)的占位(NSNull
的实例对象拥有一个有效的内存地址,是实际存在的一个对象)NSArray* array = [NSArray arrayWithObjects:@"first", @"second", [NSNull null], @"fourth", nil]; NSLog(@"array.count = %ld", array.count); // 输出结果如下: // array.count = 4
-
注意
①
nil
、Nil
、NULL
都可以用于表示一个空指针,所谓的空指针指的是:该指针指向一块没有意义的内存区域② 在 Objective-C 中:
对空指针(nil
、Nil
、NULL
)发送非法消息,不会导致程序崩溃或者异常
对NSNull
发送非法消息,会导致程序崩溃或者异常③ 因为
nil
在NSArray
中被作为结束符,所以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:
-
/* 返回一个布尔值,用于标识:消息接受者(即方法调用者)是否是给定类的实例 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;
如上所述,对于
NSArray
、NSMutableArray
要谨慎使用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 }
-
/* 返回一个布尔值,用于标识:消息接受者(即方法调用者)是否是给定类的实例 @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_msgSend
和objc_msgSendSuper
的定义如下所示:id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...); id _Nullable objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...);
从简化之后的代码可以得到以下 3 点:
- 调用
[self class]
的时候,底层是调用的objc_msgSend(...)
- 调用
[super class]
的时候,底层调用的是objc_msgSendSuper(...)
- 通过
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 系统进行交互:
- 通过 Objective-C 源代码(编译器会将 Objective-C 源代码转换成 RunTime 的数据结构与函数)
- 通过 Foundation.framework 的 NSObject 类定义的方法(让开发者自定义的类继承自 NSObject 类不仅仅是因为 NSObject 类能解决很多复杂的内存分配问题,更是因为 NSObject 类中封装了很多 RunTime 系统的功能)
- 通过对 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 表
- RunTime 维护了一个巨大的全局 Selector 表
- 当使用
@selector()
、NSSelectorFromString()
、sel_registerName()
获取 Selector 时,会根据 Selector 的名称从 Selector 表中查找对应的 Selector。如果 Selector 表中不存在该 Selector,则会生成一个对应的 Selector 并添加到 Selector 表中 - 在编译期间,编译器会扫描 Objective-C 的全部头文件和实现文件,将其中的方法以及使用
@selector()
生成的 Selector 都添加到 Selector 表中
-
NSObject 中获取 IMP 的方法:methodForSelector 与 instanceMethodForSelector
/* 获取当前方法调用者(即消息接受者)给定方法选择器所标识的方法实现 此方法既可以获取对象方法的实现,也可以获取类方法的实现 @param aSelector 用于标识要获取方法实现的方法选择器 参数 aSelector 必须有效并且不能为空 @return 当前方法调用者(即消息接受者)中给定方法选择器 aSelector 所标识的方法实现 @note 如果当前方法调用者(即消息接受者)是一个实例对象,则方法选择器 aSelector 所标识的就是对象方法 如果当前方法调用者(即消息接受者)是一个类对象,则方法选择器 aSelector 所标识的就是类方法 @note 如果不确定当前对象是否存在方法选择器 aSelector 所标识的方法,则在调用此方法前请使用 respondsToSelector: 检查当前对象是否有 aSelector 所标识的方法 @note 如果得到了某个方法的实现,就可以绕开消息传递过程,直接执行该方法 */ -(IMP)methodForSelector:(SEL)aSelector;
/* 获取当前方法调用者(即消息接受者)给定方法选择器所标识的对象方法的实现 此方法只能获取对象方法的实现,不能获取类方法的实现 @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 具有健壮的成员变量
当一个类被编译后,其成员变量的内存布局就固定了。成员变量的内存布局用于标识访问实例对象的成员变量时,成员变量相对于实例对象的位置。实例对象中的成员变量依次根据自己所占空间而产生位移:
上图左是NSObject
类的实例对象的成员变量的内存布局,上图右是自定义类MyObject
的实例对象的成员变量的内存布局。这样的成员变量内存布局方式有一个很大的缺陷,那就是缺乏拓展性。如果哪天苹果更新了NSObject
类的话,就会出现问题:(自定义类
MyObject
的实例对象存储成员变量的区域)和(父类NSObject
类的实例对象存储成员变量的区域)重叠了
在脆弱的成员变量(Fragile Ivars)的环境下,开发者需要重新编译继承自 Apple 的类来恢复兼容性
在健壮的成员变量(Non Fragile Ivars)的环境下,开发者不需要重新编译继承自 Apple 的类来恢复兼容性。当 RunTime 系统检测到子类的成员变量与父类的成员变量有部分重叠时,它会调整子类新添加的成员变量的位移,那样在子类中新添加的成员变量就被保护起来了:
注意,在健壮的成员变量(Non Fragile Ivars)的环境下:- 获取类的实例对象的大小时,不要使用
sizeof(SomeClass)
,而是要使用class_getInstanceSize([SomeClass class])
- 获取成员变量相对于实例对象的偏移量时,不要使用
offsetof(SomeClass, SomeIvar)
,而是要使用ivar_getOffset(SomeIvar)
- 获取类的实例对象的大小时,不要使用