内存管理

04 Dec 2019

不管是NSTimer还是CADisplayLink都依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时

这个两个定时器使用时需要注意循环引用的问题


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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

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

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

- (void)dealloc
{
#warning 因为runloop里边还存有timer,所以这里必须注销 timer 或者link
    [self.link invalidate];
    self.link = nil;
    [self.timer invalidate];
    self.timer = nil;
    
    NSLog(@"%s", __func__);
}
@end

使用上面的方法会造成循环引用,ViewController持有timertimer持有target(ViewController)不会进入dealloc

@interface AYProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end

@interface AYProxy ()
@property(nonatomic, weak) id target;
@end

@implementation AYProxy
+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy 对象不需要调用init方法,分配内存之后可以直接应用
    AYProxy *proxy = [AYProxy alloc];
    proxy.target = target;
    
    return proxy;
}

//// 生成方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

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

定义一个AYProxy,让它继承NSProxy,内部使用弱引用的target,并实现消息转发的方法,这样可以使用它来作为timertarget,使用的时候,调用如下方法

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.link = [CADisplayLink displayLinkWithTarget:[AYProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

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

这样可以解除循环引用问题

dealloc的时候如果不释放timer会出现如下的错误

- (void)dealloc
{
#warning 因为runloop里边还存有timer,所以这里必须注销 timer 或者link
    // [self.link invalidate];
    // self.link = nil;
    // [self.timer invalidate];
    // self.timer = nil;
    
    NSLog(@"%s", __func__);
}

timer的任务会在runloopdotimers中执行,displayLink的任务会在runloop的dosource1中执行

NSProxy

@interface NSProxy <NSObject> {
    Class	isa;
}
+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);

// - (id)forwardingTargetForSelector:(SEL)aSelector;

@end

NSProxy 不继承 NSObject,是专门用来做消息转发的类,继承了NSProxy的类不会走NSObject的消息发送流程,会跳过消息动态解析的过程,直接进入消息转发

[[AYProxy proxyWithTarget:self] isKindOfClass:[ViewController class]];

上面的代码返回是true,因为AYProxy所有方法都会进入消息转发,跟直接调用ViewController的方法的结果是一样的。

注意: NSProxy 在swift中不适用

GCD timer

GCD 的 timer 不需要添加到 runloop, 相对来说会比较准,而且也不用担心界面活动时timer会停止。

{
/// 让timer在这个队列中处理事件,使用主队列,timer里边的事件会在主线程处理
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 开始时间,当前时间往后延迟的秒数
    NSTimeInterval start = 2;
    // 定时器的时间间隔
    NSTimeInterval interval = 0.5;
    
    // 创建一个source,存储timer信息
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置timer参数,最后一个参数是能够容忍的误差,这里设定为时间间隔的十分之一
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW+start, interval * NSEC_PER_SEC, interval*0.1);
    
    // 设置定时到回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"%s,%@", __func__, [NSThread currentThread]);
    });

    // 也可以使用函数作为回调, 需要传入c语言的函数指针
    dispatch_source_set_event_handler_f(timer, timerEvent);
    
    # warning event_handler 只能有一个

    // 开启定时器
    dispatch_resume(timer);
    
    // 需要持有timer,不然创建之后被释放会失效
    self.timer = timer;
    
    // 关闭定时器,关闭之后将不能重新开启
    dispatch_cancel(self.timer);
}

void timerEvent()
{
   NSLog(@"%s,%@", __func__, [NSThread currentThread]);
}

iOS程序的内存布局

程序启动之后,会把程序的mach-o文件加在到内存中, 从保留段到__DATA段是mach-o加载出来的。下面那些是程序运行起来后产生的内存。

代码验证

int b1;
float b2;

int a1 = 0;
float a2 = 120;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSString *str1 = @"1123";
        NSString *str2 = @"afgfffdfffffffff";
        
        int c1 = 23;
        int c2 = 234;
        
        NSObject *object1 = [[NSObject alloc] init];
        NSObject *object2 = [[NSObject alloc] init];
        
        NSLog(@"字符串常量");
        NSLog(@"str1->%p", str1);
        NSLog(@"str2->%p", str2);
        
        NSLog(@"已初始化全局数据");
        NSLog(@"a1->%p", &a1);
        NSLog(@"a2->%p", &a2);
        
        NSLog(@"未初始化全局数据");
        NSLog(@"b1->%p", &b1);
        NSLog(@"b2->%p", &b2);
        
        
        NSLog(@"堆");
        NSLog(@"object1->%p", object1);
        NSLog(@"object2->%p", object2);
        
        NSLog(@"栈");
        NSLog(@"c1->%p", &c1);
        NSLog(@"c2->%p", &c2);
        
        
        
    }
    return 0;
}
/**
字符串常量
str1->0x100001018
str2->0x100001038
已初始化全局数据
a1->0x100002044
a2->0x100002040
未初始化全局数据
b1->0x100002048
b2->0x10000204c
堆
object1->0x1006020a0
object2->0x100602ed0
栈
c1->0x7ffeefbff49c
c2->0x7ffeefbff498
 */

Tagged Pointer

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

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

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

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

NSNumber *num1 = @10;
NSNumber *num2 = @11;
NSNumber *num3 = @12;
NSNumber *num4 = @184523374875933234;
NSLog(@"num1-(%@)->%p", [num1 class], num1);
NSLog(@"num2-(%@)->%p", [num2 class], num2);
NSLog(@"num3-(%@)->%p", [num3 class], num3);
NSLog(@"num4-(%@)->%p", [num4 class], num4);
/**
 num1, num2, num3 是tagged Pointer
 num4, 是正常的对象
num1-(__NSCFNumber)->0x5236f0387428a1e5
num2-(__NSCFNumber)->0x5236f0387428a0e5
num3-(__NSCFNumber)->0x5236f0387428a7e5
num4-(__NSCFNumber)->0x10186c570
*/

NSString *str1 = @"123asdfghjkl";
NSString *str2 = [NSString stringWithFormat:@"abc"];
NSString *str3 = [NSStrinstringWithFormat:@"abcasdfghjkllfgzcv"];

NSLog(@"str1->(%@)->%p", [str1 class], str1);
NSLog(@"str2->(%@)->%p", [str2 class], str2);
NSLog(@"str3->(%@)->%p", [str3 class], str3);

/**
 不同方式创建的NSString
str1->(__NSCFConstantString)->0x1000020d8

// NSString 的 tagged pointer 类型
str2->(NSTaggedPointerString)->0x1d9a38f64f7be7d9

str3->(__NSCFString)->0x1020004a0
 */

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

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

OC对象的内存管理

每个位都代表不同的意思,有一个位来表示是否有弱指针,还有19位用来存引用计数,不够存的时候会存附表

OC 对象的释放过程(delloc)

查看objc4 源码

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    // 如果是TaggedPointer,没有引用,直接返回
    if (isTaggedPointer()) return;  // fixme necessary?

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

/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id 
object_dispose(id obj)
{
    if (!obj) return nil;
    
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

关键逻辑在这一段

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is 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.
        // 调用 C++ 析构函数
        if (cxx) object_cxxDestruct(obj);
        // 移除关联对象
        if (assoc) _object_remove_assocations(obj);
        // 移除ivar,还有弱引用
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    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());
}


// 清除附表,并开始清理弱引用
void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
// 把所有weak引用置为nil

总结:

  1. 如果是TaggedPointer,没有引用,直接返回
  2. 调用 C++ 析构函数
  3. 移除关联对象
  4. 移除ivar
  5. 清除附表
  6. 把所有weak引用置为nil

copy的使用

自定义类使用copy属性需要注意一下,比如

@interface AYPerson : NSObject<NSCopying>
@property(nonatomic, copy) NSMutableString *name;
@property(nonatomic, copy) NSMutableArray *list;
@end

这种定义方式会导致namelist的实际类型为NSString,NSArray,因为它们的set方法会这样实现

-(void)setName:(NSMutableString *)name
{
    if (_name != name)
    {
        [_name release];
        _name = [name copy];
    }
}
- (void)setList:(NSMutableArray *)list
{
    if (_list != list)
    {
        [_list release];
        _list = [list copy];
    }
}

所以可变数据类型应该用strong修饰,正确的姿势是这样的

@interface AYPerson : NSObject<NSCopying>
@property(nonatomic, strong) NSMutableString *name;
@property(nonatomic, strong) NSMutableArray *list;
@end

NSString, NSArray, NSDictionary不希望得到的数据被意外修改的时候,建议用copy修饰,可以保证数据是独享的 比如UILabeltext就是这样声明的

autorelease原理

 @autoreleasepool {
    NSObject *obj = [[[NSObject alloc] init] autorelease];
}

在自动释放池里边的对象,只要调用了autorelease在超出作用域的时候都会自动释放。

@autoreleasepool到底做了什么? 有两种方式可以窥探

查看汇编代码

可以看到打印 @"hello world"的前后分别调用了 objc_autoreleasePoolPushobjc_autoreleasePoolPop

截取关键代码如下

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_h7_s2yhr_2526d_0tv661_8qbdr0000gn_T_main_1940b1_mi_0);
    }
    return 0;
}

在声明@autoreleasepool处实际上定义了一个 __AtAutoreleasePool结构体,创建的时候会调用cpp结构体的构建函数,内部调用了objc_autoreleasePoolPush并返回一个指针void * atautoreleasepoolobj,在即将出作用域的时候,这个结构体会调用析构函数

 ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

内部调用了objc_autoreleasePoolPop(atautoreleasepoolobj),跟前面查看汇编看到的结果是一样的。


接下来查看objc4 源码NSObject.mm,研究一下这两个函数都干了什么。

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

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

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

源码过于复杂在这里就不粘贴太多了,有兴趣的可以去读源码,下面说一下结论

另外补充一个tip:

// 导出 私有方法,打印 autoreleasePool 状态
extern void
_objc_autoreleasePoolPrint(void);

可以打印出当前自动释放池中有多少待释放的对象,使用这个方式的时候需要把编译模式改成MRC,并调用autorelease方法

NSObject *obj = [[[NSObject alloc] init] autorelease];

Runloop中的Autorelease

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

打印 [NSRunLoop mainRunLoop], 可以找到这样这两个observer

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

 
 activities = 0x1 -> kCFRunLoopEntry
 "<CFRunLoopObserver 0x600003134500 [0x7fff80615350]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray 0x600000e26190 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd973802048>\n)}}",
 
 activities = 0xa0 -> kCFRunLoopBeforeWaiting | kCFRunLoopExit
 "<CFRunLoopObserver 0x6000031345a0 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray 0x600000e26190 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd973802048>\n)}}"

所以iOS中的OC对象,如果放到了自动释放池,会在每个runloop开始休眠之前,或者runloop退出的时候释放。