iOS底层-内存管理(一)

iOS程序的内存布局

  1. 如图所示

    图4

    1. 保留区
      1. 从地址0开始,比较小的哪一段是我们不能使用的,保留的
    2. 代码段:
      1. 编译之后的代码
      2. 高级语言->汇编语言->机器语言
      3. 我们写的每一句代码都存储在这里
    3. 数据段:
      1. 字符串常量:比如NSString *str = @"123";
      2. 已初始化数据:已初始化的全局变量、静态变量等
      3. 未初始化数据:未初始化的全局变量、静态变量等
      4. 特点: 整个应用程序中都存在的
    4. 栈:
      1. 函数调用开销,比如局部变量。分配的内存空间地址从大到小
      2. 特点: 内存临时开辟,用完就自动回收
    5. 堆:
      1. 通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址从小到大
      2. 特点:根据需要程序员手动分配内存,并且手动管理内存的释放
  2. 代码验证:

     int a = 10; //初始化的全局变量
     int b ; //未初始化的全局变量
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
             //初始化的静态局部变量
             static int c = 20;
             //未初始化的静态局部变量
             static int d;
                
             int e = 30;
             int f;
             //字符串常量
             NSString *str = @"123";
             //堆
             NSObject *obj = [[NSObject alloc] init];
                
             NSLog(@"=a=========%p",&a);
             NSLog(@"=b=========%p",&b);
             NSLog(@"=c=========%p",&c);
             NSLog(@"=d=========%p",&d);
             NSLog(@"=e=========%p",&e);
             NSLog(@"=f=========%p",&f);
             NSLog(@"=str=======%p",str);
             NSLog(@"=obj=======%p",obj);
         }
         return 0;
     }
    
    1. 打印:

       =a=========0x100001180
       =b=========0x10000118c
       =c=========0x100001184
       =d=========0x100001188
       =e=========0x7ffeefbff5ac
       =f=========0x7ffeefbff5a8
       =str=======0x100001040
       =obj=======0x1006826d0
      
    2. 整理:

       //常量
       str = 0x100001040
       //已初始化:全局变量、静态变量
       &a  = 0x100001180
       &c  = 0x100001184
       //未初始化:全局变量、静态变量
       &d  = 0x100001188
       &b  = 0x10000118c
       //堆
       obj = 0x1006826d0
       //栈:栈中的地址从大到小
       &f  = 0x7ffeefbff5a8
       &e  = 0x7ffeefbff5ac
      

Tagged Pointer (小型对象的内存管理)

引入:

  1. 为什么要使用TaggedPointer?
    1. 以前我们初始化一个对象(64位为例),开发的代码如下

       NSNumber *number2 = [NSNumber numberWithInteger:2];
      
    2. 这句代码的内存分析:
      1. 一个栈上分配8个字节内存给number2变量,用于存储NSNumber对象在堆上的地址
      2. 一个堆上分配16个字节(isa、2),用于存储NSNumber对象
      3. 总共消耗24个字节
    3. 由于NSNumber和NSDate对象的值一般不需要8个字节,4个字节的长度即可,为了不造成内存的浪费,想到将指针的值(8个字节)进行拆分,一部分表示数据,一部分用来表示数据的类型,他不是任何对象,这就是TaggedPointer技术,这样指针 = Data + Tag,那么我们的存一个数字只需要8个字节就够了。

总结

  1. 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
  2. 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
  3. 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中(Tag指的是数据类型)
    1. 即用指针类型(8个字节)存储Tag+Data
  4. 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
  5. objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
  6. 如何判断一个指针是否为Tagged Pointer
    1. iOS平台,最高有效位是1(第64bit)
    2. Mac平台,最低有效位是1
    3. objc源码证明:
      1. struct objc_object这个结构体对象有个判断当前对象是否是taggedPointer的成员函数isTaggedPointer
         //判断当前对象是否是TaggedPointer
         bool isTaggedPointer();
         inline bool objc_object::isTaggedPointer() 
         {
             return _objc_isTaggedPointer(this);
         }
                    
         static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
         {
             return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
         }
                    
         //_OBJC_TAG_MASK的宏定义
         #if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
         // 64-bit Mac - tag bit is LSB //mac开发64位
         #   define OBJC_MSB_TAGGED_POINTERS 0
         #else
             // Everything else - tag bit is MSB 其他开发
         #   define OBJC_MSB_TAGGED_POINTERS 1
         #endif
                    
         #if OBJC_MSB_TAGGED_POINTERS
         #   define _OBJC_TAG_MASK (1UL<<63)  //第一位为1
         #else
         #   define _OBJC_TAG_MASK 1UL //最后一位为1
         #endif
        

例证

  1. 上面的结论我们来证实一下:

     NSNumber *number1 = @1;
     NSNumber *number2 = @2;
     NSNumber *number3 = @3;
     NSNumber *numberFFFF = @(0xFFFF);
        
     NSLog(@"number1 pointer is %p", number1);
     NSLog(@"number2 pointer is %p", number2);
     NSLog(@"number3 pointer is %p", number3);
     NSLog(@"numberffff pointer is %p", numberFFFF);
    
    1. 打印:

       number1 pointer is 0xd56dc97421ba919c
       number2 pointer is 0xd56dc97421ba91ac
       number3 pointer is 0xd56dc97421ba91bc
       numberffff pointer is 0xd56dc97421b56e7c
      
    2. 很惊讶的发现这个地址好像并不是Tag + Data,而且还跟网上很多博客上说的不一样,这是什么问题呢?
    3. 继续研究一下objc源码
  2. objc源码分析
    1. 在源码中找到_objc_makeTaggedPointer,从注释来看,这个方法就是对TaggedPointer的编码

       static inline void * _Nonnull  _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
       {
           // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
           // They are reversed here for payload insertion.
              
           // assert(_objc_taggedPointersEnabled());
           if (tag <= OBJC_TAG_Last60BitPayload) {
               // assert(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
               uintptr_t result =
                   (_OBJC_TAG_MASK | 
                    ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
                    ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
               //编码
               return _objc_encodeTaggedPointer(result);
           } else {
               // assert(tag >= OBJC_TAG_First52BitPayload);
               // assert(tag <= OBJC_TAG_Last52BitPayload);
               // assert(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
               uintptr_t result =
                   (_OBJC_TAG_EXT_MASK |
                    ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
                    ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
               //编码
               return _objc_encodeTaggedPointer(result);
           }
       }
              
       //编码
       static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr)
       {
           return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
       }
       //解码
       static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr)
       {
           return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
       }
              
       //objc_debug_taggedpointer_obfuscator 的初始化
       static void initializeTaggedPointerObfuscator(void)
       {
           //mac_os< 10.14 和 iOS< 12.0 
           if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
               // Set the obfuscator to zero for apps linked against older SDKs,
               // in case they're relying on the tagged pointer representation.
               DisableTaggedPointerObfuscation) {
               objc_debug_taggedpointer_obfuscator = 0;
           } else {
               // Pull random data into the variable, then shift away all non-payload bits.
               arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                              sizeof(objc_debug_taggedpointer_obfuscator));
               objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
           }
       }
      
    2. 分析:

      1. 可以看到一个对象通过_objc_makeTaggedPointer函数创建一个TaggedPointer
      2. _objc_makeTaggedPointer内部本质调用了_objc_encodeTaggedPointer进行编码
      3. _objc_encodeTaggedPointer的本质是对ptr进行了异或操作与objc_debug_taggedpointer_obfuscator
      4. objc_debug_taggedpointer_obfuscator这个值很有特点
        1. 在MAC_OS<14.0,iOS<12.0时,objc_debug_taggedpointer_obfuscator = 0;,与0异或,原值不变
        2. 反之:objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;,非零值,再异或肯定不是原值了
        3. _OBJC_TAG_MASK又根据当前是mac或者非mac取不同的值
      5. 因此,分析出打印结果不正常的原因:因为当前我用的是MAC_OS>14.0,iOS>12.0,当前打印的是与objc_debug_taggedpointer_obfuscator异或后的值
  3. 那么就不能查看真正的tag+Data了吗?
    1. 可以的—可以手动实现_objc_decodeTaggedPointer
    2. 因为上面打印的是_objc_encodeTaggedPointer编码后的,因此我们可以手动解码_objc_decodeTaggedPointer然后打印
  4. 示例

     #import "ViewController.h"
     #import <objc/runtime.h>
        
     @implementation ViewController
    
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         NSNumber *number1 = @1;
         NSNumber *number2 = @2;
         NSNumber *number3 = @3;
         NSNumber *numberFFFF = @(0xFFFF);
            
         NSLog(@"number1 pointer is %p---真实地址:==0x%lx", number1,_objc_decodeTaggedPointer_(number1));
         NSLog(@"number2 pointer is %p---真实地址:==0x%lx", number2,_objc_decodeTaggedPointer_(number2));
         NSLog(@"number3 pointer is %p---真实地址:==0x%lx", number3,_objc_decodeTaggedPointer_(number3));
         NSLog(@"numberffff pointer is %p---真实地址:==0x%lx", numberFFFF,_objc_decodeTaggedPointer_(numberFFFF));
     }
        
        
     //看源码知道objc_debug_taggedpointer_obfuscator是个外部全局变量,只要在我们用的地方申明一下即可
     extern uintptr_t objc_debug_taggedpointer_obfuscator;
        
     //手动实现解码
     uintptr_t _objc_decodeTaggedPointer_(id  ptr) {
    
         NSString *p = [NSString stringWithFormat:@"%ld",ptr];
         return [p longLongValue] ^ objc_debug_taggedpointer_obfuscator;
     }
        
     @end
    
    1. 打印:

       number1 pointer is 0xb12a3e5504bc342f---真实地址:==0xb000000000000012
       number2 pointer is 0xb12a3e5504bc341f---真实地址:==0xb000000000000022
       number3 pointer is 0xb12a3e5504bc340f---真实地址:==0xb000000000000032
       numberffff pointer is 0xb12a3e5504b3cbcf---真实地址:==0xb0000000000ffff2
      
    2. 可以看到数据是直接存储在指针中的

Tagged Pointer的应用

  1. 下面代码会发生什么问题?

     dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
     for (int i = 0; i<1000; i++) {
         dispatch_async(queue, ^{
             //会崩溃
             //self.name = [NSString stringWithFormat:@"abcdefghijk"];
             //不会崩溃
             self.name = [NSString stringWithFormat:@"abc"];
         });
     }
    
    1. self.name的本质

       -(void)setName:(NSString *)name{
            if (name != _name) {
            //n个线程同时访问,就会一个对象多次释放,崩溃
            [_name release];
            _name = [name retain];
            }
       }    
      
    2. self.name = [NSString stringWithFormat:@"abcdefghijk"];会崩溃
      1. 是真正的oc对象,由于是多线程会出现[_target release];被调用多次,从而闪退;
    3. self.name = [NSString stringWithFormat:@"abc"];不会崩溃
      1. 不是oc对象,而是TaggedPointer,在release和retain的时候都会判断是不是TaggedPointer
      2. objc源码

         objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
         {
             if (isTaggedPointer()) return false;
                    
             bool sideTableLocked = false;
             ...
         }
            
         ALWAYS_INLINE id 
         objc_object::rootRetain(bool tryRetain, bool handleOverflow)
         {
             if (isTaggedPointer()) return (id)this;
            
         }
        
Table of Contents