iOS循環引用常見場景和解決辦法
好多場景會導致循環引用,例如使用Block、線程、委托、通知、觀察者都可能會導致循環引用。
1、委托
遵守一個規則,委托方持有代理方的強引用,代理方持有委托方的弱引用。
實際場景中,委托方會是一個控制器對象,代理方可能是一個封裝著網絡請求並獲取數據的對象。
例如:ViewController中需從網絡中獲取數據,讓後展示到列表當中,從網絡獲取的類是 DataUpdateOp
//ViewController.m - (IBAction )onRefreshClicked:(id)sender { //場景獲數據的操作對象 self.updateOp = [DataUpdateOp new]; [self.updateOp startUsingDelegate:self withSelector:@selector(onDataAvailable:)]; } - (void)onDataAvailable:(NSArray *)records { //任務完成時,將操作對象置nil self.updateOp = nil; } //如果控制器 delloc 則取消操作 - (void)delloc { //取消 if(slef.updateOp !=nil){ [self.updateOp cancel]; } }//DataUpdateOp.h @protocol DataUpdateOpDeleate<NSObject> - (void)onDataAvailable:(NSArray *)records; @end @interface DataUpdateOp @property (nonatomic, weak)id <DataUpdateOpDeleate> delegate; - (void)startUpdate; - (void)cancel; @end //DataUpdateOp.m @implementation DataUpdateOp - (void)startUpdate { dispatch_async{ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{ //執行網絡請求後獲取到結果 NSArray *records = ... dispatch_async(dispatch_get_main_queue(),^{ //嘗試獲取委托對象的強引用 id<DataUpdateOpDeleate> delegate = self.delegate; if (!delegate){ return; }else{//判斷原始對象仍然存在嗎? //回傳數據 [delegate onDataAvailable:records]; } }) }; }); } //顯示的要求廢棄回調對象 - (void)cancel { //取消執行中的網絡請求 self.delegate = nil; }
當然,大多數情況下,很多人願意用block 回傳網絡請求數據,像對AFNetworking做一個簡單的二次封裝。
這裏只是將一下如果用代理的話,應該如何避免循環引用。而且做了驗證控制器對象在沒有被回收的時候才做響應的操作。
實際場景中,因為網絡請求的封裝不盡相同,可能會更復雜。
2、Block
block捕獲外部變量(一般是控制器本身或者控制器的屬性)會導致循環引用
-(void)someMethod { SomeViewController *vc = [[SomeViewController alloc] init];
[self presentViewController:vc animated:YES completion:^{ self.data = vc.data; [self dismissViewControllerAnimated:YES completion:nil]; }]; }
這時候引起了循環引用,present vc之後,vc被展示出來,子視圖一致存在,在completion塊中,有引用了self,也就是父控制器。這時父控制器子控制器都在內存當中,如果子控制器裏面做了耗時操作,耗內存的操作,可能會導致內存不足。
解決方法: 使用 ‘weak strong dance‘ 技術
-(void)someMethod { SomeViewController *vc = [[SomeViewController alloc] init]; __weak typeof(self) weakSelf = self; //弱引用self 方便被 completion捕獲 [self presentViewController:vc animated:YES completion:^{ typeof(self) theSelf = weakSelf; //通過一弱引用獲取一個強引用 if(theSelf != nil) { //只在控制器 不為nil的時候才繼續 theSelf.data = vc.data; [theSelf dismissViewControllerAnimated:YES completion:nil]; } }]; }
3、線程與計時器
不正確是使用 NSThread 和 NSTimer對象也可能導致循環引用
運行異步操作的典型步驟:
1、如果沒有編寫更高級的代碼來管理自定義的隊列,則在全局隊列上使用 dispatch_async方法。
2、在需要的時間和地點用NSThread開啟異步執行。
3、使用NSTimer周期性的執行一短代碼
錯誤示例:
@implementation SomeViewController - (void)startPollingTask { self.timer = [NSTimer scheduledTimerWithTimeInterval:120 target:self selector:@selector(updateTask:) userInfo:nil repeats:YES]; } - (void)updateTask:(NSTimer *)timer { //... } - (void)delloc { [self.timer invalidated]; } @end
以上代碼:對象持有了計時器,同時計時器也持有了對象,運行循環也持有了計時器,直到計時器的invalidate方法被調用。
這就造成對計時器對象的附加引用,即使代碼中沒有顯示的引用關系。這仍然會導致循環引用。
實際上:NSTimer對象導致了被運行時持有的間接引用,這些引用是強引用,而且目標的引用計數器會以2(而不是1)增長。必須對計時器調用 inivalidatae方法,移除引用。
如果以上代碼中,控制器被創建多次,那麽控制器是不會被銷毀的。會造成嚴重的內存泄漏。
如果使用了NSThread,也同樣會發生這樣的問題。
解決辦法:
1、主動調用invalidated,
2、將代碼分離到多個類中。
首先,不要指望delloc方法會被調用,因為一旦和控制器發生循環引用,那麽delloc方法永遠不會被調用。delloc()中的 [self.timer invalidated];永遠不會被執行。
因為運行循環會跟蹤活躍的計時器對象和線程對象,所以在代碼找那個設置為nil並不能銷毀對象。想要解決這個問題,可以創建一個自定義的方法,以更加明確的方式執行清理操作。
在一個視圖控制器中,調用這個清理方法的最佳時機是用戶離開視圖控制器的時候,這個時機既可以是點擊返回按鈕,也可可以是其他類似的行為(類直到此事發生的地方),我們可以定義一個cleanUp()方法.
@implementation SomeViewController - (void)startPollingTask { self.timer = [NSTimer scheduledTimerWithTimeInterval:120 target:self selector:@selector(updateTask:) userInfo:nil repeats:YES]; } - (void)updateTask:(NSTimer *)timer { //... } - (void)delloc { [self.timer invalidate]; } @end
上面的這種寫法不能清除timer
3.1清理Timer的方案兩種方法:
1、方法一,在用戶離開當前視圖控制器的時候清理timer
//當視圖控制器進入或者離開視圖控制器時,調用 該方法 - (void)didMoveToParentViewController:(UIViewController *)parent { //如果是離開父控制器, if中判斷為YES, 才執行 cleanUp if (parent == nil) { [self cleanUp]; } } - (void)cleanUp { [self.timer invalidate]; } //2、方法二 通過攔截返回按鈕 執行清理 - (id)init { if (self = [super init]) { self.navigationItem.backBarButtonItem.target = self; self.navigationItem.backBarButtonItem.action = @selector(backButtonPressDetected:); } return sel; } - (void)backButtonPressDetected:(id)sender { [self cleanUp]; [self.navigationController popViewControllerAnimated:YES]; }
3.2 方案二 將持有關系分散到多個類中---任務類執行具體動作,所有者類調用任務
優點1、清理器有良好的職責持有者
優點2、需要時任務可以被多個持有者重復使用
具體:控制器只負責展示數據, 新建一個類NewFeedUpdateTask,周期性的執行任務,檢查填充視圖控制器的最新的數據
//NewFeedUpdateTask.h @interface NewFeedUpdateTask @property (nonatomic, weak) id target;//target是弱引用,target會在這裏實例化任務,並持有它 @property (nonatomic, assign) SEL selector; @end //NewFeedUpdateTask.m @implementation NewFeedUpdateTask //推薦使用的構造方法 外部最好不要用哪個alloc init了 - (void)initWithTimerInterval:(NSTimerInterval )interval target:(id)target selector:(SEL)selector{ if (sellf = [super init]) { self.target = target; self.selector = selector; self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(fetchAndUpdate:) userInfo:nil repeats:YES]; } return self; } //周期性執行的任務 - (void)fetchAndUpdate:(NSTimer *)timer { //檢索feed NewsFeed *feed = [self getFromServerAndCreateModel]; //用weak修飾,確保,使用異步塊的時候不會造成循環引用 __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(),^{ __strong typeof(self) strongSlef = weakSelf; if (!strongSlef){ return; } if (strongSlef.target == nil){ return; } /** strongSlef.target 和strongSlef.selector 是控制器傳過來的,也就是可能有不同的控制器創建本對象,進而初始化 target 和 selector 使用本地變量 target 和 selector 有一個好處: 避免了在以下執行序列中發生競爭的情況 1】在某一個線程A中調用 [target responsToSelector:selector]; 2】在線程B中修改 target 或者 selector 3】在線程A中調用[target performSelector:selector withObject:feed]; 有了這個代碼,即使 target 或者 selector 此刻已經發生了變化,performSelector 仍然會被正確的 target 和 selecctor所調用 **/ id target = strongSlef.target; SEL selector = strongSlef.selector; if ([target respondsToSelector:selector]){ [target performSelector:selector withObject:feed];; } }); } - (void)shutDown { [self.timer invalidate]; self.timer = nil; } //viewController.m @implementation viewController - (void)viewDidLoad { //初始化 定時執行任務的對象 ,內部會觸發計時器 self.updateTask = [NewFeedUpdateTask initWithTimerInterval:120 target:self selector:@selector(updateUsingFeed:)]; } //是 NewFeedUpdateTask 對象周期性的回調方法。 - (void)updateUsingFeed:(NewsFeed *)feed { //根據返回的數據 feed 更新ui } //調用 任務對象的 shutDown方法,其內部會銷毀定時器 - (void)delloc { [self.updateTask shutDown]; } @end
從使用方面來看,viewController 持有了 NewFeedUpdateTask對象, 控制器沒有被除了父控制器之外的對象所持有。
因此,當用戶離開頁面時,也就是點擊了返回按鈕時,引用計數器會被降為0,視圖控制器會被銷毀。這反過來會導致跟新任務停止。
進而導致計時器會被設定為無效,從而觸發關聯對象包括(timer 和 updateTask )的析構。
註意
當使用 NSTimer 和 NSThread 時,總應該通過間接的層實現明確的銷毀過程。這個間接層應該使用弱引用,從而保證所有的對象能夠在停止使用後執行銷毀動作,
iOS循環引用常見場景和解決辦法