Words Of Life

UIResponder, Weak Or Strong?

  • 摘要:一个小问题引起的两三思


    前言

  • 一朋友在看视频学习时敲下以下代码(涉及到的主要代码如下),然后问了我一个小问题,问题是:”为什么要有weakLabel指向aLabel?”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #import "ViewController.h"
    @interface ViewController ()
    @property (weak, nonatomic) IBOutlet UILabel *aIBLabel;
    @property (nonatomic, strong, readwrite) UILabel *strongLabel;
    @property (nonatomic, weak, readwrite) UILabel *weakLabel;
    @end
    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *aString = @"Tesing";
    CGSize viewSize = self.view.frame.size;
    UILabel *aLabel = [[UILabel alloc] initWithFrame:CGRectMake(viewSize.width / 2 - 40, viewSize.height / 2 - 40, 80, 80)];
    aLabel.text = aString;
    self.weakLabel = aLabel;
    [self.view addSubview:aLabel];
    }
    @end
  • 我给他的答案比较简单:”因为新建的aLabel可能在后续的地方用到,所以使用weakLabel指向,这样做主要是方便以后访问”

  • 问题本该到此就结束了,本篇文章也许就不复存在。然而,后来朋友走后,看到属性的weak定义,自己又产生了一些后续的思考:”我们使用属性指向UIResponder控件的时候 ,应该是weak还是strong呢?” 针对这个问题,我认为,应该有以下几种情况可以用作讨论。以下纯属个人之见,如有不对,还望指正


Using Interface Builder

  • 结论:在使用IB构建界面的时候,应该使用弱属性

  • 讨论:

    • 我们使用IB”画界面”的时候,IB自动为我们生成的代码就是weak的(如下)

      1
      @property (weak, nonatomic) IBOutlet UILabel *aIBLabel;
    • 这是为什么呢?主要是因为我们使用IB设计界面时,View因为是Controller的属性,会被自动持有,而我们在界面设计中的UILabel则被View持有的SubViews的其中一个SubView所持有,即持有关系如下图。

      Holding
    • 既然aIBLabel已经被最顶层的Controller所持有了,那么,我们再去使用一个同样被Controller所持有强属性aIBLabel去持有UILabel也就变得没有意义了。因为UILabel的生命周期不是由属性aIBLabel去维持的,所以这里我们只需用弱属性指向(见上图),便于后续使用即可

    • 那么我们这里使用强属性会有问题吗?

      • 没有问题。当使用强属性指向控件的时候,UILabel的引用计数为2。当Controller释放时,Controller指向的Properties(包含View与aIBLabel属性)也会释放。而他们的释放又会导致UILabel的引用计数下降至0,一切都安全释放,没有问题。不过苹果更加建议我们在IB中使用弱属性,主要原因可能是因为:如果View都被释放了,那View上的控件即使存在,又有什么意义呢?

Using Code - Code Style 1

  • 结论:在使用代码构建界面的时候,不同的代码风格应当使用不同的持有方式。采用局部变量创建并立即添加到View上的控件,可用弱属性指向;采用局部变量创建,但出了局部变量作用域后,再延后添加到View上的控件,需用强属性指向

  • 讨论:

    • 第一种代码风格:采用临时变量创建并立即添加控件,采用弱属性指向,代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      // ...
      @property (nonatomic, weak, readwrite) UILabel *weakLabel;
      // ...
      // Code Style 1:
      UILabel *aLabel = [[UILabel alloc] initWithFrame:CGRectMake(viewSize.width / 2 - 40, viewSize.height / 2 - 40, 80, 80)]; // ①New - retainCount = 1
      aLabel.text = aString;
      self.weakLabel = aLabel; // ②Point To It - retainCount = 1
      [self.view addSubview:aLabel]; // ③Add SubView - retainCount = 2
    • 此时的持有关系图如下

      Holding Of Code Style 1
    • UILabel的引用计数

      1. 局部变量aLabel创建了UILabel,并持有,这个局部变量在出了作用域之后就被内存收回
      2. 我们为了方便下文继续引用,使用了一个弱属性指向他(如果下文不需要这个变量,不指向他也是可以的,因为他不会影响对象的生命周期)
      3. 把这个UILabel添加到View的Subview上,简单而言,就是View间接持有了这一个UILabel。至此,UILabel才”安全着陆”,哪怕此时局部变量突然因为某些突发情况/出了作用域被收回,UILabel这一块内存也不会被收回(只是引用计数从二将至一罢了)
    • 如果我们想要延后添加控件(不立即添加到View上,但依旧使用weakLabel指向他)。在过后某个事件中使用[self.view addSubview:self.weakLabel];添加可以吗?

      • 不可以!因为当出了这一块函数作用域后,局部变量aLabel的内存将被收回。此时因为再也没有强属性/变量指向我们创建的UILabel了,所以UILabel的retainCount = 0,UILabel的内存也因此被收回。我们的弱属性weakLabel因为指向的对象已经被释放,所以他将自动将自己设为nil,以避免出现野指针问题。所以我们在后面使用[self.view addSubview:self.weakLabel];时,实际上是向空指针发送消息,在OC中,向空指针发送消息不会被处理,也不会抛出异常,所以一切事情看似”平静”度过了,UILabel也因此没有被添加到View上
    • 我们在这里使用强属性指向会有问题吗?

      • 没有问题,而且需要延后添加控件的情况需要使用强属性指向。当使用强属性指向控件的时候,UILabel的引用计数为2。如果没有使用[self.view addSubview:self.weakLabel];,在出了变量作用范围后(即aLabel已经无法访问)时,引用计数降至一,此时可以在其他方法使用[self.view addSubview:self.weakLabel];延后添加;如果是立即添加,内存亦会被安全释放,理由同上文Using Interface Builder中的强属性指向讨论

Using Code - Code Style 2

  • 结论:在使用代码构建界面的时候,不同的代码风格应当使用不同的持有方式。直接采用属性创建的控件,应该使用强属性指向

  • 讨论:

    • 第二种代码风格:直接使用属性创建的控件,应用强属性指向,代码如下

      1
      2
      3
      4
      5
      6
      7
      // ...
      @property (nonatomic, strong, readwrite) UILabel *strongLabel;
      // ...
      // Code Style 2:
      self.strongLabel = [[UILabel alloc] initWithFrame:CGRectMake(viewSize.width / 2 - 40, viewSize.height / 2 - 40, 80, 80)]; // ①New - retainCount = 1
      self.strongLabel.text = aString;
      [self.view addSubview:self.strongLabel]; // ②Add SubView - retainCount = 2
    • 此时的持有关系图如下

      Holding Of Code Style 2
    • UILabel的引用计数

      1. strongLabel属性创建并持有UILabel控件,strongLabel被Controller所持有,所以在Controller死亡之前,无论strongLabel属性还是UILabel控件的内存,都不会被收回
      2. 把这个UILabel添加到View的Subview上,View间接持有了这一个UILabel。此时UILabel控件的引用计数增至二。当Controller死亡时,属性View与属性strongLabel均死亡,直接导致UILabel控件引用计数降至零,UILabel控件安全释放
    • 我们在这里使用弱属性指向会有问题吗?

      • 有!实际上,当你这样做的时候,我们的Xcode就会出现以下提示提醒你不应该这样做了

        Assigning retained object to weak property; object will be released after assignment

      • 当我们使用weak属性创建UILabel控件的时候,创建确实是成功创建了,然而,引用没有其他强属性/变量指向它,所以他一创建不久,他就自怨自艾的觉得自己没有用,”自尽”了。所以我们采用第二种代码风格的时候,一定要使用强属性进行指向


  • 总结:本文讨论了三种不同代码风格指向控件时,应采用的属性内存管理特性,分析如有错误,还望各位指出。一言以蔽之,使用IB构建视图的时候尽量使用强属性,使用代码构建视图的时候尽量使用强属性。
  • 另外个人还有一些小的思考:Code Style 1 与 Code Style 2的区别是什么?我认为:
    • 第一种代码风格主要秉承”先配置好控件,控件再上”的理念,这样能够减少调用View的setter/getter,某种程度上可能加快了效率(虽然只是一点点)。
    • 第二种代码风格主要秉承”控件先上,其他慢慢来”的理念,这样虽然会大量调用到View的setter/getter,但是在某种程度上,减少了创建局部变量的内存消耗(虽然很少)。
    • 个人的话,比较钟爱第二种方式,至于为什么?无非是因为第二种方式代码量较少罢了,毕竟,代码这东西少一句有可能就少一次Debug :)
  • 本文代码下载

  • Last Edited:2016.11.23
  • Author:@Seahub
  • Please contact me if you want to share this Article, 3Q~
五毛也是情, 一元也是爱