建议先看下
IOS底层(三): alloc相关1.初探 alloc, init, new源码分析
我们先看一个main
项目(创建只有main.m项目)
其中我们在main中加了一个TestObj
类
// 声明
@interface TestObj : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 实现
@implementation TestObj
@end
接下来介绍一个重要的编辑器Clang
Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是一个由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。
介绍的目的是为了将main.m
转成main.cpp
, 便于我们探索源码
- 打开终端
cd 进入项目文件夹
clang -rewrite-objc main.m -o main.cpp //将 main.m 编译成 main.cpp
我们再看项目会生成一个cpp文件
- 打开cpp文件
- 搜索
TestObj
代码很多, 我们就找自定义类的关键词就行, 搜索TestObj
可看到, 我们自定义的TestObj
对象在底层会被编译成结构体struct
(OC对象的本质其实就是结构体)
其中name
是我们自定义的一个NSString
属性,
而 NSObject_IMPL
也可搜索到
struct NSObject_IMPL {
Class isa;
};
补充个知识点: 结构体是可以继承的, 在C++是可以继承的, 在C可以伪继承, 伪继承的方式是直接将NSObject结构体定义为 TestObj 中的第一个属性, 意味着 TestObj 拥有 NSObject中的所有成员变量。
所以NSObject_IVARS
等效于 NSObject中的 isa
而这里我们可看到是自定属性的get
, set
方法, 这里的set
会调用一个objc_setProperty
方法, 这是一个Runtime的API
objc_setProperty
- 回到objc源码看一下
objc_setProperty
方法
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
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); // 设置isa指向
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); // 对旧值release
}
这里可看到 objc_setProperty
原理就是对旧值的release
, 对新值的retain
这里对objc_setProperty
总结下
objc_setProperty
相当于一个承上启下的接口, 上层的许多个set方法(setName, setAge......), 如果直接对接底层llvm
会有生成很多临时变量(sel
等), 当你如果想要查找某一个时候会很麻烦。
所以设计了一个objc_setProperty
令所有属性的set方法都经过它, 区分根据cmd
, 以便达到无论上层怎么变,下层都是不变的, 以及下层的变化也无法影响上层,主要是达到上下层接口隔离的目的。
cls 与 类 的关联原理
alloc
关键三步的最后一个指针关联
obj->initInstanceIsa(cls, hasCxxDtor);
看下 initInstanceIsa
源码
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
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
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
// 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;
}
isa_t newisa(0);
其中这里, 我们可以看到新的isa是一个isa_t
类型的, isa_t
又是什么呢? 点击进入
isa_t (0~63)
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// 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
是一个union
联合体
这里我们先看下联合体
联合体 union
常见的构造数据类型有两种 结构体(struct)
, 联合体/共用体(union)
- 结构体 : 不同的数据结构整合成一个整体, 其变量是共存, 变量不管是否使用, 都会被分配内存。
优点:
包容性强且成员之间不会互相影响。
缺点:
所有属性都被分配内存, 比较浪费。
例如: 假设定一个4个int型属性, 但是我只用了1个4字节, 却开辟了16字节(128位)内存空间, 浪费12字节。
- 联合体 : 不同的数据结构整合成一个整体, 其变量是互斥, 所有成员共占用一段内存。而且联合体采用内存覆盖技术, 同一时间段只能保存一个成员值, 如果对新成员赋值, 就会将原来的成员覆盖掉。
优点:
内存精细灵活, 节省内存空间。
缺点:
包容性弱。
- 结构体与联合体对比
内存占用情况:
结构体: 各个成员占用不同段内存, 互不影响
联合体: 所有成员占用同一段内存, 修改一个成员会影响其他成员
内存分配大小:
结构体: 大于等于所有成员的内存总和(成员之间可能有缝隙)
联合体: 占用的内存等于最大成员占用的内存
isa_t
也是基于内存优化
考虑使用联合体, isa通过 char + 位域
(即二进制中每一位均可代表不同信息)的原理实现。通常来说isa占用8字节(64位), 足够存储很多信息, 这样的极大节省内存, 以提高性能。
其中bits
与cls
两个成员变量是互斥关系, 如果是 nonpointer isa 就会有bit结构
如果这个类不是, 就当前的地址就是类地址。(联合体,同一时间,只会存储一个值,如果在存储另一个,会将前面存储的覆盖,所以是互斥的)
ISA_BITFIELD
这个是ISA位域信息 (arm64是真机的, x86_64是Mac或者模拟器的我们看arm64即可)
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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 : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
上面的判断针对于simulators模拟器, 我们重点看下真机的下面判断
-
nonpointer
: 是否对isa指针开启指针优化占1位
-
0
: 纯isa指针 -
1
: 不只是类对象地址
, isa中包含类信息, 对象引用计数等 大部分自定义的类都为, nonpointer isa
-
-
has_assoc
: 是否有关联对象占1位
-
0
: 没有关联对象 -
1
: 存在关联对象
-
-
has_cxx_dtor
: 该对象是否有C++/OC的析构器(类似于dealloc)占1位
-
0
: 无, 可以更快释放对象 -
1
: 有, 需要做析构逻辑(析构函数就是dealloc)
-
-
shiftcls
: 存储类指针的值。开启指针优化的情况下, 用来存储类指针(即类信息)-
arm64
: 占33位 -
arm64-not-e
: 和sig 合一起占52位 -
x86_64
: 占44位
-
magic
: 用于调试器判断当前对象是真的对象还是没有初始化的空间占6位
weakly_referenced
: 指对象是否被指向或者曾经指向一个ARC弱变量, 没有弱引用可以更快释放(如果有弱引用对象, 需要引用计数移除)deallocating
: 标志对象是否正在释放内存has_sidetable_rc
: 是否有外挂的散列表, 当对象引用计数大于10时, 则需要借用该变量存储进位-
extra_rc
: 额外的引用计数, 表示该对象的引用计数值, 实际上是引用计数值减1- 例如: 如果对象的引用计数为10,那么extra_rc为9,真机上的 extra_rc 是使用 19位来存储引用计数的
-
对象的引用计数存在isa里面
,
关于dealloc
补充点只是点, 我们先看下dealloc
源码
dealloc
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
可看到dealloc主要做了移除 引用计数
, 关联对象
, 散列表
等, 之后再做了些销毁处理
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj); // c++析构
if (assoc) _object_remove_assocations(obj, /*deallocating*/true); // 移除关联对象
obj->clearDeallocating();
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating(); // 散列表的释放
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this); // weak 弱引用表的释放
}
table.refcnts.erase(it);
}
table.unlock();
}
从dealloc
源码中也能看出来, 这些对象是否能被释放其实都是通过标识位来进行读取的, 对象信息都已经存在isa
中, 而dealloc
相当于做了最后的熄火
。
验证 isa
- main中定义自定义一个类
SATest
-
class_createInstanceFromZone
这里打个断点, 跟下initInstanceIsa
这个流程(同时别忘确保进来的类确实是SATest
)
- 看到自定义类走了
initInstanceIsa
方法, 依次initInstanceIsa
→initIsa
继续跟进
-
initIsa
加个断点p
一下初始化的isa
, 可看到
- 走过
newisa.bits = ISA_MAGIC_VALUE;
, 这个方法主要是对isa位域赋给一个初始值/默认值, 再p
一下, 可看到
其中
__x86_64__
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
nonpointer
变成1
: 不难理解, isa中需要包含类信息, 对象引用计数等而且大部分自定义的类都为nonpointer isa
magic
变成59
: 首先magic
是用于调试器判断当前对象是真的对象还是没有初始化的空间, 很明显有。nonpointer占1位
,has_assoc占1位
,has_cxx_dtor占1位
,shiftcls x86_64 占44位
, 那么从47
起数6
位(magic 占6位
), 其中cls = 0x001d800000000001
转成2进账从从47
起数6
位有111011
而这个就是10进制59
。这块最好需要结合计算器一起看, 如图
- 继续走, 走过
newisa.setClass(cls, this);
再p
一下, 可看到
newisa.setClass(cls, this)
其实就是cls类
与指针
关联方法, 进入源码可看到
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
// No signing, just use the raw pointer.
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
// We're only signing Swift classes. Non-Swift classes just use
// the raw pointer
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
// We're signing everything
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3; // 右移3位, 赋值到shiftcls位
#endif
}
重点这段代码 shiftcls = (uintptr_t)newCls >> 3;
, 将cls信息转换赋值给isa
中的shiftcls
位(shiftcls
主要用来存储类指针(即类信息))
同时当前的cls已经变为SATest
, 即isa指针
与类
关联起来
这里验证下
当然我们也可以这样验证
(上面例子不小心让我关了, 重新运行个, 原理一样)
x86_64 模拟器下, shiftcls
占44位, 后面3位, 前面17位(一共64位, 8字节)
当前obj为SATest指针, 我们x/4gx一下, 第一个是
isa
-
取
isa
, 右移3位, 去掉后面3个
-
左移20位, 去掉前面17个
-
右移17位, 得到
shiftcls
我们将它与cls对比一下可发现相等, 也可以验证cls
与isa
已经关联
总结
cls 与 isa 关联原理
就是isa指针
中的shiftcls
位域中存储了类信息
, 其中initInstanceIsa
的原理就是将calloc
开辟出来的obj指针
与当前类
进行关联
补充
应该有些人会有疑问, isa
继承isa_t
, 而由于isa_t
是个联合体, 呢么isa应该也是联合体, 而所有自定义类都是继承NSObject
, 而NSObject
底层
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
这里面isa不是Class
么, 两者有矛盾
其实这里是为了方便开发人员开发做了一层小小的转换, 看下源码(下面是781源码, 个人感觉781这块写的更直接一点)
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
#if SUPPORT_TAGGED_POINTERS
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;
}
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相关信息, 但是做了一层强转, 转成Class类型
返回。
下面是818源码, 改变一些不过原理相同, 都是强转Class
inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}
inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
return cls;
#else
uintptr_t clsbits = bits;
# if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
// Most callers aren't security critical, so skip the
// authentication unless they ask for it. Message sending and
// cache filling are protected by the auth code in msgSend.
if (authenticated) {
// Mask off all bits besides the class pointer and signature.
clsbits &= ISA_MASK;
if (clsbits == 0)
return Nil;
clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
} else {
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
}
# else
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
# endif
# else
clsbits &= ISA_MASK;
# endif
return (Class)clsbits;
#endif
}