iOS-- 内存管理
手动目录
- 内存分布及存储
静态变量安全- taggedPointer
特点
taggedPointer 演变
taggedPointer 存储方式
引用计数处理方式
alloc 出来的对象引用计数
dealloc 干了什么
内存分布及存储
内存分布为什么堆区比栈区的访问速度慢?
栈区是寄存器直接读取。
堆区的访问,是寄存器先读取栈区的指针地址,然后通过这个地址去堆区找到相应的数据。
栈区内存地址:一般0x7开头
堆区内存地址:一般0x6开头
数据段、BSS段地址:一般0x1开头
// 全局变量、全局静态变量 初始化的在常量区(.data) ,未初始化的在静态区(.bss)
int clA; //静态区
int clB = 10; //常量区
static int bssA; //静态区
static NSString *bssStr1; //静态区
static int bssB = 10; //常量区
static NSString *bssStr2 = @"name"; //常量区
- (void)testStack{
int a = 10; // 栈区
int b = 20; // 栈区
NSObject *object = [NSObject new]; // *obj 栈区 , obj 堆区
NSString *str = @"aaa"; // *str 栈区 , str 常量区(.data)
NSString *str1; // *str1 栈区 , str1 0x0
}
静态变量安全
如下代码
// Person 类 定义一个静态变量 personHei
static int personHeig = 180;
@interface Person : NSObject
- (void)growUp;
@end
@implementation Person
- (void)growUp {
personAge = 30;
NSLog(@"person age = %d %p",personAge,&personAge); // person age = 30 0x10be0fe10
}
@end
// 在另外一个类中区访问并修改
- (void)task8 {
NSLog(@"person age = %d %p",personAge,&personAge); // person age = 18 0x10be0fc18
personAge = 30;
NSLog(@"person age = %d %p",personAge,&personAge); // person age = 30 0x10be0fc18
[[Person new] growUp];
personAge ++;
NSLog(@"person age = %d %p",personAge,&personAge); // person age = 31 0x10be0fc18
}
//最后打印结果:
person age = 18 0x10be0fc18
person age = 30 0x10be0fc18
person age = 30 0x10be0fe10
person age = 31 0x10be0fc18
对于静态全局变量,对比内存地址和值,我们发现:
在同一个文件中,访问的全局静态变量 地址相同,而且可以正常的修改
,
而对于不同文件中的全局静态变量 地址是不同的,而且修改是相互不影响
。
personAge 在Person类中,地址和在另外一个类中的地址不同,而且修改之后相互不影响。
taggedPointer
taggedPointer是干嘛用的? 用于优化内存的
。
特点
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,
它的内存并不存储在堆中,也不需要 malloc 和 free
。不需要引用计数处理,与系统自动回收 - 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
在iPhone5s之前,苹果系统是32位,而5s出来之后,是64位,
在32位系统中,小对象(比如NSNumber、NSData)用8位去存储值都能满足大部分情况,如果还用这8位去存储一个地址指针,就太浪费内存了。苹果为了优化这个问题,而引入taggedPointer
的方式。
简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。
taggedPointer 演变
-
早期
taggedPointer 的地址 直接存储值 比如 Number = @(1)的对象,其存储形式为:
__NSCFNumber, 0xb000000000000013
参考文章- (void)task { NSNumber *number1 = @1; NSNumber *bigNumber = @(0x7fffffffffffff + 1); // 14位 NSLog(@"number1 pointer is %p", number1); NSLog(@"bigNumber pointer is %p", bigNumber); } // 打印结果 number1 pointer is 0xb000000000000012 bigNumber pointer is 0x10921ecc0
-
优化
在10.14之后,又进行了一次优化:
在 Objc源码中有这样一段:
在程序启动的源码中:objc_init() -> map_images() -> read_image()
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 { // 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; } } // taggedPointer 对象进行编码解码的过程中,进行了异或运算 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; }
优化之后要打印出实际指针信息 ,采用以下方式:
#import <objc/runtime.h> extern uintptr_t objc_debug_taggedpointer_obfuscator; uintptr_t _objc_decodeTaggedPointer_(id ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; } NSNumber *number1 = @(1); NSLog(@"%@-%p---%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1)); // 打印结果 __NSCFNumber-0xcdfb8d4002f69cf0---1 - 0xb000000000000012
taggedPointer 存储方式
最后一位表示的含义:
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,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
引用计数处理方式
如何维护引用计数呢?
在之前的的 isa结构探究 中 提到 isa结构中,有引用计数的记录。
extra_rc :19
当表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9.如果引用计数大于10,则需要使用上面提到的has_sidetable_rc。
那么在对象进行retain的时候, 那么具体是进行了什么操作呢?
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj; // ⚠️ 如果是 taggedPointer ,就不进行retain
return obj->retain();
}
ALWAYS_INLINE id
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)) { // 如果不是 nonpoint isa 直接存散列表
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++ // 如果是nonpoint isa 就进行isa 的extra_rc++ 操作
if (slowpath(carry)) { // 如果isa的 extra_rc 超过 容量,
// 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; // isa 中的 extra_rc 保存一半
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;
}
引用计数的处理
retain
- 如果不是 nonpoint isa 直接加入散列表
- 如果是 nonpoint isa 在isa的 extra_rc 进行++操作
如果超过容量,isa 中的 extra_rc 保存一半 散列表中的引用计数表 存一半引用计数的处理
release
- 如果不是 nonpoint isa 直接对散列表进行操作
- 如果是 nonpoint isa 在isa的 extra_rc 进行-- 操作
如果减到最后没了,看有没有相应的散列表的引用计数表,如果有,把引用计数表的计数赋值给extra_rc。
补充:
散列表有多张(据说最多64张),每张散列表维护 三张表,多表结构的目的是为了:安全、高效
。对表进行操作,需要加锁/解锁,同时可能由多个任务需要加锁/解锁。所以用多表可以提高查询速度、提高执行速度。
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; //引用计数表
weak_table_t weak_table; // 弱引用表1
}
alloc 出来的对象引用计数
这也是一个面试题:alloc出来的对象,其引用计数是多少?
答案是0。
在之前的iOS底层-alloc与init 中详细的说了alloc的过程,在此过程中,我们并没有看到任何与引用计数有关的内容,也就是说,并没有操作引用计数,所以为0 。
但是 为什么通过打印的方式得到的是1 呢?
printf("Retain Count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(obj)));
通过源码来看:
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) {
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();
}
打印出来的引用计数值的又来: 1 + isa.extra_rc + sidetable_rc(散列表的引用计数)
既然打印结果为1,那么 其 isa.extra_rc 必然为0, 所以其真正的引用计数为0。
为什么默认要给个1呢,因为 它要被autorelease(自动释放池)所持有。
dealloc 干了什么
看源码:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return;
object_dispose((id)this);
}
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(); // 非 nonpointer 散列表清空
}
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());
}
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this); // 清空弱引用表中的弱引用对象
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this); // 清空散列表中的引用计数表的引用计数
}
table.unlock();
}
dealloc 干了什么
- 清空关联对象
- 清空 弱引用对象
- 清空 散列表中的引用计数表