iOS 底层原理 runtime

知识点

  • 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;// 对象内部存储的引用计数

关键字段说明

  1. nonpointer(第 0 位)
    • 0:isa 直接就是 Class 指针,没有额外信息;老版本 / 某些环境
    • 1:开启 isa 优化,isa 不再单纯是指针,把引用计数、weak 标记等塞到位域里
  2. shiftcls:真正存储 Class 地址。arm64 内存地址对齐,低 3 位永远 0,所以可以把地址右移 3 位存到 shiftcls。

获取真实类地址:shiftcls << 3

  1. extra_rc对象内部引用计数 。当引用计数比较小时,直接存在 isa 的 extra_rc;计数太大放不下时,启用 sidetable 存 rc。
    • extra_rc 存的是:引用计数 - 1
    • has_sidetable_rc = 1:引用计数存在全局 SideTable 哈希表。
  2. has_assoc :是否设置过关联对象(objc_setAssociatedObject);销毁时判断是否清理关联对象。
  3. has_cxx_dtor:是否有自定义 C++ 析构 /dealloc,标记是否需要走完整析构逻辑。
  4. weakly_referenced:是否有 weak 指针指向该对象;对象销毁时需要清空 weak 表。
  5. 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:函数入口地址

字段详解(面试重点)

  1. _bucketsAndMaybeMask

    • arm64 真机:64bit 一字两用

      • 低 48bit:bucket_t *,哈希数组首地址 buckets
      • 高 16bit:mask 掩码
    • x86 模拟器是分开存储(OUTLINED 模式):buckets 指针 + 单独_maybeMask

  2. maskmask = capacity -1;capacity 一定是2 的 n 次方 。哈希算法:index = SEL & mask,快速取模

  3. _occupied :当前哈希表里已经存入的方法个数

  4. 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:

如果第一步没有返回可以接受的对象,才会进入这个流程。

  1. methodSignatureForSelector: 返回方法签名NSMethodSignature;返回 nil 直接崩溃

  2. 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;
  1. isMemberOfClass

判断当前对象的类 严格等于 参数 cls不包含父类

  • -isMemberOfClass:实例的 == cls
  • +isMemberOfClass元类 == cls(类对象本身存在元类里)
  1. 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 : NSObjectStudent : 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

解释:

  1. 程序的内存分栈空间,栈空间的内存是由高到低。
  2. p本身是一个栈内存,执行p print的时候,就是找到p的isa,刚好p的地址保存了Person的类对象,可以调用Perons的print方法。
  3. 找name的时候,刚好p的地址加8个字节。 当self = (__bridge id)p,访问self.name等价于:*( (void**)p + 1 ),也就是 p地址 + 8字节 的内存。
  4. 执行viewdidload的时候,执行了super方法。super方法的本质是有一个结构体,第一个参数是self,第一个对象是superclass。刚好这个有16字节的内存,其中低8字节保存了self对象就是ViewController本身。
  5. 如果定义了其它变量,比如上面的tempName。就是在p地址加8个字节就变了新的tempName。
相关推荐
for_ever_love__1 天前
iOS: GCD进阶,全局队列与自定义队列
macos·ios·objective-c·cocoa·多线程·gcd
用户2181697049302 天前
iOS 底层原理 block
objective-c
2501_915106322 天前
Swift 4开发环境设置教程:xCode安装与Playground入门
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
9765033352 天前
iOS 上架/审核 4.3a全面解读-最新
flutter·ios·objective-c·uniapp
用户2181697049303 天前
iOS 底层原理 Category load initialize 关联对象
objective-c
秋雨梧桐叶落莳6 天前
【iOS】从源码深入理解RunLoop机制
macos·ios·objective-c·cocoa·cocoapods·uikit
2501_915106326 天前
第一次开发 iPhone App,可能碰到的问题,解决办法
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
sakiko_9 天前
OC基础语法(与Swift对比)-1
开发语言·ios·objective-c·swift