isa分析之类的探究(上)

469 阅读8分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

上篇文章,我们讲到了,通过isa+掩码就可以访问到类的信息。直接上图:

isa和掩码得到类信息的地址.png

那么类是否也像对象一样,有isa、有它的上一层结构呢?继续操作:

02.png

看到这里,我们发现虽然打印的两个都叫Direction,但是地址不一样,那么下面那个3d810地址是什么东西呢?我们打开Xcode工程(isa分析)-》Products-》isa分析.app-》show in finder -》右键显示包内容。把isa分析可执行文件拖到MachOView工具里展开,找到Symbol Table ->*Symbols,右边搜索我们的类名“Direction”,如图:

元类.png

我们会发现在符号表里,多了一个叫METACLASS,翻译过来就是“元类”的意思。这个东西是编译的时候,系统帮我们生成的。所以我们上面打印的3d810就是Direction的元类。

在元类的基础上,继续重复上面的操作,我们会得到一个根元类地址,而且发现元类的isa指向的就是根元类;顺便打印了一下根元类地址,发现打印出来是NSObject,我们针对NSObject类重复上面操作,发现NSObject类的isa指向的就是根元类地址:

04.png

由此我们可以得到这样一副关系:

1、对象的isa -》类,类的isa -》元类,元类的isa  -》根元类
2、根类(NSObject类)的isa -》根元类

为了验证以上的猜测,我们写了一些代码:

  • 定义了两个类Direction和定义DirectionChild
//定义Direction,继承自 - NSObject
    @interface Direction:NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,assign) NSInteger indexValue;
@end
@implementation Direction
@end

//定义DirectionChild,继承自 - Direction
@interface DirectionChild:Direction
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
@end
@implementation DirectionChild
@end
  • 在main函数里实现:
int main(int argc, char * argv[]) {
    //    Direction *st = [Direction alloc];
    //    NSLog(@"%@",st);
    //NSObject对象
    NSObject *obj01  = [NSObject new];
    //NSObject类
    Class obj02 = NSObject.class;
    //NSObject元类
    Class obj03 = object_getClass(obj02);
    //根元类
    Class obj04 = object_getClass(obj03);
    NSLog(@"\n NSObject对象-%p\n NSObject类-%p\n NSObject元类-%p\n NSObject根元类-%p\n\n ",obj01,obj02,obj03,obj04);
    
    //Direction的元类
    Class dMetaClass = object_getClass(Direction.class);
    //元类的父类
    Class dSuperClass = class_getSuperclass(dMetaClass);
    NSLog(@"\n Direction情况打印 --> \n 元类-%p\n 元类的父类-%p %@\n \n ",dMetaClass,dSuperClass,dSuperClass);
    
    //DirectionChild的元类
    Class dChildMetaClass = object_getClass(DirectionChild.class);
    //根元类
    Class dChildSuperClass = class_getSuperclass(dChildMetaClass);
    NSLog(@"\n DirectionChild情况打印 --> \n 元类-%p\n 元类的父类-%p %@\n ",dChildMetaClass,dChildSuperClass,dChildSuperClass);
    
    //打印NSObject类的父类
    Class nSuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"打印NSObject类的父类-->%@ - %p",nSuperClass,nSuperClass);
    //打印根元类的父类
    Class objectSuperClass = class_getSuperclass(obj04);
    NSLog(@"打印根元类的父类-->%@ - %p",objectSuperClass,objectSuperClass);

    ...
    ...
}

输出结果如下:

06.png

由此可以得出结论:

  • 如果一个类继承自NSObject,这个类的元类指向的是根元类;
  • 如果一个类继承自NSObject的子类,这个类指向的是它父类的元类;
  • NSObject没有父类
  • 根元类的父类是NSObject类 由上可知,元类直接也存在着继承链关系。

下面我们来看一张关系图:

关系图.png 解释如下

  • 1 -》2 -》3 -》4 -》4 (isa走位图)
    实例对象isa -》类,isa -》元类,isa -》NSObject根元类,isa -》NSObject
  • 7 -》8 -》9 -》4 -》4 (isa走位图)
    父类实例对象isa -》父类,isa -》父类元类,isa -》NSObject根元类,isa -》NSObject
  • 6 -》5-》4 -》4 (isa走位图)
    NSObject实例对象isa -》NSObject类,isa -》NSObject根元类,isa -》NSObject
  • 2 -》8 -》 5(类的继承链)
    子类 -》父类 -》NSObject类 -》nil
  • 3 -》9 -》4 -》5 (元类的继承链)
     子类元类 -》NSObject根元类 -》NSObject类;
     父类元类 -》NSObject根元类 -》NSObject类;

文章iOS底层实现分析之对象的本质提到过,类对象包含一个isa,它的类型是Class,继而我们又知道Class在底层的实现是objc_class,那么objc_class的内部实现又是怎么样的呢?接下来我们就研究下:

typedef struct objc_class *Class;

打开objc4-818.2源码,搜索“struct objc_class” 可以看到这样一段代码:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    ...
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

#endif

if判断意思是,非2.0版本才会进入这个判断, 很明显我们当前用的都是最新的2.0版本,所以段代码不会走,我们不用管它,继续找下一个搜索结果:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    Class getSuperclass() const {
        ...
    }
    void setSuperclass(Class newSuperclass){
        ...
    }
    
    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...
};

我们可以看到,它有一个被注释了的CLASS ISA,搜索"objc_object":

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

我们发现objc_object有一个ISA指针,所以可以得出objc_class也有isa 所以objc_class包含的信息大概如下:

    1Class ISA;//
    2Class superclass;//父类
    3、cache_t cache;             // 方法缓存
    4、class_data_bits_t bits;    // 用于获取类的信息,通过isa+掩码得到类的地址

我们在struct objc_class : objc_object还看到这样一个方法:

    class_rw_t *data() const {
        return bits.data();
    }

点开data进去:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

大概意思是bits & FAST_DATA_MASK返回一个class_rw_t结构体。
command+单机class_rw_t点开看一下的结构:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
    ...
    const class_ro_t *ro() const {...}
    //方法列表
    const method_array_t methods() const {...}
    //属性列表
    const property_array_t properties() const {...}
    ///协议列表
    const protocol_array_t protocols() const {...}
    
    
  };
    解释下~
    rw:readWrite 可读可写;
    ro:readOnly  只读
    t:列表

我们可以看到,class_rw_t包含了一些属性、方法列表、属性列表、协议列表,还有class_ro_t,那么class_ro_t是什么呢?
点进去class_ro_t可以看到:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    ...
    method_list_t *baseMethods() const {...};
    ...
    
  }

class_ro_t里也包含了一些属性和方法,跟class_rw_t所不同的是,class_ro_t只读。

至此,我们已经对objc_class结构有了基本的认识,这会儿我有个疑问,系统是怎么通过class类找到属性值的呢?这里我们做一个测试,写一段代码:

    int a[5] = {11,22,33,44,55};
    /** 正常情况下,我们打印输出是这样写的:
        for (int i = 0; i<5; i++) {
            printf("\n%d", a[i]);
        }
     */
    ///接下来打印一下每个值及值对应的地址
    for (int i = 0; i<5; i++) {
        printf("\n打印第%d个 -->%d - %p",i,a[i],&a[i]);
    }
    printf("\na数组的地址 - %p",&a);

打印结果如下

07.png

我们发现,直接打印a的地址,结果和a数组第一个值的地址是一样的,我们还发现每两个值之间的地址都差了4个字节,这是因为64位下,int类型占用4个字节。 接下来,我们可以做一个猜想,把上面打印的a[i]换成*(a+i)

    ///这段代码追加到上面的代码后面
    printf("\n\n下面是验证猜想:");
    for (int i = 0; i<5; i++) {
        printf("\n打印第%d个 -->%d - %p",i,*(a+i),&a[i]);
    }

打印结果:

08.png

由此可以得出结论,数组指针是通过每个元素占用字节的偏移量来访问属性值,这也叫内存偏移。那么问题又来了,类是如何通过内存偏移找到属性值呢?
通过上面,我们知道了一个Class类,它的成员排序是ISA、superclass、cache、bits,我们通过计算前三个大小,就可以得到bits的地址。(属性列表就放在bits里)

  • ISA是结构体指针,64位下占用8个字节。
  • superclass类型是Class,它是一个结构体指针,也就是8字节大小。
  • cache,它是一个cache_t类型,我们点击进入:
struct cache_t {
private:
    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;
    };
    ...

    /**
    省略部分的代码就是一些static修饰、方法,
    前者存放在全局区、后者存放在方法区,都不占用结构体大小,
    所以可以忽略那部分,只需要计算上面的大小
    */
    
    /**
    分析一下上面:
    包含了_bucketsAndMaybeMask和一个联合体,
    uintptr_t无符号整型,即_bucketsAndMaybeMask占用8个字节;
    根据联合体互斥的特性,_originalPreoptCache是指针,可以得出联合体占用8个字节。
    
    所以cache_t共占用16个字节。
    */
    
    
  };

打开objc4-818.2源码工程,写入代码,ldb调试一下:

09.png

如图所示,一顿ldb命令下来,得到了class_rw_t的地址,有同学可能会对$2->data()有疑问,上面有说到过class_rw_t是通过bits.data()去获取的,而$2明显是一个指针,指针的访问用->,继续输入ldb命令:

10.png 最终,我们打印了DirectionChild的第一个属性hobby。
我们继续打印一下第二个属性:

11.png

ldb命令的执行过程中可能很容易出现,内存数据读取失败的提示

Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x40).
The process has been returned to the state before expression evaluation.

出现这样的提示不要慌,可以从前面几步重新开始执行一边,实在不行就重新build一下再走一遍流程。

接下来,我们按照读取属性的方式去读取一下class_rw_t里的方法,在那之前,我们先给DirectionChild类添加几个方法:

//定义DirectionChild,继承自 - Direction
@interface DirectionChild:Direction
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
///方法
-(void)runSpeedNonTime;
+(void)runSpeedWithTime:(NSString *)time;
-(void)eatSomethingNonTime;
+(void)eatSomethingWithTime:(NSString *)time;
@end

@implementation DirectionChild
-(void)runSpeedNonTime{}
+(void)runSpeedWithTime:(NSString *)time{}
-(void)eatSomethingNonTime{}
+(void)eatSomethingWithTime:(NSString *)time{}
@end

ldb命令调试:

12.png

13.png

到这里,似乎翻车了?打印的方法信息都为空,为什么这样的方式打印,属性信息会出来呢? 我们打印属性信息是通过打印property_t,打印方法是通过打印method_t,我们来看下这两位哥们儿的结构:

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
//看这里 <--
struct property_t {
    const char *name;
    const char *attributes;
};
class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};

//看这里 <--
struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    ...
};

我们发现method_t并不像property_t一样直接把属性暴露在外,而是用big结构体再包一层,那么既然如此,我们就再多访问一层big()

14.png

15.png

如图所示,方法列表里包含了以下6个方法:

  • runSpeedNonTime
  • eatSomethingNonTime
  • hobby
  • setHobby
  • runSpeed
  • setRunSpeed 但是没有下面的类方法
  • +(void)runSpeedWithTime:(NSString *)time;
  • +(void)eatSomethingWithTime:(NSString *)time;

1、那么类方法存放在哪里呢?
2、于此同时,我又给DirectionChild增加了一个成员变量bgColorValue,按照上面的操作,发现属性列表里也没有打印出bgColorValue,它又存放到哪里了呢?

@interface DirectionChild:Direction
{
    NSString *bgColorValue;
}
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
///方法
-(void)runSpeedNonTime;
+(void)runSpeedWithTime:(NSString *)time;
-(void)eatSomethingNonTime;
+(void)eatSomethingWithTime:(NSString *)time;
@end

相关代码已经上传百度网盘: