内存管理-定时器循环、内存布局、tagged pointer、weak指针、copy、自动释放池

先上代码,我们平时用的定时器,cadisplaylink、nstimer,CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

{
    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];
    [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(test) userInfo:nil repeats:YES];
}

- (void)test{
    
}

解决方案就是,nstimer的时候使用block执行任务,cadisplaylink只能使用代理对象来解决循环引用,使用NSProxy这个类或者nsblock,前者效果更好,或者我们使用gcd的定时器,这是跟内核打交道的,更加精准。

{
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf test];
    }];
}

@interface FGTimer : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
 
@end

@implementation FGTimer
+ (instancetype)proxyWithTarget:(id)target{
    FGTimer *timer = [self alloc];
    timer.target = self;
    return timer;
}

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

@end

如果是nsobject,他找不到方法的时候会调用消息发送,动态方法解析、消息转发,而nsproxy直接是消息转发,提高笑了,代理对象就是专门做这个的。

iOS程序的内存布局

Tagged Pointer

  • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
  • 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
  • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
  • objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
  • 如何判断一个指针是否为Tagged Pointer? iOS平台,最高有效位是1(第64bit)

这是源码里面判断是否是tagged pointer指针,下面是一道关于tagged pointer有关的面试题,问分别执行代码1和代码2,会发生生么?

// 代码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:@"abcdefghijkl"];
    });
}

// 代码2
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
for (int i = 0; i<1000; i++) {
    dispatch_async(queue1, ^{
            self.name = [NSString stringWithFormat:@"abc"];
    });
}


// set方法底层实现
- (void)setDog:(Dog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

这两段代码区别就是字符串的长度了,先说会发生生么吧,代码一会crash,坏内存访问: *** -[CFString release]: message sent to deallocated instance 0x600001967020,代码2,正常执行,原因呢,就是我们学的tagged pointer,代码1里面的字符串就不再是常量区了,而是变成一个oc对象,而set方法底部实行是先release前对象,再retain一次,因为线程异步执行,导致release的对象被释放了,又进行一次release,所以发生这个现象,而代码2,保存在常量区,是直接赋值,所以不会发生。

OC对象的内存管理

  • 在iOS中,使用引用计数来管理OC对象的内存
  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void);

copy

面试中,我们经常遇到问,深拷贝,浅拷贝,深拷贝就是内容拷贝,产生了新对象,浅拷贝就是指针拷贝,都指向同一块内存地址

 

copy

mutableCopy

NSString

NSString

浅拷贝

NSMutableString

深拷贝

NSMutableString

NSString

深拷贝

NSMutableString

深拷贝

NSArray

NSArray

浅拷贝

NSMutableArray

深拷贝

NSMutableArray

NSArray

深拷贝

NSMutableArray

深拷贝

NSDictionary

NSDictionary

浅拷贝

NSMutableDictionary

深拷贝

NSMutableDictionary

NSDictionary

深拷贝

NSMutableDictionary

深拷贝

 

dealloc

objc4 源码
- (void)dealloc {
    _objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  // 被优化过的指针
                 !isa.weakly_referenced  &&  // 是否弱指针
                 !isa.has_assoc  &&   // 是否有关联对象
                 !isa.has_cxx_dtor  &&   // 是否含有c++的析构函数
                 !isa.has_sidetable_rc)) // 是否内存指针保存不够,存在sidetable中
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

// 
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

NEVER_INLINE void objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    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指针,会调用objc_object::clearDeallocating_slow(),会在散列表里面将对应的value设置为nil,这也就是我们weak指针的实现原理,weak修饰的对象会存放在散列表中,地址作为key,value为对象,当对象销毁时,会以地址为key搜索到对应的对象,释放掉,这样就避免僵尸对象。

自动释放池

自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage;

调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的,下面是源码里面的结构

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

具体使用为

  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

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

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


最后的结构就是
    atautoreleasepoolobj = objc_autoreleasePoolPush();
 
    FGPerson *person = [[[FGPerson alloc] init] autorelease];
 
    objc_autoreleasePoolPop(atautoreleasepoolobj);

那么问问题来了,什么时候去调用呢,也就是说什么时候释放对象呢?这里牵扯到nsrunloop了,我们打印NSLog(@"%@",[NSRunLoop currentRunLoop]);

// 监听0x1 为kCFRunLoopEntry

"<CFRunLoopObserver 0x600001f14140 [0x7fff805ed4e0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4715bddc), context = <CFArray 0x600002058c30 [0x7fff805ed4e0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7ff238802038>\n)}}",

// 0xa0 kCFRunLoopExit + kCFRunLoopBeforeWaiting
"<CFRunLoopObserver 0x600001f141e0 [0x7fff805ed4e0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4715bddc), context = <CFArray 0x600002058c30 [0x7fff805ed4e0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7ff238802038>\n)}}"

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

iOS在主线程的Runloop中注册了2个Observer

第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()

第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()

监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值