内存布局
- IOS的内存布局除了我们知道的内存五大区,还有内核区和保留区,我们知道虚拟内存分配了4GB的空间,前面3GB分配给了保留区和五大区,剩下的1GB是给内核区使用的
- 内核区是用来给系统内核操作处理的区域,保留区是给系统处理nil等
- 内存五大区的介绍内存五大区
内存管理方案
ARC和MRC
- 在早期的苹果系统里面是需要我们手动管理内存的,手动内存管理遵循谁创建,谁释放,谁引用,谁管理的原则
- IOS5之后苹果引入了ARC(自动引用计数),ARC是一种编译器特性,只是编译器在对应的时间给我们插入了内存管理的代码,其本质还是按照MRC的规则
TaggedPointer
- 小对象处理方案,苹果会对NSNumber、NSDate、小NSString进行处理,小对象的值会存储在常量区,有系统分配管理内存,并且能通过地址直接看到对应的值,在
objc
源码的_read_images
会调用initializeTaggedPointerObfuscator
方法对小对象进行处理,在ios12之后会对小对象进行混淆处理,打印地址将不能直接看到小对象的值。
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {// 在高于上面的版本做了 objc_debug_taggedpointer_obfuscator 混淆
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
小对象的内存管理
- 运行下面这段代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self taggedPointerMemoryManagement];
}
- (void)taggedPointerMemoryManagement
{
for (int i = 0; i < 5000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.testStr = [NSString stringWithFormat:@"test"];
NSLog(@"%@",self.testStr);
});
}
}
- (void)nonTaggedPointerMemoryManagement
{
for (int i = 0; i < 5000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.testStr = [NSString stringWithFormat:@"test-长一点的字符串"];
NSLog(@"%@",self.testStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self nonTaggedPointerMemoryManagement];
}
- 发现一个如下现象,当我们点击屏幕执行
nonTaggedPointerMemoryManagement
,会出现崩溃如下
- 原因是多线程调用release引起的,但是为什么
taggedPointerMemoryManagement
方法没有崩溃了呢,原因是两个字符串类型不一样。
- 通过源码知道,对象在retain和release的时候对taggedpointed对象没有处理,所以不会有多线程release造成的崩溃
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
判断小对象
- 代码调用顺序
objc_object::isTaggedPointer()
->_objc_isTaggedPointer(const void * _Nullable ptr)
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
- 从源码可知:在ios环境下是判断isa的最高位的值,为1则为小对象;在macos上是判断最低位。
- 可以看到这些小对象的最高位都是1。
小对象地址
- 小对象的编码与解码,通过上面
read_images
里面的initializeTaggedPointerObfuscator
方法得到的objc_debug_taggedpointer_obfuscator
进行异或操作。
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- 在我们的demo里面实现
_objc_decodeTaggedPointer
解码方法
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- 打印解码后的地址,可以看到字符串的值直接显示出来了,它的高四位和低四位都有其他的用处,中间的56位用来存放值
- 它的最高位用来判断是否为小对象,其他3位用来判断什么类型
enum objc_tag_index_t : uint16_t #else typedef uint16_t objc_tag_index_t; enum #endif { // 60-bit payloads OBJC_TAG_NSAtom = 0, OBJC_TAG_1 = 1, OBJC_TAG_NSString = 2, //字符串 OBJC_TAG_NSNumber = 3, //nsnumber OBJC_TAG_NSIndexPath = 4, OBJC_TAG_NSManagedObjectID = 5, OBJC_TAG_NSDate = 6, //nsdate // 60-bit reserved OBJC_TAG_RESERVED_7 = 7, // 52-bit payloads
- 低四位是系统处理位
-
Tagged Pointer
是为了节省内存,和提高执行效率的,Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
,在内存读取上有着3倍的效率,创建时比以前快106倍。
这个是iOS交流圈,:iOS技术交流
Nonpointer_isa
- nonpointer(1位)
- 0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
- 1,代表优化过,使用位域存储更多的信息
- has_assoc(1位)
- 是否有设置过关联对象,如果没有,释放时会更快
- has_cxx_dtor(1位)
- 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
- shiftcls(33位)
- 存储着Class、Meta-Class对象的内存地址信息
- magic(6位)
- 用于在调试时分辨对象是否未完成初始化
- weakly_referenced(1位)
- 是否有被弱引用指向过,如果没有,释放时会更快
- deallocating(1位)
- 对象是否正在释放
- has_sidetable_rc(1位)
- 引用计数器是否过大无法存储在isa中
- 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
- extra_rc(19位)
- 里面存储的值是引用计数器减1
SideTables散列表
- 我们对对象retain操作会对isa的
extra_rc
加1,当extra_rc
加满之后,则会存储到散列表中。
objc_retain
- 按照执行顺序找到
objc_retain
->objc_object::retain
->objc_object::rootRetain
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
- 1,判断是否为
nonpointer
,如果为否直接操作散列表对引用计数加1 - 2,如果对象正在释放,则执行
dealloc
流程。 - 3,对
extra_rc
进行+1,并标识一个carry
字符,记录extra_rc
是否满了。 - 4,如果
carry
标识满了,就需要操作散列表,extra_rc
在真机上只有19位用于存储引用计数的值,当存储满了时,需要借助散列表用于存储。需要将满了的extra_rc
对半分,一半(即2^18)存储在散列表中。另一半还是存储在extra_rc中,用于常规的引用计数的+1或者-1操作,再将has_sidetable_rc
标记位设置为true
。这样做的原因是:操作散列表需要开解锁,消耗性能,不用每次+1都操作散列表开解锁。
散列表在内存中有多张,如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全。 如果每个对象都开一个表,会耗费性能,所以也不能有无数个表。
散列表的结构
- 散列表的结构
struct SideTable {
spinlock_t slock; // 锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table;// 弱引用表
}
- 散列表的开解锁代码如下,都是通过操作
SideTables
void
objc_object::sidetable_lock()
{
SideTable& table = SideTables()[this];
table.lock();
}
void
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
- 查看
StripedMap
的结构可以看到,散列表在真机和模拟器上会分配8张表,macos等会分配64张
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
......
}
- 为什么在用散列表,而不用数组、链表?
数组:特点在于查询方便(即通过下标访问),增删比较麻烦(类似于之前讲过的 methodList,通过memcopy、memmove增删,非常麻烦),所以数据的特性是读取快,存储不方便
链表:特点在于增删方便,查询慢(需要从头节点开始遍历查询),所以链表的特性是存储快,读取慢
散列表的本质就是一张哈希表,哈希表集合了数组和链表的长处,增删改查都比较方便.
- 可以通过哈希算法计算下标
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
objc_release
-
release
的调用流程objc_release
->objc_object::release()
->objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {//判断是否为nonpointer isa
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
// Try to remove some retain counts from the side table.
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
// Really deallocate.
// dealloc流程
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
-
release
的执行和retain
差不多,只不过一个是增加,一个是减少。release
减少到0时且散列表里面也没有时就会自动触发dealloc流程 - 1,先判断是否为
nonpointer isa
,如果为否则直接操作散列表减1; - 2,对
extra_rc
进行-1,并标识一个carry
字符,记录散列表是否还有计数。 - 3,如果
carry
标识有计数,就会执行underflow
流程,extra_rc
-到0之后就需要操作散列表,从散列表中拿出extra_rc
的一半继续进行-1操作,知道散列表和extra_rc
都为0,就会自动触发dealloc
流程
dealloc
-
dealloc
的调用流程dealloc
->_objc_rootDealloc
->objc_object::rootDealloc()
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
- 1,首先判断是否为
isTaggedPointer
,是则直接返回 - 2,判断是否有弱引用、关联对象、c++析构方法、引用计数表,如果都没有,直接
free
,如果有则进入object_dispose
方法
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);
if (assoc) _object_remove_assocations(obj);
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());
}
-
object_dispose
方法就是会依次执行C++析构函数(如果有)、移除关联对象(如果有)、
清空弱引用表、清空引用计数表,然后执行free
函数。
retainCount
- 我们先来看下面这段代码
- 我们创建的obj对象的引用计数为1,而我们知道在alloc里面并没有对引用计数的操作,所以我们来看获取
retainCount
的源码。
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
// 对引用计数的结果进行+1,并没有直接操作引用计数
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
- 可以看到是在获取
retainCount
对它的结果进行了+1处理,并没有对引用计数进行操作。
作者:xq113