内存布局

  • 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
            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是为了节省内存,和提高执行效率的,Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree,在内存读取上有着3倍的效率,创建时比以前快106倍。
    这个是iOS交流圈,:iOS技术交流 分享BAT,阿里面试题、面试经验,讨论技术,可以一起交流学习。进群回复简书就好;

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
链接:https://juejin.cn/post/6901555021175848973