iOS底层探索--内存管理
1. 五大分区
在一个4G
内存的移动设备中,内核区约占1GB
。
内存分区:代码段、数据段、BSS段,栈区,堆区。栈区地址一般为0x7
开头,堆区地址一般为0x6
开头。数据段一般0x1
开头。
0x70000000
对其进行转换,刚好为3GB
- 栈区:存储函数,方法,快速高效,
- 堆区:通过alloc分配的对象,
block copy
,灵活方便,数据适应面广泛, - BSS段:未初始化的全局变量,静态变量,程序结束后有系统释放。
- 数据段:初始化的全局变量,静态变量,程序结束后有系统释放。
- 代码段:程序代码,加载到内存中
栈的内存是访问寄存器直接访问其内存空间,堆里的对象的访问是通过存在栈区的指针存储的地址,再找到堆区对应的地址。
全局变量和局部变量在内存中是否有区别?有什么区别?
- 存储位置不同,全局变量存在相应的全局存储区域。局部变量定义在局部的空间,存储在栈中
Block中是否可以直接修改全局变量
Block
中可以修改全局变量。
全局静态变量的修改
在LGPerson
中,定义一个全局变量personNum
,并定义两个方法,对全局变量++
,然后在ViewController
调用打印,结果是什么样的?
static int personNum = 100;
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
- (void)run;
+ (void)eat;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)run{
personNum ++;
NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}
+ (void)eat{
personNum ++;
NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}
- (NSString *)description{
return @"";
}
@end
NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[LGPerson eat]; // 102
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson alloc] cate_method];
打印结果如下:
static
修饰的静态变量,只针对文件有效。在vc中和LGPerson中的两个全局变量的地址不相同。
2. TaggedPointer
使用TaggedPointer存储小对象NSNumber、NSDate
,优化内存管理。
首先,看下面的代码,能正常执行么?点击屏幕时会有什么问题?
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"和谐学习不急不躁"];
NSLog(@"%@",self.nameStr);
});
}
}
通过测试,上述代码,能正常执行,当点击屏幕时,发生崩溃。那么为什么在点击屏幕是,会发生崩溃呢?
其实在多线程代码块中赋值,打印,是调用的setter
和getter
,当setter
和getter
加入多线程时,就会不安全。
在setter
方法底层是retian newvalue
,然后realase oldvalue
。多加入多线程时,就会出现多次释放,造成野指针。
那么,为什么第一段能够正常执行呢?
通过上面的断点调试,发现第一段代码中_nameStr
的类型并不是NSString
而是taggedPointer
类型,而第二段中是NSString
类型。
接下来看一下objc_release
和objc_retain
的源码:
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
在release
时,先判断是否是isTaggedPointer
,是,则直接返回,并没有真的进行release
操作,而在retain
时也是同样的操作,先判断isTaggedPointer
,并没有进行retain
,这也就解释了为什么第一段代码能正常执行,因为其底层并没有retain
和release
,即使搭配多线程,也不会出现多次释放的问题,也就不会出现野指针,也不会崩溃。
其实在read_images
中的的initializeTaggedPointerObfuscator()
中,会初始化一个objc_debug_taggedpointer_obfuscator
,在构造TaggedPointer
时,通过对这个值的^
操作,进行编码和解码。
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 {
// 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;
}
}
在initializeTaggedPointerObfuscator
中,在iOS之前低版本时,objc_debug_taggedpointer_obfuscator = 0,之后的版本objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK
在构建TaggedPointer
时,会进行编码,在获取TaggedPointer
时,解码。
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
// ✅ 返回一个编码的值
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
_objc_getTaggedPointerTag(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
// ✅ 解码,然后进行一系列偏移运算,返回
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t extTag = (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
} else {
return (objc_tag_index_t)basicTag;
}
}
其实编码和解码的操作就是与上objc_debug_taggedpointer_obfuscator
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
/**
1000 0001
^0001 1000
1001 1001
^0001 1000
1000 0001
*/
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
我们可以调用解码方法,来打印一下具体的TaggedPointer
:
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));
打印结果:
上图打印结果中,0xb000000000000012
,b
表示数字,1
就是变量的值。
不同类型的标记:
{
// 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
};
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再 是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储 在堆中,也不需要malloc
和free
,也不用retain
和release
在内存读取上有着3倍
的效率,创建时比以前快106倍
,一般一个变量的位数在8-10位时,系统默认会使用Tagged Pointer
3.NONPOINTER_ISA的优化
通过对NONPOINTER_ISA
64个字节位置的存储,来内存管理。
isa
结构:
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
};
-
nonpointer:表示是否对
isa
指针开启指针优化0:纯isa指针,1:不止是类对象地址,
isa
中包含了类信息、对象的引用计数当对象引用技术大于 10 时,则需要借用该变量存储进位等 -
has_assoc:关联对象标志位,0没有,1存在
-
has_cxx_dtor:该对象是否有
C++
或者Objc
的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
-
magic :用于调试器判断当前对象是真的对象还是没有初始化的空间
-
weakly_referenced:标志对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放 -
deallocating:标志对象是否正在释放
-
has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
-
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么
extra_rc
为 9。如果引用计数大于 10, 则需要使用到下面的has_sidetable_rc
。
3. retain & release & retainCount & dealloc分析
retain 和 release 分析
首先我们看一下retain
的源码:
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
从源码中可以看出,在retain
时,先判断是否是isTaggedPointer
,是则直接返回,不是,则开始retain
。
最终进入到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;
// retain 引用计数处理
//
do {
transcribeToSideTable =