iOS面试6 - 内存管理

内存布局

    1. 内存区域: 内核区(low) - 程序加载 - 保留(high)
    • 程序:
      • 未初始化数据(.bss)
      • 已初始化数据(.data)
      • 代码段(.text)
    • 栈(stack) : 方法函数(high->low), 方法的调用
    • 堆(heap) : 对象和block(low->high), 通过alloc分配的对象
    • 内存布局
      • bss: 未初始化的全局变量等
      • data: 已初始化的全局变量等
      • text: 程序代码

内存管理方案

iOS如果对内存进行管理? 针对不同场景, 进行不同的管理

    1. 小对象: TaggedPointer(NSNumber等)
    1. 64位对象: NONPOINTER_ISA(非指针型isa)
    • 第1位: 0-> 指针型isa; 1-> 非指针型
    • 第2位: has_assoc当前对象是否有关联对象(0: 无, 1: 有)
    • 第3位: has_cxx_dtor当前对象是否有使用到C++语言或者代码
    • 第4-36位: 保存当前对象的类对象的指针地址
    • 第37-42位: magic
    • 第43位: waekly_referenced标识当前对象是否有弱引用指针
    • 第44位: deallocating当前对象是否正在做dealloc操作
    • 第45位: has_sidetable_rc当前isa指针中所存储的引用计数已经达到上限的话
    • 第45-64位:extra_rc额外的引用计数
    1. 散列表: 是一个复杂的数据结构, 其中包含了引用计数表和弱引用表
    • SideTables()结构:
      • SideTables
    1. Side Table:
    • Side Table
    • 为什么不是一个Side Table?
      • 如果只有一个SideTable, 那么在不同线程的所有的数据都会访问这个表, 因此改表需要加锁, 而当所有数据都要访问的时候, 此时就会产生效率的问题, 所以此时系统引入了分离锁
    • 分离锁: 拆分为多个SideTable, 提高访问效率
    • 怎样实现快速分流?(我通过一个对象的指针, 如何快速的定位到它属于sideTables中的哪一张表)
      • SideTables的本质是一张Hash表
        • Hash查找
        • Hash查找的过程:
          • 哈希取余查找, 提高查找效率 - 防止遍历

数据结构

    1. 自旋锁(Spinlock_t)
    • 忙等的锁, 如果当前锁, 已经被其他线程获取, 那么当前现场会不断探测该锁有没有被释放, 如果释放了, 第一时间获取该锁
    • 适用于轻量访问
    • 是否使用过自旋锁, 自旋锁与普通锁有何区别, 自旋锁的使用场景?
      • !!!
    • 自旋锁与普通锁有何区别?
      • 正常的信号量 : 当获取不到锁的时候, 会把自己的线程阻塞休眠; 当其他线程用完的时候唤醒线程
    1. 引用计数表(RefcountMap)
    • hash查找: (插入和获取是通过同一个hash函数决定的, 避免了for循环遍历, 提高效率)
      • 屏幕快照 2019-02-18 下午7.37.07.png
      • RC引用计数值
    1. 弱引用表(weak_table_t)
    • 弱引用表的数据结构

ARC & MRC

  • MRC: 手动引用计数
    • MRC(红色为MRC特有方法)
  • ARC: 自动引用计数
    • 什么是ARC? ARC是LLVM和Runtime协作的结果
    • ARC中禁止手动调用retain, release, retainCount, dealloc
    • ARC中新增weak, strong属性关键字
      • weak变量为什么在自动释放的时候指向nil

引用计数管理

    1. alloc:
    • 经过一系列调用, 最终调用了C函数calloc,
    • 此时并没有应用计数+1(因为下面的refcnt-result=1, 所以调用alloc的时候retainCount是1)
    1. retain:
    • SIDE_TABLE_RC_ONE对应的偏移量
    • 我们在进行retain的时候, 系统是如何查找其对应的应用计数的?
      • 经过2次的hash查找, 找到对应的引用计数的值, 然后在进行相应的+1操作
    1. release实现
    • SIDE_TABLE_RC_ONE
    1. retainCount实现
    • alloc的时候it为空的, 因为refcnt_result = 1, 所以alloc的retainCount为1
    1. dealloc实现
    • 实现原理
      • 是否可以释放的判断标准: (以下都为no, 则C函数free())
      • nonpointer_isa : 判断当前对象是否使用了非指针型的isa, (非关系型会存储引用计数, 超过了则会使用has_sidetable_rc)
      • weakly_referenced : 判断当前对象是否有weak指针指向它
      • has_assoc : 判断当前对象是否有关联对象
      • has_cxx_dtor : 判断当前对象内部实现是否涉及到C++, 当前对象是否使用ARC管理内存; 若有即为YES
      • has_sidetable_rc : 判断当前对象的引用计数是否通过sidetable的引用计数表来维护的
    • object_dispose实现():

      -
      object_dispose流程
    • objc_destructInstance()实现:
      • objc_destructInstance流程
      • 通过关联对象的技术, 为一个类添加了一些实例变量, 在对象dealloc方法中, 是否有必要对其关联对象进行移除呢? (不需要, 系统会自动帮助我们移除的)
    • clearDeallocating()实现:
      • clearDeallocating内部实现

弱引用管理

    1. 一个weak变量是如何添加到弱引用表中的?
    • 一个__weak修饰的变量, 编译之后的样子
      • 添加weak变量流程
    1. weak变量是如何添加到弱引用表中 : 通过弱引用对象进行hash算法的计算, 然后计算查找期所对应的位置
    • 源码实现

      id
      objc_initWeak(id *location, id newObj)
      {
          if (!newObj) {
              *location = nil;
              return nil;
          }
      
          return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
              (location, (objc_object*)newObj);
      }
      
      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;
          }
      
          // Prevent a deadlock between the weak reference machinery
          // and the +initialize machinery by ensuring that no 
          // weakly-referenced object has an un-+initialized isa.
          if (HaveNew  &&  newObj) {
              Class cls = newObj->getIsa();
              if (cls != previouslyInitializedClass  &&  
                  !((objc_class *)cls)->isInitialized()) 
              {
                  SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
                  _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
      
                  // If this class is finished with +initialize then we're good.
                  // If this class is still running +initialize on this thread 
                  // (i.e. +initialize called storeWeak on an instance of itself)
                  // then we may proceed but it will appear initializing and 
                  // not yet initialized to the check above.
                  // Instead set previouslyInitializedClass to recognize it on retry.
                  previouslyInitializedClass = cls;
      
                  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);
              // weak_register_no_lock returns nil if weak store should be rejected
      
              // Set is-weakly-referenced bit in refcount table.
              if (newObj  &&  !newObj->isTaggedPointer()) {
                  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);
      
          return (id)newObj;
      }
      
      weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                            id *referrer_id, bool crashIfDeallocating)
      {
          objc_object *referent = (objc_object *)referent_id;
          objc_object **referrer = (objc_object **)referrer_id;
      
          if (!referent  ||  referent->isTaggedPointer()) return referent_id;
      
          // ensure that the referenced object is viable
          bool deallocating;
          if (!referent->ISA()->hasCustomRR()) {
              deallocating = referent->rootIsDeallocating();
          }
          else {
              BOOL (*allowsWeakReference)(objc_object *, SEL) = 
                  (BOOL(*)(objc_object *, SEL))
                  object_getMethodImplementation((id)referent, 
                                                 SEL_allowsWeakReference);
              if ((IMP)allowsWeakReference == _objc_msgForward) {
                  return nil;
              }
              deallocating =
                  ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
          }
      
          if (deallocating) {
              if (crashIfDeallocating) {
                  _objc_fatal("Cannot form weak reference to instance (%p) of "
                              "class %s. It is possible that this object was "
                              "over-released, or is in the process of deallocation.",
                              (void*)referent, object_getClassName((id)referent));
              } else {
                  return nil;
              }
          }
      
          // now remember it and where it is being stored
          weak_entry_t *entry;
          if ((entry = weak_entry_for_referent(weak_table, referent))) {
              append_referrer(entry, referrer);
          } 
          else {
              weak_entry_t new_entry;
              new_entry.referent = referent;
              new_entry.out_of_line = 0;
              new_entry.inline_referrers[0] = referrer;
              for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) {
                  new_entry.inline_referrers[i] = nil;
              }
              
              weak_grow_maybe(weak_table);
              weak_entry_insert(weak_table, &new_entry);
          }
      
          // Do not set *referrer. objc_storeWeak() requires that the 
          // value not change.
      
          return referent_id;
      }
      
      weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
      {
          assert(referent);
          weak_entry_t *weak_entries = weak_table->weak_entries;
          if (!weak_entries) return nil;
          size_t index = hash_pointer(referent) & weak_table->mask;
          size_t hash_displacement = 0;
          while (weak_table->weak_entries[index].referent != referent) {
              index = (index+1) & weak_table->mask;
              hash_displacement++;
              if (hash_displacement > weak_table->max_hash_displacement) {
                  return nil;
              }
          }   
          return &weak_table->weak_entries[index];
      }
      
    1. 当一个weak变量被释放或者废弃之后, 系统是如何处理的?
    • 清除weak变量, 同时设置指向为nil:
      • 清除weak流程
        • 源码分析: *referrer = nil;
          weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
          {
              objc_object *referent = (objc_object *)referent_id;
              weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
              if (entry == nil) {
                  /// XXX shouldn't happen, but does with mismatched CF/objc
                  //printf("XXX no entry for clear deallocating %p\n", referent);
                  return;
              }
          
              // zero out references
              weak_referrer_t *referrers;
              size_t count;
              
              if (entry->out_of_line) {
                  referrers = entry->referrers;
                  count = TABLE_SIZE(entry);
              } 
              else {
                  referrers = entry->inline_referrers;
                  count = WEAK_INLINE_COUNT;
              }
              
              for (size_t i = 0; i < count; ++i) {
                  objc_object **referrer = referrers[i];
                  if (referrer) {
                      if (*referrer == referent) {
                          *referrer = nil;
                      }
                      else if (*referrer) {
                          _objc_inform("__weak variable at %p holds %p instead of %p. "
                                       "This is probably incorrect use of "
                                       "objc_storeWeak() and objc_loadWeak(). "
                                       "Break on objc_weak_error to debug.\n", 
                                       referrer, (void*)*referrer, (void*)referent);
                          objc_weak_error();
                      }
                  }
              }
              
              weak_entry_remove(weak_table, entry);
          }
          

自动释放池

    1. 自动释放池内部实现:
    • 编译器将@autoreleasepool{}修改为上图
    1. objc_autoreleasePoolPush:
    • objc_autoreleasePoolPush内部流程
    1. objc_autoreleasePoolPop
    • 屏幕快照 2019-02-18 下午8.21.21.png
      • 如何理解一次pop相当于一次批量的pop操作: {}代码中所有的数据, 都会被pop一次
    1. AutoreleasePool的实现结构是怎么样的 / 什么是自动释放池?
    • 以栈为节点, 通过双向链表的形式组合而成的数据结构
      • 双向链表
      • 栈节点
    • 是与线程一一对应的
    1. AutoreleasePoolPage :
    • AutoreleasePoolPage内部结构
      • id * next : 指向栈当中下一个可填充的位置
      • AutoreleasePoolPage * const parent : 双向链表中的父指针
      • AutoreleasePoolPage * child : 双向链表中的子指针
      • pthread_t const thread; : 线程对象, 所以说autoreleasePool是与线程一一对应的
    • AutoreleasePoolPage的栈内存: 栈底保存自身占用的内存
      • AutoreleasePoolPage的占内存
    1. AutoreleasePoolPage::push:
    • AutoreleasePoolPage::push
      • 1> 将当前next指针指向nil
      • 2> 然后next指针指向下一个可入栈的内存地址(每次push的时候, 相当于不断的插入哨兵对象)
    1. [obj autorelease] :
    • autorelease流程
    • 栈内存的流程
    1. AutoreleasePoolPage::pop :
    • 根据传入的哨兵对象找到对应位置
    • 给上次push操作之后添加的对象一次发送release消息
    • 回退next指针到正确位置
    • 原始
    • 处理之后
    1. 关于自动释放池的面试
      1. viewDidLoad中创建一个array, 什么时候释放?
      • array何时释放呢?
        • 在每一次runloop的循环过程当中, 都会再其将要结束的时候, 对前一次创建的pool进行pop操作, 同时push进来新的pool; 所以创建的arr对象, 在当次runloop将要结束的时候调用AutoreleasePoolPage::pop(), 让arr对象调用release方法, 进行释放
      1. AutoreleasePool中为何可以嵌套使用?
      • 多层嵌套就是多次插入哨兵对象
      1. autoreleasePool的使用场景: 在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool
      1. 自动释放池的双向链表应用体现在哪里?

循环引用

    1. 循环应用的分类:
    • 自循环引用:
    • 相互循环应用:
      • 自循环引用
    • 多循环引用:
      • 大环多循环引用
    1. 循环引用的场景
    • 代理
    • block
    • NSTimer
    • 大环引用
    1. 如何破除循环引用?
    • 避免产生循环应用
    • 在合适的时机手动断环
    1. 具体的解决方案?
    • __weak:
      • __weak破解
    • __block:
      • 在ARC下, __block修饰的对象会被强引用, 无法避免循环应用, 需要手动破环
      • 在MRC下, __block修饰的对象不会增加其应用计数, 避免了循环引用
    • __unsafe__unretained : 与__weak在效果上是等效的
      • 修饰对象不会增加其应用计数, 避免了循环引用
      • 如果被修饰对象在某一时机被释放, 会产生悬垂指针, 导致内存泄漏!
    1. 循环引用NSTimer示例:
    • 场景: 页面中有一个广告栏, 定时滚动播放动画, VC强引用广告栏, 广告栏添加NSTimer, NSTimer会对Target进行强引用(即VC), 此时就会产生了循环应用; 若把对象弱引用NSTimer也无用, 因为NSTimer会被当前的RunLoop进行强引用, NSTimer对对象的强引用, 当VC退出的时候, 对象和NSTimer被RunLoop持有, 也不会被释放
    • 屏幕快照 2019-02-18 下午8.46.42.png
    • NSTimer分为重复定时器和非重复定时器
      • 重复定时器: 添加一个中间对象, RunLoop --> NSTimer <-=> 中间对象 ->对象 <--VC
        • 添加中间对象, 利用了一个对象被释放的时候, 其指针会置为nil
        • 代码实现:
          #import "NSTimer+WeakTimer.h"
          
          @interface TimerWeakObject : NSObject
          @property (nonatomic, weak) id target;
          @property (nonatomic, assign) SEL selector;
          @property (nonatomic, weak) NSTimer *timer;
          
          - (void)fire:(NSTimer *)timer;
          @end
          
          @implementation TimerWeakObject
          
          - (void)fire:(NSTimer *)timer
          {
              if (self.target) {
                  if ([self.target respondsToSelector:self.selector]) {
                      [self.target performSelector:self.selector withObject:timer.userInfo];
                  }
              }
              else{
                  [self.timer invalidate];
              }
          }
          
          @end
          
          @implementation NSTimer (WeakTimer)
          
          + (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                                   target:(id)aTarget
                                                 selector:(SEL)aSelector
                                                 userInfo:(id)userInfo
                                                  repeats:(BOOL)repeats
          {
              TimerWeakObject *object = [[TimerWeakObject alloc] init];
              object.target = aTarget;
              object.selector = aSelector;
              object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
              
              return object.timer;
          }
          
          @end
          
      • 非重复定时器: 在定时器的回调方法中调用invalid方法, 使timer置为nil, 即可以解除Runloop对timer的强引用, 同时也可以解除timer对对象的强应用, 此时即可解除循环引用

内存管理面试总结:

  • 什么是ARC?
    • ARC是由LLVM编译器和Runtime共同协作来实现对自动引用计数的管理
  • 为什么weak指针指向的对象那个在废弃之后会被自动置为nil?
    • 当对象被废弃之后, dealloc方法的内部实现当中, 会调用清除弱引用的方法, 在清除弱引用的方法中, 会通过hash算法, 来查找被废弃对象在弱引用表中的位置, 来提取其对应弱引用指针的列表数组, 然后进行for循环遍历, 把每一个weak指针都置为nil
  • 苹果是如何实现AutoreleasePool的?
    • AutoReleasePool是以栈为结点, 由双向链表形式来合成的数据结构来实现的
  • 什么是循环引用? 你遇到过哪些循环引用, 是怎样解决的?
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容