iOS runtime—看这一篇就够了
本文篇幅比较长,创作的目的为了自己日后温习知识所用,希望这篇文章能对你有所帮助。
如发现任何有误之处,肯请留言纠正,谢谢。
一、深入代码理解 instance、class object、metaclass
1、instance对象实例
我们经常使用id来声明一个对象,那id的本质又是什么呢?查看objc/objc.h文件
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的id也就是这个结构体的指针。
这个结构体只有一个成员变量,这是一个Class类型的变量isa,也是一个结构体指针,isa指针就指向对象所属的类。
一个 NSObject 对象占用多少内存空间?
一个NSObject实例对象只有一个isa指针,所以一个isa指针的大小,他在64位的环境下占8个字节,在32位环境上占4个字节。
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize--%zd", class_getInstanceSize([NSObject class]));
输出结果:
class_getInstanceSize--8
2、class object(类对象)/metaclass(元类)
看结构体objc_class的定义
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *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 superclass;——用于获取父类,也就是元类对象,它也是一个Class类型
- cache_t cache;——是方法缓存
- class_data_bits_t bits;——用于获取类的具体信息,看到bits
- class_rw_t *data()函数,该函数的作用就是获取该类的可读写信息,通过class_data_bits_t的bits.data()方法获得,class_rw_t后面会介绍
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,我们称之为类对象。类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息。
3、isa指针与superclass相关逻辑图
4、总结 + 代码校验
- 对象 的类(Superclass)是 类(对象) ;
- 类(对象) 的类(Superclass)是 元类,和类同名;
- 元类 的类(Superclass)是 根元类 NSObject;
- 根元类 的类(Superclass)是 自己 ,还是NSObject;
- 对象的isa指针指向类(对象) ;
- 类对象的isa指针指向元类,和类同名;
- 元类的isa指针指向跟根元类 NSObject;
- 根元类 NSObject的isa指针指向自己。
isa验证
NSString *string = @"字符串";
Class class1 = object_getClass(string);//NSString类对象
Class metaClass = object_getClass(class1);//NSString元类
Class rootMetaClass = object_getClass(metaClass);//根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);//根元类
NSLog(@"%p 实例对象 ",string);
NSLog(@"%p 类 %@",class1,NSStringFromClass(class1));
NSLog(@"%p 元类 %@",metaClass,NSStringFromClass(metaClass));
NSLog(@"%p 根元类 %@",rootMetaClass,NSStringFromClass(rootMetaClass));
NSLog(@"%p 根根元类 %@",rootRootMetaClass,NSStringFromClass(rootRootMetaClass));
Class rootMetaClass_superclass = rootMetaClass.superclass;//根元类的superclass
NSLog(@"根根元类的superclass:%@",NSStringFromClass(rootMetaClass_superclass));
输出结果:
0x102d48078 实例对象
0x1d80e3d10 类 __NSCFConstantString
0x1d80e3cc0 元类 __NSCFConstantString
0x1d80c66c0 根元类 NSObject
0x1d80c66c0 根根元类 NSObject
根根元类的superclass:NSObject
superclass验证
NSString *string = @"字符串";
Class class1 = object_getClass(string);//NSString类对象
Class class2 = class1.superclass;
NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class1),NSStringFromClass(class2));
Class class3 = class2.superclass;
NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class2),NSStringFromClass(class3));
Class class4 = class3.superclass;
NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class3),NSStringFromClass(class4));
Class class5 = class4.superclass;
NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class4),NSStringFromClass(class5));
Class class6 = class5.superclass;
NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class5),NSStringFromClass(class6));
输出结果:
__NSCFConstantString 的superclass是 __NSCFString
__NSCFString 的superclass是 NSMutableString
NSMutableString 的superclass是 NSString
NSString 的superclass是 NSObject
NSObject 的superclass是 (null)
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
二、class_rw_t 与 class_ro_t
1、class_ro_t 一"码"当先:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
- uint32_t instanceSize;——instance对象占用的内存空间
- const char * name;——类名
- const ivar_list_t * ivars;——类的成员变量列表
class_ro_t存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。
ro即表示read only,是无法进行修改的。
2、class_rw_t 一"码"当先:
// 可读可写
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // 指向只读的结构体,存放类初始信息
/*
这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
*/
method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
property_array_t properties; // 属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
//...
}
3、class_rw_t生成时机
class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。
类的realizeClass运行之前:
然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:
- 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
- 初始化一个 class_rw_t 结构体
- 设置结构体 ro 的值以及 flag
- 最后设置正确的 data。
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
但是,在这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。
realizeClass 方法执行过后的类所占用内存的布局:
细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。
4、method_t
上面我们剖析了class_rw_t、class_ro_t这两个重要部分的结构,并且主要关注了其中的方法列表部分,而从上面的分析,可发现里面最基本也是重要的单位是method_t,这个结构体包含了描述一个方法所需要的各种信息。
struct method_t {
SEL name;
const char *types;
IMP imp;
};
变量介绍可以参考之前文章:iOS 代码注入—— hook 实践
三、Runtime 初始化函数
1、一"码"当先
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
_dyld_objc_notify_register(&map_images, load_images, unmap_image)。这个函数里面的三个参数分别是另外三个函数:
- map_images -- Process the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件)
- load_images -- Process +load in the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件中的+load方法)
- unmap_image -- Process the given image which is about to be unmapped by dyld.(处理那些将要被dyld进行去映射操作的镜像文件)
我们查看一下map_images方法,点进去:
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
四、分类底层原理
根据map_images函数,继续点进去看,可以看到如下代码:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
根据代码:
category_t *cat = catlist[i];
一开始的那个catlist是一个二维数组,里面的成员也是一个一个的数组,也就是代码里面的cat所指向的数组,它的类型是category_t *,说明cat数组里面装的就是category_t,一个cat里面装的就是某个class所对应的所有category。
那么什么决定了这些category_t在cat数组中的顺序呢?
答案是category文件的编译顺序决定的。先参与编译的,就放在数组的前面,后参与编译的,就放在数组后面。我们可以在xcode-->target-->Build Phases-->Compile Sources列表查看和调整category文件的编译顺序
加载分类的最后,执行方法:remethodizeClass(cls->ISA());
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
然后在这里面找到一个方法attachCategories,看名字就知道,附着分类,也就是把分类的内容添加/合并到class里面,感兴趣的可以自己查看一下这个方法,这个理就不做解释了。
五、方法缓存
1、数据结构
它的底层是通过散列表(哈希表)的数据结构来实现的,用于缓存曾经调用过的方法,可以提高方法的查找速度。
首先,回顾一下正常情况下方法调用的流程。假设我们调用一个实例方法[obj XXXX];
- obj -> isa -> obj的Class对象 -> method_array_t methods -> 对该表进行遍历查找,找到就调用,没找到继续往下走
- obj -> superclass -> obj的父类 -> isa -> method_array_t methods -> 对父类的方法列表进行遍历查找,找到就调用,没找到就重复本步骤
- 直到NSObject -> isa -> NSObject的Class对象 -> method_array_t,如果还是没有找到就会crash
如果XXXX方法在程序内会被频繁的调用,那么这种逐层便利查找的方式肯定是效率低下的,因此苹果设计了cache_t cache,当XXXX第一次被调用的时候,会按照常规流程查找,找到之后,就会被加入到cache_t cache中,当再次被调用的时候,系统就会直接现到cache_t cache来查找,找到就直接调用,这样便大大提升了查找的效率。
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
- struct bucket_t *_buckets; —— 用来缓存方法的散列/哈希表
- mask_t _mask; —— 这个值 = 散列表长度 - 1
- mask_t _occupied; —— 表示已经缓存的方法的数量
_buckets散列表里面的存储单元是bucket_t,
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
- cache_key_t _key; —— 这个key实际上就是方法的SEL,也就是方法名
- IMP _imp; —— 这个就是方法对应的函数的内存地址
2、缓存逻辑
- (1) 当一个对象接收到消息时[obj message];,首先根据obj的isa指针进入它的类对象class里面。
- (2) 在obj的class里面,首先到缓存cache_t里面查询方法message的函数实现,如果找到,就直接调用该函数。
- (3) 如果上一步没有找到对应函数,在对该class的方法列表进行二分/遍历查找,如果找到了对应函数,首先会将该方法缓存到obj的类对象class的cache_t里面,然后对函数进行调用。
- (4) 在每次进行缓存操作之前,首先需要检查缓存容量,如果缓存内的方法数量超过规定的临界值(设定容量的3/4),需要先对缓存进行2倍扩容,原先缓存过的方法全部丢弃,然后将当前方法存入扩容后的新缓存内。
- (5) 如果在obj的class对象里面,发现缓存和方法列表都找不到mssage方法,则通过class的superclass指针进入它的父类对象father_class里面
- (6) 进入father_class后,首先在它的cache_t里面查找mssage,如果找到了该方法,那么会首先将方法缓存到消息接受者obj的类对象class的cache_t里面,然后调用方法对应的函数。
- (7) 如果上一步没有找到方法,将会对father_class的方法列表进行遍历二分/遍历查找,如果找到了mssage方法,那么同样,会首先将方法缓存到消息接受者obj的类对象class的cache_t里面,然后调用方法对应的函数。需要注意的是,这里并不会将方法缓存到当前父类对象father_class的cache_t里面。
- (8) 如果还没找到方法,则会通过father_class的superclass进入更上层的父类对象里面,按照(6)->(7)->(8)步骤流程重复。如果此时已经到了基类对象NSObject,仍没有找到mssage,则进入步骤(9)
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
六、消息转发
第一步:Method resolution 方法解析处理阶段
如果调用了对象方法首先会进行+(BOOL)resolveInstanceMethod:(SEL)sel判断
如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel判断
两个方法都为类方法;
+ (BOOL)resolveClassMethod:(SEL)sel {
///这里动态添加方法
return YES;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
///这里动态添加方法
return YES;
}
_class_resolveInstanceMethod源码解析
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(id inst, SEL sel, Class cls)
{
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
从runtime的源码,resolveInstanceMethod的返回值对于消息转发流程没有任何意义,这个返回值只和debug的信息相关。
这两个方法是最先走到的方法,可以在这两个方法中动态的添加方法,进行消息转发。这里有一个需要特别注意的地方,类方法需要添加到元类里面,原因这里就不赘述了。
**第二步:Fast forwarding 快速转发阶段 **
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [xxx new];
}
这个里可以快速重定向成其他对象,已经让备用的对象去响应了该对象本身无法响应的一个SEL
第三步:Normal forwarding 常规转发阶段
//返回方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx"]) {
return [[xxx new] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
//处理返回的方法签名
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"xxx"]) {
[anInvocation invokeWithTarget:[xxx new]];
}else{
[super forwardInvocation:anInvocation];
}
}
自动签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
//如果返回为nil则进行自动签名
if ([super methodSignatureForSelector:aSelector]==nil) {
NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
//创建备用对象
xxx * backUp = [xxx new];
SEL sel = anInvocation.selector;
//判断备用对象是否可以响应传递进来等待响应的SEL
if ([backUp respondsToSelector:sel]) {
[anInvocation invokeWithTarget:backUp];
}else{
// 如果备用对象不能响应 则抛出异常
[self doesNotRecognizeSelector:sel];
}
}
////触发崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector {
}
七、super的本质
1、定义
- super—— 是一个指向结构体指针struct objc_super *,它里面的内容是{消息接受者 recv, 消息接受者的父类类对象 [[recv superclass] class]},objc_msgSendSuper会将消息接受者的父类类对象作为消息查找的起点。
2、流程
[obj message] -> 在obj的类对象cls查找方法 -> 在cls的父类对象[cls superclass]查找方法 -> 在更上层的父类对象查找方法 -> ... -> 在根类类对象 NSObject里查找方法
[super message] -> 在obj的类对象cls查找方法(跳过此步骤) -> (直接从这一步开始)在cls的父类对象[cls superclass]查找方法 -> 在更上层的父类对象查找方法 -> ... -> 在根类类对象 NSObject里查找方法
3、实例
NSLog(@"[self class] = %@",[self class]);
- 接受者 当前class实例对象
- 最终调用的方法:基类NSObject的-(Class)class方法
NSLog(@"[super class] = %@",[super class]);
- 接受者 当前class实例对象
- 最终调用的方法:基类NSObject的-(Class)class方法
NSLog(@"[self superclass] = %@",[self superclass]);
- 接受者 当前class实例对象
- 最终调用的方法:基类NSObject的-(Class) superclass方法
NSLog(@"[super superclass] = %@",[super superclass]);
- 接受者 当前class实例对象
- 最终调用的方法:基类NSObject的-(Class) superclass方法
因此 [self class] [super class] [self superclass] [super superclass] 的值都相等
至此,runtime相关的知识点全部总结完毕,该文章将会持续更新迭代!!
看到就是缘分😁,如发现任何有误之处,肯请留言纠正,谢谢。