iOS-内存管理

一.内存的五大区

  • 栈区:编译器自动分配并释放,存放函数的参数值、局部变量、基本类型的变量或对象引用类型
  • 堆区:由程序员分配和释放
  • 全局区: 全局变量和静态变量是放在一块的
  • 常量区: 常量、字符串
  • 代码区


二.ARC的核心思想

  • 自己生成的对象,自己持有.
  • 非自己生成的对象,也可以持有.
  • 自己持有的对象不再需要时,需要对其进行释放.
  • 非自己持有的对象无法释放.

三. MRC的内存泄漏问题

- (void)setName:(NSString *)name
{
//如果_name==name,你先release再retain没有任何意义,还会导致崩溃
    if(_name != name)
    {
        //因为已经重新设值新的name了,所以要把原来的_name引用计数-1
        [_name release];
        //同时要将新的name引用技术+1
        _name = [name retain];
    }
}
  • 在dealloc()方法里面需要将_name置nil.

四.copy问题

  • 浅拷贝是拷贝了指向对象的指针
    深拷贝不但拷贝了对象的指针,还在系统中再分配一块内存,存放拷贝对象的内容
  • 不管是NSString还是NSMutableString
    1.copy都产生NSString类型,mutableCopy都产生NSMutableString类型.
    2.不过NSSring的copy只是拷贝指针(浅拷贝)不可变对象进行copy操作都是浅拷贝
    其他的都会产生新的对象.(深拷贝)不可变对象进行mutableCopy和可变对象进行copy或mutableCopy
    copy.png

五.内存优化问题

  • apple使用taggedPointer技术来优化内存
    taggedPointer:如果定义的常量内容较小,64bit的地址完全可以存下这个内容,那么会直接把内容存到64位的数据里面,也就是指针+内容.
  • arm64后,isa指针的33位存储真正的地址,其他的位用来存储引用计数等信息.
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };

extra_rc:表示引用计数-1.如果19位不够存储,那么会使用has_sidetable_rc.
has_sidetable_rc:是否使用SideTable

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;  //弱引用表
}

SideTable里面有个RefcountMap散列表,key为对象value为引用计数.
为什么每个对象都有一个SideTable呢?分离锁:增加读写效率

六.dealloc方法

  • 什么时候调用dealloc()?
    当一个对象调用release()方法的时候会检查引用计数,如果引用计数=0则会调用dealloc()方法.
  • dealloc()都做了什么?
//释放对象
- (void)dealloc {
    //---
    _objc_rootDealloc(self);
}

判断是否是taggedPointer对象,判断是否有弱引用等,如果都没有则直接释放该对象.

objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    /*
     *是否TaggedPointer,它并不是真正的指针
     *是否有弱引用
     *是否有关联
     *是否有析构器
     *是否引用计数过大
     */
    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);
    }
}
id object_dispose(id obj)
{
    if (!obj) return nil;

    //---释放实例变量、移除关联属性、弱引用指向nil
    objc_destructInstance(obj);
    //释放自己
    free(obj);

    return nil;
}

移除关联属性,将弱引用置为nil
这些都是在运行时做的,不是编译的时候就做的..毕竟类似关联对象都是运行时的东西.

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.
        //---释放实例变量
        if (cxx) object_cxxDestruct(obj);
        //---移除关联属性
        if (assoc) _object_remove_assocations(obj);
        //---将弱引用置为nil
        obj->clearDeallocating();//清空弱引用表中的指针
    }

    return obj;
}
  • 所以说:arc是llvm和runtime协作的结果(llvm的编译器会给对象增加retain release, runtime会将弱引用置为nil)

七.autoreleasePool原理

   @autoreleasepool {
       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
   }
  • 源码
class AutoreleasePoolPage : private AutoreleasePoolPageData

struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

每个pool的大小为4096字节

define I386_PGBYTES 4096

  • 命令行objc_rewrite可得:
    1.autoreleasepool会自动在大括号第一行加入autoreleasepoolobj = objc_autoreleasePoolPush(),并且插入一个POOL_BOUNDARY
    2.在最后一行加入objc_autoreleasePoolPop(autoreleasepoolobj),当调用pop时,autoreleasePage会从栈顶找到POOL_BOUNDARY,将其中的每一个对象都执行一遍release
    3.为什么需要POOL_BOUNDARY呢,是为了解决pool大于4096时,如果本页都清空了还没找到POOL_BOUNDARY,就代表需要去上一页继续release直到遇到POOL_BOUNDARY 跨页问题
  • autoreleasepool是以栈为结点的双向链表结构.
  1. 调用了autorelase方法的对象,而且没有被@autorelasePool{}包裹,在什么时候被释放,例如这样的NSObject *objc = [[[NSObject alloc] init] autorelease];
    答:系统在runloop里面增加了一个observer,在当次runloop将要休眠或者退出loop的时候调用AutoreleasePoolPage::pop().

如果是被@autorelasePool{}包裹的对象,则是在大括号结束的时候就释放(调用release方法)。

 @autoreleasepool {
     NSObject *objc = [[NSObject alloc] init];
 }
  1. 在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool.

八.其他

九. 定时器的循环引用

9.1 为什么会有循环引用

不管是手动加入runloop的timer

    //这种写法,必须自己将timer加入到runloop,否则不会生效的
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(action) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    [self.timer fire];

还是系统帮你开启runloop的timer

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(action) userInfo:nil repeats:YES];

他们手动调用invalidate方法都可以销毁定时器

- (void)p_stopTimer {
    [self.timer invalidate];
    self.timer = nil;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self p_stopTimer];
}

但是如果不是手动调用invalidate方法,而是在dealloc里面停止定时器,则不起作用。
因为timer强引用了self,根本不会走dealloc方法,所以无法销毁定时器

- (void)dealloc
{
    [self p_stopTimer];
    NSLog(@"%s",__func__);
}

那我给target传个弱引用怎么样?
不行的,虽然weakSelf是个若指针,不会让self的应用计数加1,但是当参数传给NSTimer时,weakSelf指向的对象作为参数,还是会被强引用的。

__weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(action) userInfo:nil repeats:YES];
9.2 使用block解决循环引用
    //因为block内部实现(block捕获局部变量的时候,也会捕获它的修饰符)
    //所以:self强引用了timer,timer有一个block,但block弱引用了self,
    //所以不会造成循环引用,控制器最终打印了dealloc方法.
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf action];
    }];
9.3 使用NSProxy或者@interface MindleProxy : NSObject解决循环引用

加一个中间件,中间件弱引用self,就能解决循环引用了
同时使用forwardingTargetForSelector,将消息转发给self,完美解决

  • NSProxy的效率更高、更专业一些,因为继承自NSObject会进行方法查找,找不到方法才会走动态方法解析和消息转发,而NSProxy则是直接进行消息转发(包含任何消息,只要是给NSProxy发消息,就会被他转发)
    MindleProxy *proxy = [[MindleProxy alloc] init];
    proxy.target = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(action) userInfo:nil repeats:YES];

//弱引用target
@interface MindleProxy : NSObject
@property (nonatomic, weak) id target;
@end

@implementation MindleProxy
//动态方法解析
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
@end
  • NSProxy则是直接进行消息转发(包含任何消息,只要是给NSProxy发消息,就会被他转发)
    //使用NSProxy
    RealProxy *realProxy = [RealProxy alloc];
    realProxy.target = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:realProxy selector:@selector(action) userInfo:nil repeats:YES];
    // isKindOfClass也被消息转发了
    NSLog(@"%ld", [realProxy isKindOfClass:UIViewController.class]);//1


@implementation RealProxy
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

十.

你可能感兴趣的:(iOS-内存管理)