iOS底层探索之OC对象的本质

309 阅读4分钟

前言

本篇主要利用Clang将OC.m编译为.cpp文件的探索方式,探索OC对象的本质->对象底层的数据结构。

Clang

正文开始之前,先简单介绍一下ClangClang是基于LLVMCC++Objective-CObjective-C++ 语言编译器。

传统编译器的工作原理,基本上都是三段式的,可以分为前端、优化器和后端。 前端负责解析源代码,检查语法错误,并将其翻译为抽象的语法树;优化器对这一中间代码进行优化,试图使代码更高效;后端则负责将优化器优化后的中间代码转换为目标机器的代码。

对于我们iOS开发者来说,command+b整个流程可以简要概括为Clang对代码进行处理形成中间层作为输出,llvmCLang的输出作为输入生成机器码。可以参考简述 LLVM 与 Clang 及其关系

image.png Clang提供了编译命令可以将OC代码转换为C++代码,这样我们更好的观察对象的底层的结构。

Clang编译命令

clang -rewrite-objc main.m -o main.cpp 
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//3、模拟器编译
 xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
//4、真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp

探索开始

编译.m文件

1.定义两个类PersonStudentStudent继承于PersonPerson继承于NSObject,如下:

@interface Person : NSObject{
    @public
    NSString *age;
}

@property (nonatomic, copy) NSString *name;
-(void)sayHello;

@end


@interface Student : Person

@property (nonatomic, copy) NSString *grade;
@end

2.在终端输入命令 clang -rewrite-objc Person.m -o Person.cpp,将Person.m编译成 Person.cpp。可以看到Person.cpp文件非常大,足足有10w多行代码。

对象的底层数据结构

3.在10w多行代码里,找到了几个结构体,如下:

struct NSObject_IMPL {
	Class isa;
};
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *age;
	NSString *_name;
};
struct Student_IMPL {
	struct Person_IMPL Person_IVARS;
	NSString *_grade;
};

观察可以得出几个小结论:

  • 某某类在编译之后会变成某某类_IMPL的结构体。
  • 类的成员变量和属性都会作为结构体的成员变量
  • 类的属性前面会上"_",所以要加"_"来访问属性
  • 子类会将父类作为第一个成员变量,继承父类的所有成员变量,所以子类会拥有父类的成员变量以及isa

对象方法存在哪

4.Person类中我定义了一个对象方法sayHello,但是没有在Person_IMPL中没有方法的成员变量,那么方法去哪了,查找一下关键字sayHello

static void _I_Person_sayHello(Person * self, SEL _cmd) {}

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

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


static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	5,
	{{(struct objc_selector *)"sayHello", "v16@0:8", (void *)_I_Person_sayHello},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}}
};

可以看到对象的方法经过编译,成了一个静态方法,同时编译之后自动为name属性生成了setget方法,最后所有的对象方法存在_OBJC_$_INSTANCE_METHODS_Person这个结构体中。

5.搜索关键字_OBJC_$_INSTANCE_METHODS_Person

struct _class_ro_t {
	unsigned int flags;
	unsigned int instanceStart;
	unsigned int instanceSize;
	unsigned int reserved;
	const unsigned char *ivarLayout;
	const char *name;
	const struct _method_list_t *baseMethods;
	const struct _objc_protocol_list *baseProtocols;
	const struct _ivar_list_t *ivars;
	const unsigned char *weakIvarLayout;
	const struct _prop_list_t *properties;
};

static struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	0, __OFFSETOFIVAR__(struct Person, age), sizeof(struct Person_IMPL), 
	(unsigned int)0, 
	0, 
	"Person",
	(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
	0, 
	(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
	0, 
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};

6._OBJC_CLASS_RO_$_Person最后被存在_class_t类型的结构体中,OBJC_CLASS_$_Person是类对象的实例,对象的属性 方法 成员变量以及协议都存在类对象中。

struct _class_t {
	struct _class_t *isa;
	struct _class_t *superclass;
	void *cache;
	void *vtable;
	struct _class_ro_t *ro;
};
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_Person,
	0, // &OBJC_CLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_CLASS_RO_$_Person,
};

类方法存在哪

7.通过同样的查找方法

static struct _class_ro_t _OBJC_METACLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	1, sizeof(struct _class_t), sizeof(struct _class_t), 
	(unsigned int)0, 
	0, 
	"Person",
	(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_Person,
	0, 
	0, 
	0, 
	0, 
};

extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_NSObject,
	0, // &OBJC_METACLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_METACLASS_RO_$_Person,
};

发现类方法跟对象方法类似,只不过类的_class_ro_t只有方法列表,最后存到OBJC_METACLASS_$_Person,就是Person元类对象实例,所以类方法存在元类里。

set方法和get方法

set方法和get方法中是怎么对属性操作的呢?分析nameget方法

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

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

extern "C" unsigned long int OBJC_IVAR_$_Person$_name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person, _name);

和别的方法一样,get方法也有默认的参数self_cmd,通过self的地址加上OBJC_IVAR_$_Person$_name,也就对象的首地址加上偏移地址得到一个地址,然后对地址取值。 OBJC_IVAR_$_Person$_nam就是这个偏移地址,等于__OFFSETOFIVAR__(struct Person, _name),而__OFFSETOFIVAR__是一个宏方法,传入一个结构体结构体的成员变量就能拿到该成员变量的对于结构体首地址偏移

OFFSETOFIVAR

来分析一下这个方法 ((long long) &((TYPE *)0)->MEMBER)

  1. (TYPE *)0,把0强转为TYPE类型的指针
  2. (TYPE *)0)->MEMBER,取该指针的MEMBER这个成员变量,
  3. &((TYPE *)0)->MEMBER)对该成员变量取地址
  4. 由于首地址为0,该成员变量的地址就是偏移地址 比如定义了一个Offset结构体:
struct Offset {
    int x;
    int y;
};

通过lldb的演示 image.png 4就是y的偏移地址。

总结

日常开发没有机会去了解对象的数据结构,通过对编译后C++文件产物的探索,对对象的底层数据结构更清晰了。