【问题标题】:Why does presenting a modal UIViewController cause setNeedsLayout on the presenting controller?为什么呈现模态 UIViewController 会导致呈现控制器上的 setNeedsLayout?
【发布时间】:2014-05-01 07:41:50
【问题描述】:

似乎呈现和关闭视图控制器都会提示 呈现 视图布局其子视图和/或更新其约束。对于繁重的视图层次结构,这会引入性能问题。再次 - 这是现有的,当前显示的视图。正在创建和显示的模态非常轻便。

无论我是否使用自动布局(如在我的示例项目中),都会发生这种情况。

我已经构建了一个demo project,它与我正在开发的应用程序相似。有一个水平滚动的主父控制器UIScrollView。多个子控制器被添加到父控制器,它们的视图被添加到滚动视图并使用NSLayoutConstraints 排列。每个子视图本身都有一个子视图,一个简单的UIView,也有一个约束。

在导航栏中,有一个用于启动模式的按钮。当出现时,父控制器在每个子视图上多次调用setNeedsLayout。在我的演示项目中,我将覆盖setNeedsLayout 以在访问它时进行记录。关闭模态时也会发生同样的情况。打开和关闭模态几次并观察控制台。

我看不出为什么需要新的布局,而且对于更复杂的视图,我发现有数百个这样的调用正在触发,对性能产生了明显的影响。

注意,当ChildView 中的布局代码被省略时,setNeedsLayout 不会被调用。我鼓励您注释掉约束并查看日志记录中的差异。

为什么会这样?在呈现和关闭模式时如何防止不必要的布局传递?

【问题讨论】:

  • 我认为setNeedsLayout是系统调用的,我测试了我的项目,你的情况也是这样。

标签: ios cocoa-touch uiview uiviewcontroller autolayout


【解决方案1】:

首先,您正在记录setNeedsLayout,这只是一个标记机制,还没有真正产生任何工作。多次调用setNeedsLayout 可能只会触发一个布局。您应该记录 -[UIView layoutSubviews]-[UIViewController viewDidLayoutSubviews],因为这些才是真正的繁重工作发生的地方。

第二,与布局相关的方法意味着在演示过程中被反复快速调用,因为:

  1. 窗口需要旋转其所有子视图以尊重呈现的视图控制器的首选界面方向。
  2. 动画需要知道视图的初始和最终状态。
  3. 无论出于何种原因在父视图上发生布局时,它们的所有子视图(可能包括视图控制器的视图)当然也需要更新其布局。

如果您想尽量减少布局传递的数量,您可以尝试放弃使用presentViewController:animated:,而改用addChildViewController: 并手动仅对必要的视图进行动画处理。但即便如此,您仍然可能触发父控制器的布局。

【讨论】:

  • 谢谢 - 但仍然不清楚为什么在呈现模式时需要这三个中的任何一个?虽然动画我自己的子视图控制器是一个有趣的想法,但我想知道它是否也会触发布局传递。我可能会检查。
  • 1 和 2 将触发,因为即使演示者不会改变,为了进行旋转/动画,图层系统需要知道下面视图的最终状态(可能还有其他东西例如,在动画中间)。视图控制器以类似方式处理所有情况(布局代码应该是轻量级的)比假设是否更新布局更安全。解决这个问题的更好方法是在布局代码中查找紧密循环,并防止触发子视图布局,除非您已经确定它们的最终帧。
【解决方案2】:

您正在做一件非常、非常、非常奇怪的事情:您正在维护一个自定义的父视图控制器,其中包含 10 个子视图控制器,所有子视图控制器同时在界面中。视图控制器不是为这类事情设计的。正是这个触发了您看到的多个layoutSubviews 调用。有多个子视图控制器很好,但它们的 views 不应该都在层次结构中 - 特别是在你的情况下,只有 一个 这样的子视图实际上是可见的。

事实上,您构建的界面——分页滚动视图,每个“页面”都是由视图控制器管理的视图——已经由 UIPageViewController 为您实现, em> 更高效,因为它实际上一次最多只维护三个视图控制器:视图控制器管理滚动视图中的可见视图,视图控制器管理其左右视图。它也非常方便且易于使用。

所以要么:

  • 你应该使用 UIPageViewController,或者

  • 您应该模仿 UIPageViewController 所做的事情,在视图控制器的视图滚动到视线之外时移除它们(甚至可能释放视图控制器)——就像我们过去所做的那样在 UIPageViewController 存在之前 - 请参阅 WWDC 2011 的高级滚动视图技术视频。在 UIPageViewController 出现之前,我的拉丁语“抽认卡”应用程序就是这样工作的;它有 数千 个词汇卡,每一个都由一个视图控制器管理,但任何时候最多只存在三个卡片视图控制器。

(顺便说一下,您也不应该使用自己的 self.childControllers 可变数组,因为该列表已为您维护为 self.childViewControllers。)

【讨论】:

  • 很好的答案。你应该写一本书什么的;)
  • 这更像是对应用程序架构的回顾,而不是对为什么必须布置视图的答案。至于您的建议 - 想象一个应用程序,其中初始加载时间和内存使用是滚动性能的次要问题。假设每个视图都足够复杂,以至于滚动延迟加载会引入丢帧(这很容易重现,而不是学术断言)。现在最好在启动时加载所有十个控制器和视图。滚动很流畅,但在呈现模式时必须等待成为唯一的问题。
  • PS - 延迟加载方法是我最初的设计,我认为在大多数情况下它是正确的方法。但是考虑到固定数量的重视图,并坚持避免滚动时“颤抖”,我不太确定。另外,作为一个有趣的(对我而言)...如果您优先考虑滚动性能,并实现视图和控制器的回收,瓶颈实际上变成了添加(已经存在的)子视图。我发现最好将视图保留在层次结构中,将其隐藏,然后在回收时将其移动到正确的位置。
  • 为了后代并测试了一些东西 - UIPagedViewController 的性能相当差,所以我推荐上面的选项 2。如果滚动性能很重要,UIPagedViewController 将不会重用您的控制器或视图。即使您在数据源中自行处理回收,删除和添加视图也会产生影响。到目前为止,我的最佳表现来自重用控制器和移动视图,而不是删除它们。很遗憾,因为我基本上不得不完全重写 UIPagedViewController 来提供这个功能。
【解决方案3】:

我认为 layoutSubviews 被调用是因为呈现控制器的 view 在被呈现的视图隐藏后动画出屏幕时会更改超级视图。

如果您想在帧未更改时跳过layoutSubviews,只需保存对最后一帧的引用,如果相等则返回而不做任何事情。也不需要在子视图上调用setNeedslayout,因为如果您调整它们的大小,系统会自动触发它。

无论如何,你的主要问题是你的方法:

  1. 仅在您打算使用视图控制器时才使用它们(在标签栏控制器内,推送到导航控制器,作为窗口的rootController,以模态方式呈现,等等。)。如果您想手动添加视图,请不要使用视图控制器,而只需使用自定义视图!这是一个非常常见的错误,您可以查看更多详细信息here

  2. 延迟加载视图和对象并重复使用它们。例如,您应该只加载 1~3 页的内容,并且只有在用户滚动到它们时才加载新的内容。加载新视图时,请删除其中一个旧视图,或者更好地重用它。


您不仅可以使用控制器分离逻辑,还可以使用自定义视图分离逻辑。在特定情况下不应使用控制器的一些原因:

  • 当您手动添加视图时,容器控制器或窗口不会保留控制器。
  • 控制器不会获得方向、内存、viewDidAppear 等事件。再次因为您没有将它们用作正确的视图控制器。

如果您正确实现了custom container controller(要正确完成很多工作),那么您可以使用控制器。否则坚持自定义视图。

【讨论】:

  • 谢谢,这很有帮助,我会调查的。你知道在演示过程中演示视图移动到哪里/为什么?这是过渡工作方式的一部分吗?关于您的实施要点 - 1)我认为视图控制器包含的存在破坏了这一论点。例如,如果您有非常不同的子视图具有非常不同的功能,那么每个控制器都拥有该行为会更好。 2) 请参阅我对@matt 回答的评论 - 回收和按需加载通常更好,但在某些情况下,我认为提前加载是​​合理的。
  • 提前加载是​​(我的“1~3页”部分),完全加载不是。
  • 一个 5 页的应用程序,重的、复杂的视图,不允许滚动帧丢失。对于这样的示例,我会使用完全加载(除了模态演示问题,因此是问题)。
猜你喜欢
  • 1970-01-01
  • 2018-05-01
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 2011-06-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多