iOS底层探索五(isa与类的关系)

前言   

   
    相关文章:   

iOS底层探索一(底层探索方法)       

iOS底层探索二(OC 中 alloc 方法 初探)

iOS底层探索三(内存对齐与calloc分析)      

iOS底层探索四(isa初探-联合体,位域,内存优化)   

iOS底层探索六(类的分析上)

iOS底层探索七(类的分析下)

iOS底层探索八(方法本质上)

iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

iOS底层探索十(方法的本质下-消息转发流程)

相关代码:

      objc4_752源码     isa_object      对象本质分析   

     前几篇文章对alloc方法进行了探究,有兴趣的小伙伴可以点击进入进行查看,这篇文章我们主要对isa进行分析:首相先上一张官方isa的走位图,在文章中会分析到如何实现的

要对isa进行分析,我们首先先看先isa的初始化代码

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{
    //isTaggedPointer这个涉及内存管理,后续在进行详细解释
    assert(!isTaggedPointer());
    if (!nonpointer) {
//        isa对类的绑定
//       使用位域属性进行赋值
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
//        #   define ISA_INDEX_MAGIC_VALUE 0x001C0001 直接使用bit进行赋值
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
//        #   define ISA_MAGIC_VALUE 0x000001a000000001ULL  直接使用bit进行赋值
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
//       使用位域进行赋值
        newisa.has_cxx_dtor = hasCxxDtor;
//      这里就就标明右移三为就是类的结构
        newisa.shiftcls = (uintptr_t)cls >> 3; //这个就是对类的绑定
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

根据newisa.shiftcls = (uintptr_t)cls >> 3; 我们可以看到isa右移3位就可以得到类的结构;

这里我们可以先看一个骚操作,怎么通过一个对象找到类的

我们可以通过isa指针的偏移可以得到类的地址这里我们先进行右移3位,左移3位(这两步操作主要是去除前三位影响数据),同样的先左移17位,再右移17位,是为了去除后17位操作我们可以得到两个地址:

0b0000000000000000000000000000000100000000000000000010011001011000   person地址

0b0000000000000000000000000000000100000000000000000010011001011000   isa偏移后地址

可以看出isa的偏移后地址和Person地址是一样的,

这里解释下为什么要进行偏移操作,我们来看下ISA的结构

# if __arm64__       //真机
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
//这里的总和为1+1+1+33+6+1+1+1+19=64位,即占用了8个字节
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__    //模拟器
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

因为我们使用的是模拟器,要取出shiftcls的值我们可以看出他的值为(3,44),从第三位开始往后取44位,所以我们需要偏移

如果要使用真机进行操作,取出shiftcls值(3,33),就需要右移3位,左移三位,左移28位,右移28位,才可以取出。当然这种取法主要是为了xx;

下面描述下另一种取法,通过对象找到类,我们OC中有方法object_getClass(),传入对象就可以直接获取类,下面看下源码

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
inline Class 
objc_object::getIsa() 
{
 //一般情况都会进入
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}


模拟器: #   define ISA_MASK        0x00007ffffffffff8ULL
真机:   #   define ISA_MASK        0x0000000ffffffff8ULL
inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

这里实际操作就是,拿出isa.bits 与上ISA_MASK这个值然后就能直接拿到类信息,这个就是传说中的面具取值发,实际上其实很简单就是进行了个与操作

例如我们要取出2-4位置所表示的值且不受其他位置影响,我们只需要找到3-4位都是1的值就行,及1100及12

例如给出值为: 110111我们要取出3-4位值

110111 & 1100得出值为0100(切记不是01,前面的0可以不要,后面的是需要补齐的),就可以拿出3-4位的值表示的值,前面补0

我们可以看到模拟器中0x00007ffffffffff8ULL这里的ULL其实就是unsigned long long ,所以我们取0x00007ffffffffff8,这个值其实就是从第3位开始到第44位都是1的值,与上这个值就可以得到3-44位所表示的值,

我们可以看出,使用这2种方式,都可以获取到isa中类的地址;

下面我们来看一个面试题,在一个Object-C工程中对象可以创建多个,类可以创建多个吗?

我们来用工程验证一下

//MARK: - 分析类对象内存存在个数
void xzTestClassNum(){
    Class class1 = [XZPerson class];
    Class class2 = [XZPerson alloc].class;
    Class class3 = object_getClass([XZPerson alloc]);
    Class class4 = [XZPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

根据输出结果,我们可以看到类在内存中只创建一份,对象当然我们都知道可以创建多个,只是用不同对象中的isa指向同一个类,当然类中不仅仅只有isa,还有其他的,后面文章进行详细描述;

我们使用对象查看一下isa的走向:

这里我们就需要描述一下:类的isa指向的这个XZPerson 叫做元类:

我对这几个概念是这么理解的:

对象:根据类实例化出来                              --->开发人员创建  --->运行时创建

类   :内存中只有一份,系统创建出来         --->编译器创建      --->编译时创建

元类:系统编译时发现有类,系统创建出来    --->编译器创建      --->编译时创建

这里插入一下,对象是运行由开发人员创建,没有问题,类和元类是有编译器在编译时进行创建,怎么来证明呢(可以在load,cxx,main函数打断点),为了方便在main函数打断点,在LLDB 中输出类,元类进行验证一下:

由此我们可以得出,类和元类是由编译器在编译时进行创建的!更进一步判断我们可以值进行编译使用编译出的macho文件用MachOView(百度云盘可下载链接:https://pan.baidu.com/s/1VwybGf87Fi1hQveLTPcExA  密码:h4kl)对二进制文件进行分析查看Section(objc_data)段可以直接看到生成的

我们继续对上面的进行继续分析:

我们也可以使用代码进行佐证:

void xzTestNSObject(){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
}

输出打印结果:

我们可以看到根源类和根根元类地址是相同的,由此佐证苹果官方isa走向图。

这里将苹果官方图解释一下:这个图主要描述的是2个概念:

一个就是isa走向图是虚线:

这里就描述下坑点:XZTearch元类isa,是直接指向NSObject元类的,而不是XZPerson元类

一个是继承链为实线:

这里我们需要注意的是NSObject元类是继承于NSObject的,这里需要特别注意(在消息转发中这里会有坑点)。

这里我们来进行验证一下继承链:要验证这个,我们需要小大概了解一下类的结构体

struct objc_class : objc_object {
//    Class ISA;  //第一个指针为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_rw_t *data() { 
        return bits.data();
    }
 ...
}

对象的继承大家应该都没有什么问题,我们主要看一下元类的继承

使用这种方法,也可以看类继承,看完后发现,和官方图是一样的。

这里我们就对isa和类之间的关系进行解释了,顺便来看一下对象底层是怎么编译的,这里顺便介绍一下将.m文件编译成底层C++语言方式:clang 

主要语句: clang -rewrite-objc main.m -o main.cpp 这种方式直接进行编译文件将main.m 输出为 main.cpp文件

clang -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m -o main.cpp  这种方式,一般导致UIKit报错编译文件,需要引入模拟器SDK资源其中(iPhoneSimulator.platform为模拟器资源,iphones为真机资源,)

或者使用xcode中自带工具xrun:

xcrun -sdk iphonesimulator clang -rewrite-objc main.m -o main4.cpp   (iphonesimulator使用模拟器库资源)

xcrun -sdk iphones clang -rewrite-objc main.m -o main4.cpp   (iphones使用真机库资源)

使用clang可以将OC高级语言转换为C++语言,例如我们将如下代码进行clang

#import <Foundation/Foundation.h>
@interface XZPerson : NSObject{
@end

@implementation XZPerson


@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XZPerson *p = [XZPerson alloc];
        NSLog(@"hello world");
    }
    return 0;
}

编译完成:

我们可以看出,高级语言写了12行,编译为C++后111775行,太吓人了,不过我们不用把这些都看了,看其中部分代码,理解一下大概逻辑即可:

我们研究这个首先就是要找到我们知道的代码,必然是先找到main函数;

找到main函数后,我们刚好就看到了要研究的XZPerson实现确实为结构体,我们看一下struct NSObject_IMPL 结构

struct NSObject_IMPL 中只有一个从父类继承过来的isa;

我们给XZPerson添加一个属性进行查看一下:

#import <Foundation/Foundation.h>
//对象-->编译后-->结构体  父类会继承过来
@interface XZPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation XZPerson


@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XZPerson *p = [XZPerson alloc];
        NSLog(@"hello world");
    }
    return 0;
}

这次我们编译完成后直接struct XZPerson_IMPL这个结构体

我们可以看到自己的属性,直接就添加进去了,而且有添加set,和get方法会自动添加,

那我们给Person其中添加一个成员变量会出现什么情况呢,

#import <Foundation/Foundation.h>
//对象-->编译后-->结构体  父类会继承过来
@interface XZPerson : NSObject{
//   成员变量
    NSString *nickName;
}
@property (nonatomic, copy) NSString *name;// 属性的区别,底层会生成setgetter方法
@end

@implementation XZPerson


@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XZPerson *p = [XZPerson alloc];
        NSLog(@"hello world");
    }
    return 0;
}

这次我们编译完成后直接struct XZPerson_IMPL这个结构体

我们可以看到添加的属性,在属性中出现了nickName,但是并没有生成get,和set方法,

这样我们就对属性,成员变量有了更深刻的认知。

在编译后文件中我们也会看到一些_method_list_t_ivar_list_t,_prop_list_t等等一系列东西,这个分析类的时候在进行详细分析。

总结:

这篇文章主要分析了isa和类的关系,其中包括,isa的走位分析,和类的继承关系,顺便说了下对象的底层编译;如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!

写给自己:

生活总有那么多的不如意,可还是要以乐观的心态去坚持,不要总是诉说着自己的悲哀,别人更多的只会当成笑话来听,要学会自己承受,坚持,未完待续。。。

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/104746630