知识点
- isa
- method_t
- cache_t
- objc_msgSend
- super
- isKindOfClass isMember
isa
在arm64之前,实例对象的isa就是类对象的地址。arm64之后,因为5个字节可以表示1TB的内存,因此,如果isa都用来存类对象的地址造成了浪费。
c
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 在 arm64 和 x86_64 定义不一样
};
#endif
};
arm64 下 ISA_BITFIELD 位域(从低位到高位)
arduino
uintptr_t nonpointer : 1; // 0:纯指针;1:开启位域优化(现在默认开启)
uintptr_t has_assoc : 1; // 是否有关联对象
uintptr_t has_cxx_dtor : 1; // 是否有C++析构函数
uintptr_t shiftcls : 33; // 存放Class指针,arm64 占33位
uintptr_t magic : 6; // 校验魔数,调试用
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1; // 引用计数是否存在sidetable(散列表)
uintptr_t extra_rc : 19;// 对象内部存储的引用计数
关键字段说明
- nonpointer(第 0 位)
0:isa 直接就是 Class 指针,没有额外信息;老版本 / 某些环境1:开启 isa 优化,isa 不再单纯是指针,把引用计数、weak 标记等塞到位域里
- shiftcls:真正存储 Class 地址。arm64 内存地址对齐,低 3 位永远 0,所以可以把地址右移 3 位存到 shiftcls。
获取真实类地址:
shiftcls << 3
- extra_rc :对象内部引用计数 。当引用计数比较小时,直接存在 isa 的 extra_rc;计数太大放不下时,启用
sidetable存 rc。- extra_rc 存的是:
引用计数 - 1 - 当
has_sidetable_rc = 1:引用计数存在全局 SideTable 哈希表。
- extra_rc 存的是:
- has_assoc :是否设置过关联对象(
objc_setAssociatedObject);销毁时判断是否清理关联对象。 - has_cxx_dtor:是否有自定义 C++ 析构 /dealloc,标记是否需要走完整析构逻辑。
- weakly_referenced:是否有 weak 指针指向该对象;对象销毁时需要清空 weak 表。
- magic:固定魔数,调试器判断是不是合法 isa。
isa 与 SideTable 关系
- SideTable 是一个全局哈希表,每个 SideTable 管理多张对象的引用计数、weak 表。
- isa 的 extra_rc 容量有限,引用计数超出后,就把计数放到 SideTable 的
refcnt,并把has_sidetable_rc置 1。 - weak 信息不在 isa 里,存在 SideTable 的 weak_table。
方法
arduino
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // ro是只读原始类信息(编译期)
method_array_t methods; // 二维数组:多个method_list_t(原生+所有分类)
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
};
class_ro_t->baseMethodList:编译期原生方法列表(只读,基础方法)class_rw_t->methods:二维数组,所有方法列表合集,包含分类添加的方法,运行时可修改
方法查找的过程
scss
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
{
// 遍历class_rw_t里面全部method_list_t
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end; ++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
优先查找category的方法,再找类本身的方法。类本身的方法编译的时候做了排序,因此查找的时候可以采用二分查找。
常用method的方法
arduino
// 获取实例方法
Method class_getInstanceMethod(Class cls, SEL name);
// 获取类方法(本质去元类查找)
Method class_getClassMethod(Class cls, SEL name);
// 获取类全部方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount);
// 添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
// 交换IMP
void method_exchangeImplementations(Method m1, Method m2);
cache_t
如果每次调用的时候都去遍历方法列表,自己没有找到,又去找父类,会很慢。因此,使用过的方法进行缓存。
arduino
struct cache_t {
private:
// 8字节:低48位 buckets指针;高16位存mask(arm64真机)
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied; // 当前已缓存方法数量
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
};
bucket_t 哈希桶单元(cache 里面真正存方法)
arduino
struct bucket_t {
private:
cache_key_t _key; // SEL(经过简单hash处理)
IMP _imp; // 方法实现地址
};
_key:SEL,哈希查找的 key_imp:函数入口地址
字段详解(面试重点)
-
_bucketsAndMaybeMask
-
arm64 真机:64bit 一字两用
- 低 48bit:
bucket_t *,哈希数组首地址 buckets - 高 16bit:mask 掩码
- 低 48bit:
-
x86 模拟器是分开存储(OUTLINED 模式):buckets 指针 + 单独_maybeMask
-
-
mask :
mask = capacity -1;capacity 一定是2 的 n 次方 。哈希算法:index = SEL & mask,快速取模 -
_occupied :当前哈希表里已经存入的方法个数
-
flags:标记,预优化缓存、锁相关标记
查找的过程
index = key & mask 根据当前cache_t的索引查到缓存的这行数据,然后对比key。如果key一样就返回,key不对,就从数组的索引的前一个元素接着查找(如果当前索引是0,则索引改成mask,就数组的最后一个元素),一直查找到这行数据为空,则说明没有缓存命中。
arduino
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
插入过程
第一次插入的时候,初始化长度为4的数组。
扩容行为:容量 ×2;新建 buckets 数组,旧 buckets 直接丢弃,occupied 置 0,所有缓存全部清空
arm64 真机:capacity>8 时,7/8才扩容;capacity ≤8(4、8)可以 100% 填满。
MAX_CACHE_SIZE = 1 << 16 = 65536,最多 65536 个 bucket,不会无限扩容。
objc_msgSend
三大阶段:1. 方法查找(快速 cache 查找 + 慢速 methodList 查找) → 2. 动态方法决议 → 3. 消息转发
阶段一 方法查找,先查找缓存,接着慢速查找
阶段二 动态方法决议 Dynamic Method Resolution
lookUpImpOrForward 发现找不到方法,会调用动态决议 ,给开发者运行时添加方法实现的机会:
- 实例方法:
+resolveInstanceMethod:(SEL)sel - 类方法:
+resolveClassMethod:(SEL)sel
scss
测试代码
Person *p = [[Person alloc] init];
[p performSelector:@selector(a_hello)];
Person.m
- (void)defaultMethod{
NSLog(@"method not found");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// 1. 将 SEL 转换为字符串,方便判断前缀
NSString *methodName = NSStringFromSelector(sel);
// 2. 判断方法名是否以 "a" 开头(不区分大小写)
if ([methodName hasPrefix:@"a"]) {
// 3. 动态添加方法
// 参数1: 类对象
// 参数2: 方法选择器
// 参数3: 要绑定的 C 语言函数(这里直接复用 defaultMethod 的实现)
// 参数4: 方法的类型编码。"v@:" 表示: v(void返回值), @(id类型的self), :(SEL类型的_cmd)
SEL defaultSEL = @selector(defaultMethod);
Method method = class_getInstanceMethod(self, defaultSEL);
IMP defaultIMP = class_getMethodImplementation(self, defaultSEL);
IMP defaultIMP1 = method_getImplementation(method);
const char *type = method_getTypeEncoding(method);//"v@:"
class_addMethod(self, sel, defaultIMP, type);
// 4. 成功添加后,必须返回 YES,告诉 Runtime 我们已经处理了这个消息
return YES;
}
return [super resolveInstanceMethod: sel];
}
阶段三:消息转发 Message Forwarding
- 第一步,快速转发:
-forwardingTargetForSelector:
erlang
- (id)forwardingTargetForSelector:(SEL)aSelector;
或者
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 返回一个其它的对象去处理
- (id)forwardingTargetForSelector:(SEL)aSelector{
return [[NSString alloc] init];
}
- 第二步,完整转发:
methodSignatureForSelector:+forwardInvocation:
如果第一步没有返回可以接受的对象,才会进入这个流程。
-
methodSignatureForSelector:返回方法签名NSMethodSignature;返回 nil 直接崩溃 -
forwardInvocation:(NSInvocation *)anInvocation- anInvocation 包装了本次消息全部信息:SEL、target、参数
- 你可以修改 target、修改参数、或者直接吞掉消息,避免崩溃
less
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(hello_world)){
// v表示void ,如果返回值是int,则是i16@0:8
// 第一个字符和@中间的内容表示参数的长度。
// @表示方法,用来区分第一个第一个参数和总长度
// 0表示第一个参数从0开始读
// : 表示开始下一个参数
// 8 表示第二个参数是从第8个字节接收数据
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 1. 从信封中取出方法名(SEL)
SEL sel = [anInvocation selector];
// 2. 判断我们要转发的目标对象是否能处理这个方法
// (这里假设你有一个真正能处理该方法的备用对象 backupObject)
if ([self.backupObject respondsToSelector:sel]) {
// 3. 目标对象能处理,安全转发!
[anInvocation invokeWithTarget:self.backupObject];
} else {
// 4. 目标对象也不能处理,必须交给父类!
// 父类最终会调用 doesNotRecognizeSelector: 抛出异常,优雅地结束流程。
[super forwardInvocation:anInvocation];
}
}
三步都过了,无力回天。
erlang
- (void)doesNotRecognizeSelector:(SEL)aSelector;
super
objectivec
@interface Person : NSObject
@end
@implementation Person
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"Perosn [self class] %@",[self class]);
NSLog(@"Perosn [super class] %@",[super class]);
}
return self;
}
@end
@interface Student : Person
@end
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"Student [self class] %@",[self class]);
NSLog(@"Student [super class] %@",[super class]);
NSLog(@"Student [self superclass] %@",[self superclass]);
NSLog(@"Student [super superclass] %@",[super superclass]);
}
return self;
}
@end
初始化Student *s = [[Student alloc] init];
Perosn [self class] Student
Perosn [super class] Student
Student [self class] Student
Student [super class] Student
Student [self superclass] Person
Student [super superclass] Person。这里等价于[self superclass]。以为不论怎么写,都是从NSObject的superclass的实现。
[super class]表示从父类查找,其实所有的class都是NSObject方法的实现。所以这些class方法都是一样的。方法的objc_msgSend的接受者都是当前对象。
isKindOfClass isMemberOfClass
两个都是 NSObject 的方法,一个是实例方法,一个是类方法,很容易踩坑
erlang
// 实例方法
- (BOOL)isKindOfClass:(Class)cls;
- (BOOL)isMemberOfClass:(Class)cls;
// 类方法
+ (BOOL)isKindOfClass:(Class)cls;
+ (BOOL)isMemberOfClass:(Class)cls;
isMemberOfClass
判断当前对象的类 严格等于 参数 cls ,不包含父类
-isMemberOfClass:实例的类 == cls+isMemberOfClass:元类 == cls(类对象本身存在元类里)
isKindOfClass
判断当前对象的类,是 cls 或者 cls 的子类(沿着继承链)
-isKindOfClass:实例所属类 是 cls /cls 子类+isKindOfClass:元类的继承链 向上比对
一句话记忆: ✅
isMemberOfClass:必须一模一样,父类不算 ✅isKindOfClass:自己或者它的子类都算
底层源码(objc4)
实例方法版本
ini
- (BOOL)isMemberOfClass:(Class)cls {
return object_getClass(self) == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class t = object_getClass(self); t; t = t->superclass) {
if (t == cls) return YES;
}
return NO;
}
object_getClass(self):拿 isa 指向的类。实例对象 isa → 类 Class
类方法版本(重点坑!很多人翻车在这里)
ini
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass(self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class t = object_getClass(self); t; t = t->superclass) {
if (t == cls) return YES;
}
return NO;
}
类方法调用时,self 是类对象 ;object_getClass(self)拿到的是元类 (meta-class)
⚠️ 类方法的isKindOfClass / isMemberOfClass,比较的是元类继承链!不是类的继承链!
面试题
objectivec
BOOL res1 = [[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject alloc] isMemberOfClass:[NSObject class]];
// 特别注意,这里的res其实主要利用的是 NSObject 的元类的 super 指向 NSObject 类对象
BOOL res3 = [NSObject isKindOfClass:[NSObject class]];
BOOL res4 = [NSObject isMemberOfClass:[NSObject class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
isKindOfClass /isMemberOfClass,面试真题)
前提:
Person : NSObject,Student : Person
less
@interface Person : NSObject
@end
@interface Student : Person
@end
请写出下面每一行结果(YES / NO)
lua
// 1.实例对象
BOOL b1 = [[Student alloc] isKindOfClass:[Person class]];
BOOL b2 = [[Student alloc] isMemberOfClass:[Person class]];
BOOL b3 = [[Student alloc] isKindOfClass:[Student class]];
BOOL b4 = [[Student alloc] isMemberOfClass:[Student class]];
// 2.类对象调用 +isKindOfClass / +isMemberOfClass(元类大坑)
BOOL b5 = [Student isKindOfClass:[Person class]];
BOOL b6 = [Student isMemberOfClass:[Person class]];
BOOL b7 = [Student isKindOfClass:[Student class]];
BOOL b8 = [Student isMemberOfClass:[Student class]];
b1 true
b2 false
b3 true
b4 true
b5 flase
b6 false
b7 false
b8 false
objectivec
// 第1组
BOOL r1 = [[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL r2 = [[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL r3 = [NSObject isKindOfClass:[NSObject class]];
BOOL r4 = [NSObject isMemberOfClass:[NSObject class]];
// 第2组 Person : NSObject
BOOL r5 = [[Person alloc] isKindOfClass:[NSObject class]];
BOOL r6 = [[Person alloc] isMemberOfClass:[NSObject class]];
BOOL r7 = [Person isKindOfClass:[NSObject class]];
BOOL r8 = [Person isMemberOfClass:[NSObject class]];
// 第3组 Student : Person : NSObject
BOOL r9 = [Student isKindOfClass:[NSObject class]];
BOOL r10 = [Student isMemberOfClass:[NSObject class]];
// 第4组 极易踩坑变种
BOOL r11 = [[NSObject alloc] isKindOfClass:object_getClass([NSObject class])];
BOOL r12 = [NSObject isKindOfClass:object_getClass([NSObject class])];
r1 true
r2 true
r3 true
r4 false
r5 true
r6 false
r7 true
r8 false
r9 true
r10 false
r11 false
r12 true
ini
Person *p = [[Person alloc] init];
object_setClass(p, [NSObject class]); // 修改实例的isa,把p的类改成NSObject
BOOL s1 = [p isKindOfClass:[Person class]];
BOOL s2 = [p isMemberOfClass:[NSObject class]];
BOOL s3 = [p isMemberOfClass:[Person class]];
s1 false
s2 true
s3 fasle
地狱级难度面试题
objectivec
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
@implementation Person
- (void)print{
NSLog(@"Person print %@ %@ self.name %@",self, NSStringFromSelector(_cmd),self.name);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// NSString *tempName = @"123";
id cls = [Person class];
void *p = &cls;
[(__bridge id)p print];
// 输入结果是Person print <Person: 0x16b033318> print self.name <ViewController: 0x106ae8000>
// 如果取消掉123的注释,那么就是Person print <Person: 0x16ba93310> print self.name 123
解释:
- 程序的内存分栈空间,栈空间的内存是由高到低。
- p本身是一个栈内存,执行p print的时候,就是找到p的isa,刚好p的地址保存了Person的类对象,可以调用Perons的print方法。
- 找name的时候,刚好p的地址加8个字节。 当
self = (__bridge id)p,访问self.name等价于:*( (void**)p + 1 ),也就是p地址 + 8字节的内存。 - 执行viewdidload的时候,执行了super方法。super方法的本质是有一个结构体,第一个参数是self,第一个对象是superclass。刚好这个有16字节的内存,其中低8字节保存了self对象就是ViewController本身。
- 如果定义了其它变量,比如上面的tempName。就是在p地址加8个字节就变了新的tempName。