viewDidLoad 实际上是在每次有 segue 转换时调用的

Posted

技术标签:

【中文标题】viewDidLoad 实际上是在每次有 segue 转换时调用的【英文标题】:viewDidLoad is in fact called every time there is a segue transition 【发布时间】:2012-08-15 12:41:50 【问题描述】:

我看过很多关于堆栈溢出的帖子,说控制器的 viewDidLoad 方法只在第一次访问控制器时调用,不一定每次都调用一次,但总是至少调用一次。

这根本不是我看到的!我整理了一个简单的测试来强调这一点: https://github.com/imuz/ViewDidLoadTest

似乎对于导航控制器 segues 和模态视图 viewDidLoad 总是被调用。唯一不调用它的是在标签之间切换时。

我能找到的关于 viewDidLoad 的所有解释都与此相矛盾:

When is viewDidLoad called? UIViewController viewDidLoad vs. viewWillAppear: What is the proper division of labor? http://www.manning-sandbox.com/thread.jspa?threadID=41506

苹果自己的文档表明只有在内存不足时才会卸载视图。

我目前正在 viewDidLoad 中进行初始化,并假设每次 segue 转换都会调用它。

我错过了什么吗?

【问题讨论】:

【参考方案1】:

菲利普米尔斯的回答是正确的。这只是对它的增强。

系统正在按记录运行。

您看到 viewDidLoad 是因为被推送到导航控制器上的视图控制器是一个 实例。它必须调用 viewDidLoad。

如果您进一步调查,您会发现这些视图控制器中的每一个在弹出时都被释放(只需在 dealloc 中放置一个断点或 NSLog)。这种释放与视图控制器容器无关……它不控制它使用的控制器的生命周期……它只是持有对它的强引用。

当控制器从导航控制器堆栈中弹出时,导航控制器释放它的引用,由于没有其他对它的引用,视图控制器将释放。

导航控制器仅持有对其活动堆栈中的视图控制器的强引用。

如果您想重复使用同一个控制器,有责任重复使用它。当您使用故事板转场时,您(在很大程度上)放弃了该控制。

假设你有一个push segue 来查看控制器Foo 作为点击某个按钮的结果。当点击该按钮时,“系统”将创建Foo 的实例(目标视图控制器),然后执行转场。控制器容器现在拥有对该视图控制器的唯一强引用。完成后,VC 将解除分配。

由于它每次都会创建一个新控制器,因此每次呈现该控制器时都会调用viewDidLoad

现在,如果您想更改此行为,并缓存视图控制器以供以后重用,您必须专门执行此操作。如果您不使用故事板转场,这很容易,因为您实际上是将 VC 推送/弹出到导航控制器。

但是,如果您使用故事板转场,那就有点麻烦了。

有很多方法可以做到这一点,但都需要某种形式的黑客攻击。故事板本身负责实例化新的视图控制器。一种方法是覆盖instantiateViewControllerWithIdentifier。这是当 segue 需要创建视图控制器时调用的方法。它甚至会为您不指定标识符的控制器调用(如果您不指定,系统会提供一个虚构的唯一标识符)。

注意,我希望这主要用于教育目的。我当然不是建议这是解决您的问题的最佳方法,无论它们是什么。

类似...

@interface MyStoryboard : UIStoryboard
@property BOOL shouldUseCache;
- (void)evict:(NSString*)identifier;
- (void)purge;
@end
@implementation MyStoryboard
- (NSMutableDictionary*)cache 
    static char const kCacheKey[1];
    NSMutableDictionary *cache = objc_getAssociatedObject(self, kCacheKey);
    if (nil == cache) 
        cache = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, kCacheKey, cache, OBJC_ASSOCIATION_RETAIN);
    
    return cache;

- (void)evict:(NSString *)identifier 
    [[self cache] removeObjectForKey:identifier];

- (void)purge 
    [[self cache] removeAllObjects];

- (id)instantiateViewControllerWithIdentifier:(NSString *)identifier 
    if (!self.shouldUseCache) 
        return [super instantiateViewControllerWithIdentifier:identifier];
    
    NSMutableDictionary *cache = [self cache];
    id result = [cache objectForKey:identifier];
    if (result) return result;
    result = [super instantiateViewControllerWithIdentifier:identifier];
    [cache setObject:result forKey:identifier];
    return result;

@end

现在,您必须使用这个故事板。不幸的是,虽然 UIApplication 保留了主故事板,但它并没有公开 API 来获取它。但是,每个视图控制器都有一个方法 storyboard 来获取创建它的故事板。

如果您要加载自己的故事板,则只需实例化 MyStoryboard。如果您使用的是默认情节提要,则需要强制系统使用您的特殊情节提要。同样,有很多方法可以做到这一点。一种简单的方法是覆盖视图控制器中的故事板访问器方法。

您可以使 MyStoryboard 成为将所有内容转发到 UIStoryboard 的代理类,或者您可以对主故事板进行 isa-swizzle,或者您可以让本地控制器从其故事板方法中返回一个。

现在,请记住,这里有一个问题。如果你将同一个视图控制器多次推送到堆栈上怎么办?使用缓存,完全相同的视图控制器对象将被多次使用。这真的是你想要的吗?

如果不是,那么您现在需要管理与控制器容器本身的交互,以便它们可以检查该控制器是否已为它们所知,在这种情况下需要一个新实例。

所以,一种在使用默认故事板转场的同时获取缓存控制器的方法(实际上有很多方法)......但这不一定是一件好事,当然也不是什么你默认得到。

【讨论】:

感谢您的详细解答。我很好,不缓存控制器。我只是想多了解一下加载卸载周期,这样我就可以准确地了解我可以和不能放入 viewDidLoad 中的内容。现在清楚多了。 实际上,viewWillUnload 和 viewDidUnload 已被弃用,因此您不应该在其中添加任何代码。处理 didReceiveMemoryWarning 以在内存压力下释放资源。 啊,在 ios6 中。听起来 viewDidLoad 没有真正意义了,它也可能是一个构造函数。我认为这意味着内存警告将不再导致 viewDidUnload 被调用?这种改变意味着 viewDidLoad 只被调用一次?我目前没有IOS6来测试。 @JodyHagins 使用相同的缓存 UIViewController 有什么问题?是否会调用除 viewDidLoad 之外的所有方法? @Odelya 是的,但是您在视图控制器中可能拥有的任何状态也将被覆盖。因此,诀窍是避免重用已经在堆栈上的控制器,因为你会弄乱它们的实例变量。【参考方案2】:

我相信 Apple 文档描述了视图控制器没有被释放的情况。如果您使用 segue,那么您将导致新目标控制器的实例化,并且作为一个新对象,它需要加载一个视图。

在基于 xib 的应用程序中,我有时会缓存一个我知道我可能会经常重复使用的控制器对象。在这些情况下,它们的行为与文档在何时必须加载视图方面保持一致。

编辑: 在阅读您包含的链接时,我看不出它们有任何矛盾。他们也在谈论在视图控制器对象的生命周期中发生的事情。

【讨论】:

这是他们谈论在“低内存条件”下卸载视图的位,这意味着默认情况下它们会被保留,我没有看到这种情况。所以实际上它取决于父控制器的实现。如果您使用导航控制器,则每次都会创建并加载一个新实例。选项卡控制器并非如此。 一种测试低内存行为的方法(在模拟器上)是放置一个视图控制器,用模态视图控制器覆盖它,然后使用硬件->模拟内存警告选项。隐藏控制器的视图应该卸载,然后在模式视图被关闭时重新加载。 oic 很有趣,我试试看。我想任何当前未激活的视图都是卸载的候选者。 是的,它确实卸载了,更新了我的测试项目:github.com/imuz/ViewDidLoadTest。 有趣的是,在正常情况下,当您关闭模式对话框或向后导航时,不会调用 unload。【参考方案3】:

每次从头开始加载控制器的视图时都会调用它(即请求但尚不可用)。如果您取消分配控制器并且视图随之而来,那么它将在您下次实例化控制器时再次调用(例如,当您创建控制器以模态或通过 segue 推送它时)。选项卡中的视图控制器不会被释放,因为选项卡控制器会保留它们。

【讨论】:

以上是关于viewDidLoad 实际上是在每次有 segue 转换时调用的的主要内容,如果未能解决你的问题,请参考以下文章

Pan Segue 调用 viewDidLoad 循环

在 ViewDidLoad 上执行 Segue

对 segue 源视图控制器的引用

performeguewithidentifier 在一个 viewDidLoad 中有效,但在另一个 viewDidLoad 中无效,使用“Show”segues

TableView、数组和segue

Swift 4:在 viewDidLoad 之后调用 prepare(for segue:)