iOS內存管理01-定時器

這一階段我們主要來講講iOS內存管理方面的知識,面試的時候可能大家多多少少都會被問及這方面的問題,那我們就從常見的面試題開講

  • 使用CADisplayLink、NSTimer有什麼注意點?
  • 介紹下內存的幾大區域
  • 講一下你對iOS內存管理的理解
  • ARC都幫我們做了什麼?
  • weak指針的實現原理
  • autorelease對象在什麼時機會被調用release
  • 方法裡有局部對象,出了方法後會立即釋放嗎?

我們一個一個來,今天我們就先來講講第一條使用CADisplayLink、NSTimer有什麼注意點?

主要就是CADisplayLink、NSTimer會對target產生強引用,如果target又對它們產生強引用,那麼就會引發循環引用

循環引用大家都知道吧,這樣就會導致內存洩漏,我們寫代碼來看看

首先我們看下CADisplayLink,我們新建一個NavigationController,然後點擊一個按鈕push到我們ViewController,NavigationController就一個按鈕比較簡單我就不寫了,下面看下我們的ViewController裡的代碼

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


@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 保證調用頻率和屏幕的刷幀頻率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

}

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

- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate];
}
/<code>

可能有些同學沒用過CADisplayLink,

我們先簡單瞭解下,CADisplayLink其實也是一個定時器,只不過這個定時器不用你來設置時間,它是要保證調用頻率和屏幕的刷幀頻率一致,通常來說大概是60FPS,當然如果你主線程要是做了很多耗時操作的話也可能就不到60了,也就是說我們的linkTest方法大概一秒鐘會調用60次的樣子, 那我們運行程序看下控制檯的輸出也證實了這一點

那接下來我們來看問題

我們看到我們displayLinkWithTarget這個類方法會傳入一個self,這樣,CADisplayLink對象就會強引用self,而self強引用了@property (strong, nonatomic) CADisplayLink *link,所以就產生了循環引用,導致兩者都不會被釋放,可能我們很多同學也都會在dealloc方法裡調用 [self.link invalidate] 其實是沒有用的,我們點下返回鍵就可以看到,dealloc根本不用被調用,那如何解決呢,我們先來看看Timer是不是也有這個問題,給Controller添加一個屬性

<code>@property (strong, nonatomic) NSTimer *timer;
/<code>

然後初始化並每隔一秒調用timerTest方法

<code>self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

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

/<code>

我們運行程序看到,返回的時候timer並沒有停止,dealloc也沒有調用說明timer也存在這個問題,那接下來我們先來解決timer的問題,然後再解決CADisplayLink的問題

可能很多同學會說我來個弱指針__weak typeof(self) weakSelf = self ,把weakSelf傳入target, 不就行了嗎,那大家想想管用嗎?我們運行下程序發下然並卵返回timer依舊跑著,dealloc也沒調用,那為什麼之前的都好使這次不好使了呢,之前是因為我們在block裡的循環引用可以用weakSelf來解決,我們現在是沒有block的,而且,weakSelf和self其實都是同一個內存地址,我們只是把它當做參數來傳給target這個形參的,所以沒用,依舊還是強引用,我們可以這樣認為

<code>@interface NSTimer()
@property(strong, nonatomic) id target
@end
/<code>

NSTimer 內部強引用了target跟你外部傳入強引用還是弱引用沒有半毛錢關係,所以這是不能解決問題的,那怎麼辦?其實要是NSTimer的話有好幾種解決方案,我們先來看看第一種方案

1、更換timer的初始化方法,用帶block的方法,這個時候就可以用weakSelf了

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

我們再運行下程序,我們看到dealloc調用了timer也停止了,所以問題也解決了

2、那我們再換回第一種方法,是否可以解決呢,答案是肯定的,我們來引入一個OtherObject當target,我們來畫個圖來理解下

iOS內存管理01-定時器

timer

這樣,timer強引用OtherObject,OtherObject弱引用Controller就這樣就行了,那具體如何來做呢

我們新建一個XXProxy類

<code>@interface XXProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation XXProxy

+ (instancetype)proxyWithTarget:(id)target
{
XXProxy *proxy = [[XXProxy alloc] init];
proxy.target = target;
return proxy;
}
/<code>

然後修改我們的Controller

<code>self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XXProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
/<code>

這樣就解決循環引用了,但是我們發現另一個問題,現在target裡並沒有timerTest這個方法,運行肯定會crash,當然我們也可以直接在XXProxy 類裡寫一個timerTest

<code>- (void)timerTest
{
[self.target timerTest];
}
/<code>

但是大家想沒想過,XXProxy這個可能不止被一個timer用,要是有很多timer總不至於把所有的方法都寫上吧,大家想想有沒有什麼更好的方法呢?其實,這個時候我們就可以用消息轉發機制,之後可以給大家詳細說說,其實消息轉發是有三個階段的forwardInvocation、methodSignatureForSelector、forwardingTargetForSelector,我們現在就直接用第三階段了直接

<code>- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
/<code>

這樣不管外界調用我什麼方法我都直接轉發給控制器對應的方法,這樣是不是就一勞永逸了

現在我們運行下程序,我們可以看到dealloc是有調用的,所以問題解決那我們現在再看下CADisplayLink是不是可以用相同的辦法解決,我們修改下代碼

<code>// 保證調用頻率和屏幕的刷幀頻率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:[XXProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
/<code>

我們運行下程序,看下日誌,同樣解決問題了


分享到:


相關文章: