在初学Objective-C的时候,觉得有很多陌生且奇怪的语法和特性。
比如NSObject *obj = [[NSObject alloc] init];
这种语法;比如尝试调用空指针的函数并不会导致crash这种特性。直到有机会深入了解Objective-C Runtime,才多少有了一些理解。
如果你也对这种看似奇怪的写法感到好奇的话,相信这篇文章能够解答你的疑问。
背景
动态编程语言
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically.
Objective-C是一门"动态编程语言",也就是尽可能把决策推迟到运行时。
什么是Objective-C Runtime
苹果官方文档给出的定义如下:
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.
Objective-C的动态性主要体现在如下3个方面 :
- 动态类型:在运行时才确认对象的类型
- 动态绑定:在运行时才确定对象的调用方法
- 动态加载:在运行时才加载需要的资源或代码
runtime就是为Objective-C提供上述动态特性的库,runtime赋予了C语言面向对象的能力。
为什么要了解Runtime
Runtime为Objective-C提供的动态特性,给开发者提供了更多灵活性,这种灵活性可以在解决一些复杂问题时提供更多方案。 Method Swizzling就是一个很好的例子,大家不妨思考一下下面问题的解决方案:
为现有项目中所有view设置统一的背景颜色。(或者更具实际意义的问题,在某些情况下需要将App中所有页面置灰)
一般想到的方案可能是统一继承一个父类,在父类中实现上述要求。基于Runtime的Method Swizzling可以提供更方便的解决方案 。
另外了解Runtime也可以从底层辅助Debug。
总之,Runtime是一个重要知识点,接下来我们一起深入了解Runtime的运行机制。
Runtime
源码面前,了无秘密 ------《STL源码剖析》
对象和类的实现
上面提到Runtime为Objective-C提供动态类型、动态绑定等动态特性,在运行时才确定对象的类型、调用方法等信息。为此,我们首先需要了解Objective-C中对象是如何表示的,源码objc.h 中利用结构体表示对象:
objc
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa;
};
可以看出,对象只是对Class(类)的简单封装,我们继续探究Class的含义:
objc
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
objc_class在runtime.h 中给出了定义 :
objc
struct objc_class {
Class _Nonnull isa;
Class _Nullable super_class;
const char * _Nonnull name;
long version;
long info;
long instance_size;
struct objc_ivar_list * _Nullable ivars;
struct objc_method_list * _Nullable * _Nullable methodLists;
struct objc_cache * _Nonnull cache;
struct objc_protocol_list * _Nullable protocols;
};
/* Use `Class` instead of `struct objc_class *` */
上述结构体也就是Objective-C中类的表示方式,其中比较重要的成员包括:
-
ivars ,表示该Objective-C类的成员变量。
objcstruct objc_ivar { char * _Nullable ivar_name; char * _Nullable ivar_type; int ivar_offset; };
objcstruct objc_ivar_list { int ivar_count; /* variable length structure */ struct objc_ivar ivar_list[1]; };
-
methodLists,表示该Objective-C类中的函数
objcstruct objc_method { SEL _Nonnull method_name; char * _Nullable method_types; IMP _Nonnull method_imp; };
objcstruct objc_method_list { struct objc_method_list * _Nullable obsolete; int method_count; /* variable length structure */ struct objc_method method_list[1]; };
-
cache ,用于缓存函数,提升性能。关于cache在性能优化方面的作用,美团技术团队的文章《 深入理解 Objective-C:方法缓存》值得一看 。
objcstruct objc_cache { unsigned int mask /* total = mask + 1 */; unsigned int occupied; Method _Nullable buckets[1]; };
-
protocols:该Objective-C类中的协议(链表)
objcstruct objc_protocol_list { struct objc_protocol_list * _Nullable next; long count; __unsafe_unretained Protocol * _Nullable list[1]; };
对象、类和元类
从上述源码中可以看到,对象结构体中只有一个类结构体指针,对象可以通过该指针找到与之对应的类,进而可以在类结构体中找到成员变量、成员函数等信息。
类结构体中还包含两个类结构体指针:
- isa
- super_class
super_class比较容易理解,是指向父类的指针,利用该指针可以实现继承。
isa指针存在的作用又是什么呢?个人理解,一个重要作用是实现类函数。
In Objective-C, a class is itself an object with an opaque type calledClass. Classes can't have properties defined using the declaration syntax shown earlier for instances, but they can receive messages.
类函数,顾名思义就是类拥有的函数。如果类本身也是一个对象 的话,只要找到它对应的类结构体(meta-class)的话,就可以找到类函数了。
类这个"对象"对应的类,称为元类(meta-class),每个类都有一个与之对应的元类。 这样设计虽然方便,但也带来了一些问题,比如元类中isa指向哪里?元类的super_class指向哪里? 下图给出了答案:
- 元类的isa指向哪里? 所有元类的isa指针都指向基类的元类。如果一个类没有父类,其元类就指向自身。
- 元类的super_class指向哪里? 类对应元类的super_class指向该类父类的元类。
函数调用
In Objective-C, messages aren't bound to method implementations until runtime. The compiler converts a message expression [receiver message] into a call on a messaging function,objc_msgSend. This function takes the receiverand the name of the method mentioned in the message---that is, the method selector---as its two principal parameters objc_msgSend(receiver, selector). Any argumentspassed in the message are also handed toobjc_msgSend objc_msgSend(receiver, selector, arg1, arg2, ...)
我们知道,在Objective-C中,[object methodName]
表示调用对象object 的methodName 方法。和C语言中直接按函数地址取用不同,Objective-C中的函数调用是通过Runtime中的objc_msgSend()
实现的,也就是会将[object methodName]
翻译成objc_msgSend(id self, SEL op, ...)
。
也就是说,Objective-C将函数调用,转化成了消息的传递,也正是这种转化,造成了文章开头提到的,尝试调用空指针函数不会导致crash的反常现象。 调用某个对象的函数,就是给对象发送消息,消息中携带的SEL
可以将其理解为方法的ID,通过SEL
可以在objc_class
中的methodLists
查找到方法的具体实现,进而执行。
如果在类中没有找到该函数,会通过类的super_class
指针,去父类中查找,如图所示,循环往复直到基类。
如果基类中也没有找到,该消息就会被丢弃,但不会引发崩溃。
- 类函数调用 上面提到过,Objective-C中类也是对象,对应元类中存储的就是类函数。所以类函数的调用,就是通过元类查找并执行。
到这里,我们对Objective-C的类和函数调用有了浅显的理解,现在看文章开头提到的语法NSObject *obj = [[NSObject alloc] init];
,你有没有自己的理解呢?
[NSObject alloc]
是给NSObject发送消息,也就是调用它的alloc
方法:
Returns a new instance of the receiving class.
根据官方文档,作用是得到类NSObject的实例。然后调用这个实例的初始化函数init
参考
- Objective-C Runtime Programming Guide developer.apple.com/library/arc...
- iOS程序员面试笔试宝典 weread.qq.com/web/bookDet...
- Method Swizzling: What, Why & How medium.com/@grow4gaura...
- github.com/apple-oss-d...
- github.com/apple-oss-d...
- github.com/apple-oss-d...
- 深入理解 Objective-C:方法缓存 tech.meituan.com/2015/08/12/...
- Programming with Objective-C developer.apple.com/library/arc...
- Objective-C Runtime Programming Guide developer.apple.com/library/arc...
- alloc developer.apple.com/documentati...
- init developer.apple.com/documentati...