使用CADisplayLink顽耳、NSTimer有什么注意點坠敷?
CADisplayLink妙同、NSTimer會對target產(chǎn)生強引用 如果target又對他們產(chǎn)生強引用 那么就會引發(fā)循環(huán)引用
介紹下內存的幾大區(qū)域
代碼段 - 編譯之后的代碼
數(shù)據(jù)段 - 已初始化的數(shù)據(jù) (已初始化的全局變量、靜態(tài)變量等)
- 為初始化的數(shù)據(jù) (未初始化的全局變量膝迎、靜態(tài)變量等)
棧 - 函數(shù)調用開銷粥帚,比如局部變量。分配的內存空間地址越來越小
堆 - 通過alloc限次、malloc芒涡、calloc等動態(tài)分配的空間,分配的內存空間地址越來越大
講一下你對 iOS 內存管理的理解
ARC 都幫我們做了什么卖漫?
LLVM + Runtime 相互協(xié)作的結果
- ARC利用LLVM編譯器自動幫我們生成release retain autorelease
- 弱引用利用Runtime 在程序運行時 進行操作
weak指針的實現(xiàn)原理
將弱引用存在哈希表里面费尽,如果對象要銷毀 就取出當前對象所對應的弱引用表 把弱引用表里面存儲的弱引用都清除
autorelease對象在什么時機會被調用release
autorelease對象什么時候調用release 由RunLoop來控制
它可能是在某次RunLoop循環(huán)中 RunLoop休眠之前調用了release
方法里有局部對象, 出了方法后會立即釋放嗎
會馬上釋放
使用ARC 如果編譯器加入的是autorelease 那么它可能是在某次RunLoop循環(huán)中 RunLoop休眠之前調用了release
如果是加入的是relese 那么出了方法后會立即釋放
思考以下2段代碼能發(fā)生什么事羊始?有什么區(qū)別旱幼?
圖1 崩潰 因為set方法的本質是釋放舊值 存儲新值 (有可能是多條線程同時釋放舊值 報壞內存訪問)
解決方案 1.設置name為atomic 2.在self.name設置加鎖解鎖
圖2 name是Tagger Pointer 不是指針
CADisplayLink、NSTimer使用注意
CADisplayLink突委、NSTimer會對target產(chǎn)生強引用 如果target又對他們產(chǎn)生強引用 那么就會引發(fā)循環(huán)引用
//保證調用頻率和平面的刷幀頻率一致 60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
解決NSTimer的循環(huán)引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
會產(chǎn)生循環(huán)引用
把弱引用的對象傳入到target也無法解決循環(huán)引用的原因是 NSTimer內部對target進行了強引用 無關是傳入的是強引用對象還是弱引用對象
解決方案一
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
解決方案二
創(chuàng)建一個中間對象 弱引用target
@interface LMProxy : NSObject
+(instancetype) proxyWithTarget:(id)target;
@property (weak,nonatomic) id target;
@end
#import "LMProxy.h"
@implementation LMProxy
+(instancetype) proxyWithTarget:(id)target{
LMProxy *proxy = [[LMProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
ViewController.m
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LMProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
解決方案三
NSProxy 沒有init方法 直接alloc就行
NSProxy 跟 NSObject 都是基類 專門用于消息轉發(fā) (如果想直接消息轉發(fā) 直接使用NSProxy 這樣效率更高)
如果繼承于NSProxy 大部分的方法就會自動進行消息轉發(fā)
@interface LMProxy : NSProxy
+(instancetype) proxyWithTarget:(id)target;
@property (weak,nonatomic) id target;
@end
@implementation LMProxy
+(instancetype) proxyWithTarget:(id)target{
LMProxy *proxy = [LMProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
解決CADisplayLink的循環(huán)引用
創(chuàng)建中間對象的方式解決
GCD定時器
RunLoop每跑一圈 他的時間是不固定的
NSTimer依賴于RunLoop柏卤,如果RunLoop的任務過于繁重,可能會導致NSTimer不準時
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//幾秒之后開始執(zhí)行
NSTimeInterval start = 2.0;
//時間間隔
NSTimeInterval interval = 1.0;
dispatch_source_set_timer(self.timer , dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
dispatch_source_set_event_handler(self.timer, ^{
});
dispatch_resume(self.timer);
封裝GCD定時器
@interface MJTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeates:(BOOL)repeates
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeates:(BOOL)repeates
async:(BOOL)async;
//取消任務
+ (void)cancelTask:(NSString *)name;
@end
@implementation MJTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore;
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeates:(BOOL)repeates
async:(BOOL)async{
if (!task || start < 0 || (interval <= 0 && repeates)) return nil;
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//定時器唯一表示
NSString *name = [NSString stringWithFormat:@"%lu",(unsigned long)timers_.count];
//存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore);
//設置回調
dispatch_source_set_event_handler(timer, ^{
if (!repeates) {
//如果非重復 取消定時器
[self cancelTask:name];
}
});
//啟動計時器
dispatch_resume(timer);
return name;
}
+ (void)cancelTask:(NSString *)name{
if (name.length == 0) return;
dispatch_source_t timer = timers_[name];
if (!timer) return;
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
dispatch_semaphore_signal(semaphore);
}
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeates:(BOOL)repeates
async:(BOOL)async{
if (!target || start < 0 || (interval <= 0 && repeates)) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeates:repeates async:async];
}
@end
iOS程序的內存布局
代碼段 - 編譯之后的代碼
數(shù)據(jù)段 - 已初始化的數(shù)據(jù) (已初始化的全局變量鸯两、靜態(tài)變量等)
- 為初始化的數(shù)據(jù) (未初始化的全局變量闷旧、靜態(tài)變量等)
棧 - 函數(shù)調用開銷,比如局部變量钧唐。分配的內存空間地址越來越小
堆 - 通過alloc忙灼、malloc、calloc等動態(tài)分配的空間钝侠,分配的內存空間地址越來越大
Tagged Pointer
節(jié)省內存空間
iOS平臺该园,最高有效位是1(第64bit) 或者 Mac平臺,最低有效位是1 就是Tagged Pointer
當指針不夠存儲數(shù)據(jù)時帅韧,才會使用動態(tài)分配內存的方式來存儲數(shù)據(jù)
objc_msgSend能識別Tagged Pointer里初,比如NSNumber的intValue方法,直接從指針提取數(shù)據(jù)忽舟,節(jié)省了以前的調用開銷
OC對象的內存管理
在iOS中 使用引用計數(shù)來管理OC對象的內存
- 一個新創(chuàng)建的OC對象的引用計數(shù)默認是1 當引用計數(shù)減為0 OC對象就會銷毀双妨,釋放其占用的內存空間
- 調用retain會讓OC對象的引用計數(shù)+1,調用release會讓OC對象的引用計數(shù)-1
copy 和 mutableCopy
拷貝的目的
產(chǎn)生一個副本對象叮阅,跟源對象互不影響
修改了源對象刁品,不會影響副本對象
修改了副本對象,不會影響源對象
copy 不可變拷貝浩姥,產(chǎn)生不可變副本
mutableCopy 可變拷貝挑随,產(chǎn)生可變副本
淺拷貝 指針拷貝 沒有產(chǎn)生新對象
深拷貝 內容拷貝 有產(chǎn)生新對象
內存管理的經(jīng)驗總結
- 當調用alloc new copy mutbaleCopy 方法返回了一個對象 在不需要這個對象時 要調用release或者autorelease來釋放它
- 想擁有某個對象 就讓它的引用計數(shù)+1 不想再擁有某個對象 就讓它的引用計數(shù)-1
引用計數(shù)的存儲
在64bit中 引用計數(shù)可以直接存儲在優(yōu)化過的isa指針中 如果不夠存儲 就會存儲在SideTable中 SideTale中有個
RefcountMap的散列表
refcnts是一個存放著對象引用計數(shù)的散列表
自動釋放池
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 構造函數(shù),在創(chuàng)建結構體的時候調用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析構函數(shù)勒叠,在結構體銷毀的時候調用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
自動釋放池的主要底層數(shù)據(jù)結構是:__AtAutoreleasePool兜挨、AutoreleasePoolPage
調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
AutoreleasePoolPage的結構
AutoreleasePoolPage對象占用4096字節(jié)內存 除了用來存放他的成員變量 剩下的空間用來存放autorelease對象的地址
所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
程序運行過程中 存在著多個AutoreleasePoolPage對象
child 存放著下一個AutoreleasePoolPage對象的地址(如果是最后一個 那么地址為空)
parent 存放著上一個AutoreleasePoolPage對象的地址 (如果是第一個 那么地址為空)
AutoreleasePoolPage的調用
1.調用push會將一個POOL_BOUNDARY入棧 并返回其存放的內存地址
2.調用pop方法時傳入一個POOL_BOUNDARY的內存地址 會從最后一個入棧的對象開始發(fā)送release消息 直到遇到
POOL_BOUNDARY
id *next指向了下一個能存放autorelease對象地址的區(qū)域
可以通過以下私有函數(shù)來查看自動釋放池的情況 extern void _objc_autoreleasePoolPrint(void);
RunLoop和Autorelease
iOS在主線程的RunLoop注冊了2個Observer
- 第一個Observer監(jiān)聽了kCFRunLoopEntry事件 會調用objc_autoreleasePoolPush()
- 第二個Observer監(jiān)聽了
a.監(jiān)聽了kCFRunLoopBeforeWaiting事件 會調用objc_autoreleasePoolPop() objc_autoreleasePoolPush()
b.監(jiān)聽了kCFRunLoopBeforeExit事件 會調用objc_autoreleasePoolPop()