iOS循環參考常見情境和解決辦法

來源:互聯網
上載者:User

標籤:syn   3.2   就是   複雜   應該   管理   prot   date   完成   

好多情境會導致循環參考,例如使用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循環參考常見情境和解決辦法

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.