本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。
第一节中,在alloc
的源码分析中,我们已经知道,alloc
申请了内存空间,并且利用isa
关联了obj
和cls
,而init
只是提供了一个工厂模式,方便我们来重写或者自定义类,那么对象的alloc
研究完了,就看看这个对象归属的类是怎么回事。
从本节开始,由alloc
初始化出来的对象的探索,进步到类的探索。
OC对象的本质是什么?
OC对象的本质是结构体。
这个结论怎么得来的?
那就需要用到Clang
来帮助我们验证,OC对象的本质到底是不是结构体。
一、关于Clang
什么是Clang
?
Clang
是由苹果基于LLVM
编写的,用于C
/C++
/OC
的轻量级编译器。
什么是LLVM
?
LLVM
是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
为什么要用到Clang
因为Clang
可以看到底层编译,例如我们平时用到的main.m
经过clang
编译后会变成main.cpp
。
通过Clang
的编译,我们可以看到OC底层的结构。
二、OC对象的本质
我们在objc
的部分开放源码可编译文件的main.m
中创建一个继承于NSObject
的JDPerson
这个类。强调是在main.m
文件中,不要去文件夹里面直接创建出来JDPerson
的.h
和.m
,因为我们只是编译main.m
,没必要自己建个类,不然还要再编译这个类,作为探索,有点麻烦。
- 随意给这个类添加一个属性,我添加了
myHobby
属性。
@interface JDPerson : NSObject
@property (nonatomic, copy) NSString *myHobby;
@end
@implementation JDPerson
@end
- 然后我们用
终端Terminal
进入你的可编译的objc-781
的源码文件夹下。一直cd
命令进入文件夹,直到你的main.m
的文件夹下,然后执行一下命令,将main.m
编译成main.cpp
文件。
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//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
- 打开编译好的
main.cpp
文件,commond + F
找你刚才自己创建的类名或属性名。
这里可以看到,JDMan
的确是结构体,而且拥有的第一个属性是NSObject
结构体,这其实就是isa
只不过是一种伪继承,而这个NSObject_IMPL
在JDMan_IMPL
里面,也就导致了JDMan_IMPL
拥有着NSObject_IMPL
的所有成员变量。
那这个Class
又是个什么东西?怎么就是isa
的类了?
上一节探索alloc的时候,是不是有一个callAlloc
,如下:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
我们在这里找到了fastpath
当cls
的isa
没有自定义allocWithZone
的时候,我们才开始了alloc
申请内存,绑定isa
和obj
的关系的,这里的ISA()
就是isa
指针的初始化。我们进去再看一下。
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
的内存。这里在返回的时候,明显的做了强转,转成了Class
类。为什么非要把它转成Class
类呢?其实只是个备注,就是让大家知道,我isa
是跟类有关系的指针,具体是什么,下一节会说明。
那isa
原来又是什么类呢?
直接commond
点击isa
,发现:
struct objc_object {
private:
isa_t isa;
}
就是objc_object
私有变量,是一个isa_t
,继续跟进isa_t
:
#include "isa.h"
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
一样遵循着类的本质——结构体。
到这里,我们可以确定,OC的类的本质就是结构体,而继承与NSObject
的类所拥有的isa
指针,也继承于NSObject
的isa
指针。
到这里本节的探索重点已经结束了。
下面的内容是顺带着来的,因为刚好利用Clang
编译好了main.cpp
文件,那么就多看两行。
附加
// @implementation JDPerson
static NSString * _I_JDPerson_myHobby(JDPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JDPerson$_myHobby)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_JDPerson_setMyHobby_(JDPerson * self, SEL _cmd, NSString *myHobby) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JDPerson, _myHobby), (id)myHobby, 0, 1); }
// @end
这是紧跟着刚才的JDPerson_IMPL
的内容,仔细一看,原来一个是属性中的get
,一个是set
,配上成员变量__myHobby
,一个@property
出来了。。。
那就看一下。
这里说一下,看到
get
和set
方法的参数了吗?突然多出来了两个参数,一个是JDPerson* self
,还有一个SEL _cmd
。这两个参数是编译的时候,自动加进来的,一个指定了当前方法的对象,一个指定了当前方法实现的名字。那个self
也是为什么我们在set
和get
方法里面可以使用self
来调用属性或者函数的原因。
get
没什么看的,直接就return
了,但是那个set
里面扩展了一个函数objc_setProperty
。
我们去objc781
里面看一下objc_setProperty
这是什么。
不要管.h
头文件,直奔.mm
的实现。发现一个reallySetProperty
,点进去看一下。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
//设置isa的指向
object_setClass(self, newValue);
return;
}
//旧的value
id oldValue;
//插槽,自己的的位置前面还要有isa,就把isa的位置留出来
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
//传进来的新值进行retain+1
newValue = objc_retain(newValue);
}
if (!atomic) {
//老的值拿插槽里面的值
oldValue = *slot;
//新的值放入插槽
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
//老的值release-1
objc_release(oldValue);
}
自己加了两句注释,不知道对不对,以我的理解来的。
那么从这里可以看出来:
objc_setProperty
主要是一个接口,主要处理了两个拷贝参数的真假,利用这个来把上层的set
和下层的set
的处理分开,处理set
逻辑的主要函数是reallySetProperty
。- 为什么要分开上下层的
set
?因为上层的set
方法很多,直接使用底层的reallySetProperty
会出现很多的临时变量,这就会导致你想找到一个方法sel
的时候变得复杂。
因为(2)中的因素,苹果采用了适配器设计模式,把底层的接口适配成客户端需要的接口。
这样做的好处就是你上面随便的改,最后我用这个适配的接口把需要的东西传给底层的
reallySetProperty
,具体是谁的,通过SEL
和_cmd
的区分。最后由reallySetProperty
来处理。
上层处理你要处理的,随意的变,但是不影响我底层的处理逻辑,我底层的处理逻辑也不影响你上层的处理逻辑。相当于给老板找了个秘书。
放个图: