问题: 简述一下内存的几大区域?


iOS的内存分布如下图所示 .主要分为

  • 代码段

    • 代码段主要存储编译后的代码
  • 数据段

    • 字符串常量
    • 初始化完成的全局变量和静态变量
    • 未初始化的全局变量和静态变量
  • 堆区

    • 内存地址分配由低到高
    • 通过 alloc malloc calloc 进行内存分配
  • 栈区

    • 内存地址分配由高到底
    • 函数调用开销,比如局部变量
  • 内核区



问题: 简述一下Targged Pointer?


在 ARM 64 操作系统下, iOS引入了 Tagged Pointer 技术,用于存储长度较短的 NSNumber NSString NSDate. Targged Pointer数据存放于常量区.

使用 Tagged Pointer 之后, NSNumber NSString NSDate 的指针里面存储的数据变为 Tag + Data, 也就是将数据直接存储在指针当中.

在iOS中, 当对象的指针最高有效位为1的时候,则该指针为 Tagged Pointer 类型数据.

#define _OBJC_TAG_MASK (1UL<<63)

MacOS中, 当对象的指针最低有效位为1的时候,则该指针为 Tagged Pointer 类型数据.

#define _OBJC_TAG_MASK (1UL<<63)

当指针不够存储数据时,才会动态地在堆区分配内存进行数据的创建与存储.


问题: 简述一下拷贝相关内容?


iOS中拷贝根据方法分类主要与 copymutableCopy, 根据是否产生新对象分为 深拷贝浅拷贝.

四者的关系如下表示.

不可变对象可变对象
copy (返回不可变对象)浅拷贝深拷贝
mutableCopy (返回可变对象)深拷贝深拷贝

问题: 简述一下引用计数的存储位置?


  • arm64架构下, 由于isa指针是一个共用体, 引用计数主要存储在 extra_rc 的位域中.

  • extra_rc 存储的是引用计数 - 1 的值.

  • 如果 extra_rc 空间不足, 则 has_sidetable_rc 的值为 1, 引用计数则会存储到sidetable类中 RefcountMap 类型 refcnts.

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

问题: 简述一下weak指针的实现原理?


  • ARM64架构下,如果当前对象曾经被weak指针修饰过,那么它的ISA指针就会 weakly_referenced 的值就为1,代表对象曾经被weak指针修饰过.

  • 在释放对象的过程中,主要进入的调用的函数是 objc_object 的 rootDealloc 函数. 函数中会判断当前对象是否是被关联对象,是否含有C++的析构函数,是否被weak修饰过等,这些信息都是通过 ISA 指针来获取.如果上述任何条件都不成立,那么直接调用 free 函数进行释放.

    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);
        }
    }
    
  • 如果当前对象含有被关联对象信息、弱引用信息、C++的析构函数,其主要操作函数是 objc_destructInstance 函数.

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            Class isa = obj->getIsa();
    
            if (isa->hasCxxDtor()) {
                object_cxxDestruct(obj);
            }
    
            if (isa->instancesHaveAssociatedObjects()) {
                _object_remove_assocations(obj);
            }
    
            objc_clear_deallocating(obj);
        }
    
        return obj;
    }
    

问题: 简述一下@autoreleasepool的实现原理?


  • @autoreleasepool 的本质实际上是一个会生成一个C++的结构体 __AtAutoreleasePool,最终自动释放池的管理是由 AutoreleasePoolPage 来进行实现的.

    • 每一个 AutoreleasePoolPage 对象占用4096个字节内存,除了存储自身的成员变量外, 剩下的空间用来存储 autorelease 对象的地址. 可以通过 beginend 函数获取内存地址区间.

      id * begin() {
          return (id *) ((uint8_t *)this+sizeof(*this));
      }
      
      id * end() {
          return (id *) ((uint8_t *)this+SIZE);
      }
      
    • 所有的 AutoreleasePoolPage 对象是通过 双向链表 的形式连接在一起的. 这个是可以通过 AutoreleasePoolPage的父类 AutoreleasePoolPageData 结构体可以得到答案.

      class AutoreleasePoolPage : private AutoreleasePoolPageData
      {
         ...
      };
      struct AutoreleasePoolPageData
      {
         ... 
      
         AutoreleasePoolPage * const parent; // 上一个Page对象
         AutoreleasePoolPage *child; // 下一个Page对象
      
         ...
      };
      
  • 在开始时会调用结构体的构造函数,构造函数中会通过 objc_autoreleasePoolPush 生成一个 pool 对象.

  • 在结束时会调用结构体的析构函数,析构函数中会 objc_autoreleasePoolPop 释放该pool 对象.

  • objc_autoreleasePoolPush 函数内部会调用 C++的 AutoreleasePoolPage 类的中 push 方法,会生成并返回一个释放边界值 POOL_BOUNDARY.

        static inline void *push() 
        {
            id *dest;
            if (slowpath(DebugPoolAllocation)) {
                // Each autorelease pool starts on a new pool page.
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    
    • 如图所示,这个边界值会插入到当前的AutoreleasePoolPage 内存中. 所以 @autorelease的并不一定创建新的 AutoReleasePage,但一定会创建一个边界值 POOL_BOUNDARY.
    • 可以通过 _objc_autoreleasePoolPrint 函数来确认.

  • 任意对象的 autorelease 操作都会通过AutoReleasePage 的 autoreleaseFast 方法将 该对象的内存地址存放到 链表结构中,也就是说会入栈.

        static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();
            if (page && !page->full()) {
                return page->add(obj);
            } else if (page) {
                return autoreleaseFullPage(obj, page);
            } else {
                return autoreleaseNoPage(obj);
            }
        }
    
  • objc_autoreleasePoolPop 函数内部会调用 C++的 AutoreleasePoolPage 类的中 pop 方法.释放的过程是 从最后往前释放,并且根据边界值 POOL_BOUNDARY 来确认是否停止.

    static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            page = hotPage();
            if (!page) {
                return setHotPage(nil);
            }
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
            } else {
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }

问题: 简述一下autorelease与RunLoop的关系.


autorelease的 Push 和 Pop 是通过主线程监听 RunLoop的状态来实现的,一共是两个Observer.

  • 第一个Observer 用来监听 RunLoopEntry 状态,这时候会处理 autorelease 的 objc_autoreleasePoolPush.

  • 第二个Observer 用来监听 RunLoopExit | RunLoopBeforeWaiting 状态.

    • RunLoopBeforeWaiting状态下,会调用一次 objc_autoreleasePoolPopobjc_autoreleasePoolPush.
    • RunLoopExit 状态下,会调用一次 objc_autoreleasePoolPop.

IT界无底坑洞栋主 欢迎加Q骚扰:676758285