TaggedPointer/retain/release/dealloc/retainCount 分析

TaggedPointer/retain/release/dealloc/retainCount 分析

ARC & MRC

MRC -> 谁创建,谁释放,谁引用,谁管理
ARC -> WWDC2011和iOS5引入 -> ARC模式下不需要手动retain、release、autorelease。编译器会在适当的位置插入release和autorelease

内存布局

内核区 -> 系统用来进行内核处理操作的区域
内存五大区 -> -> 以0x00400000开始
保留区 -> 预留给系统处理nil等 其主要原因是0x00000000表示nil,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil等情况

问题: 全局变量和局部变量的区别

全局变量 -> 全局存储区(即bss+data段) -> 静态的存储单元
局部变量 -> 栈 -> 函数被调用时才动态的为变量分配存储单元

问题: Block中可以修改全局变量,全局静态变量,局部静态变量,局部变量吗?

  • 可以修改全局变量. 全局静态变量

  • 可以修改局部静态变量, 不可以修改局部变量

    • 局部静态变量(static修饰的) 和 局部变量 -> __main_block_impl_0
    • 局部变量 -> 值方式
    • 局部静态变量 -> 指针形式
  • __block -> copy -> 从栈区copy到堆区 -> 堆区block

  • block -> id类型数据 (无论有没有__block修饰,都会retain) -> 基础数据类型, 没有__block修饰就无法修改变量值.

内存管理方案

  • Tagged Pointer: 专门用来处理小对象,例如NSNumber、NSDate、小NSString等.
  • Nonpointer_isa : 非指针类型的isa,主要是用来优化64位地址.
  • SideTables : 散列表,在散列表中主要有两个表,分别是引用计数表、弱引用

Tagged Pointer

NSTaggedPointerString -> 常量区: nameStr在alloc分配时在堆区 -> 由于较小 -> 所以经过xcode中iOS的优化 -> 成了NSTaggedPointerString类型
setProperty -> reallySetProperty -> 新值retain,旧值releas -> objc_retain、objc_release(底层代码) -> 如果对象是小对象,不会进行retain 和 release

NSString的内存管理

  • __NSCFConstantString : -> 编译时常量 -> 不会引起引用计数变化,存储在字符串常量区
  • __NSCFString -> 运行时 -> NSString子类 -> 引用计数会加1,存储在堆上
  • NSTaggedPointerString: 是苹果在64位环境下对NSString、NSNumber等对象做的优化
    • 字符串是由数字、英文字母组合且长度小于等于9 -> NSTaggedPointerString -> 常量区
    • 中文或者其他特殊符号 -> __NSCFString -> 堆区
  1. 一般的NSString对象指针 -> string值 + 指针地址,两者是分开的
  2. Tagged Pointer -> 指针+值, 都能在小对象中体现 -> 既包含指针,也包含值

Tagged Pointer总结

  • Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString)-> 地址 + 值,即真正的值 -> 可以直接进行读取 -> 优点是占用空间小 节省内存
  • Tagged Pointer -> 不会进入retain 和 release -> 直接返回了 -> 不需要ARC进行管理,所以可以直接被系统自主的释放和回收
  • Tagged Pointer -> 在常量区中,也不需要malloc和free,-> 直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右
  • taggedPointer的内存管理方案,比常规的内存管理,要快很多
  • Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值
  • 优化内存建议:对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取。会比WithFormat初始化方式更加快速

SideTables

引用计数存储到一定值 -> 不会再存储到Nonpointer_isa的位域的extra_rc中 -> 会存储到SideTables

retain源码分析

objc_retain -> retain -> rootRetain

  1. 判断是否为Nonpointer_isa
  2. 操作引用计数
    1. 不是Nonpointer_isa -> 操作SideTables (并不是只有一张, 而是有多张)
    2. 判断是否正在释放 -> 如果正在释放 -> dealloc
    3. 执行extra_rc+1,即引用计数+1操作 -> 并给一个引用计数的状态标识carry -> 用于表示extra_rc是否满了
    4. carray的状态表示extra_rc的引用计数满了 -> 操作散列表 -> 满状态的一半拿出来存到extra_rc,另一半存在 散列表的rc_half. (这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分操作的目的在于提高性能)
散列表为什么在内存有多张?最多能够多少张?
  • 如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全
  • 如果每个对象都开一个表,会耗费性能,所以也不能有无数个表
struct SideTable {  
    spinlock_t slock;//开/解锁  
    RefcountMap refcnts;//引用计数表  
    weak_table_t weak_table;//弱引用表  
    
    ....  
}    
  • StripedMap -> 同一时间,真机中散列表最多只能有8张
为什么在用散列表,而不用数组、链表?
  • 数组: 查询方便(即通过下标访问),增删比较麻烦(类似于之前讲过的methodList,通过memcopy、memmove增删,非常麻烦),所以数据的特性是读取快,存储不方便.
  • 链表: 增删方便,查询慢(需要从头节点开始遍历查询 -> 存储快,读取慢
    *散列表: 本质哈希表 -> 集合了数组和链表的长处,增删改查都比较方便 (拉链哈希表)

retain完整回答

  • retain -> 判断是否是 Nonpointer isa -> 不是,则直接操作散列表 进行+1操作
  • 是Nonpointer isa -> 判断是否正在释放 ->释放,则执行dealloc流程
  • 不是正在释放,则对Nonpointer isa进行常规的引用计数+1 -> extra_rc在真机上只有8位用于存储引用计数的值,当存储满了时,需要借助散列表用于存储 -> 需要将满了的extra_rc对半分,一半(即2^7)存储在散列表中。另一半还是存储在extra_rc中,用于常规的引用计数的+1或者-1操作,然后再返回

release 源码分析

setProperty -> reallySetProperty -> objc_release -> release -> rootRelease

  • 判断是否是Nonpointer isa -> 不是,则直接对散列表进行-1操作
  • Nonpointer isa -> extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中
  • carray为0 -> underflow
  • underflow:
    • 判断散列表中是否存储了一半的引用计数
    • 如果是,则从散列表中取出存储的一半引用计数,进行-1操作,然后存储到extra_rc中
    • 如果此时extra_rc没有值,散列表中也是空的,则直接进行析构,即dealloc操作,属于自动触发

dealloc 源码分析

dealloc -> _objc_rootDealloc -> rootDealloc

  • 根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
  • 有,则进入object_dispose
inline void
objc_object::rootDealloc()
{
    //对象要释放,需要做哪些事情?  
    //1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表  
    //2、free  
    if (isTaggedPointer()) return;  // fixme necessary?  

    //如果没有这些,则直接free  
    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);  
    }  
}  

retainCount 源码分析

  • alloc创建的对象的引用计数为多少?
    综上所述,alloc创建的对象实际的引用计数为0,其引用计数打印结果为1,是因为在底层rootRetainCount方法中,引用计数默认+1了,但是这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0

总结

  • alloc创建的对象没有retain和release
  • alloc创建对象的引用计数为0,会在编译时期,程序默认加1,所以读取引用计数时为1

你可能感兴趣的:(TaggedPointer/retain/release/dealloc/retainCount 分析)