ios采用引用计数管理对象的生命周期,开启指针优化后对象的引用计数器可能存在于isa结构体中,
- CADisplayLink:类似于定时器,跟屏幕的刷新频率一致,也就是16ms一次,但是如果掉帧就不准了,用法:
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
注意点:会对target对象产生强引用,导致其无法释放。
- NSTimer:定时器,多种用法,若使用以下用法也会对target产生强引用导致其无法释放。
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:true];
如何解决以上强引用带来的内存无法释放的问题?
中间人代理模式proxy,NSProxy专门同来解决代理问题,让代理对象对当前对象产生弱引用,CADisplayLink和NSTimer对代理对象产生强引用,这样定时器就不会对当前对象产生强引用了。proxy定义如下:
@interface TargetProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property(nonatomic, weak)id target;
@end
@implementation TargetProxy
+ (instancetype)proxyWithTarget:(id)target
{
//NSProxy类是不需要init方法的
TargetProxy *proxy = [self alloc];
proxy.target = target;
return proxy;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
proxy用法如下:
//CADisplayLink代理用法
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:[TargetProxy proxyWithTarget:self] selector:@selector(test)];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//NSTimer代理用法
[NSTimer scheduledTimerWithTimeInterval:1 target:[TargetProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:true];
NSTimer还可以使用代码库解决这个问题如下:
__weak typeof(self) ws = self;
[[NSTimer scheduledTimerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
[ws test];
}] fire];
- GCD定时器
NSTimer定时器依赖runloop,如果runloop任务繁重可能导致定时器不准时。可以使用GCD定时器来解决此问题:
dispatch_queue_t queue = dispatch_queue_create("timer_queue", DISPATCH_QUEUE_CONCURRENT);
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器时间
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(2 * NSEC_PER_SEC)), (uint64_t)(2 * NSEC_PER_SEC), 0);
//设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%s",__func__);
});
//启动定时器
dispatch_resume(timer);
-
ios程序的内存布局
地址从低到高依次是:保留区、代码段、数据段、堆区、栈区、内核区。需要注意的是堆区地址从小到大分配,栈区是从大到小分配。 -
tagged pointer技术
此技术是从64位架构开始,用于优化NSNumber、NSString等ios小对象的内存问题。此技术之前ios中小对象存储也是跟其他类是一样的,动态分布内存、维护引用计数等。使用此技术后ios中的小对象使用tag+data的方式存储,也就是将对象的值直接存储在对象指针中,不会区堆区开辟内存空间了。当指针无法存储数据后才会动态分配内存来存储数据。objc_ msgSend能够识别指针类型是否是使用了此技术,若使用此技术会直接从指针中获取数据,节省了方法调用开销,大大提升了ios的运行效率。通过源码可以看到mac平台上指针地址最低位用来标记是否使用此技术,ios平台使用最高位(64bit)来标记。 -
对象内存管理
在iOS中,使用引用计数来管理OC对象的内存。一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1。当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。copy不可变对象为浅拷贝,不会开辟新的内存空间;copy可变对象或者使用mutaleCopy都是深拷贝,深拷贝会开辟新的内存空间,生成新对象;对象的copy需要遵守NSCopy协议,使用copyWithZone方法进行copy。
可以通过申明以下函数来查看自动释放池中的内存情况
extern void _objc_autoreleasePoolPrint(void);
- 对象的引用计数
在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
refcnts是一个存放着对象引用计数的散列表
- dealloc流程
dealloc-> _objc_rootDealloc-> rootDealloc-> object_dispose->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.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
- 自动释放池
自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage。调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。AutoreleasePoolPage数据结构:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;// 指向当前page的下一个存储对象
pthread_t const thread;
AutoreleasePoolPage * const parent;//指向上一个page,root节点为空nil
AutoreleasePoolPage *child;//指向下一个page,最后一个节点为nil
uint32_t const depth;
uint32_t hiwat;
};
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量(56字节),剩下的空间用来存放autorelease对象的地址(4040字节)。调用objc_autoreleasePoolPush方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。调用objc_autoreleasePoolPop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。id *next指向了下一个能存放autorelease对象地址的区域。
- runloop与autorelease
iOS在主线程的Runloop中注册了2个Observer,第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush(),第2个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush(),也监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。
网友评论