内存管理

在Obj-C中,如果检测内存泄漏呢?有哪些方式?

目前我们可以通过以下几种方式:
1、Menory Leaks
2、Alloctions
3、Analyse
4、Debug Memory Graph
5、MleakFinder

泄漏的内存主要分为两种
1、Laek Menory 没有进行释放导致内存泄漏
2、Abandon Memory 循环引用导致无法释放引起的内存泄漏

内存管理方案

1、使用taggedPoint 对小对象存储如NSNumber,NSDate,NSString 进行优化。
2、nonpointer isa 实际上class内存地址32位就够用了,为了提高使用率,会存储一些其他信息,包括小型的引用计数
3、散列表 isa 中有一个 has_sidetable_rc 参数,表明引用计数是否存储在 SdieTable表中,SideTable中其实有很多哈希表,如果只有一个表,引用计数的存储都需要枷锁解锁的过程,就会导致效率很低,为了提高效率,苹果引入和分离所的方案,把SideTable拆分成很多表,这样多个对象同时访问不同的表。

1、taggedPoint

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
iOS系统是判断指针最高有效位是不是1来判断是不是Tagged Pointer存储的

2、nonpointer isa

isa占用64位,实际上存储class内存地址32位就够用了,位了提高使用效率,还会存储一些其他的信息,包含消息的引用计数

3、散列表

isa中 has_sidetable_rc, 如果为1,应用计数就存储在SideTables表中,SideTable表其实是很多个哈希表,如果只有一张表,引用计数存储都会进行加锁解锁,这样就会导致效率很低,所以苹果使用分离锁的方案,把sideTable拆分成很多个表,这样多个对象可以同时访问不同的表,提高了访问效率。

#SideTable的结构,部分结构
struct SideTable {
  spinlock_t slock; // 自旋锁
  RefcountMap refcnts;//引用计数表
  weak_table_t weak_table; // 若引用表
}

retainCount的的底层实现

- (NSUInterger)retainCount {
  return ((id)self)->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount() {
  // 如果是 TaggedPointer 类型直接返回
  if(isTaggedPointer()) return (uintptr_t)this;
  sidetable_lock();
  isa_t bits = LoadExclusive(&sia.bits);
  ClearExclusive(&isa.bits);
  if {
    // 存储isa中的 extra_rc+1
    uintptr_t rc = 1 + bits.extra_t;
    if(bits.has_sidetable_rc) {
      rc += sidetable_getExtraRC_nolock();
    }
    sidetable_unlock();
    return rc;
  }
  sidetable_unlock();
  // 指针类型的返回这个
  return sidetable_retainCount();
}

看一下sidetable_getExtraRC_nolock这个内部实现

size_t
objc_objcet::sidetable_getExtraRC_nolock () {
  assert(isa.nonpointer);
  // 从SideTables 中取出 SideTable
  SideTable& table = SideTables()[this];
  //同this 这个key查找这个遍历器
  RefcountMap::iterator it = table.refcnts.find(this);
  //找不到就返回0
  if (it == table.refcnts.end()) return 0;
  //找到it -> second向右偏移2位得到值的返回
  else return it->second >> SIDE_TABLE_RC_SHIFT
}

从底层源码上看,采用 nonpointer 存储的引用计数的值实际上就存储在isa中的 extra_rc+1+SideTable

#指针类型的直接sideTable存储引用计数
objc_object::sidetable_retainCount()
{
  SideTable& table = SideTables()[this];
  size_t refcnt_result = 1; 
  table.lock();
  RefcountMap::iterator it = table.refcnts.find(this);
  if (it != table.refcnts.end()) {
      // this is valid for SIDE_TABLE_RC_PINNED too
    refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
  }
  table.unlock();
  return refcnt_result;
}

看下retain的底层实现

- (id)retain {
  return ((id)self)->rootRetain();
}
objc_object::rootRetain()
{
  return rootRetain(false, false);
}
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;
      //读取操作原子化,根据 CPU 不同实现不同,比如在 x86-64 上就是单纯的直接返回值,而在 arm64 上则使用了 ldxr 指令,获取isa.bits 的值
      oldisa = LoadExclusive(&isa.bits);
      newisa = oldisa;
      //isa是指针类型
      if (slowpath(!newisa.nonpointer)) {
          //asm("clrex" : "=m" (*dst));
          ClearExclusive(&isa.bits);
          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);
              //又调用一下自己的放rootRetain(tryRetain, true);,把处理过溢出标记为true
              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;
        #留下一半的引用计数,然后把另一半放sideTable里面去
      }
  } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
  //则将一半的引用计数加sideTable refcnts里面
  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;
}
#把引用计数存储在sideTable的RefcountMap里面,这里才是我们关注的重点
id objc_object::sidetable_retain()
{
  #if SUPPORT_NONPOINTER_ISA
  assert(!isa.nonpointer);
  #endif
 SideTable& table = SideTables()[this];
  table.lock();
  size_t& refcntStorage = table.refcnts[this];
  if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
      refcntStorage += SIDE_TABLE_RC_ONE;
  }
  table.unlock();
  return (id)this;
}
#while循环里面做的事情,不懂汇编,看着像把newisa.bit的值跟isa.bit的值做了对比修改
static ALWAYS_INLINE
bool 
StoreExclusive(uintptr_t *dst, uintptr_t oldvalue __unused, uintptr_t value)
{
  uint32_t result;
  asm("stxr %w0, %x2, [%x3]" 
    : "=r" (result), "=m" (*dst) 
    : "r" (value), "r" (dst));
  return !result;
}

指针类型的isa,引用计数直接存储在sideTable中的refcntStorage += SIDE_TABLE_RC_ONE
非指针类型的isa是先把引用计数存储在isa的extra_rc里面的,这里还会加之前还会先判断一下是否溢出,如果extra_rc ++要溢出的话就拿出extra_rc一半的引用计数存在sideTable的refcnts中,并且把extra_rc的值改成一半,这样下次引用计数仍旧可以存在extra_rc里面。

release的底层实现

- (oneway void)release {
  ((id)self)->rootRelease();
}
ALWAYS_INLINE bool 
objc_object::rootRelease()
{
  return rootRelease(true, false);
}
ALWAYS_INLINE bool 
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;
    //指针类型sidetable_release
    if (slowpath(!newisa.nonpointer)) {
        ClearExclusive(&isa.bits);
        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);
        //再执行rootRelease一次,处理下溢出
        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;
    }
//下面是从 sideTable 借 RC_HALF 的引用计数放到 extra_rc 上, 借不到的情况,对象需要被销毁了
    // 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.
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();
__sync_synchronize();
if (performDealloc) {
    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}

//指针类型的sidetable_release
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
    do_dealloc = true;
    table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
    // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
    do_dealloc = true;
    it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
    it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc  &&  performDealloc) {
    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

从sideTable的refcnts借RC_HALF过程

size_t 
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
  assert(isa.nonpointer);
  SideTable& table = SideTables()[this];

  RefcountMap::iterator it = table.refcnts.find(this);
  if (it == table.refcnts.end()  ||  it->second == 0) {
      // Side table retain count is zero. Can't borrow.
      return 0;
  }
  size_t oldRefcnt = it->second;

  // isa-side bits should not be set here
  assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
  assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

  size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
  assert(oldRefcnt > newRefcnt);  // shouldn't underflow
  it->second = newRefcnt;
  return delta_rc;
}
总结一下release操作

指针类型的isa还是根据this取出sideTable的refcnts,进行一堆判断,可以销毁的就标记为do_dealloc,最后判断do_dealloc和performDealloc同时为true,然后发送销毁消息
非指针类型isa,先让extra_rc--,如果--后下溢出,就去sideTable中取出RC_HALF,能借到就把借到的值赋值给extra_rc,然后-1保存,借不到的话就发送销毁消息了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352