isa
是一个objc_class类型的指针.
根据上面内存布局以一个objc_class指针为开始的所有都可以当做一个object来对待!
id
可以看到,iOS中很重要的id实际上就是objc_object的指针.而NSObject的第一个对象就是Class类型的isa。因此id可以标示所有基于NSObject的对象。
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
Class
struct objc_class {
Class isa;
#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;
<small>
name:一个 C 字符串,指示类的名称。我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));
version:类的版本信息,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。
instance_size:该类的实例变量大小(包括从父类继承下来的实例变量);
ivars:指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为 NULL;
methodLists:与 info 的一些标志位有关,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指向单个 objc_method_list还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法; 我们可以动态修改 *methodList
的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。这里可以参考下美团技术团队的文章:深入理解 Objective-C: Category
cache:指向 objc_cache 的指针,用来缓存最近使用的方法,以提高效率;
protocols:指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。
</small>
Ivar
表示成员变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中 ivar_offset 是基地址偏移字节
Method
Runtime内部定义的方法,Class中定义有一个objc_method_list,链表都是objc_method类型的
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;/*标示方法名称*/
char *method_types OBJC2_UNAVAILABLE;/*方法的参数类型*/
IMP method_imp 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;
}
SEL
typedef struct objc_selector *SEL;
IMP
typedef id (*IMP)(id, SEL, ...);
Cache
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。
消息
下图详细叙述消息发送的步骤:
<small>
1.首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
2.检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
3.如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
4.如果 cache 找不到就找类的方法列表中是否有对应的方法。
5.如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
6.如果还找不到,就要开始进入动态方法解析了,后面会提到。
</small>
objc_msgSend有以下伪代码
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
可见伪代码中我们看到class_getMethodImplementation(Class cls, SEL sel) 方法用来寻找IMP地址,class_getMethodImplementation的实现可参考:https://blog.csdn.net/dp948080952/article/details/52563064
当 objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:
接受消息的对象(self 所指向的内容,当前方法的对象指针)
方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)
动态方法解析
当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
重定向
消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过 -(id)forwardingTargetForSelector:(SEL)aSelector 方法
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
转发
当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。这时 forwardInvocation: 方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑,唯一参数是个 NSInvocation 类型的对象,该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些处理。也可以将消息转发给其他对象处理,而不抛出错误。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
1330553-400bc5bde4db1725.png
健壮的实例变量(Non Fragile ivars)
在 Runtime 的现行版本中,最大的特点就是健壮的实例变量了。当一个类被编译时,实例变量的内存布局就形成了,它表明访问类的实例变量的位置。实例变量一次根据自己所占空间而产生位移:
上图左是 NSObject 类的实例变量布局。右边是我们写的类的布局。这样子有一个很大的缺陷,就是缺乏拓展性。哪天苹果更新了 NSObject 类的话,就会出现问题:
1330553-33263710847f6d86.png
我们自定义的类的区域和父类的区域重叠了。只有苹果将父类改为以前的布局才能拯救我们,但这样导致它们不能再拓展它们的框架了,因为成员变量布局被固定住了。在脆弱的实例变量(Fragile ivar)环境下,需要我们重新编译继承自 Apple 的类来恢复兼容。在健壮的实例变量下,编译器生成的实例变量布局跟以前一样,但是当 Runtime 系统检测到与父类有部分重叠时它会调整你新添加的实例变量的位移,那样你再子类中新添加的成员变量就被保护起来了。我们让自己的类继承自 NSObject 不仅仅是因为基类有很多复杂的内存分配问题,更是因为这使得我们可以享受到 Runtime 系统带来的便利。
参考引用见:
https://www.cnblogs.com/ioshe/p/5489086.html
https://blog.csdn.net/dp948080952/article/details/52563064
https://blog.csdn.net/qq_32744055/article/details/52387079!
网友评论