zoukankan      html  css  js  c++  java
  • 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 时,总应该通过间接的层实现明确的销毁过程。这个间接层应该使用弱引用,从而保证所有的对象能够在停止使用后执行销毁动作,

     
  • 相关阅读:
    【代码笔记】JTable 、TableModel的使用3
    学习JSP(二)
    Tomcat文件配置
    学习JSP(一)
    配置elipse
    配置Jdk
    定义新运算练习题
    学习JAVA所需的东西
    学习秘籍
    总有那么一个人
  • 原文地址:https://www.cnblogs.com/wjw-blog/p/9186574.html
Copyright © 2011-2022 走看看