iOS底层学习——OC对象的本质与isa

简介: OC对象的本质与isa

我们已经学习了对象的初始化内存对齐等内容。这篇文章将深入学习探究对象的本质对isa进行分析

学习对象本质之前,先引入一个工具clang

一.clang

1.什么是clang

Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。
Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

lang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。

2013年4⽉,Clang已经全⾯⽀持C++11标准,并开始实现C++1y特性(也就是C++14,这是
C++的下⼀个⼩更新版本)。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是⼀个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/
Objective-C++编译器。它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过__attribute__((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC。

2.clang的作用

那么clang应该学习什么呢?做什么呢?

因为OC是C、C++的超集,通过clang可以将m文件编译成cpp文件,这样我们可以了解更多的关于底层的实现原理。

3.clang的使用方式

  1. clang -rewrite-objc main.m -o main.cpp —— 把⽬标⽂件编译成c++⽂件
  2. UIKit报错问题

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

  1. xcrun命令

xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进⾏了⼀些封装,要更好⽤⼀些。

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (⼿机)

如果代码有使用runtime的内容,如weak,可以使用一下指令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

二.探索对象的本质

1.使用clang编译生成cpp文件

直接上代码!引入一个案例,在main.m文件中添加GFPerson类的声明和实现,如下图所示:

image.png

打开系统终端,进入main.m文件所在目录,运行命令
clang -rewrite-objc main.m -o main.cpp

image.png

执行指令后,即可编译生成一个main.cpp文件。

2.cpp文件解读

打开运行指令后编译生成的cpp文件,文件很长,从我们自定义的类GFPerson开始寻找!

1.GFPerson对象

全局搜索GFPerson,获得以下核心代码:

// GFPerson结构体声明
typedef struct objc_object GFPerson;
typedef struct {} _objc_exc_GFPerson;
#endif

// GFPerson_IMPL结构体实现
struct GFPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
        NSString *_name;
};

/* @end */
// @implementation GFPerson
// @end

解读代码:

  1. 定义了一个别名GFPerson,该别名指向struct objc_object类型
  2. 在结构体实现GFPerson_IMPL中,有一个成员变量NSObject_IVARS,来自所继承的结构体,也就是isa;另一个成员变量是_name,也就是GFPerson的属性,和OC层面定义是一致的。
2.NSObjec对象

根据NSObject_IMPL进行搜索,获取NSObject类的声明和实现等相关内容。

// NSObject结构体声明
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

// NSObject实现-对象
struct NSObject_IMPL {
    Class isa;
};

解读代码:

  1. 定义别名NSObject,同样指向struct objc_object类型
  2. NSObject结构体实现中,有一个Class类型的成员变量isa
3.底层结构关系

进一步搜索Class的定义和objc_object的定义,见下面代码:

// Class定义 - 指向objc_class的指针 
typedef struct objc_class *Class;

// objc_object定义 - 根类定义
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

// id的定义指向objc_object的指针
typedef struct objc_object *id;

// SEL 方法编号,方法选择器指针
typedef struct objc_selector *SEL;

解读代码:

  1. OC层面NSObject,在底层对应objc_object结构体
  2. 子类的isa均继承自NSObject,也就是来自objc_object结构体;
  3. Objective-CNSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类
  4. isa的类型为Class,被定义为指向objc_class的指针
  5. 在开发中可以用id来表示任意对象,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针
  6. SEL方法选择器指针,方法编号。

通过上面的分析,可以得出以下结构关系:

image.png

4.get\set方法

GFPerson类的属性,自动添加get\set方法。见下面代码:

static NSString * _I_GFPerson_name(GFPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GFPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_GFPerson_setName_(GFPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GFPerson, _name), (id)name, 0, 1); }

通过以上代码可以发现,无论是get方法还是set方法,都会有两个隐藏参数self_cmd,也就是方法接收者方法编号。在获取属性时,采用指针平移的方式,获取成员变量所在地址,转换后返回对应的数值。

  • objc_setProperty,在对实例变量进行设置时,会自动调用objc_setProperty方法。该方法可以理解为set方法的底层适配器,通过统一的封装,实现set方法的统一入口。

runtime源码中,搜索objc_setProperty,可以找到最终实现方法,见下段代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue); // retain新值
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    
    // 释放旧值
    objc_release(oldValue);
}

本质是通过指针平移找到成员变量位置,然后进行新值的retain,旧值的release。

5.cpp内容补充

除了我们最关心的对象的定义外,在cpp文件中,还可一看到rorw协议分类方法等内容的定义。

  • 分类的定义。包括分类名称关联的类实例方法列表类方法列表等信息。
struct _category_t {
    const char *name; // 名称
    struct _class_t *cls; // 关联的类
    const struct _method_list_t *instance_methods; // 实例方法
    const struct _method_list_t *class_methods; // 类方法
    const struct _protocol_list_t *protocols; // 协议
    const struct _prop_list_t *properties; // 属性
};
  • 方法或函数的定义。Method是一个objc_method结构体,包括方法编号selectortype encoding方法实现地址
struct _objc_method {
    struct objc_selector * _cmd; // 方法编号
    const char *method_type; // type encodings
    void  *_imp; // 方法实现地址
};

其中_cmd_imp比较熟悉,方法编号和方法实现,那么method_type是什么呢?在苹果开发者官网,可以找到对应的type encodings对照表。

iShot2021-06-15 11.23.47.png

如本例中GFPerson的方法列表:

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[4];
} _OBJC_$_INSTANCE_METHODS_GFPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    4,
    {{(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_},
    {(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_}}
};

name属性get方法为例:@16@0:8

  • @:表示返回值,一个object对象,这里是NSString *类型;
  • 16:函数的传入的所有参数的字节数之和为16,因为有两个隐藏参数GFPerson * selfSEL _cmd,所以一共16个字节
  • @:第一个参数类型为id型GFPerson * self
  • 0:前面的参数起始的字节位置(从0开始)
  • ::第二个参数类型为selSEL _cmd
  • 8:前面的参数起始的字节位置(从8开始)

3.对象本质总结

通过工具clang,编译生成的cpp文件,我们可以发现,对象实质是一个结构体。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类NSObject仅有一个实例变量Class isaClass实质上是指向objc_class的指针objc_class的定义见下面的代码:

struct objc_class : objc_object {
    // 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_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    
    …… 省略
}

objc_class继承自objc_object,所以万物皆对象!

三.联合体位域简述

在学习isa之前,先了解一下联合体以及位域。

1.位域

引入一个案例,定义一个结构体Car,体现车的运动方向,见下面代码:

image.png

这样看上去是可以满足的的业务需求的,但是这里有个问题,这个结构体需要占用4个字节32位,使用4个字节去体现一个单一功能有些浪费空间。理论上,一个字节就可以体现车的运动状态,改进一下,见下图:

image.png

这里使用了位域,用来体现一个功能,比如有值就是用1,没有值就是用0BOOL front : 1;表示front占用一位,这样体现一个车辆的状态只需要4位即可,这样整体需要一个字节即可满足要求!

2.结构体特点

同样引入一个案例!见下图:

image.png

上面的案例中定义了一个结构体Person1char * name占用8个字节,int age占用4个字节,double height8个字节,结合8字节对齐,该结构体共占用24个字节。同时,运行代码,给结构体赋值过程中,结构体中各个属性之间并无冲突,处于共存的状态。

结构体(struct)特点总结如下:

  • 优点:共存,有容乃⼤,全⾯;
  • 缺点:struct内存空间的分配是粗放的,不管⽤不⽤,全分配。

3.联合体特点

同样引入一个案例,来区分结构体和联合体的区别!见下图:

image.png

上面的案例中定义了一个联合体Person2char * name占用8个字节,int age占用4个字节,double height8个字节,而这三个属性是互斥的,该联合体实际占用空间是8个字节。同时,运行代码,给联合体赋值过程中,联合体中各个属性之间处于互斥的状态,并且联合体实际大小与最大的属性值大小相等

联合体(union)特点总结如下:

  • 优点:内存使⽤更为精细灵活,也节省了内存空间;
  • 缺点:不够包容,各变量是互斥的。

四.isa探索

1.isa_t联合体

通过上面的案例,认识到了联合体与结构体的区别,同时了解到位域在节省内存方面的优势。而isa,就是采用联合体结合位域,对数据进行了封装。见下面源码:

// isa 联合体
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls; 
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t是一个联合体,有两属性Class cls;uintptr_t bits;,这两个属性时互斥的,该联合体占用8个字节内存空间。

  • Class cls;非nonpointer isa,没有对指针进行优化,直接指向类,typedef struct objc_class *Class;
  • uintptr_t bits;nonpointer isa,使用了结构体位域,针对arm64架构x86架构提供了不同的位域设置规则
#if SUPPORT_PACKED_ISA

// ios真机环境
# 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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

// mac、模拟器环境
# 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)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif

2.nonpointer isa各位含义

  • nonpointer:1位,表示是否对 isa 指针开启指针优化,0:纯isa指针1:不⽌是类对象地址isa中包含了类信息、对象的引⽤计数等。
  • has_assoc:1位,关联对象标志位,0没有1存在
  • has_cxx_dtor:1位,该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针,在 x86 架构中有 44 位⽤来存储类指针。
  • magic:6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:1位,指对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
  • deallocating:1位,标志对象是否正在释放内存。
  • has_sidetable_rc:1位,当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。
  • extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc

3.nonpointer isa初始化

在对象进行初始化过程中,_class_createInstanceFromZone中三个重要的初始化流程:

  1. cls->instanceSize,计算要开辟的内存大小,16字节对齐原则
  2. obj = (id)calloc(1, size);,内存空间开辟;
  3. obj->initInstanceIsaisa初始化过程。
  • 本篇重点学习nonpointer isa的初始化流程!

设置断点,运行程序,过滤出我们所需要研究的GFPerson类的初始化流程。见下图所示:

image.png

isa_t为联合体,初始化nonpointer isa,则cls属性为空,bits结构体会被初始化(互斥)8字节共64位,默认都为0。继续运行代码,bits赋值ISA_MAGIC_VALUE,赋值后,各位域的值见下图:

image.png

第一位值为1,即对 isa 指针开启指针优化。从4753位,也就是magic赋值为59,非0,表示当前对象已被初始化。通过计算器可以验证,59的二进制就是0011 1011,如下图所示:

image.png

继续运行代码,将类的地址右移3位,赋值给shiftcls,见下图。为何要右移三位呢?因为shiftcls前面还有3位存储着nonpointerhas_assochas_cxx_dtor
image.png

isa初始化流程结束,我们可以通过创建的gf对象反推isa指向的是否为GFPerson类。见下图:

image.png

获取gf对象的内存结构,右移3位左移20位,再右移17位,获取类地址,成功指向GFPerson类

4.对象获取类

平常获取对象的类会直接调用class方法,那么class方法内部实现是怎样的?见下面源码:

- (Class)class {
    return object_getClass(self);
}

// getIsa()方法;
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

// 当前不是taggedPointer,而是nonpointor isa, 直接返回ISA()
inline Class 
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

// ISA——返回:return (Class)(isa.bits & ISA_MASK);
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
}

通过解读上面的代码,发现获取对象的类,最终是实现代码是(Class)(isa.bits & ISA_MASK);,是通过对象isa & ISA_MASKISA_MASK是什么呢?见下图:

image.png

在计算器中可以发现,该掩码低三位高17位全部是0,通过对象isa & ISA_MASK运算,会将对象isa低三位高17位全部抹零,等价于上面的右移3位左移20位,再右移17位操作流程。

ISA_MASK 也即是ISA的一个面具!验证一下:

image.png

相关文章
|
4月前
|
前端开发 开发工具 Swift
学习iOS开发的准备
准备学习iOS开发?确保有Mac和最新Xcode,先学好编程基础特别是Swift。利用Apple官方文档、在线课程和书籍作为资源。熟悉Xcode及Git,通过实践项目和开源代码积累经验。深研架构模式、核心框架和优化技巧。加入开发者社区,关注行业动态,持续学习。
42 1
|
安全 前端开发 Android开发
鸿蒙开发|鸿蒙系统的介绍(为什么要学习鸿蒙开发|鸿蒙系统的官方定义|鸿蒙和安卓、ios的对比)
鸿蒙开发学习是一项探索性的工作,旨在开发一个全场景分布式操作系统,覆盖所有设备,让消费者能够更方便、更直观地使用各种设备。
671 6
鸿蒙开发|鸿蒙系统的介绍(为什么要学习鸿蒙开发|鸿蒙系统的官方定义|鸿蒙和安卓、ios的对比)
|
机器学习/深度学习 API iOS开发
iOS MachineLearning 系列(17)—— 几个常用的对象识别 CoreML 模型
上一篇文章中,我们介绍了几个官方的图片分类的模型,图片分类模型的应用场景在于将图片中最主要的事物进行识别,在已有的词库中找到最可能得事物。而对象识别则要更高级一些。再之前的文章,我们介绍过可以使用官方提供的API来进行矩形识别,文本识别,二维码识别以及人脸识别等,这类识别功能的特点是我们不仅可以将图片中的物体位置和尺寸分析出来,还可以对其进行类别的分类。
353 0
|
iOS开发
iOS UIKit Dynamics Demo 学习地址列表
iOS UIKit Dynamics Demo 学习地址列表
50 0
|
Swift iOS开发
iOS OC混编Swift 后者无法走断点
iOS OC混编Swift 后者无法走断点
80 0
|
安全 数据安全/隐私保护 iOS开发
iOS 初探代码混淆(OC)
自己做iOS开发也有几年的时间了,平时做完项目基本就直接打包上传到Appstore上,然后做上架操作了。但是最近,客户方面提出了代码安全的要求。说是要做代码混淆,这方面的工作之前从来没有接触过。然后就上网查了一下,原来有很多应用程序都做了代码混淆。看来是我固步自封了......
iOS 初探代码混淆(OC)
|
XML JSON 编解码
IM通讯协议专题学习(九):手把手教你如何在iOS上从零使用Protobuf
接上篇《金蝶随手记团队的Protobuf应用实践(原理篇)》,本文将以iOS端的Objective-C代码为例,图文并茂地向您菔救绾卧趇OS工程中快速使用Protobuf,希望对你有帮助。
173 0
IM通讯协议专题学习(九):手把手教你如何在iOS上从零使用Protobuf
|
iOS开发
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
148 0
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
|
物联网 Android开发 iOS开发
iOS开发 - 蓝牙学习的总结
iOS开发 - 蓝牙学习的总结
184 0
|
6天前
|
安全 数据处理 Swift
深入探索iOS开发中的Swift语言特性
本文旨在为开发者提供对Swift语言在iOS平台开发的深度理解,涵盖从基础语法到高级特性的全面分析。通过具体案例和代码示例,揭示Swift如何简化编程过程、提高代码效率,并促进iOS应用的创新。文章不仅适合初学者作为入门指南,也适合有经验的开发者深化对Swift语言的认识。
24 9