iOS内存管理和定时器的循环引用

/ 0评 / 0

iOS程序的内存布局

代码段:编译之后的代码

数据段

:函数调用开销,比如局部变量。分配的内存空间地址越来越小

:通过 allocmalloccalloc 等动态分配的空间,分配的内存空间地址越来越大

OC对象的内存管理

在 iOS 中,使用 引用计数 来管理 OC 对象的内存。

一个新创建的 OC 对象引用计数默认是 1,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间。

调用 retain 会让 OC 对象的引用计数 +1,调用 release 会让OC对象的引用计数 -1

可以通过以下私有函数来查看自动释放池的情况,可以在 objc4 源码中查看函数实现:

extern void _objc_autoreleasePoolPrint(void);

内存管理的经验总结

当调用 allocnewcopymutableCopy 方法返回了一个对象,在不需要这个对象时,要调用 release 或者 autorelease 来释放它。

想拥有某个对象,就让它的引用计数 +1;不想再拥有某个对象,就让它的引用计数 -1

MRC手动内存管理

Car 类:

@interface Car : NSObject
- (void)drive;
@end

@implementation Car
- (void)drive
{
    NSLog(@"%s", __func__);
}

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

Person 类:

@interface Person : NSObject {
    Car *_car;
}
- (void)setCar:(Car *)car;
- (Car *)car;
@end

@implementation Person
- (void)setCar:(Car *)car
{
    if (car != _car) {
        if (_car) {
            [_car release];
        }
        _car = [car retain];
    }
}

- (Car *)car
{
    return _car;
}

- (void)dealloc
{
    // 相当于调用 [self setCar:nil];,在 setter 方法内 release 并置空。
    self.car = nil;
    NSLog(@"%s", __func__);
    [super dealloc];
}
@end

手动内存管理使用 PersonCar

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *car1 = [[Car alloc] init];
        Car *car2 = [[Car alloc] init];

        Person *percon = [[Person alloc] init];
        [percon setCar:car1];
        [percon setCar:car2];

        [[percon car] drive];

        [car1 release];
        [car2 release];
        [percon release];
    }
    return 0;
}

拷贝

拷贝的目的是为了产生一个副本对象,跟源对象互不影响。

copy 不可变拷贝,产生不可变副本。

mutableCopy 可变拷贝,产生可变副本。

引用计数的存储

64bit 中,引用计数可以直接存储在优化过的 isa 指针中,也可能存储在 SideTable 中。

union isa_t 中,有类似下面代码,不同平台可能存储的位数不同:

uintptr_t has_sidetable_rc  : 1;
uintptr_t extra_rc          : 8;

如果 has_sidetable_rc1 则引用计数存储在 extra_rc,否则存储在SideTable 中。

SideTable 结构

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; // 是一个存放着对象引用计数的散列表
    weak_table_t weak_table; // 弱引用表

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

weak指针的实现原理

一个对象的所有弱引用都存储在一个 hash 表里,对象销毁时会取出 hash 表里所有弱引用赋值 nil

ARC做了什么

利用 LLVM 编译器自动添加 MRC 需要手动添加的内存管理代码(retain/release/autorelase等),利用 Runtime 监控对象销毁时,清空实例变量和弱引用指针置nil。

自动释放池

自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage

调用了 autorelease 的对象最终都是通过 AutoreleasePoolPage 对象来管理的。

每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址

所有的 AutoreleasePoolPage 对象通过双向链表的形式连接在一起。

示例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[[NSObject alloc] init] autorelease];
    }
    return 0;
}

转换 C++ 代码:

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; // 指向了下一个能存放autorelease对象地址的区域 
    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)
    {
    }
};

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    static inline void *push()
    {
        id *dest;
        if (slowpath(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.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }
};

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

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

struct __AtAutoreleasePool {
    // 构造函数
    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }

    // 析构函数
    ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    {
        __AtAutoreleasePool __autoreleasepool;
        NSObject *obj = objc_msgSend(objc_msgSend(objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }
    return 0;
}

autorelease对象什么时候调用release

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

1Observer

2Observer

如果 autorelease 对象包含在 @autoreleasepool {} 自动释放池内,则在最近的自动释放池结束时调用。

Tagged Pointer

64bit 开始,iOS 引入了 Tagged Pointer 技术,用于优化 NSNumberNSDateNSString 等小对象的存储。

在没有使用 Tagged Pointer 之前,NSNumber 等对象需要动态分配内存维护引用计数等,NSNumber 指针存储的是堆中 NSNumber 对象的地址值。

使用 Tagged Pointer 之后,NSNumber 指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。

objc_msgSend 能识别 Tagged Pointer,比如 NSNumberintValue 方法,直接从指针提取数据,节省了以前的调用开销。

如何判断一个指针是否为 Tagged Pointer

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif 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;
}

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    NSNumber *number1 = @1;
    NSNumber *number2 = @2;
    NSNumber *number3 = @(0xFFFFFFFFFF);
    NSLog(@"%d %d %d", _objc_isTaggedPointer(&number1), _objc_isTaggedPointer(&number2), _objc_isTaggedPointer(&number3));
}
@end

可能多个线程同时执行了 [_name release],出现坏内存访问。用 atomic 或者加锁可以防止崩溃。

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

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

下面代码不会崩溃,因为这个 NSString 是一个 Tagged Pointer。也就意味着它不会像 OC 对象那样处理引用计数,而是直接赋值数据:

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

可以通过调用 class 来证实:

NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"abc"];
NSLog(@"%@ %@", [str1 class], [str2 class]); // __NSCFString NSTaggedPointerString

CADisplayLink、NSTimer使用注意

CADisplayLinkNSTimer 会对 target 产生强引用,如果 target 又对它们产生强引用,那么就会引发循环引用。

也就是 CADisplayLinkNSTimer 可能会有循环引用和计时不准的问题。

使用 block 解决循环引用:

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%@", weakSelf);
    }];
}

- (void)dealloc
{
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}
@end

使用 NSProxy 代理对象解决循环引用:

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

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // self对link强引用,proxy对self弱引用,link对proxy强引用
    DisplayLinkProxy *proxy = [DisplayLinkProxy proxyWithTarget:self];
    self.link = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(test)];
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)test
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    if (self.link) {
        [self.link invalidate];
        self.link = nil;
    }
}
@end

GCD定时器解决计时不准

NSTimer 依赖于 RunLoop,如果 RunLoop 的任务过于繁重,可能会导致 NSTimer 不准时。而 GCD 的定时器会更加准时。

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    // 设置延迟开始时间和间隔时间
    dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), (uint64_t)(1.0f * NSEC_PER_SEC), 0);
    // 设置定时器回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
    // 启动定时器
    dispatch_resume(self.timer);
}
@end

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注