* author:conowen@大钟
* E-mail:conowen@hotmail.com
1、Runtime简介
首先说说编程语言里面的Run time(运行时)对应的有Compile time(编译时),简单来说,rumtime(运行时)就是把代码装载到内存里面,由CPU运行的时候,这过程同时包括链接各种库,Compile time(编译时),顾名思义就是正在编译的时候.编译器帮你把代码翻译成“机器语言”。一般来说这种“机器语言”是一种中间状态语言,如Java语言中,编译器会把Java源代码翻译为JVM字节码,而Objc语言里面,编译器会把Objc源代码翻译为runtime代码。而这些runtime代码将会被runtime系统执行。Objc 的Runtime其实是一个Runtime库,它是用C和汇编写成的,这个库使得C语言有了面向对象的特性。
以下代码将会展示编译时与运行时的区别。
NSArray *arr0 = @[@"a",@"b"];
NSLog(@"str =%@",[arr0 objectAtIndex:2]);//crash
NSArray *arr1 = [[NSString alloc] initWithString:@"test"];
NSLog(@"count =%lu",arr1.count);//crash
上述Objc代码在编译的时候并没有error,但是运行的时候就会crash。Objc在编译的时候并不会去真正地调用这个方法,只要你声明了这个方法,Objc在运行的时候,才会根据这个方法名,找到对应的方法,然后真正地执行。但是对于C语言,在编译的时候,若是你调用此方法,而这个方法并没有实现,那么在编译的时候就会出现error。Objc这个特性让Objc语言具有动态性,也被称为动态语言。
简单来说,动态语言就是在运行时才会执行静态语言的编译、链接工作,而这些工作,静态语言在编译时就已经完成了。那Objc为嘛要采用这种方式呢?这是因为Objc是由Smalltalk语言演化而来,(Smalltalk语言是消息性语言的始祖,例如a + b这个代码,是“对象a”发送“+”消息,参数是“对象b”)。虽然平常写消息型语言(例如Objc)代码看起来是调用哪个方法,实际编译的时候编译器会把这些“方法”转化为runtime系统通用的“发送消息”方法。
例如NSString的初始化操作:
NSString *str = [NSString stringWithFormat:@"%@",@"conowen"];
实际就是向消息接收者NSString发送了stringWithFormat消息,而消息接收者NSString接收到消息消息的时候,才会查找这个消息对应的方法。这一步实在运行时才执行,编译的时候,编译器并不关心消息接收者NSString是什么类型的对象,或者这个消息对应的方法是否能找得到。
详细过程可以看以下代码,新建一个Objc文件,写入以下代码
#import
int main (int argc, const char * argv[])
{
@autoreleasepool
{
NSString *str = [NSString stringWithFormat:@"%@",@"conowen"];
NSLog(@"length = %lu",[str length]);
}
return 0;
}
在终端输入编译命令:clang -rewrite-objc Test.m
编译成功之后就会在同目录下生成一个同名的cpp文件,这就是runtime代码。打开cpp文件,在最后cpp的末尾可以发现以下runtime代码,就是我们初始化NSString,然后用NSLog方法打印的实现方式。
int main (int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
NSString *str = ((NSString *(*)(id, SEL, NSString *, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_9t__l4hvdvx6q7cmddt7cdf8xw00000gn_T_Test_084efb_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_9t__l4hvdvx6q7cmddt7cdf8xw00000gn_T_Test_084efb_mi_1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t__l4hvdvx6q7cmddt7cdf8xw00000gn_T_Test_084efb_mi_2,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)str, sel_registerName("length")));
}
return 0;
}
可以发现关键的
objc_msgSend
关于这个是啥东西,下一篇博文再详细解说。因为Objc代码最终都会转成这种Runtime代码,那么我们可以简单地把这部分的Runtime代码直接用起来。
新建一个空白工程,写入如下代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString *str0 = [NSString stringWithFormat:@"%@",@"conowen"];
NSLog(@"length = %lu",[str0 length]);
NSString *str = ((NSString *(*)(id, SEL, NSString *, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"),@"%@", @"conowen");
NSLog(@"length = %lu",((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)str, sel_registerName("length")));
}
最终 console打印出来的结果如下
2016-05-20 15:00:39.495 Runtime[3066:1128470] length = 7
2016-05-20 15:00:39.496 Runtime[3066:1128470] length = 7
效果是一样的。
2、runtime能干嘛
说了这么多,那Objc的runtime到底能干嘛?看到这里,你应该对runtime有个大概的了解,Objc代码编译后皆转成runtime系统所能认识的编码。也就是说,我可以直接编写runtime代码来实现Objc代码所实现的功能,这样看起来更cool一点,然而并不止是为了cool才去写runtime代码。因为runtime更加底层,我们可以动态地去修改、拦截或者替换系统的一些方法,因为这些都会转成runtime代码。我们在Objc层改不了的话,那就直接在runtime改咯。具体的应用如json转model的第三方库,Category的实现,JSpatch库的实现等等。
3、objc_msgSend
objc_msgSend
函数原形为
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)//后面省略号表示若干个参数,可有可无
上述的[str0 length]
会被转换为objc_msgSend(str0,@selector(length));
同样的还有以下:
objc_msgSend_stret
当发送的消息要返回的是结构体类型时
objc_msgSend_fpret
当发送的消息要返回的是浮点数时
objc_msgSendSuper
如果要给超类(父类)发消息,就用这个函数处理
同理,还有objc_msgSendSuper_stret
和objc_msgSendSuper_fpret
。通常来说,编译器会根据实际情况,动态地调用这个几个方法中的一个,还有需要明确一点的是,objc_msgSend
这个函数并没有返回数据,而是调用相对应的方法所返回来的数据。objc_msgSend
的第一个参数是消息接收者id,是一个指向任意对象的类型,而选择子SEL可以理解指向任意方法的类型,同id。
3.1、id
在objc.h可以看到以下定义
// A pointer to an instance of a class.
typedef struct objc_object *id;
// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
然后再查看objc_object的定义如下,表示这是一个类的对象,这个结构体中只包含一个Class类型的isa指针(isa的缩写是 is a,可以理解为:This object "objectA"is a object of Class A,这个对象是一个A类的对象。),所以根据这个isa指针就能找到这个对象所属的类。
(引申:在objc里面,任何对象其实都是C语言里面的结构体)
//Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
而Class的定义如下,之所以说isa是指针,是因为Class指向的是objc_class类型的指针。
// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;
而objc_class的定义如下,其中!OBJC2表示非Object c 2.0的版本,Object c是从1.0发展到现在的2.0,这个声明是为了兼容1.0的Object c,若是Object c 2.0的话,可见objc_class与objc_object的定义是一样的,都是只有一个isa指针,所以说在Object c里面,类也是一个对象。关于Object c的类与对象关系,将在下一篇博文详细说明。
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const charchar *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
从上往下可包含了父类指针,类名,类的版本信息,类的信息,类的实例大小,类的成员变量,类的方法表,类的方法缓存表,还有类的协议。
注意到里面有两个Class成员,isa
与super_class
。
3.2、category 增加成员变量
注意一点:可以看到这个结构体里面有包含instance_size这个变量,表示类的实例变量大小,当实例化一个类的时候,指的是开辟一块内存区域,这块内存区域包含了isa指针与类的所有成员变量,也就是说当实例化一个类时,内存已经布局好了,当然不允许你直接增加成员变量,虽然不能直接增加成员变量,但是可以通过以下方法添加:
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const voidvoid *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const voidvoid *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
/**
* Removes all associations for a given object.
*
* @param object An object that maintains associated objects.
*
* @note The main purpose of this function is to make it easy to return an object
* to a "pristine state”. You should not use this function for general removal of
* associations from objects, since it also removes associations that other clients
* may have added to the object. Typically you should use \c objc_setAssociatedObject
* with a nil value to clear an association.
*
* @see objc_setAssociatedObject
* @see objc_getAssociatedObject
*/
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
3.3、category 增加成员方法
那为何又可以增加类方法呢(category),我们可以观察到struct objc_method_list **methodLists
这是一个指向objc_method_list
的二级指针(指向指针的指针,因为指针也是一个变量,本身也要存储在内存中。)这就为什么方法列表为什么能增加的原因,因为objc_class存储是方法列表地址的地址,而指向的这些方法所需要的内存空间并没有在rumtime就布局好了,所以就不担心category增加方法的时候会影响类实例的内存布局。
附上objc_ivar_list
与objc_method_list
的定义
struct objc_ivar {
charchar *ivar_name OBJC2_UNAVAILABLE;
charchar *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
charchar *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
3.4、IMP是什么
以上主要的可以关注objc_method结构体里面的IMP类型,在objc.h头文件中定义如下
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
简单来说,IMP(implement)是一个函数指针,指向了方法的具体实现。实际的应用场景是这样的,当你写了Object c代码,如[NSString alloc]的时候,在运行时转换为objc_mesgSend函数形式,最终这个alloc消息最终会执行什么代码,是有IMP来决定的,IMP直接指向了方法的实现。这样就会很有趣了,应该可以直接操作方法的入口,那不就是可以更改替换系统方法,这种技术被称为“方法调配”(method swizzling)。原理就是让IMP指针本来指向改到其他方法实现那里去。
3.5、指针函数与函数指针
引申:指针函数与函数指针的区别
- a、指针函数
表示函数返回值是一个指针类型,定义如下
//类型标识符 *函数名(参数表)
int * fun(x,y);
- b、函数指针
表示指向这个函数的指针变量,定义如下
类型标识符 (*函数名)(参数表)
int (*fun) (int x,y); //函数名前面有一个星号,然后用小括号包起来
fun=funTest; /* 将funTest函数的首地址赋给指针
3.6、objc_class的cache是什么
我们在rumtime.h头文件可以看到它的定义如下
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
如果知道CPU的L1 cache和L2 cache,就大概知道这玩意设计有什么作用了。摘自某度百科的解说
“L1 cache(一级缓存)主要将CPU的硬指令长期存储,以便CPU在调用指令时不必再通过与内存交换数据来取得,另外,还将最近处理的进程数据(中间数据)存放在一级缓存;而L2 cache(二级缓存)则是完全存放最近处理的进程数据(中间数据)和即将调用的数据。通过这样一来设置,就可以避免CPU运算过程中要频繁与内存交换数据,减少CPU的等待时间,提高CPU的利用效率。”
没错,objc_class结构体里面的cache就是这样的作用,每次消息接收者接收到消息的时候,都是先在cache里面查找是否有对应的方法,而不是直接到isa指向类的方法列表里面查找,这样会导致效率太低。同时,rumtime系统会把最近执行的方法存储在cache里面去(若是cache里面没有存储到的话)。
4、实例方法调用流程
- 1、首先会根据这个对象的
isa
指针,找到这个对象是属于哪个类。 - 2、找到类之后,现在类结构体里面的cache里面查找这个消息对应IMP指针(找到就直接跳到IMP指针指向的方法,然后执行)
- 3、若是在cache找不到就会到该类的方法列表里面找。
- 4、若是在该类的方法列表找不到,就通过
super_class
指针,在父类的方法列表里面找,一直往上,根据继承关系寻找。 - 5、若是找到NSObject还找不到,就检查是否有动态创建方法体处理这个消息,如果还是没有就执行“消息转发”(message forwarding)操作。
元类MetaClass
在面向对象程序设计中,元类(metaclass)是一种 实例是类的类。也就是说,元类的实例也是类,在iOS中,简单来说,无论是对象还是类,一切皆是对象。所以以一般也称类Class为类对象
。在iOS中,元类(metaclass)是系统编译时,系统编译器动态创建的。
在OC中,只有对象才能发送消息,而OC中,类方法的执行本质上也是发送消息,所有说类也是对象。
从上面可以知道,对一个实例对象发送消息,首先会根据这个对象的isa指针,找到这个对象是属于哪个类。我们发现 Class 本身也有一个 isa 指针,指向的是它的 MetaClass。每一个 Meta Class 的 isa 指针都是直接指向最上层基类的 Meta Class,即 NSObject 的 MetaClass
- 当我们对一个实例发送消息时(-开头的方法),实际上,通过这个实例对象的isa指针找到所属的Class,然后在对应的类的 methodLists 里查找方法,如果找不到,就通过super_class指针,依次往上找到
父类
method。 - 当我们对一个类发送消息时(+开头的方法),实际上会通过这个Class的isa指针,找到类的MetaClass,然后再在MetaClass的 methodLists 里查找方法。,如果找不到,就通过super_class指针,依次往上找到
父元类
的method。也就说,元类存储的是类方法。
这一过程如下图所示: - 每个 Class 都有一个 isa 指针指向一个唯一的 Meta Class
- 每一个 Meta Class 的 isa 指针都是直接指向最上层基类的 Meta Class,即 NSObject 的 MetaClass,而最上层基类的 MetaClass 的 isa 指针又指向自己。
- 可以理解Class其实也是对象,实例对象和Class都是id类型的对象。
参考文章
https://hit-alibaba.github.io/interview/iOS/ObjC-Basic/Runtime.html