OC底层原理三十五:内存管理(TaggedPointer、引用计数)

OC底层原理 学习大纲

  • 本节,进入内存管理篇章,将从以下几部分讲解:
  1. 内存布局
  2. TaggedPointer
  3. 引用计数(retain、release、dealloc) & SideTables 散列表
  4. retainCount

准备工作:

  • 可编译的objc4-781源码: https://www.jianshu.com/p/45dc31d91000

1. 内存布局

按照地址排列: 栈区 -> 堆区 -> 全局静态区 -> 常量区 -> 代码区内核区保留部分不在考虑范围内)

image.png

补充说明:

  1. 内存五大区,实际是指虚拟内存,而不是真实物理内存。(详情可查看 本文第3点 虚拟内存与物理内存)
  2. iOS系统中,应用虚拟内存默认分配4G大小,但五大区占3G,还有1G五大区之外的内核区

内存五大区详细功能,可查看 内存五大区

2.TaggedPointer

  • TaggedPointer标记指针。标记的是小对象,可以记录小内容的NSString、NSDate、NSNumber等内容。是内存管理(节省内存开销)的一种有效手段。

例如:使用NSNumber记录10

  • 【常规操作】
    开辟一个内存,用于存储这个对象,在对象内部记录这个内容10,然后需要一个指针,指向中的内存首地址
    ( 内存开销指针8字节+对象空间大小)

  • TaggedPointer小对象】
    记录一个10,根本不用开辟内存,直接利用指针拥有的8字节空间即可。
    (类似NonPointer_isa非指针型isa,使用union联合体位域,中间shiftcls部分存储类信息其他部位记录其他有效信息 。 相关参考 剖析isa)
    ( 内存开销指针8字节)

结论: 占用空间对象,直接使用指针内部空间节约内存开销

2.1案例分析

@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, copy) NSString * name;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10000 ; i++) {
        dispatch_async(self.queue, ^{
            self.name = [NSString stringWithFormat:@"ht"];
            NSLog(@"%@ %p %s",self.name, self.name, object_getClassName(self.name));
        });
    }
    
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"来了");
    for (int i = 0; i < 10000; i++) {
        dispatch_async(self.queue, ^{
            self.name = [NSString stringWithFormat:@"ht_学习不行,回家种田"]; //setter - 新值retain,旧值release
            NSLog(@"%@ %p %s",self.name, self.name, object_getClassName(self.name)); // getter
        });
    }
    
}
@end
  • 上面打印结果
    image.png
  • 点击触发TouchBegin后的打印结果:
    image.png

Q: 两次10000次循环,都是对name进行赋值,为什么上面不会崩溃,下面会崩溃

A: 因为上面name存储在中,不需要手动释放,而下面name存储在中,在setter赋值时会触发retainrelease,异步线程中,可能导致指针过度释放,造成了崩溃

  • name赋值为ht时,是小对象(NSTaggedPointerString),直接将内容存储在指针内部指针存储在中,由系统负责管理

  • name赋值为ht_学习不行,回家种田时,由于内容过多指针内部空间不够存储,所以去开辟空间,需要管理引用计数了。每次setter都会触发新值retain和旧值release异步线程中,可能导致retain未完成,但提前release了,导致指针过度释放,造成了崩溃

底层探索:

  • clangViewController.m文件编译.cpp文件
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
  • 可以看到namesetter方法实际是调用objc_setProperty,打开objc4源码,搜索objc_setProperty
    image.png
  • 可以看到taggedPointer对象参与引用计数计算

2.2 NSTaggedPointerString底层探索

  • 沿着上面看到的isTaggedPointer()的判断,我们进入isTaggedPointer()内部:
    image.png

【拓展】:taggedPointer混淆机制

  • APP启动时,dyld调用_read_images时,第一步有一个初始化taggedPointer混淆机制
    image.png
  • 内部为:iOS10.14之后,且打开了小对象混淆机制,就进行小对象混淆:
    image.png
  • 搜索objc_debug_taggedpointer_obfuscator,可以看到混淆编码解码:
    image.png
  • 编码: 原地址进行^异或操作一次,得到编码地址
  • 解码: 将编码地址再进行^异或操作一次,得到原地址
  • 混淆验证:
// 声明(在其他库中实现)
extern uintptr_t objc_debug_taggedpointer_obfuscator;

// 从objc4源码拷贝解码代码, 入参改为id类型
static inline uintptr_t
_objc_decodeTaggedPointer(id ptr) {
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

- (void)demo {
    NSString * str1 = [NSString stringWithFormat:@"a"];
    NSNumber * num1 = @(1);
    
    NSLog(@"str1: %p-%@", str1, str1);
    NSLog(@"num1: %p-%@", num1, num1);
    
    NSLog(@"str1 解码地址: 0x%lx", _objc_decodeTaggedPointer(str1));
    NSLog(@"num1 解码地址: 0x%lx", _objc_decodeTaggedPointer(num1));
}
image.png
  • 可以看到解码后,从地址就可以直接当前内容。所以混淆机制就是为了让小对象地址降低识别度

Q: 解码后小对象地址,前面a/b是啥意思?

image.png

  • 当前系统,为了新旧兼容TaggedPointer指针的后四位存储值

taggedpointer类型的优点:

    1. 节省内存开销:充分利用指针地址空间
    1. 执行效率高: 不需要retainrelease系统直接管理释放、回收少执行很多代码,不需要堆空间,直接创建读取。(官方说内存读取3倍创建106倍)

建议:

  • 字符串较长时,主动使用@""创建,而不用stringFormat创建。因为如果不是taggedPointer,反而会更耗时间

    image.png

  • 不同字符串长度类型

    image.png

3. 引用计数(retain、release、dealloc) & SideTables 散列表

  • 回忆:
    在熟悉isa指针内部结构时(参考 剖析isa),我们有提到:

    isa结构图

  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。
    如: 如果对象的引用计数10,那么extra_rc 为 9。如果引用计数大于 10, 则需要使用到has_sidetable_rc。(这只是举例,具体方式,我们下面解析)

  • 说到引用计数,自然就想到了retainrelease。上面示例中,如果不是taggedPointer,就会进入obj->retain,我们进入retain内部:
    (ps: 为了内容不太分散,我梳理成一张大图,可查看原图放大观看)

    image.png

  • 哈希表结构如下:


    image.png

【reatin总结】

  1. taggedPointer类直接返回原对象
  2. 操作时,使用新isa拷贝当前isa对象,隔离操作
  3. do-while】循环,是由于多线程环境下,当前isa可能在变化,只要变化就需要再次操作
  4. 如果支持指针优化,直接操作散列表进行计数
  5. 如果isa记录了正在释放,就retain
  6. 引用计数+1,首先尝试isaextra_rc+1:
    • 成功:【do-whileretain操作结束
    • 失败: 表示extra_rc存储满了,此时将extra_rc计数减半has_sidetable_rc(使用散列表)标记truetranscribeToSideTable(转移给散列表)标记true
  7. do-while】外,判断transcribeToSideTable散列表添加extra_rc最大容量的一半计数。
  • 接着,我们来了解release:
    image.png

【release总结】

  1. taggedPointer类直接返回原对象
  2. 操作时,使用新isa拷贝 当前isa对象,隔离操作
  3. 【retry】:
    do-while循环,监测多线程环境下,当前isa是否变化
    - 如果支持指针优化,直接操作散列表进行计数
    - 尝试isaextra_rc - 1: 如果失败跳转【underflow】
  4. 【underflow】:
    (表示extra_rc计数为不可以进行-1,需要去散列表计数操作直接释放)
    • 如果有散列表has_sidetable_rctrue):
      • 如果散列表没锁关锁重新 【retry】
      • 尝试从散列表读取RC_HALF(extra_rc一半容量)计数
        • 如果取到值,尝试更新extra_rcisa更新失败再次读取更新。如果还失败,就将borrowed加回给sidetable
    • 没散列表或散列表内没计数,如果正在释放中,就不处理。 否则,将deallocating设为true(去释放),重新【retry】
    • 如果实现dealloc函数,就给发送消息调用dealloc
  • 引用计数0时,会调用dealloc:
    image.png

【dealloc总结】

    1. 优化指针,但无弱引用表,无关联对象,无析构函数、无散列表,直接free
    1. 其他情况,依次检查:
    • 析构函数:调用析构函数
    • 关联对象:移除关联对象
    • 指针优化:直接清除散列表
    • 弱引用表:直接清除弱引用表内容
    • 使用散列表移除表内引用计数

-现在,我们已熟悉alloc->retain->release->dealloc完整流程

4. retainCount

  • 关于retainCount,有一个有意思面试题:
// Q:打印的引用计数为多少,alloc、init改变了引用计数吗?
- (void)demo {
    
    NSObject * objc = [NSObject alloc];
    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
    
    objc = [objc init];
    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
}
  • 打印结果:


    image.png
  • 进入源码,搜索_objc_rootRetainCount->rootRetainCount:

    image.png

结论

  1. allocinit不处理对象引用计数
  2. retainCount返回的计数,是读取内存+1的。 (实际计数打印值-1)
  3. 引用计数0对象为啥释放
    因为ARC自动将对象加入autoRelasePool中,autoReleasePool对象被持有时间,就是代码作用域{ }周期,所以达到延迟释放功能。

下一节,将进行强弱引用分析。

你可能感兴趣的:(OC底层原理三十五:内存管理(TaggedPointer、引用计数))