精华内容
下载资源
问答
  • iOS内存管理机制

    2021-03-29 19:45:16
    iOS内存管理机制是开发中一项很重要的知识,了解iOS内存管理的规则不管是在开发中还是在学习中都能很大程度的帮助我们提升效率。下面我就根据自己的理解,详细梳理一下内存管理相关的知识。 在说内存管理之前,...

    iOS中内存管理机制是开发中一项很重要的知识,了解iOS中内存管理的规则不管是在开发中还是在学习中都能很大程度的帮助我们提升效率。下面我就根据自己的理解,详细梳理一下内存管理相关的知识。

    在说内存管理之前,我们首先要了解什么内存。一块内存条,是一个从下至上地址依次递增的结构,内存条中主要分为几大类:栈区(stack)、堆区(heap)、常量区、代码区(.text)、保留区。常量区分为未初始化区域(.bss)和已初始化区域(.data),栈区stack存储顺序是由高地址存向低地址,而堆区是由低地址向高地址存储。内存条中地址由低到高的区域分别为:保留区,代码区,已初始化区(.data),未初始化区(.bss),堆区(heap),栈区(stack),内核区。而程序员操作的主要是栈区与堆区还有常量区。

    关于iOS内存管理的方案其实并非只有散列表一种,还有一种更为高效且为内存高效节省空间的方法叫做TaggedPointer,表明加标记的指针,我们可以理解为在是指针内部增加一些特殊的信息。

    那么为什么要使用taggedPointer这种内存管理方法呢,其如何达到节省内存的目的呢。举个例子,比如在OC中一个NSNumber对象,在32位中的系统中占用4个字节的空间,但是迁移至64位系统中后,其占用空间达到了8字节,以此类推,所有在64位系统中占用空间会翻倍的对象,在迁移后会导致系统内存剧增,即时他们根本用不到这么多的空间,所以苹果对于一些小型数据,采用了taggedPointer这种方式管理内存。

    其主要的原理就是在对象的指针中加入特定需要记录的信息,以及对象所对应的值,在64位的系统中,一个指针所占用的内存空间为8个字节,已足以存下一些小型的数据量了,当对象指针的空间中存满后,再对指针所指向的内存区域进行存储,这就是taggedPointer。距离NSNumber,最低4位用于标记是什么类型的数据(long为3,float则为4,Int为2,double为5),而最高4位的“b”表示是NSNumber类型;其余56位则用来存储数值本身内容。

    之前runtime文章中有提到过objc_objcet对象中isa指针分为指针型isa与非指针型isa(NONPOINTER_ISA),运用的便是类似这种技术。下面详细解读一下NONPOINTER_ISA:

    在一个64位的指针内存中,第0位存储的是indexed标识符,它代表一个指针是否为NONPOINTER型,0代表不是,1代表是。第1位has_assoc,顾名思义,1代表其指向的实例变量含有关联对象,0则为否。第2位为has_cxx_dtor,表明该对象是否包含C++相关的内容或者该对象是否使用ARC来管理内存,如果含有C++相关内容或者使用了ARC来管理对象,这一块都表示为YES,第3-35位shiftcls存储的就是这个指针的地址。第42位为weakly_referenced,表明该指针对象是否有弱引用的指针指向。第43位为deallocing,表明该对象是否正在被回收。第44位为has_sidetable_rc,顾名思义,该指针是否引用了sidetable散列表。第45-63位extra_rc装的就是这个实例变量的引用计数,当对象被引用时,其引用计数+1,但少量的引用计数是不会直接存放在sideTables表中的,对象的引用计数会先存在NONPOINTER_ISA的指针中的45-63位,当其被存满后,才会相应存入sideTables散列表中。

    所以综上所述,iOS中内存管理的方式主要有三大,1.taggedPointer,2.NONPOINTER_ISA,3.散列表。

    下面再进行散列表的分析:

    散列表在系统中的提现是一个sideTables的哈希映射表,其中所有对象的引用计数(除上述存在NONPOINTER_ISA中的外)都存在这个sideTables散列表中,而一个散列表中又包含众多sideTable。每个SideTable中又包含了三个元素,spinlock_t自旋锁,RefcountMap引用计数表,weak_table_t弱引用表。所以既然SideTables是一个哈希映射的表,为什么不用SideTables直接包含自旋锁,引用技术表和弱引用表呢?因为在众多线程同时访问这个SideTables表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张SideTable的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,而且会带来不好的用户体验,针对这种情况,将一张SideTables分为多张表的SideTable,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,所以这就是一张SideTables表下涵盖众多SideTable表的原因。

    基于此,我们进行SideTable的表分析,那么当一个对象的引用计数增加或减少时,需要去查找对应的SideTable并进行引用计数或者弱引用计数的操作时,系统又是怎样实现的呢。

    当一个对象访问SideTables时,首先会取到对象的地址,将地址进行哈希运算,与SideTables的个数取余,最后得到的结果就是该对象所要访问的SideTable所在SideTables中的位置,随后在取到的SideTable中的RefcountMap表中再次进行一次哈希查找,找到该对象在引用计数表中所对应的位置,如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的size_t对象,其实就是一个uint类型的无符号整型。

    对于Spinlock_t自旋锁,其本质是一种“忙等”的锁,所谓“忙等”就是当一条线程被加上Spinlock自旋锁后,当线程执行时,会不断的去获取这个锁的信息,一旦获取到这个锁,便进行线程的执行。这对于一般的高性能锁比如信号量不同,信号量是当线程获取到信号量小于等0时,便自动进行休眠,当信号量发出时,对线程进行唤醒操作,这样就致使了两种锁的性质不同。Spinlock自旋锁只适用于一些小型数据操作,耗时很少的线程操作。

    对于每张SideTable表中的弱引用表weak_table_t,其也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表weak_entry_t,而weak_entry_t是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。

    以上大概就是内存在ios中的管理方式,以及关于内存管理相关的一些数据类型。

    关于iOS的两种管理方式:MRC与ARC

    MRC是上古时期iOS开发程序员用的一种手动管理对象引用计数的方式,但这也是内存管理的立足之本,ARC就是现代程序员常用的对象引用计数管理方式,ARC是由编译器和runtime协作,共同完成对对象引用计数的控制,而不需要程序员自己手动控制。在MRC中可以调用alloc,retain,release,retainCount,dealloc等方法,这些方法在ARC中只能调用alloc方法,调用其他的会引起编译报错,不过在ARC模式中可以重写dealloc方法。相比起MRC,在ARC中新增了weak和strong等属性关键字。下面详细解读一下MRC ARC中的各个方法。

    alloc:这个方法实质上是经过了一系列封装调用之后的calloc,需要注意的是调用该方法时,对象的引用计数并没有+1.

    retain:这个方法是先在SideTables中通过哈希查找找到对象所在的那张SideTable表,随后在SideTable中的引用计数表中再次通过哈希查找找到对象所对应的size_t,再加上一个系统的(引用计数+1宏)。为什么这里没有+1而是加上一个系统的宏呢,因为在size_t结构中,前两位不是储存引用计数的,第一位存储的是是否有弱引用指针指向,第二位存储的是对象是否在被回收中。所以,在增加其引用计数时需要右移两位再进行增加,所以用到了这个系统的宏SIDE_TABLE_RC_ONE。

    release:这个方法跟retain方法原理一样,只不过是减一个系统的宏SIDE_TABLE_RC_ONE

    retainCount:这个方法的实现同样是先查找系统的SideTables表,并找到对象对应的SideTable表,但在之前要先申明一个size_t为1的对象,随后在对应的引用计数表中找到了对象对应的引用计数后,通过右移找到的count对象,与之前创建好的1相加,最后返回其结果便是引用计数。所以这就是为什么系统在调用alloc方法后并没有给对象的引用计数+1,但retainCount方法调用后对象的引用计数就是1的原因。

    dealloc:对象在被回收时,就会调用dealloc方法,其内部实现流程首先要调用一个_objc_rootDealloc()方法,再方法内部再调用一个rootDealloc()方法,此时在rootDealloc中会判断该对象的isa指针,依次判断指针内的内容:nonpointer_isa,weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc,如果判断结果为:该isa指针不是非指针型的isa指针,没有弱引用的指针指向,没有相应的关联对象,没有c++相关的内容,没有使用ARC模式,没有关联到散列表中,即判断的内容都为否,则可以直接调用c语言中的free()函数进行相应的内存释放,否则就会调用objc_dispose()这个函数。

    而objc_dispose()函数的内部是经由objc_destructInstence()函数调用,随后调用c函数的free()的,顾名思义,objc_destructInstence()函数就是一个销毁对象的函数,那么objc_destructInstence()函数内部实现是什么样的结构呢?

    首先,destructInstence()函数内部会来判断该对象是否有C++相关内容以及ARC相关的内容,如果有的话就会调用object_cxxDestruct函数来销毁相应的内容,随后会判断改对象是否有关联对象相关的内容,如果有的话就会调用_object_remove_associations()这个方法来清楚相关的关联对象内容,在这两个判断步骤完成之后,调用clearDeallocating()方法。

    在clearDeallocating()方法中,会调用一个sidetable_clearDeallocating()的方法,在方法内部会调用两个方法,1.weak_clear_no_lock()这个方法会将每个弱引用表中的指向该对象的弱引用指针,置为nil。2.table.refcnts.erase()方法,这个方法会从引用计数表中,擦除该对象的引用计数。最后再调用c函数的free()方法,完成一次对象的回收。

    所以总结一下dealloc方法的内容大致就是:1.先调用_objc_rootDealloc()方法——>2.在方法内部调用rootDealloc()方法——>3.依次判断5个要素:是否非指着型isa,是否含有关联对象相关内容,是否含有弱引用指针的指向,是否含有c++相关内容以及在ARC模式下使用,是否使用了散列表,如果判断都为否,则直接调用C函数的free()释放对象空间,反之则调用object_dispose()方法。

    在object_dispose()方法中的调用流程为:1.调用objc_destructInsetence()方法——>2.调用C函数的free()释放对象空间

    在objc_destructInstence()方法内部实现原理为:1.判断是否含有C++相关内容以及使用了ARC模式——>2.调用objcet_cxxDestruct()函数进行清除相关内容——>3.判断是否含有关联对象相关内容——>4.调用_object_remove_associations()方法清除关联对象相关内容——>5.调用clearDeallocating()方法。

    在clearDeallocating()方法内部,调用了一个sidetable_clearDeallocating()方法,旨在清除散列表相关的数据信息。

    在sidetable_clearDeallocating()方法内部,进行了两个步骤:1.调用weak_clear_no_lock()方法的调用,旨在将对象对应的弱引用表中对应的弱引用结构体数组中的指针全部置为nil——>2.调用table.refcnts.erase(),将对象对应的引用计数表中的引用计数擦除。

    完成之后回到之前的步骤,调用free()函数,完成对一个对象的内存回收。

    其实通过以上对一些内存管理数据结构的解释以及对象回收的一个完整过程,相应的弱引用创建的的过程以及回收的过程自然也一目了然了。

    当我们创建一个弱引用变量weakPointer的时候在编译器中可以这么写

    id __weak weakPointer = object;

    这行代码实际上在系统内部实现的时候转化为了两行代码:

    id weakPointer;

    objc_initWeak(&weakPointer,object);

    首先定义了一个变量weakPointer,其次调用objc_initWeak方法来给weakPointer的这个弱引用指针来赋值。

    内部原理与上面相似,当弱引用指针指向这个objcet变量时,首先去SideTables散列表中通过哈希查找,来找到object这个对象的SideTable表,再通过一次哈希查找,利用object对象的地址,在SideTable中的弱引用表中找到其对应的弱引用结构体数组,如果这个数组存在则在里面添加一个之前weakPointer的地址作为弱引用指针指向object,如果没有这个结构体数组,则创建一个数组,将这个指针添加到第0个元素,同时给第2,3,4个元素设置为nil。这样就完成了一个弱引用指针的定义实现过程了。

    关于如何清除弱引用指针的,Dealloc方法调用过程已经说的很明白了,过程也与上面一行说的类似,就是在最后调用sidetable_clearDeallocating()方法中将对象对应的弱引用列表找到,将所有弱引用指针置为nil的时候就把相应的弱引用指针擦除了,这样说就一目了然了。



     

    展开全文
  • iOS 内存管理

    2021-04-10 15:16:46
    四、OC 对象的内存管理 1、内存管理 2、copy、mutableCopy 关键字 3、引用计数 4、weak 指针 5、autorelease 一、 NSProxy 1、作用 : 用于消息转发 NSProxy没有父类,和 NSObject 算是同一级别的存在
    文章目录
       一、NSProxy
          1、作用: 用于消息转发
          2、定时器的细节
       二、内存布局
       三、标记指针(Tagged pointer)
       四、OC 对象的内存管理 
          1、内存管理
          2、copy、mutableCopy 关键字
          3、引用计数
          4、weak 指针
          5、autorelease  
    

    一、 NSProxy

    1、作用 : 用于消息转发

    NSProxy没有父类,和 NSObject 算是同一级别的存在,都遵循 NSObject 协议,分别定义如下:

    @interface NSProxy <NSObject> {
        Class	isa;
    }
    
    @interface NSObject <NSObject> {
        Class isa ;
    }
    

    那么两者有什么区别?
    首先,NSProxy 是没有 init方法的,直接 alloc就产生实例
    其次,NSProxy是专门用来处理消息转发的,也就是说,对 NSProxy 实例对象发送任何消息,直接进入消息转发环节。

    举一个实际例子,以前用定时器,用方法scheduledTimerWithTimeInterval:target: selector: userInfo: repeats:创建一个定时器,该方法会对 target产生强引用。无论 target传的是弱引用进去,NSTimter里面都有一个成员对 target进行强引用。这样就会造成 viewcontroller 强引用 timer,timer 强引用 self,也就是 target 。造成循环引用问题,导致内存泄露。
    在这里插入图片描述一般来说,我们解决方法有三种
    1、第一种, 使用 scheduledTimerWithTimeInterval: repeats: block: API 去创建定时器,这样就没有 NSTimertarget产生强引用问题,NSTimer只对 block产生强引用,只要在block内部使用弱引用的话,就不会造成内存泄露。

    2、第二种,使用中间对象和消息转发
    在这里插入图片描述截取部分代码如下

    中间对象的代码
    
    #import "TargetObject.h"
    
    @interface TargetObject : NSObject
    
    + (id)generalWithTarget:(id)target;
    
    @property (nonatomic,weak) id target;
    
    @end
    
    @implementation TargetObject
    
    + (id)generalWithTarget:(id)target
    {
        TargetObject *anyObject = [[TargetObject alloc]init];
        anyObject.target = target;
        return  anyObject;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return self.target;
    }
    
    @end
    

    第三种 : 使用 NSProxy + 消息转发

    @interface ProxyObject : NSProxy
    @property (nonatomic,weak) id target;
    
    + (id)generalWithTarget:(id)target;
    
    @end
    
    @implementation ProxyObject
    
    + (id)generalWithTarget:(id)target
    {
        ProxyObject  *anyObject = [ProxyObject alloc];
        anyObject.target = target;
        return  anyObject;
    }
    
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
        return [self.target methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation
    {
        [invocation invokeWithTarget:self.target];
    }
    
    
    @end
    

    关于定时器

    - (void)viewDidLoad {
        [super viewDidLoad];
        //self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TargetObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ProxyObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
    }
    

    那么 方案二 和 方案三 有什么区别呢?
    区别主要是在性能上,具体指方法查找上的性能。
    方案二,TargetObject : NSObject 需要从TargetObject类对象的 cache、methods查找方法到NSObject,最后触发消息转发机制。
    方案三: ProxyObject : NSProxy ,并不会去父类查找方法, 直接进入消息转发.

    要深刻理解 专门用于消息转发 的功用

    ProxyObject *proxy = [ProxyObject generalWithTarget:self];
    NSLog(@"%d",[proxy isKindOfClass:[self class]]);  //结果为 1 
    

    2、定时器的细节
    NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能导致NSTimer不准确。
    比较通俗易懂的话就是说: RunLoop每跑一圈计算下时间,如果遇到 NSTimer,发现时间还没到,继续跑圈。但是 RunLoop每跑一圈的时间是不固定的。如果中间插入一些比较重的任务,就会导致要执行定时器的时候被延迟了。

    那如何保证定时器准确呢? 使用 GCD的定时器,因为 GCD 定时器直接跟系统内核挂钩,不依赖于 RunLoop

        //创建下定时器(注意,这里需要强引用)
       self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,  dispatch_get_main_queue());
        //设置时间(什么时候开始,间隔几秒,误差多少) 立即执行,1s 执行一次
        dispatch_source_set_timer(self.timer ,
                                  dispatch_time(DISPATCH_TIME_NOW, 0*NSEC_PER_SEC),
                                  1 * NSEC_PER_SEC,
                                  0);
        //设置回调
        dispatch_source_set_event_handler(self.timer , ^{
            NSLog(@"TO DO");
        });
        //启动定时器
        dispatch_resume(self.timer);
    

    二、内存布局

    任何一个程序在运行的时候时机上是运行在内存中的,这个内存也就是我们通常说的主存,也叫运行内存,是可以直接与 CPU进行交换数据的内部存储器。内存读取速度很快,所以作为操作系统运行程序的区域。通常我们将内存划分为以下几大块:栈区、堆区、全局区、常量区、代码区

    如图所示(哈哈,这张图是扒别人博客的)
    在这里插入图片描述下面来详细说明下每个分区的功能:
    栈区(stack): 栈区是有系统来自动分配释放,是一个栈的数据结构,存储函数的参数、局部变量、引用。高地址向低地址扩展。

    堆区(heap) : 堆区是由开发者"手动管理"或程序结束时由系统全部回收。是一种树状的数据结构,一般用于存储 alloc、new等方式创建的对象。在 iOS 开发中,大多数内存管理方面的问题也多出于此,开发者没有及时回收内存、内存溢出、内存泄露等。低地址向高地址扩展。

    全局区 : 用于存放全局变量和静态变量。存储方式是 未经初始化的全局变量和静态变量放在一块区域,初始化后的全局变量和静态变量在另外一个区域。回收也是等进程结束后由系统回收。

    常量区 : 字符串常量和基本数据类型值。回收也是进程结束后系统自动回收。

    代码区 : 存储编译后的代码

    三、标记指针(Tagged pointer)

    64bit开始,iOS 引入了 Tagged Pointer技术,用于优化NSString、NSNumber、NSDate等小对象存储。在没有使用 Tagged Pointer 之前,NSString、NSNumber、NSDate等对象需要动态分配内存,维护引用计数,对应的指针存储堆上对象的地址值。使用Tagged Pointer之后,指针存储的数据变成:Tag+Data将数据直接存储在指针中,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。对于 objc_msgSend或者objc_msgSendSuper能够识别 Tagged Pointer,直接从指针中提取数据,节省了调用的开销。

    如何判断一个指针是否是标记指针?在 runtime源码objc-internal.h中有给出一个方法用于判断一个指针是否为标记指针。

    #if TARGET_OS_OSX && __x86_64__
        // 64-bit Mac - tag bit is LSB
    #   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)
    #else
    #  define _OBJC_TAG_MASK 1UL
    #endif
    
    static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) 
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    

    从上面可以看出,在 iOS 中,指针的最高有效位为 1 (1<<63)是标记指针。

    标记指针有什么用?
    节省内存 (只有在 8 字节无法存储的条件下才需要在堆上动态分配内存),
    提高访问速度 (objc_msgSend能够识别 标记指针,无法去进行方法查找,直接从指针取值)。

    看下具体的应用

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
        for (int i = 0 ; i < 10000; i ++) {
            dispatch_async(queue, ^{
                self.name = [NSString stringWithFormat:@"%@",@"fnawggjoowrgnwg4y34y"];
            });
        }
        
    

    上面,运行 crash,访问坏内存。分析: 多个线程同时访问 self.name 的 setter 方法。可能对同一块内存释放两次。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
        for (int i = 0 ; i < 10000; i ++) {
            dispatch_async(queue, ^{
                self.name = [NSString stringWithFormat:@"%@",@"abc"];
            });
        }
    

    上面,运行正常。因为,"abc"这个字符串的值被存储在标记指针中,取值的时候被objc_msgSend识别,直接从指针地址直接读取。不存在访问堆上对象。

    为验证想法,进一步写了个 demo,验证

    bool isToggedPointer(id obj)
    {
      return (long)(__bridge void *)obj & 1;
    }
    
    - (void)viewDidLoad {
      [super viewDidLoad];
      NSString *name = [NSString stringWithFormat:@"fnawggjoowrgnwg4y34y"];
      NSString *toggedStr = [NSString stringWithFormat:@"abc"];
      NSLog(@"%p----%p",name,toggedStr);
      NSLog(@"%@----%@",[name class],[toggedStr class]);
    
    }
    

    运行结果:

    0x60400024e9d0----0xa000000006362613
    __NSCFString----NSTaggedPointerString
    

    iOS 内存是 小端模式,即 数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。第一位 : 0x61,第二位: ox62,第三位 ,ox63。6*16+1 = 97,依次类推。另一方面,0x a000 0000 0636 2613指针最高有效位为 a, 转成二进制 : 0b1010,最高位为 1,满足条件。

    四、OC 对象的内存管理

    1、内存管理
    在 iOS 中,使用引用计数来管理 OC 对象的内存。
    一个新创建的对象的引用计数(retainCount)默认为 1,当引用计数为 0, 对象就会销毁,释放其占用的空间。
    调用 retain会让对象的引用计数 +1,调用release回让引用计算 -1

    当调用 alloc、new、copy、mutableCopy方法返回一个对象,在不需要这个对象时,要调用release、autorelease来释放他
    想拥有某个对象,就让它引用对象 + 1;不想再拥有某个对象,就让它引用计算 -1

    内存管理解决的问题:
    内存泄露(该释放的对象没有释放) 和 野指针( 坏内存访问 )

    2、copy 关键字

    拷贝的目的: 产生一个副本对象,跟源对象互不影响

    iOS 提供了两个拷贝方法
    copy : 不可变拷贝,产生不可变副本
    mutableCopy : 可变拷贝,产生可变副本,

    深拷贝 :内容拷贝,产生新的对象
    浅拷贝: 指针拷贝,没有产生新的对象

    不可变可变
    copy不可变副本(不产生新对象,被拷贝对象引用计数+1)不可变副本(产生新对象)
    mutableCopy可变副本 (产生新对象)可变副本(产生新对象)

    总结: 不可变对象调用copy 是浅拷贝,其他都是深拷贝

    3、引用计数

    64 bit 中,引用计数存储在 isa 指针中, 有可能存在 sideTable类中

    union isa_t 
    {
        Class cls;
        uintptr_t bits;
        //此结构体仅为了增加可读性,纯属摆设
        struct {
            uintptr_t nonpointer        : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 33; 
            uintptr_t magic             : 6;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;  //当 19 位不够存储的时候,为1 ,存在sideTable_t 结构中
            uintptr_t extra_rc          : 19;  //存储引用计数  19 位
        };
    

    其中 SideTable 定义如下

    struct SideTable {
        spinlock_t slock;      //自旋锁
        RefcountMap refcnts;   // 引用计数 散列表
        weak_table_t weak_table; //弱引用 
    }; 
    

    weak_table_t定义如下

    struct weak_table_t {
        weak_entry_t *weak_entries;
        size_t    num_entries;
        uintptr_t mask;
        uintptr_t max_hash_displacement;
    };
    

    看下 获取引用计数的底层实现

    - (NSUInteger)retainCount {
        return ((id)self)->rootRetainCount();
    }
    

    继续看 RootRetainCount函数实现

    inline uintptr_t objc_object::rootRetainCount()
    {
        if (isTaggedPointer()) return (uintptr_t)this;
    
        sidetable_lock();
        isa_t bits = LoadExclusive(&isa.bits);
        //获取 bits
        ClearExclusive(&isa.bits);   
        //判断是不是优化过的指针
        if (bits.nonpointer) {
            // extra_rc 本来存储就是引用计算 -1 ,直接加上 1 上去
            uintptr_t rc = 1 + bits.extra_rc;
            // 如果存在 sideTable_t
            if (bits.has_sidetable_rc) {
                //就继续去 sideTable_t结构中去找
                rc += sidetable_getExtraRC_nolock();
            }
            sidetable_unlock();
            return rc;
        }
    
        sidetable_unlock();
        return sidetable_retainCount();
    }
    

    4、 weak 指针

    __weak 和 _unsafe_unretain 共同点都不会产生强引用,指向对象如销毁了,__weak 会将指向对象的指针置为 nil,而 _unsafe_unretain 不会。
    看下 weak修饰的指针如果在对象销毁的时候(即调用 dealloc)如何将指针设置成 nil
    还是查找 runtime源码

    dealloc 源码

    - (void)dealloc {
        _objc_rootDealloc(self);
    }
    

    _objc_rootDealloc源码

    void _objc_rootDealloc(id obj)
    {
        assert(obj);
        obj->rootDealloc();
    }
    

    rootDealloc源码

    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        /**
         如果是优化过的 isa,
         且没有弱引用
         且没有关联对象
         且没有 c++ 析构函数
         且没有 sizdeTable_t
         直接 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 函数
    */
            object_dispose((id)this);
        }
    }
    
    

    object_dispose 函数

    id object_dispose(id obj)
    {
        if (!obj) return nil;
    
        objc_destructInstance(obj);  
        // 在对象销毁之前,调用了  objc_destructInstance 函数
        free(obj);
    
        return nil;
    }
    

    看下,在释放对象之前,调用了 objc_destructInstance函数

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
    
            // This order is important.
            /**
             如果存在 c++ 析构函数、存在关联对象
             先进行释放
             后,继续调用  clearDeallocating 函数
            */
            if (cxx) object_cxxDestruct(obj);
            if (assoc) _object_remove_assocations(obj);
            obj->clearDeallocating();
        }
        return obj;
    }
    

    clearDeallocating函数的调用

    inline void objc_object::clearDeallocating()
    {
        if (slowpath(!isa.nonpointer)) {
            // Slow path for raw pointer isa.
            sidetable_clearDeallocating();
        }
        /**
         如果存在弱引用 和 has_sidetable_rc不为空
        */
        else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
            // Slow path for non-pointer isa with weak refs and/or side table data.
            clearDeallocating_slow();
        }
    
        assert(!sidetable_present());
    }
    

    clearDeallocating_slow函数

    void objc_object::clearDeallocating_slow()
    {
        assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    
        SideTable& table = SideTables()[this];
        table.lock();
        //将散列表取出来,并 weak_clear 清除 weak 修饰的指针
        if (isa.weakly_referenced) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    

    继续看下 weak_clear_no_lock的实现,结合 weak_table_t的结构

    struct weak_table_t {
        weak_entry_t *weak_entries;
        size_t    num_entries;
        uintptr_t mask;  //这个 mask 应该是容量 -1 
        uintptr_t max_hash_displacement;
    };
    
    void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
        
        //取出来 entry 
        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;
        }
        
        /**
        这里可以证明,entry 里面有个类似数组的数据结构,存放这被 weak 修饰的指针
        遍历,然后设置成 nil 
    */
        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);
    }
    

    看下如何取 entry

    static weak_entry_t *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;
        //看到这里,也是进行 & 操作 得到 index
        size_t begin = hash_pointer(referent) & weak_table->mask;
        size_t index = begin;
        size_t hash_displacement = 0;
        while (weak_table->weak_entries[index].referent != referent) {
            index = (index+1) & weak_table->mask;
            if (index == begin) bad_weak_table(weak_table->weak_entries);
            hash_displacement++;
            if (hash_displacement > weak_table->max_hash_displacement) {
                return nil;
            }
        }
        
        return &weak_table->weak_entries[index];
    }
    

    总上所述,可以得出如下结果: 当出现不存在 关联对象、弱引用、析构函数,对象是直接调用 free()函数直接释放。如果存在,需要对这些进行额外的释放工作。其中,对于弱引用,需要调用 clearDeallocating函数对齐进行释放,需要根据当前对象找到 sideTable,进入通过 sideTabl->weakTable,最后,根据当前对象的地址去 weak_table_t这张散列表表中根据 &obj & mask取出weak_entry_t,先把 weak_entry_t 中的指针数组取出来,遍历设置为 nil,然后从 weakTableremove 这个weak_entry_t。其中这个 weak_entry_t里面有个 弱引用指针数组。这个过程跟runtime消息发送过程中,查找类对象方法缓存cache非常类似。

    5、 autorelease

    对一个对象发送 autorelease消息后,这个对象会被放进 autoreleasepool(自动释放池)。

    为了,看清楚 autoreleasepool的结构,创建了命令行工程,并对其进行重写 生成 c++代码

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    经过命令行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m得到 main.cpp

    struct __AtAutoreleasePool {
        
        void * atautoreleasepoolobj;
        
        //构造函数(默认初始化方法)
        __AtAutoreleasePool() {
            atautoreleasepoolobj = objc_autoreleasePoolPush();
            
        }
        //析构函数(相当于  dealloc )
        ~__AtAutoreleasePool() {
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
    
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { 
           __AtAutoreleasePool __autoreleasepool;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hm_w26qvm0x6ld5k2pfy0007h3r0000gn_T_main_8920aa_mi_0);
        }
        return 0;
    }
    

    可以知道,生成是个对象,相当于 大括号开始调用objc_autoreleasePoolPush,大括号结束之前调用 objc_autoreleasePoolPop()
    这两个函数可在 runtime源码中,NSObject.mm找打对应是实现

    void *objc_autoreleasePoolPush(void)
    {   //调用  AutoreleasePoolPage 对象的 push()方法
        return AutoreleasePoolPage::push();
    }
    
    void objc_autoreleasePoolPop(void *ctxt)
    {
       //调用  AutoreleasePoolPage 对象的 pop()方法
        AutoreleasePoolPage::pop(ctxt);
    }
    
    

    由此可见,自动释放池的主要底层数据结构为 : __AtAutoreleasePoolAutoreleasePoolPage。调用autorelease的对象最终都是通过 AutoreleasePoolPage 对象来管理的。

    AutoreleasePoolPage定义如何,也可在 runtime中找到对应定义如下(仅摘取部分代码)

    class AutoreleasePoolPage {
        magic_t const magic;
        id *next;  //返回下一个能存放 autorelase 对象的内存地址
        pthread_t const thread;
        AutoreleasePoolPage * const parent; // piror 指针,指向上一个 AutoreleasePoolPage 对象
        AutoreleasePoolPage *child;  // next 指针,指向下一个 AutoreleasePoolPage 对象
        uint32_t const depth;
        uint32_t hiwat;
        //返回开始存放 autorelease 对象的地址
        id * begin() {
            return (id *) ((uint8_t *)this+sizeof(*this));
        }
        //返回整个对象的内存的结束地址
        id * end() {
            return (id *) ((uint8_t *)this+SIZE); // 这个size 为 4096
        }
        //添加 autorelease 对象
        static inline id autorelease(id obj)
        {
            assert(obj);
            assert(!obj->isTaggedPointer());
            id *dest __unused = autoreleaseFast(obj);
            assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
            return obj;
        }
    }
    

    每个 AutoreleasePoolPage对象占用了 4096字节内存,除了用它来存放内部的成员变量(7*8 = 56 字节),剩下的空间(4096 - 56 = 4040 字节)用来存放 autorelease 对象的地址。
    所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
    什么是双向链表 :双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
    在这里插入图片描述在这里插入图片描述
    接下来就需要搞懂,autorelease 对象如何存放

    存放过程:
    当调用 objc_autoreleasePoolPush()的时候,

    会将 POOL_BOUNDARY存入 第一个 AutoreleasePoolPage 对象中从 第 56 字节内存并返回对应内存地址,赋值给 next 指针

    当有 autorelease 对象,继续往下面存(每次存放都会更新 next指针的值,指向下一个能存放autorelease 对象地址的区域)

    如果 第一个 AutoreleasePoolPage不够用了,会创建第二个 AutoreleasePoolPage 对象,并将该对象的 parent指针指向第一个AutoreleasePoolPage 对象,第一个AutoreleasePoolPage 对象的child指针指向第二个 AutoreleasePoolPage 对象。然后继续存放 autorelease 对象

    当调用 objc_autoreleasePoolPop(POOL_BOUNDARY)
    依次从栈顶取出 autorelease 对象 直到遇到 POOL_BOUNDARY,对其 发送 release 消息进行释放 。

    注意 :POOL_BOUNDARY 从 调用 PUSH 返回,然后 调用 Pop中传入。对于 autoreleasepool嵌套那么就有 三个 POOL_BOUNDARY。嵌套的时候,每个POOL_BOUNDARY返回的内存地址都不同。当销毁的时候,暂停遇到的POOL_BOUNDARY 也不同。

    如何嵌套是这么存储的?关键看个方法 _objc_autoreleasePoolPrint()。这个方法可以用来查看自动释放池的情况。

    #import <Foundation/Foundation.h>
    
    //在 buildsetting 搜索 automatic reference 将其改成 MRC 
    
    extern void _objc_autoreleasePoolPrint(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            NSObject *object1 = [[[NSObject alloc]init] autorelease];
            
            @autoreleasepool{
                NSObject *object2 = [[[NSObject alloc]init] autorelease];
                NSObject *object3 = [[[NSObject alloc]init] autorelease];
        
                @autoreleasepool{
                    NSObject *object4 = [[[NSObject alloc]init] autorelease];
                    _objc_autoreleasePoolPrint();
                }
            }
        }
        return 0;
    }
    

    控制台输出

    objc[27124]: ##############
    objc[27124]: AUTORELEASE POOLS for thread 0x100392380
    objc[27124]: 7 releases pending.   // 7 个对象准备 release 
    objc[27124]: [0x103001000]  ................  PAGE  (hot) (cold) // page对象(hot 表示当前正在使用的 哪一页 )
    objc[27124]: [0x103001038]  ################  POOL 0x103001038  // 这个是POOL_BOUNDRY
    objc[27124]: [0x103001040]       0x100727ff0  NSObject
    objc[27124]: [0x103001048]  ################  POOL 0x103001048 // 这个是POOL_BOUNDRY
    objc[27124]: [0x103001050]       0x1007230f0  NSObject
    objc[27124]: [0x103001058]       0x100721310  NSObject
    objc[27124]: [0x103001060]  ################  POOL 0x103001060 // 这个是POOL_BOUNDRY
    objc[27124]: [0x103001068]       0x10071dd60  NSObject
    objc[27124]: ##############
    Program ended with exit code: 0
    

    调用 autorelease

    - (id)autorelease {
        return ((id)self)->rootAutorelease();
    }
    

    函数 rootAutorelease

    inline id 
    objc_object::rootAutorelease()
    {
     /**
     如果是标记指针,直接返回当期那对象
    */
        if (isTaggedPointer()) return (id)this;
        if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    
        return rootAutorelease2();
    }
    

    调用rootAutorelease2()

    id objc_object::rootAutorelease2()
    {
        assert(!isTaggedPointer());
        return AutoreleasePoolPage::autorelease((id)this);
    }
    

    可以看到,调用 autorelease最后底层调用的是 AutoreleasePoolPage的公开方法,里面继续走了存放过程的流程。

      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);
            }
        }
    

    下面看下 pop流程, 也是调用AutoreleasePoolPage的公开方法, token参数为POOL_BOUNDARY,里面的核心方法为 page->releaseUntil(stop)

      
        static inline void pop(void *token) 
        {
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                // Popping the top-level placeholder pool.
                if (hotPage()) {
                    // Pool was used. Pop its contents normally.
                    // Pool pages remain allocated for re-use as usual.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
    
            page = pageForPointer(token);
            stop = (id *)token;  
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    // Start of coldest page may correctly not be POOL_BOUNDARY:
                    // 1. top-level pool is popped, leaving the cold page in place
                    // 2. an object is autoreleased with no pool
                } else {
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
             /**
              核心代码: 从栈顶开始将 autorelease 对象释放
              
             */
            page->releaseUntil(stop);
    
            // memory: delete empty children
            if (DebugPoolAllocation  &&  page->empty()) {
                // special case: delete everything during page-per-pool debugging
                AutoreleasePoolPage *parent = page->parent;
                page->kill();
                setHotPage(parent);
            } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                // special case: delete everything for pop(top) 
                // when debugging missing autorelease pools
                page->kill();
                setHotPage(nil);
            } 
            else if (page->child) {
                // hysteresis: keep one empty child if page is more than half full
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }
    
    

    下面看下 核心方法 void releaseUntil(id *stop)的实现

     void releaseUntil(id *stop) 
        {
            // Not recursive: we don't want to blow out the stack 
            // if a thread accumulates a stupendous amount of garbage
            
            while (this->next != stop) {
                // Restart from hotPage() every time, in case -release 
                // autoreleased more objects
                AutoreleasePoolPage *page = hotPage();
    
                // fixme I think this `while` can be `if`, but I can't prove it
                while (page->empty()) {
                    page = page->parent;
                    setHotPage(page);
                }
    
                page->unprotect();
                id obj = *--page->next;
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
                page->protect();
    
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);
                }
            }
    
            setHotPage(this);
    
    #if DEBUG
            // we expect any children to be completely empty
            for (AutoreleasePoolPage *page = child; page; page = page->child) {
                assert(page->empty());
            }
    #endif
        }
    

    那么,剩下最后一个问题,autorelease对象释放时机的问题。

    我觉得回答这个问题,得分两种情况,
    第一种,就是对象被 autoreleasepool包裹着,且有机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY),那么,对象就会立即被释放。

    第二种,就是 下面这种情况

    #import <UIKit/UIKit.h>
    #import "AppDelegate.h"
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    没机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY),因为这里会有 runloop,那是不是意味着在其他页面创建的 autorelease对象就没办法释放了?事实上,并不是。那就说明 main函数里的autoreleasepool并不管理其他地方创建的autorelease对象。事实上,这个跟 runloop有关系,iOS在主线程的RunLoop中注册了两个Observer

    第 一 个 Observer监听了 RunLoopkCFRunLoopEntry 事件,会调用 objc_autoreleasePoolPush()
    第 二 个 Observer 监听了 RunLoopkCFRunLoopBeforeWaiting事件,会调用 objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    第 二 个 Observer 还监听了 RunLoopkCFRunLoopExit事件,调用objc_autoreleasePoolPop()

    这个如何验证,简单,打印下[NSRunLoop mainRunLoop]即可。

    
     activities = 0x1 = kCFRunLoopEntry
     // 第一个观察者
    <CFRunLoopObserver 0x604000135720 [0x10bd9ec80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}",
     
     activities = 0xa0 = 160 ( = 128 + 32 ) = kCFRunLoopBeforeWaiting | kCFRunLoopExit
     
     // 第二个观察者
     <CFRunLoopObserver 0x604000135540 [0x10bd9ec80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}"
     ),
    
    

    再贴下 RunLoop的 activity

     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),                 1
     kCFRunLoopBeforeTimers = (1UL << 1),          2
     kCFRunLoopBeforeSources = (1UL << 2),         4
     kCFRunLoopBeforeWaiting = (1UL << 5),         32
     kCFRunLoopAfterWaiting = (1UL << 6),          64
     kCFRunLoopExit = (1UL << 7),                  128
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
    

    讲了这么多,其实内管管理主要关注两个问题,也是主要解决的两大问题 : 内存泄露坏内存的访问(僵尸对象)。其中,内存泄露检查,可以使用 instrument 中的 leak工具检测,僵尸对象可以勾选 Zoombie object进行捕获。了解底层实现原理的只是帮助你更好的理解整个流程,我觉得还是取决于平常的编码习惯和经验。

    展开全文
  • iOS内存管理

    2021-06-02 19:11:46
    如果要说iOS内存管理问题,先要知道Objective-C是一门什么样的语言 —> Objective-C是一门面向对象的语言。我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在Objective-C中对象时存储在堆中的...

    如果要说iOS的内存管理问题,先要知道Objective-C是一门什么样的语言 —> Objective-C是一门面向对象的语言。

    我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在Objective-C中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上入:int float 等)。如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存。其他高级语言如C#、Java都是通过垃圾回收来(GC)解决这个问题的,但在Objective-C中并没有类似的垃圾回收机制,因此它的内存管理就需要由开发人员手动维护。今天将着重介绍Objective-C内存管理:

    一、Objective-C 内存管理的对象

    1、内存管理的对象主要有两类:

    ①基本数据类型:int 、float、 struct 等。

    ②引用类型:继承自NSObject类的所有Objective-C对象。

    第一种基本数据类型不需要我们管理,第二种需要管理。

    2、管理内存的原理:

    ①值类型一般都会被放入栈中(先进后出),引用类型会被放到堆中,当给对象分配内存空间时,会随机从内存当中开辟空间,对象和对象之间可能会存在很多碎片,这些碎片空间需要我们管理。

    ②栈内存从性能上来说优于堆内存,数据量过大的时候,我们会把数据存入堆中,然后在栈中存放堆的地址,这样调用数据的时候,就可以快速的通过栈中的地址拿到堆中的数据。

    ③装箱:把int等包装成NSNumber(会降低性能);

    拆箱:把NSNumber转化为float等 (注意类型错误时候的安全性问题);

    装箱和拆箱会增加代码的运行时间,降低代码可读性,影响性能。

    二、内存管理方式 主要是MRC和ARC管理方式;

    MRC:手动管理内存有时候并不容易,因为对象的引用有时候是错综复杂的,对象之间可能互相交叉引用,此时需要遵循一个法则:谁创建,谁释放。

    ARC:在Objective-C中也有一种内存自动释放的机制叫做“自动引用计数”(或“自动释放池”),与C#、Java不同的是,这只是一种半自动的机制,有些操作还是需要我们手动设置的。自动内存释放使用@autoreleasepool关键字声明一个代码块,如果一个对象在初始化时调用了autorelase方法,那么当代码块执行完之后,在块中调用过autorelease方法的对象都会自动调用一次release方法。这样一来就起到了自动释放的作用,同时对象的销毁过程也得到了延迟(统一调用release方法)。

    autorelease方法不会改变对象的引用计数器,只是将这个对象放到自动释放池中;

    自动释放池实质是当自动释放池销毁后调用对象的release方法,不一定就能销毁对象(例如如果一个对象的引用计数器>1则此时就无法销毁);

    由于自动释放池最后统一销毁对象,因此如果一个操作比较占用内存(对象比较多或者对象占用资源比较多),最好不要放到自动释放池或者考虑放到多个自动释放池;

    ObjC中类库中的静态方法一般都不需要手动释放,内部已经调用了autorelease方法;

    三、MRC与ARC混编 在ARC项目中,对MRC文件可以添加 -fno-objc-arc标识;在MRC中,对ARC的文件可以添加 -fobjc-arc 的标识。(或者可以把MRC文件转为ARC,现在多用ARC,但是有些第三方框架还是MRC的需要处理);

    展开全文
  • iOS 内存管理(一)

    2021-06-02 19:11:55
    前言iOS开发中,内存管理是从来都不能忽视的问题,OC采用的是动态内存管理方式,跟踪每个对象被引用的次数,当对象引用次数为0时,则释放对象占用的内存。引用计数分为自动和手动计数(retain 引用、release释放,...

    前言

    iOS开发中,内存管理是从来都不能忽视的问题,OC采用的是动态内存管理方式,跟踪每个对象被引用的次数,当对象引用次数为0时,则释放对象占用的内存。引用计数分为自动和手动计数(retain 引用、release释放,autorelease 废弃),在此我主要对自动引用计数做相关的分享。

    自动引用计数

    自动引用计数顾名思义是自动计数管理,是编译器在编译过程中自动添加retain、release来确保对象被释放(注:arc 只能管理oc的对象,不能管理通过malloc申请的内存)并利用@autoreleasepool代替NSAutoreleasePool。

    首先让我们先了解下内存管理的思维方式:

    自己生成的对象,自己持有

    非自己生成的对象,自己持有

    不再需要自己持有的对象时释放

    非自己持有的对象不能释放

    了解了思维方式,那么怎么去生成并持有对象呢,在OC中有多种方法族大家并不陌生,用于初始化并持有对象,分别是alloc/new/copy/mutableCopy。另 init 方法族:以init 开头的方法必须被定义为实例方法,它一定要返回id 类型或父类、子类的指针;其他族可以是类方法也可以是实例方法。另 所有权声明 是通过 _ _strong(强引用,ARC中默认)、_ _weak(弱引用,常用于防止循环引用)、_ _unsafe_unretained(iOS 5下相当于weak)、_ _autoreleasing (自动释放池所用,id/对象  另加 星 * 类型变量 默认)。

    引用计数表,在OC 中采用hash表来管理引用计数表键值为内存块地址;这样对象内存块就无需考虑头部了,直接通过引用计数表的内存块地址就可以找到对象内存块。

    ARC规则

    在ARC中有一些规则必须遵守否则会警告甚至引起程序崩溃

    1、不能使用retain/release/retainCount/autorelease

    arc 中内存由编译器控制,不必使用上述内存管理方法

    2、不能使用NSAllocateObject/NSDeallocateObject

    3、必须遵守内存管理方法命名规则 alloc/new/copy/mutableCopy/init

    4、不可显示调用dealloc,不能使用NSZone

    5、使用@autoreleasepool块代替NSAutoreleasePool

    6、对象型变量不能作为C语言结构体的成员

    7、显示转换id 和void  如 id obj =[NSObject alloc] init]; void *p =(_ _bridge void *)obj

    属性

    1、property 指一个对象的属性或特性

    2、@synthesize :自动生成getter、setter方法;@dynamic 告诉编译器要自己手动实现        getter、setter

    3、给属性指定选项

    828e4b0b90df

    828e4b0b90df

    828e4b0b90df

    注:默认为 atomic ,必须要用lock unlock 保证属性的线程安全,如果不是频繁的使用且不考虑多线程的话,尽量用noatomic

    一些记录点:

    1、arc 的实现 是通过clang 编译器 和objc 运行时库结合进行内存管理

    2、引用计数获取方法: _objec_rootRetainCount(id obj)

    3、strong 与 retain 在 block 下,strong相当于copy ,retain 相当于 assign

    4、在block 里使用外部变量或对象的时候,用__blcok 修饰时实质是指针拷贝

    5、GC 垃圾回收机制  只支持 mac os

    参考:

    展开全文
  • Ios 内存管理

    2021-05-13 15:23:36
    文章目录内存管理整理1.为什么使用内存管理2.OC的内存管理主要有三种方式3.OC中内存管理的基本思想4.管理规则5.自动内存管理(ARC)6.自动释放池7.相关面试题 内存管理整理 1.为什么使用内存管理 严格的内存管理,...
  • iOS内存管理—MRC

    2021-04-02 21:18:37
    任何继承了NSObject的对象都需要进行内存管理,其他非对象类型(int、char、float、double、struct、enum等不需要进行内存管理 继承了NSObject的对象存储在操作系统的堆里,堆一般由程序员分配释放,程序结束时...
  • iOS 内存管理知识梳理

    2021-08-28 15:29:39
    iOS 内存管理知识梳理 一、内存泄漏 1、检测方式:Memory Leaks、Alloctions、Analyse、Debug Memory Graph、MLeaksFinder(前四种都比较麻烦,需要不断地调试运行,第五种是腾讯阅读团队出品,效果好一些) 2、...
  • iOS内存管理 —— 自动释放池和runloop1. 自动释放池1.1 自动释放池介绍1.2 自动释放池底层原理autoreleaseNoPageautoreleaseFullPage 1. 自动释放池 1.1 自动释放池介绍 自动释放池是OC中的一种内存自动回收机制,...
  • 本文是 「iOS 开发:彻底理解 iOS 内存管理」系列的「MRC 篇」。 用来对 Objective-C 语法中,手动管理内存 MRC 相关知识进行讲解。 1. 什么是内存管理 程序在运行的过程中,往往涉及到创建对象、定义变量、调用...
  • 本文是 「iOS 开发:彻底理解 iOS 内存管理」系列的「ARC 篇」。 用来对 Objective-C 语法中,自动管理内存 ARC 相关知识进行讲解。 1. 简介 Automatic Reference Counting,自动引用计数,即 ARC,WWDC 20...
  • iOS内存管理—ARC

    2021-04-18 14:45:42
    ARC式内存管理是编译器的工作,附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。 附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,因为自己生成并持有的对象不能继续为自己...
  • iOS内存管理

    2021-03-08 11:48:07
    所以理解iOS内存管理,必然要先去了解一下C语言的内存管理,这样才会更透彻。 C语言的内存四区 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。后进先...
  • iOS内存管理 —— 散列表和弱引用表1. 散列表2. 弱引用表 1. 散列表 散列表的本质是哈希表。散列表在手机上有8张,在模拟器或者其他设备上为64张。散列表里面有锁,引用计数表和弱引用表。其中锁用来保证线程安全,...
  • iOS内存管理 —— TaggedPointer 和 retain、release 流程分析1. 内存布局内存五大区2. 内存管理方案2.1 模拟器调试TaggedPointer2.2 真机调试TaggedPointer3. NONPOINTER_ISA3.1 retain 流程分析3.2 release 流程...
  • iOS-内存管理

    2021-06-02 19:12:29
    内存的五大区栈区:编译器自动分配并释放,存放函数的参数值、局部变量、基本类型的变量或对象引用类型堆区:由程序员分配和释放全局区: 全局变量和静态变量是放在一块的常量区: 常量、字符串代码区二.ARC的核心思想...
  • iOS与Android内存机制有哪些不同,说到这就不得不聊聊iOS和Android系统内存管理机制上的区别。首先要澄清,系统缓慢与卡顿并不是因为占用内存太多了,而是因为系统占用不到内存了,所以在内存和外存数据交换时就会...
  • 02.内存管理/引用计数 03.MRC手动管理引用计数 04.ARC自动引用计数 05.内存泄漏问题 06.野指针问题一、内存区域划分程序在分配内存的时,主要分为:栈区、堆区、静态区、常量区、代码区;内存区域具体说明栈区存放...
  • 终于明白那些年知其然而不知其所以然的iOS内存管理方式 前言 从我开始学习iOS的时候,身边的朋友、网上的博客都告诉我iOS的内存管理是依靠引用计数的,然后说引用计数大于1则对象保存在内存的堆中而引用计数等于0...
  • 返回上级目录:iOS面试专题一 文章目录1.什么是ARC?2.为什么weak指针指向的对象在废弃之后会被自动置为nil?3.苹果是如何实现AutoreleasePool的?4.什么是循环引用?你遇到过哪些循环引用,是怎样解决的? 1.什么是...
  • AutoreleasePool简介AutoreleasePool底层原理Autorelease与NSThread、NSRunLoop的关系AutoreleasePool在主线程上的释放时机AutoreleasePool在子...情况一、Autorelease简介iOS开发中的Autorelease机制是为了延时释放对象...
  • 我想不管是iOS的,还是Java的初学者,内存算得上心中的一个永远抹不去的痛吧,当时作为初学者的我也是一度苦恼,不知道该如何理解这个内存,随着不断的学习,自己对内存也有更深的了解. 内存 说到内存,不能不说一下内存...
  • 一、SideTables 和 weak_table ...在 runtime 内存空间中,SideTables 是一个 64 个元素长度 8 个元素长度的 hash 数组,里面存储了 SideTable。SideTables 的 hash 键值就是一个对象 obj 的 address。 因此,一个 obj
  • iOS平台的内存使用引用计数的机制,并且引入了半自动释放机制;这种使用上的多样性,导致开发者在内存使用上非常容易出现内存泄漏和内存莫名的增长情况; 本文会介绍iOS平台的内存使用原则与使用陷阱; 深度剖析...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 84,890
精华内容 33,956
关键字:

ios内存管理

友情链接: NO.3.rar