1. 程式人生 > IOS開發 >instance/class/metaclass類裡存了什麼?

instance/class/metaclass類裡存了什麼?

前言

本文已加入專輯 徹底弄懂OC 。

概述

上一篇文章我們學習過物件本質之後,我們知道物件內部有一個isa指標,本文將描述物件的分類,學習完之後,需要我們回答以下幾個問題:

  1. OC中有幾種物件,它們分別是什麼?
  2. 物件的isa指標指向哪裡?
  3. OC的類資訊存放在哪裡?
  4. 物件方法的呼叫流程。

OC物件的分類

OC中有三種物件,我們將依次討論,它們分別是:

  • instance 例項物件
  • class 類物件
  • metaclass 元類物件

instance

instance 是例項物件,通過alloc 出來的物件,每次呼叫alloc都會產生新的instance物件。

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
// obj1、obj2都是instance物件
複製程式碼

obj1、obj2是NSObject的instance物件,它們是不同的兩個物件,分別佔據著兩塊不同的記憶體空間。

instance 物件在記憶體中儲存的資訊有:

  • isa指標,isa繼承自NSObject,isa始終會排在最前面。
  • 其他成員變數

class

class是類物件,在內部中儲存的資訊包括:

  • isa指標
  • superClass指標
  • 類的屬性資訊(@property)
  • 類的物件方法資訊(instance method)
  • 類的協議資訊(protocol)
  • 類的成員變數資訊(ivar, 描述資訊)
  • ...
NSObject *object1 = [[NSObject alloc] init];       
NSObject *object2 = [[NSObject alloc] init];

Class objectClass1 = [object1 class];
Class objectClass2 = object_getClass(object1);
Class objectClass3 = [NSObject class];
Class objectClass4 = objc_getClass("NSObject"
); Class objectClass5 = [object2 class]; Class objectClass6 = object_getClass(object2); NSLog(@"%p-%p-%p-%p-%p-%p",objectClass1,objectClass2,objectClass3,objectClass4,objectClass5,objectClass6); //0x7fff9da14118-0x7fff9da14118-0x7fff9da14118-0x7fff9da14118-0x7fff9da14118-0x7fff9da14118 複製程式碼

通過 [object1 class]

或者 NSObject class] 或者 object_getClass(object1) 三個方法獲取class物件。

meta-class

//獲取元類
Class objectMetaClass = object_getClass([NSObject class]);
複製程式碼

meta-class物件內部中儲存的資訊包括:

  • isa
  • superclass
  • 類方法資訊(class method)
  • ...

classmeta-class的記憶體結構是一樣的,都是Class(struct objc_class結構體)

instance、class、meta-class結構

例項與類、元類對比

注意:

Class objectClass = [[NSObject class] class]; //返回的class物件,無論呼叫多少次class方法,返回的都是class物件。

object_getClass(obj1); //如果引數是instance物件,則返回class物件
//如果引數是class物件,則返回meta-class物件
//如果是meta-class物件,返回NSObject(基類)的meta-class物件
object_getClass([NSObject class]); 

//檢視Class是否是meta-class
BOOL isMetaClass = class_isMetaClass([NSObject class]);

//返回引數對應的class物件。
Class class = objc_getClass("NSObject");
複製程式碼

isa指標和superclass指標

瞭解了instance、class、metaclass的結構之後,我們看一下,isa指標和superclass指標。instance、class、metaclass內部都含有isa指標,而只有class、metaclass內部含有suerclass指標。

isa指標

  • instance的isa指向class。
  • class的isa指向metaclass。
  • metaclass的isa指向基類的metaclass。

super指標

  • class的superclass指向父類的class。
  • 基類的superclass指向nil。
  • metaclass的superclass指向父類的metaclass。
  • 基類的metaclass的superclass指向基類的class。

下面我們建立兩個類PersonStudent 其中,Student繼承自Person

Person類

@interface Person : NSObject<NSCopying> {
    @public
    int _age;
}
@end
@implementation Person
- (void)personInstanceMethod {
}
+ (void)personClassMethod {
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    return nil;
}
@end
複製程式碼

Student類

@interface Student : Person<NSCoding>
@end

@implementation Student
- (void)studentInstanceMethod {    
}
+ (void)studentClassMethod {    
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
    return nil;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    return nil;
}
@end
複製程式碼

然後我們分析如下呼叫:

Student *student = [[Student alloc] init];
// 例項(instance)物件,呼叫類(class)方法。

// 先通過例項物件student的isa指標找到Student類物件
// 再在Student類物件內部找到studentInstanceMethod方法,並觸發呼叫
[student studentInstanceMethod];// -> objc_msgSend(student,studentClassMethod);

// 先通過例項物件student的isa指標找到Student類物件
// 再在Student類物件內部查詢personInstanceMethod,沒有找到。
// 通過Student類物件的superclass指標查詢到Person類物件,在其內部找到personInstanceMethod方法,並觸發呼叫
[student personInstanceMethod];// -> objc_msgSend(student,personInstanceMethod);

// 類(class)物件呼叫類方法。

//在Student類物件內部查詢studentClassMethod方法,找到並觸發呼叫
[Student studentClassMethod]; // -> objc_msgSend([Student class],studentClassMethod);

//在Student類物件內部查詢personClassMethod方法,沒有找到
//通過Student類物件的superclass指標,找到Person類物件,
//在其內部找到personClassMethod,並觸發呼叫。
[Student personClassMethod]; // -> objc_msgSend([Student class],personClassMethod);
複製程式碼

通過分析,我們得出如下結論:

  • instance呼叫物件方法的軌跡,通過instance的isa找到class物件,在class物件內部查詢方法,找不到就通過superclass指標找到其父類,然後繼續查詢方法。
  • class呼叫類方法的軌跡,通過class的isa找到metaclass,在metaclass內部查詢方法,找不到就通過superclass指標到到其父類,然後繼續查詢方法。

通過上面的分析,我們畫出如下isa與superclass的指向圖,如下:

方法呼叫查詢過程

在上圖中,有一條特殊的線,基類的metaclass的superclass指標指向的不是nil,而是基類的class物件。我們證實一下。

我們給NSObject物件增加一個分類,在分類中增加一個例項方法,如下:

@interface NSObject (test)
- (void)test;
@end

@implementation NSObject (test)
- (void)test{
    NSLog(@"-NSObject (test)");
}
@end
複製程式碼

通過上面的學習,我們知道-test 是在NSObject的class物件內部實現的。

然後我們實現Person類,Person類只是基礎NSObject。

//
//   main.m
//   Class物件
//
//   Created  by MrLi on 2020/5/16
//   Modified by MrLi
//   Copyright © 2020 MrLi. All rights reserved.
//
   

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "NSObject+test.h"

@interface Person : NSObject<NSCopying>
@end

@implementation Person
@end

int main(int argc,const char * argv[]) {
    @autoreleasepool {
        [Person test];
    }
    return 0;
}
複製程式碼

我們在main方法直接呼叫[Person test]。它的呼叫軌跡是:

  1. 通過Person類物件的isa找到Person的metaclass物件,在其內部查詢test方法,沒找到。
  2. 然後通過Person metaclass的superclass指標找到其父類NSObject metaclass,在其內部找到test方法,依然找不到。
  3. 這是如果NSObject的metaclass的superclass指向為nil,test的呼叫應該報錯。但卻呼叫到了NSObject的class物件內部的-test方法。可見基類的metaclass的superclass指向基礎的class物件。

如何通過isa找到class或者metaclass

理論上,instance的isa指標指向的內容應該是class的地址,class的isa指標指向的內容應該是metaclass的地址。但其實它是通過& ISA_MASK 計算出來的。

在不同的架構下 ISA_MASK 取值不同。如下

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
複製程式碼

我們看如下程式碼:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface Student : Person<NSCoding>
@end
@implementation Student
@end

struct myclass {
    Class isa;
    Class superclass;
};

int main(int argc,const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
       struct myclass *stuclass = (__bridge struct myclass*)([Student class]);
        struct myclass *stumetaClass = (__bridge struct myclass*)object_getClass([Student class]);
        NSLog(@"%p-%p-%p",stu,stuclass,stumetaClass);
        NSLog(@"檢視記憶體"); //斷點
    }
    return 0;
}
複製程式碼

2020-05-17 17:02:06.481250+0800 Class物件[2250:1165673] 0x1031ab810-0x1000023e8-0x1000023c0

(lldb) p/x stu->isa

(Class) $0 = 0x001d8001000023e9 Student

(lldb) p/x 0x001d8001000023e9 & 0x00007ffffffffff8

(long) $1 = 0x00000001000023e8

(lldb) p/x stuclass->isa

(Class) $2 = 0x00000001000023c0

(lldb) p/x 0x00000001000023c0 & 0x00007ffffffffff8

(long) $3 = 0x00000001000023c0

(lldb) p/x stuclass->superclass

(Class) $4 = 0x0000000100002398 Person

(lldb) p/x [Person class]

(Class) $6 = 0x0000000100002398 Person

通過分析日誌,我們得出結論:

  • instance的isa & ISA_MASK 獲取到對應的class地址。
  • class的isa & ISA_MASK 獲取到對應的metaclass的地址。
  • class、metaclass的superclass地址就是其父類地址。
image-20200517171032294

class、metaclass內部結構

// objc4-781 
struct objc_object {
    Class _Nonnull isa;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;    // 使用者獲取類的具體資訊
}

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    Class firstSubclass;
    Class nextSiblingClass;

public:  
  // 獲取ro
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>()->ro;
        }
        return v.get<const class_ro_t *>();
    }

    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>()->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }
  // 獲取methods
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }
	//獲取 properties
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }
	//獲取 protocols
    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
};

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;
};
複製程式碼

總結

至此,本文到此結束,還記得開篇的4個問題嗎?想必你已經有了答案!

技術交流

技術交流