在iOS里有值类型和对象,值类型不需要我们内存管理。引用类型需要我们内存管理是为什么呢。因为值类型是放在栈里,受系统管理,栈是一块连续的内存空间,引用类型是放在堆里的。
内存5大区:
堆区 -- 通过alloc new malloc创建的对象存在堆区,需要程序员管理内存
栈区 -- 一般存储局部变量 方法参数 对象的指针,栈一般是以0x7开 头的
全局区(静态区)存全局变量\静态变量一般是 0x1开头,编译时候进行内存分配
常量区 -- 常量 一般是0x1开头,编译时候进行内存分配
代码区 -- 编译生成的二进制代码
在栈区一般主线程如果大于1M就会奔溃,会出现内存溢出,其他线程512k。栈是线程独有的空间,。
下面我们定义了一个double,这个是存在栈里的,因为达到1m所以奔溃了
什么是引用计数:管理对象生命周期的方式,通过retain,release,autorelease
iOS内存管理方案:
1.nonpointperISA (之前有讲到)
2.tagged point
3.sidetable(散列表)
newisa.extra_rc = 1这个是引用计数,isa其有8x8字节存内存地址,但是存内存地址也用不到这么多,所以其中一些是存引用计数
#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;
}
下面是nonpointperISA介绍
下面是tagged point介绍
是针对小对象类型,其实其也是一个指针,只是这个指针被打上tagged,tagged point存着的不再是地址,而是真正的值,3倍的空间效率,106倍的访问速度。
下面可以看到我们创建字符串有多种方式,通过第一种和第三种创建的字符串是__NSCFConstantString,这个是存在常量区。通过stringWithFormat创建的,而且后面字符小于9的话就会存放在NSTaggedPointer里。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *firstString = @"helloworld";
NSString *secondString = [NSString stringWithFormat:@"helloworld"];
NSString *thirdString = @"hello";
NSString *fourthSting = [NSString stringWithFormat:@"hello"];
NSLog(@"%p %@",firstString,[firstString class]);
NSLog(@"%p %@",secondString,[secondString class]);
NSLog(@"%p %@",thirdString,[thirdString class]);
NSLog(@"%p %@",fourthSting,[fourthSting class]);
}
Tagged Pointer
Tagged Pointer:在总是为0的位置打上了标记的指针,这个指针存放的是真正 的值。
Tagged Pointe会在程序启动时候进行随机值混淆,在intel最低位为1,arm最高位1
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr)这个是进行代码混淆的。其混淆是在下面方法进行操作的。
下面的是对Tagged Pointer进行解混淆。
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
return value;
}
在源码里不同的OBJC_TAG代表不同的类型,如下所示。
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,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
我们OBJC_TAG_NSNumber来验证下,Tagged Pointer里后三位存的是不同类型值,我们看到下面的是011,这里表示的就是3,也就是OBJC_TAG_NSNumber类型,上面我们确实也是定义的是NSNumber类型。
我们来看个关于Tagged Pointer的题目
下面这个代码在运行的时候会发生奔溃,这个是什么原因呢,当我们把标号3里代码注释掉是不会奔溃的,因为标号2里stringWithFormat创建的是一个Tagged Pointer,其存在指针里面,在栈的空间,系统会自己进行管理。标号3里是存的中文,因为其是异步的,所以当一个空间已经release掉了,再发起release就会奔溃了。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.queue = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<100000; i++) {//标号2
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"HPW"];
NSLog(@"%@",self.nameStr);
});
}
for (int i = 0; i<100000; i++) { //标号3
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"好好学习"];
NSLog(@"%@",self.nameStr);
});
}
}
retain原理
下面代码可以看出,当其为TaggedPointer的时候其不会进行objc_retain的插入,而是直接返回。
#if __OBJC2__
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (_objc_isTaggedPointerOrNil(obj)) return obj;
return obj->retain();
}
isa指针存放引用计数方式
这个是Retain的核心代码,首先获取对象的isa指针。下面存储对象的引用计数分为三种情况,如果不是nonpointerisa就会存在sidetable里。当是nonpointerisa里,extrc_rc能够存的下,就会直接存在extrc_rc里。当是nonpointerisa里,extrc_rc存不下的时候,就会借sidetable来存储,但是这个存储在sidetable也只会存一半,这个提高效率。
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
Retain总结
release原理
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
sidetable_release();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
release核心代码是下面的do while循环如下:release的过程也就是retain过程的逆向,
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// 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(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
Release总结
dealloc原理
当满足下面的&&条件时候就会调用free(this)进行释放,isa.has_assoc就是关联对象,isa.has_cxx_dtor就是是否有cxx的构造对象。
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);
}
}
object_dispose会调用如下的方法objc_destructInstance,下面的object_cxxDestruct是释放成员变量的。
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, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
dealloc总结
SideTable内存管理
首先其有一把spinlock_t锁,这个spinlock_t锁是保证线程安全的,其是一把互斥锁。RefcountMap这个是用用来存储引用计数的,weak_table就是一个弱引用表。这里有个问题,当每次都进行加锁和解锁的话这样的效率会很低,那苹果是怎样去处理的呢?苹果处理是其添加了多个的表,其中用到了StripedMap,提高了效率。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
StripedMap如下所示,其在真机环境里创建了64个表提高效率。其是通过indexForPointer这个函数里的算法快速均匀的找到对应的StripedMap。模拟器是8个表,真机上是64个表。每个StripedMap里有多个sideTable, StripedMap是存放sideTable的容器。
template<typename T>
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;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
weak_table是一个弱引用表
我们通过下面方式进行打印其引用计数,第一个打印P的引用计数为1很好理解,第二个用__weak修饰weakP为什么是2呢,那我们探究下这个__weak在底层干了啥?
我们在__weak处打一个断点,然后点击进去进入到如下图方法,其中工newObj也就是p对象,location代表的是前面的weak指针地址。
再点击storeWeak进去,HaveOld 是指weak指针是否指向了一个弱引用,HaveNew 是指weak指针是否需要指向一个新的引用, CrashIfDellocating是指引用的对象是否析构
下面的就是判断是否有haveOld,有的话就把weak的指针地址赋值给oldobj,然后创建sidetables表,如果有haveNew同理。 weak_unregister_no_lock(&oldTable->weak_table, oldObj, location)这个是当weak指向新的表时候,就会把之前的老表移除掉通过这个语句。通过 weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating)进行创建新表并存储。
weak_register_no_lock
weak_register_no_lock是添加弱引用,判断对象是否为nil或者是否是tagged point,如果是返回,如果不是,判断对象是否可用,不可用抛出异常。如果可用,通过weak_entry_for_referent从弱引用表里面找到weak_entry_t,插入weak指针地址,如果没有找到,新建一个weak_entry_t
weak_unregister_no_lock
weak_unregister_no_lock是删除弱引用,在弱引用表里面找到weak_entry_t,从weak_entry_t移除引用,判断weak_entry_t是否为空,如果为空,把weak_entry_t从弱引用表里面移除。
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
上面storeWeak的方法作用:
如果weak指针之前指向了一个弱引用,它就会调用这个weak_unregister_no_lock将weak指针地址进行移除。如果这个weak指针指向了一个新的引用,它就会调用weak_register_no_lock将weak指针地址添加到对应对象的弱引用表,然后调用setWeaklyReferenced_nolock,把修饰isa的weakly_referenced置为YES。
接着我们看下弱引用表, weak_entry_t *weak_entries 这里是一个数组 ,我们弱应用对象就是存在这里面的。
struct weak_table_t {
weak_entry_t *weak_entries;这里是一个数组
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
weak_entry_t的源代码如下,当对象的弱引用个数小于等于4的时候就会使用标记2里struct 结构体进行存储,大于4就会使用标记1里struct 结构体进行存储。out_of_line是判断用的哪种方式存储。
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {//标记1
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};//标记1
struct {//标记2
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};//标记2
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
sideTable与weakTable与entrt_t以及weak的关系
每个对象对应一个sideTable,sideTable里有个weakTable,weakTable里有个entrt_t的数组,其中弱引用weak就是存放在entrt_t里的。
上面讲清了sideTable,weakTable的原理,下面我们继续上面那个问题进行探讨,第二个用__weak修饰weakP为什么是2呢?,打断点调试后,然后用汇编指令操作,发现其会走到objc_loadWeakRetained这个地方,我们在源码里进行搜索,然后进行定位,其最终会走到rootretain这个地方,所以引用计数会有所增加,但是这个是一个假象,因为其在rootretain对应函数里的作用域里,但是出了这个作用域就会变为原来的值,那我们再打印一下就知道了,还是1,所以说之前的是假像。如下图: