iOS底层原理-对象的本质和ISA

377 阅读8分钟

一、对象的本质

1、大师重器:Clang

Clang是一个C语言、C++、Objective-C、C++语言的轻量级编译器。源代码发布于BSD协议下。也是Xcode 第一的编译器

1)输出可执行文件

cd 到文件目录,输入 以下指令输出可执行文件

clang -fobjc-arc -framework Foundation main.m -o hello

可执行文件

2)生成 cpp 文件
clang -rewrite-objc main.m

桌面上会出现.cpp文件,即是c++的实现文件,如下图所示

cpp

3)UIkit报错 使用复杂的clang,指定SDK路径

eg:如果文件中引入了<UIKit/UIkit.h>库,报如下错误

main.m:12:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~

此时 需要使用

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

具体SDK版本根据实际版本来书写:

否则会报错:使用的Foundation库找不到

main.m:9:9: fatal error: 'Foundation/Foundation.h' file not found
#import <Foundation/Foundation.h>
        ^~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
4)xcrun命令

Xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了封装,更为好用

模拟器的xcrun命令

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

真机的xcrun命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64-iphoneos.cpp

2、Clang探索对象的本质

@interface Person : NSObject
@property (nonatomic,copy) NSString *qzUserName;
@property (nonatomic,copy) NSString *qzSex;
@end

使用clang生成.cpp, 查看.cpp源码,搜索qzUserName,可以看到Person的结构体

#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif

extern "C" unsigned long OBJC_IVAR_$_Person$_qzUserName;
extern "C" unsigned long OBJC_IVAR_$_Person$_qzSex;
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_qzUserName;
	NSString *_qzSex;
};

// @property (nonatomic,copy) NSString *qzUserName;
// @property (nonatomic,copy) NSString *qzSex;

/* @end */


// @implementation Person
// qzUserName的getter方法
static NSString * _I_Person_qzUserName(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_qzUserName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// qzUserName的setter方法
static void _I_Person_setQzUserName_(Person * self, SEL _cmd, NSString *qzUserName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _qzUserName), (id)qzUserName, 0, 1); }

static NSString * _I_Person_qzSex(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_qzSex)); }
static void _I_Person_setQzSex_(Person * self, SEL _cmd, NSString *qzSex) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _qzSex), (id)qzSex, 0, 1); }
// @end

NSObject_IMPL

struct NSObject_IMPL {
	Class isa;
};

objc_object

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

可以发现

1、Person对象实际上是一个结构体类型Person_IMPL

2、当前结构体Person_IMPL又嵌套了一个结构体,相当于一个伪继承(不是真正的继承,但是在c++里面是可以的),它继承了NSObject_IMPL,这里的NSObject_IVARS就是成员变量的isa。

假如再有一个继承自Person的Student的对象

image

小结:

对象的底层是一个结构体

  • 扩展留意 objc_objectobjc_classid

NSObject 底层是 objc_object 结构体

typedef struct objc_object NSObject;

Class 底层是 objc_class 结构体指针

typedef struct objc_class *Class;

id底层是 objc_object 结构体指针

typedef struct objc_object *id;

OC中的任意对象id,都是以objc_object为模板创建的;所以实例对象,类对象,元类对象都会有isa指针;

OC中的class类对象,都是以objc_class为模板创建,而objc_class继承自objc_object,所以也会有isa指针。

二、ISA分析

1、基础知识

1.1、联合体 (共用体)

    union PersonUnion {
        int a;      //4
        short b;    //2
        char c;     //1
    };

    union PersonUnion    person;
    person.a = 8;
    NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
    person.b = 2;
    NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
    person.c = 'd';
    NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        
    打印结果:    
    a=8---b=8---c=
    a=2---b=2---c=
    a=100---b=100---c=d
        

1、联合体可以定义多个不同类型的成员,联合体的内存大小由其中最大的成员的大小决定。
2、联合体中修改其中的某个变量会覆盖其他变量的值。
3、联合体所有的变量公用一块内存,变量之间互斥。

联合体优缺点

优点:内存使用更为精细灵活,节省内存。
缺点:不够包容。

结构体优缺点

优点:‘有容乃大’,全面。
缺点:内存空间分配是粗放的,不管用不用,全分配。

结构体和 联合体对比

成员是否共存占用内存
结构体共存大于等于所有成员占用内存的总和(内存对齐)
联合体互斥等于最大的成员占用的内存,同一时刻只能保存一个成员的值

1.2、位域

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。

定义:在定义结构体或联合体时,成员变量后面加 : 数字,用来限定成员变量占用的位数,这就是位域;

struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
} car1;
// 位域
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
} car2;

NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));


结果:
 4-1

从结果可以看出没有位域的限制,此结构的大小为4字节,加了位域之后,变成1字节。

2、ISA

之前文章alloc探索中,我们了解到

_class_createInstanceFromZone 方法中有核心三个方法需要实现

 cls->instanceSize : 计算内存大小 8字节对齐 
 (id)calloc(1, size) : 开辟内存,返回地址指针
 obj->initInstanceIsa :初始化指针isa,和类关联起来

今天主要探讨 isa相关

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
   ...

    if (!zone && fast) {
    	 // 初始化 isa指针 和cls 关联起来
        obj->initInstanceIsa(cls, hasCxxDtor);// 关联内存空间到相应的类
    } else {
        obj->initIsa(cls);
    }
    
    ...
}

initIsa

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

     ...
   
    isa = newisa;
}

isa_t 是一个联合体

union isa_t {
    // 构造方法
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    // 成员变量
    uintptr_t bits;

private:
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

从上面源码可以看出

  • isa_t是一个联合体,联合体的成员变量存储是互斥的,成员变量bits和结构体使用同一块内存;
  • 最上面是两个构造函数,第一个构造函数没有做任何处理,第二个构造函数使用传入的参数value对成员变量bits进行赋值;
  • 下面结构体中的ISA_BITFIELD为宏定义,实际上这里使用了位域,此结构体和成员变量bits共用同一块内存空间;

结构体成员变量 ISA_BITFIELD

isa指针占用的内存大小是8字节,即64位,除了存储地址信息,结合位域还可以存储一些其他的信息了。

例如:是否为纯指针,是否关联对象,是否有析构函数,是否弱引用,是否还没有初始化空间,是否正在释放,以及引用计数,存储类指针的值等。。。

这样可以极大的节省内存,以提高性能。

分为两种模式:arm64: 和 x86_64

以x86_64为例:

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;       //是否为纯指针                                    \
      uintptr_t has_assoc         : 1;       //是否关联对象                                  \
      uintptr_t has_cxx_dtor      : 1;       //是否有析构函数                                \
      uintptr_t shiftcls          : 44;     //类的指针地址
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;      //是否弱引用                                  \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;       //散列表                                  \
      uintptr_t extra_rc          : 8       //该对象的引用计数值
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

各变量的含义:

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

ISA_MASK面具

我们更多关注的是isa的shiftcls,我们可以通过isa的地址 & ISA_MASK,去除其他信息,直接获得类的指针地址cls。

真机 arm64的 ISA_MASK
#   define ISA_MASK        0x0000000ffffffff8ULL
模拟器 x86_64的 ISA_MASK
#   define ISA_MASK        0x00007ffffffffff8ULL

iamge1

&MASK的效果相当于 如下位移操作得到shiftcls:

x86环境下: img2

操作验证 得到的cls

(lldb) x/4gx p
0x1005bf8e0: 0x001d800100008225 0x0000000000000012
0x1005bf8f0: 0x0000000100004038 0x0000000000000000
(lldb) p/x p.class 
(Class) $22 = 0x0000000100008220 LGPerson

1、通过位移isa得到cls

(lldb) p/x 0x001d800100008225 >> 3
(long) $23 = 0x0003b00020001044
(lldb) p/x 0x0003b00020001044 << 20
(long) $24 = 0x0002000104400000
(lldb) p/x 0x0002000104400000 >> 17
(long) $25 = 0x0000000100008220 // 2、得到cls 和 上面得到的结果相同

2、使用面具直接获得cls

(lldb) p/x 0x001d800100008225 & 0x00007ffffffffff8ULL
(unsigned long long) $26 = 0x0000000100008220 
(lldb)