iOS面試6 - 內(nèi)存管理

內(nèi)存布局

    1. 內(nèi)存區(qū)域: 內(nèi)核區(qū)(low) - 程序加載 - 保留(high)
    • 程序:
      • 未初始化數(shù)據(jù)(.bss)
      • 已初始化數(shù)據(jù)(.data)
      • 代碼段(.text)
    • 棧(stack) : 方法函數(shù)(high->low), 方法的調(diào)用
    • 堆(heap) : 對(duì)象和block(low->high), 通過alloc分配的對(duì)象
    • 內(nèi)存布局
      • bss: 未初始化的全局變量等
      • data: 已初始化的全局變量等
      • text: 程序代碼

內(nèi)存管理方案

iOS如果對(duì)內(nèi)存進(jìn)行管理? 針對(duì)不同場(chǎng)景, 進(jìn)行不同的管理

    1. 小對(duì)象: TaggedPointer(NSNumber等)
    1. 64位對(duì)象: NONPOINTER_ISA(非指針型isa)
    • 第1位: 0-> 指針型isa; 1-> 非指針型
    • 第2位: has_assoc當(dāng)前對(duì)象是否有關(guān)聯(lián)對(duì)象(0: 無, 1: 有)
    • 第3位: has_cxx_dtor當(dāng)前對(duì)象是否有使用到C++語言或者代碼
    • 第4-36位: 保存當(dāng)前對(duì)象的類對(duì)象的指針地址
    • 第37-42位: magic
    • 第43位: waekly_referenced標(biāo)識(shí)當(dāng)前對(duì)象是否有弱引用指針
    • 第44位: deallocating當(dāng)前對(duì)象是否正在做dealloc操作
    • 第45位: has_sidetable_rc當(dāng)前isa指針中所存儲(chǔ)的引用計(jì)數(shù)已經(jīng)達(dá)到上限的話
    • 第45-64位:extra_rc額外的引用計(jì)數(shù)
    1. 散列表: 是一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu), 其中包含了引用計(jì)數(shù)表和弱引用表
    • SideTables()結(jié)構(gòu):
      • SideTables
    1. Side Table:
    • Side Table
    • 為什么不是一個(gè)Side Table?
      • 如果只有一個(gè)SideTable, 那么在不同線程的所有的數(shù)據(jù)都會(huì)訪問這個(gè)表, 因此改表需要加鎖, 而當(dāng)所有數(shù)據(jù)都要訪問的時(shí)候, 此時(shí)就會(huì)產(chǎn)生效率的問題, 所以此時(shí)系統(tǒng)引入了分離鎖
    • 分離鎖: 拆分為多個(gè)SideTable, 提高訪問效率
    • 怎樣實(shí)現(xiàn)快速分流?(我通過一個(gè)對(duì)象的指針, 如何快速的定位到它屬于sideTables中的哪一張表)
      • SideTables的本質(zhì)是一張Hash表
        • Hash查找
        • Hash查找的過程:
          • 哈希取余查找, 提高查找效率 - 防止遍歷

數(shù)據(jù)結(jié)構(gòu)

    1. 自旋鎖(Spinlock_t)
    • 忙等的鎖, 如果當(dāng)前鎖, 已經(jīng)被其他線程獲取, 那么當(dāng)前現(xiàn)場(chǎng)會(huì)不斷探測(cè)該鎖有沒有被釋放, 如果釋放了, 第一時(shí)間獲取該鎖
    • 適用于輕量訪問
    • 是否使用過自旋鎖, 自旋鎖與普通鎖有何區(qū)別, 自旋鎖的使用場(chǎng)景?
      • !!!
    • 自旋鎖與普通鎖有何區(qū)別?
      • 正常的信號(hào)量 : 當(dāng)獲取不到鎖的時(shí)候, 會(huì)把自己的線程阻塞休眠; 當(dāng)其他線程用完的時(shí)候喚醒線程
    1. 引用計(jì)數(shù)表(RefcountMap)
    • hash查找: (插入和獲取是通過同一個(gè)hash函數(shù)決定的, 避免了for循環(huán)遍歷, 提高效率)
      • 屏幕快照 2019-02-18 下午7.37.07.png
      • RC引用計(jì)數(shù)值
    1. 弱引用表(weak_table_t)
    • 弱引用表的數(shù)據(jù)結(jié)構(gòu)

ARC & MRC

  • MRC: 手動(dòng)引用計(jì)數(shù)
    • MRC(紅色為MRC特有方法)
  • ARC: 自動(dòng)引用計(jì)數(shù)
    • 什么是ARC? ARC是LLVM和Runtime協(xié)作的結(jié)果
    • ARC中禁止手動(dòng)調(diào)用retain, release, retainCount, dealloc
    • ARC中新增weak, strong屬性關(guān)鍵字
      • weak變量為什么在自動(dòng)釋放的時(shí)候指向nil

引用計(jì)數(shù)管理

    1. alloc:
    • 經(jīng)過一系列調(diào)用, 最終調(diào)用了C函數(shù)calloc,
    • 此時(shí)并沒有應(yīng)用計(jì)數(shù)+1(因?yàn)橄旅娴膔efcnt-result=1, 所以調(diào)用alloc的時(shí)候retainCount是1)
    1. retain:
    • SIDE_TABLE_RC_ONE對(duì)應(yīng)的偏移量
    • 我們?cè)谶M(jìn)行retain的時(shí)候, 系統(tǒng)是如何查找其對(duì)應(yīng)的應(yīng)用計(jì)數(shù)的?
      • 經(jīng)過2次的hash查找, 找到對(duì)應(yīng)的引用計(jì)數(shù)的值, 然后在進(jìn)行相應(yīng)的+1操作
    1. release實(shí)現(xiàn)
    • SIDE_TABLE_RC_ONE
    1. retainCount實(shí)現(xiàn)
    • alloc的時(shí)候it為空的, 因?yàn)閞efcnt_result = 1, 所以alloc的retainCount為1
    1. dealloc實(shí)現(xiàn)
    • 實(shí)現(xiàn)原理
      • 是否可以釋放的判斷標(biāo)準(zhǔn): (以下都為no, 則C函數(shù)free())
      • nonpointer_isa : 判斷當(dāng)前對(duì)象是否使用了非指針型的isa, (非關(guān)系型會(huì)存儲(chǔ)引用計(jì)數(shù), 超過了則會(huì)使用has_sidetable_rc)
      • weakly_referenced : 判斷當(dāng)前對(duì)象是否有weak指針指向它
      • has_assoc : 判斷當(dāng)前對(duì)象是否有關(guān)聯(lián)對(duì)象
      • has_cxx_dtor : 判斷當(dāng)前對(duì)象內(nèi)部實(shí)現(xiàn)是否涉及到C++, 當(dāng)前對(duì)象是否使用ARC管理內(nèi)存; 若有即為YES
      • has_sidetable_rc : 判斷當(dāng)前對(duì)象的引用計(jì)數(shù)是否通過sidetable的引用計(jì)數(shù)表來維護(hù)的
    • object_dispose實(shí)現(xiàn)():

      -
      object_dispose流程
    • objc_destructInstance()實(shí)現(xiàn):
      • objc_destructInstance流程
      • 通過關(guān)聯(lián)對(duì)象的技術(shù), 為一個(gè)類添加了一些實(shí)例變量, 在對(duì)象dealloc方法中, 是否有必要對(duì)其關(guān)聯(lián)對(duì)象進(jìn)行移除呢? (不需要, 系統(tǒng)會(huì)自動(dòng)幫助我們移除的)
    • clearDeallocating()實(shí)現(xiàn):
      • clearDeallocating內(nèi)部實(shí)現(xiàn)

弱引用管理

    1. 一個(gè)weak變量是如何添加到弱引用表中的?
    • 一個(gè)__weak修飾的變量, 編譯之后的樣子
      • 添加weak變量流程
    1. weak變量是如何添加到弱引用表中 : 通過弱引用對(duì)象進(jìn)行hash算法的計(jì)算, 然后計(jì)算查找期所對(duì)應(yīng)的位置
    • 源碼實(shí)現(xiàn)

      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) {
              // 向弱引用表中, 插入對(duì)象
              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. 當(dāng)一個(gè)weak變量被釋放或者廢棄之后, 系統(tǒng)是如何處理的?
    • 清除weak變量, 同時(shí)設(shè)置指向?yàn)閚il:
      • 清除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);
          }
          

自動(dòng)釋放池

    1. 自動(dòng)釋放池內(nèi)部實(shí)現(xiàn):
    • 編譯器將@autoreleasepool{}修改為上圖
    1. objc_autoreleasePoolPush:
    • objc_autoreleasePoolPush內(nèi)部流程
    1. objc_autoreleasePoolPop
    • 屏幕快照 2019-02-18 下午8.21.21.png
      • 如何理解一次pop相當(dāng)于一次批量的pop操作: {}代碼中所有的數(shù)據(jù), 都會(huì)被pop一次
    1. AutoreleasePool的實(shí)現(xiàn)結(jié)構(gòu)是怎么樣的 / 什么是自動(dòng)釋放池?
    • 以棧為節(jié)點(diǎn), 通過雙向鏈表的形式組合而成的數(shù)據(jù)結(jié)構(gòu)
      • 雙向鏈表
      • 棧節(jié)點(diǎn)
    • 是與線程一一對(duì)應(yīng)的
    1. AutoreleasePoolPage :
    • AutoreleasePoolPage內(nèi)部結(jié)構(gòu)
      • id * next : 指向棧當(dāng)中下一個(gè)可填充的位置
      • AutoreleasePoolPage * const parent : 雙向鏈表中的父指針
      • AutoreleasePoolPage * child : 雙向鏈表中的子指針
      • pthread_t const thread; : 線程對(duì)象, 所以說autoreleasePool是與線程一一對(duì)應(yīng)的
    • AutoreleasePoolPage的棧內(nèi)存: 棧底保存自身占用的內(nèi)存
      • AutoreleasePoolPage的占內(nèi)存
    1. AutoreleasePoolPage::push:
    • AutoreleasePoolPage::push
      • 1> 將當(dāng)前next指針指向nil
      • 2> 然后next指針指向下一個(gè)可入棧的內(nèi)存地址(每次push的時(shí)候, 相當(dāng)于不斷的插入哨兵對(duì)象)
    1. [obj autorelease] :
    • autorelease流程
    • 棧內(nèi)存的流程
    1. AutoreleasePoolPage::pop :
    • 根據(jù)傳入的哨兵對(duì)象找到對(duì)應(yīng)位置
    • 給上次push操作之后添加的對(duì)象一次發(fā)送release消息
    • 回退next指針到正確位置
    • 原始
    • 處理之后
    1. 關(guān)于自動(dòng)釋放池的面試
      1. viewDidLoad中創(chuàng)建一個(gè)array, 什么時(shí)候釋放?
      • array何時(shí)釋放呢?
        • 在每一次runloop的循環(huán)過程當(dāng)中, 都會(huì)再其將要結(jié)束的時(shí)候, 對(duì)前一次創(chuàng)建的pool進(jìn)行pop操作, 同時(shí)push進(jìn)來新的pool; 所以創(chuàng)建的arr對(duì)象, 在當(dāng)次runloop將要結(jié)束的時(shí)候調(diào)用AutoreleasePoolPage::pop(), 讓arr對(duì)象調(diào)用release方法, 進(jìn)行釋放
      1. AutoreleasePool中為何可以嵌套使用?
      • 多層嵌套就是多次插入哨兵對(duì)象
      1. autoreleasePool的使用場(chǎng)景: 在for循環(huán)中alloc圖片數(shù)據(jù)等內(nèi)存消耗較大的場(chǎng)景手動(dòng)插入autoreleasePool
      1. 自動(dòng)釋放池的雙向鏈表應(yīng)用體現(xiàn)在哪里?

循環(huán)引用

    1. 循環(huán)應(yīng)用的分類:
    • 自循環(huán)引用:
    • 相互循環(huán)應(yīng)用:
      • 自循環(huán)引用
    • 多循環(huán)引用:
      • 大環(huán)多循環(huán)引用
    1. 循環(huán)引用的場(chǎng)景
    • 代理
    • block
    • NSTimer
    • 大環(huán)引用
    1. 如何破除循環(huán)引用?
    • 避免產(chǎn)生循環(huán)應(yīng)用
    • 在合適的時(shí)機(jī)手動(dòng)斷環(huán)
    1. 具體的解決方案?
    • __weak:
      • __weak破解
    • __block:
      • 在ARC下, __block修飾的對(duì)象會(huì)被強(qiáng)引用, 無法避免循環(huán)應(yīng)用, 需要手動(dòng)破環(huán)
      • 在MRC下, __block修飾的對(duì)象不會(huì)增加其應(yīng)用計(jì)數(shù), 避免了循環(huán)引用
    • __unsafe__unretained : 與__weak在效果上是等效的
      • 修飾對(duì)象不會(huì)增加其應(yīng)用計(jì)數(shù), 避免了循環(huán)引用
      • 如果被修飾對(duì)象在某一時(shí)機(jī)被釋放, 會(huì)產(chǎn)生懸垂指針, 導(dǎo)致內(nèi)存泄漏!
    1. 循環(huán)引用NSTimer示例:
    • 場(chǎng)景: 頁(yè)面中有一個(gè)廣告欄, 定時(shí)滾動(dòng)播放動(dòng)畫, VC強(qiáng)引用廣告欄, 廣告欄添加NSTimer, NSTimer會(huì)對(duì)Target進(jìn)行強(qiáng)引用(即VC), 此時(shí)就會(huì)產(chǎn)生了循環(huán)應(yīng)用; 若把對(duì)象弱引用NSTimer也無用, 因?yàn)镹STimer會(huì)被當(dāng)前的RunLoop進(jìn)行強(qiáng)引用, NSTimer對(duì)對(duì)象的強(qiáng)引用, 當(dāng)VC退出的時(shí)候, 對(duì)象和NSTimer被RunLoop持有, 也不會(huì)被釋放
    • 屏幕快照 2019-02-18 下午8.46.42.png
    • NSTimer分為重復(fù)定時(shí)器和非重復(fù)定時(shí)器
      • 重復(fù)定時(shí)器: 添加一個(gè)中間對(duì)象, RunLoop --> NSTimer <-=> 中間對(duì)象 ->對(duì)象 <--VC
        • 添加中間對(duì)象, 利用了一個(gè)對(duì)象被釋放的時(shí)候, 其指針會(huì)置為nil
        • 代碼實(shí)現(xiàn):
          #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
          
      • 非重復(fù)定時(shí)器: 在定時(shí)器的回調(diào)方法中調(diào)用invalid方法, 使timer置為nil, 即可以解除Runloop對(duì)timer的強(qiáng)引用, 同時(shí)也可以解除timer對(duì)對(duì)象的強(qiáng)應(yīng)用, 此時(shí)即可解除循環(huán)引用

內(nèi)存管理面試總結(jié):

  • 什么是ARC?
    • ARC是由LLVM編譯器和Runtime共同協(xié)作來實(shí)現(xiàn)對(duì)自動(dòng)引用計(jì)數(shù)的管理
  • 為什么weak指針指向的對(duì)象那個(gè)在廢棄之后會(huì)被自動(dòng)置為nil?
    • 當(dāng)對(duì)象被廢棄之后, dealloc方法的內(nèi)部實(shí)現(xiàn)當(dāng)中, 會(huì)調(diào)用清除弱引用的方法, 在清除弱引用的方法中, 會(huì)通過hash算法, 來查找被廢棄對(duì)象在弱引用表中的位置, 來提取其對(duì)應(yīng)弱引用指針的列表數(shù)組, 然后進(jìn)行for循環(huán)遍歷, 把每一個(gè)weak指針都置為nil
  • 蘋果是如何實(shí)現(xiàn)AutoreleasePool的?
    • AutoReleasePool是以棧為結(jié)點(diǎn), 由雙向鏈表形式來合成的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)的
  • 什么是循環(huán)引用? 你遇到過哪些循環(huán)引用, 是怎樣解決的?
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容