在开始之前,请注意这与后台处理无关。没有涉及到后台处理的"计算"。
- 仅限UIKit**。
view.addItemsA()
view.addItemsB()
view.addItemsC()
比如说在6s的iPhone上
UIKit构建每个文件需要一秒钟。
这将发生:
它们会同时出现。重复一遍,UIKit执行大量工作时,屏幕会暂停3秒钟。然后它们会同时出现。
但假设我希望这样:
它们会逐渐出现。UIKit创建一个时,屏幕会暂停1秒钟。然后屏幕会出现。创建下一个时,屏幕会再次暂停。然后屏幕会出现。依此类推。
(Note"一秒钟"只是一个简单的例子,为了清晰起见。更完整的例子请看这篇文章的结尾。)
在iOS中如何做到这一点?
您可以尝试以下操作。* 似乎不起作用 *。
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
你可以试试这个:
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()_b()
delay(0.1) { self._b() }
}
func _b() {
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
delay(0.1) { self._c() }...
- 注意,如果值太小-这种方法简单,而且很明显,什么也不做. UIKit将继续工作。(它还能做什么?)如果值太大,就没有意义了。
- 请注意,目前(iOS10),如果我没有记错的话:如果你用零延迟的技巧来尝试这个技巧,它最多也只能不稳定地工作。(正如你所预料的那样。)
使运行循环跳闸...
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
RunLoop.current.run(mode: RunLoop.Mode.default, before: Date())
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
合理。但我们最近的实际测试表明,这在许多情况下似乎不起作用。
(ie,苹果的UIKit现在已经足够成熟,足以让UIKit的工作超越这个"把戏"。)
思考:在UIKit中,有没有可能有一种方法,当它基本上绘制了所有你堆叠起来的视图时,得到一个回调?有没有其他的解决方案?
一个解决方案似乎是..将子视图*放在控制器*中,这样您就可以获得"didAppear"回调,并跟踪它们。这似乎很幼稚,但也许这是唯一的模式?无论如何,它真的会工作吗?(只有一个问题:我看不出didAppear可以保证所有子视图都已绘制。)
∮如果你还不明白∮
日常使用案例示例:
·假设可能有7个部分。
·假设每个UIKit通常需要0.01到0.20来构建(取决于您显示的信息)。
·如果你只是**"让整件事一次完成",它通常是可以接受的(总时间,比如0.05~0.15)......但是......
·当"新屏幕出现"时,用户通常会有一段冗长的停顿(.1到.5或更糟**)。
·然而,如果你按照我的要求去做,它总是会平滑地出现在屏幕上,一次一个区块,每个区块的时间都尽可能短。
5条答案
按热度按时间xtupzzrd1#
TLDR
使用
CATransaction.flush()
将挂起的UI更改强制到呈现服务器上,或使用CADisplayLink
将工作拆分到多个帧(下面的示例代码)。总结
在UIKit中,有没有一种方法可以在绘制完您堆叠的所有视图后获得回调?
没有
iOS就像一个游戏一样,每帧最多渲染一次更改(无论你做了多少)。确保在屏幕上渲染更改后运行一段代码的唯一方法是等待下一帧。
有别的解决办法吗?
是的,iOS可能每帧只渲染一次更改,但您的应用并不负责渲染,而是窗口服务器进程负责。
您的应用执行布局和渲染,然后将其对layerTree的更改提交给渲染服务器。它将在runloop结束时自动执行此操作,或者您可以通过调用
CATransaction.flush()
强制将未完成的事务发送给渲染服务器。然而,阻塞主线程通常是不好的(不仅仅是因为它阻塞了UI更新),所以如果可以的话,你应该避免它。
可能的解决方案
这是你感兴趣的部分。
说真的,iPhone7是我家里第三强大的电脑(不是手机),仅次于我的游戏PC和Macbook Pro。它比我家里的其他电脑都快。渲染应用程序UI不应该暂停3秒。
编辑:正如rob mayoff所指出的,您可以通过调用
CATransaction.flush()
强制CoreAnimation将挂起的更改发送到渲染服务器这实际上不会在那里呈现更改,而是将挂起的UI更新发送到窗口服务器,以确保它们包含在下一次屏幕更新中。
这将工作,但附带这些警告,在苹果的文档。
但是,你应该尽量避免显式调用flush。通过允许flush在runloop期间执行......事务和动画在事务之间工作将继续工作。
但是
CATransaction
头文件包含了这个引号,这似乎暗示了,即使他们不喜欢它,这也是官方支持的用法。在某些情况下(例如,没有运行循环,或运行循环被阻塞),可能需要使用显式事务来获得及时的渲染树更新。
一对一对-一对二对。
dispatch_after()
**只需将代码延迟到下一个runloop。
dispatch_async(main_queue)
将不起作用,但您可以使用dispatch_after()
,没有延迟。你在你的回答中提到这对你来说已经不起作用了。但是,它在测试Swift Playground和我在这个回答中包含的示例iOS应用程序中运行良好。
CADisplayLink
**CADisplayLink每帧调用一次,允许您确保每帧仅运行一个操作,从而保证屏幕能够在操作之间刷新。
需要此帮助器类才能工作(或类似)。
我不喜欢它,因为可能会出现无限循环。但是,我承认这是不可能的。我也不确定这是否得到官方支持,或者苹果工程师会读到这段代码,然后看起来很震惊。
除非(在响应runloop事件时)您执行了其他操作以阻止runloop调用完成,否则此操作应该有效,因为CATransaction将在runloop结束时发送到窗口服务器。
示例代码
Demonstration Xcode Project & Xcode Playground (Xcode 8.2, Swift 3)
我最喜欢
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0)
和CADisplayLink
,但是DispatchQueue.main.asyncAfter
不能保证它在下一个runloop tick上运行,所以你可能不想信任它?CATransaction.flush()
将强制您的UI更改推送到呈现服务器,这种用法似乎符合Apple对该类的评论,但附带了一些警告。在某些情况下(例如,没有运行循环,或运行循环被阻塞),可能需要使用显式事务来获得及时的渲染树更新。
详细说明
这个答案的其余部分是关于UIKit内部发生的事情的背景,并解释了为什么最初的答案尝试使用
view.setNeedsDisplay()
和view.layoutIfNeeded()
没有做任何事情。UIKit布局和渲染概述
CADisplayLink与UIKit和运行循环完全无关。
不完全是。iOS的用户界面是GPU渲染的,就像3D游戏一样。而且尽可能少做一些事情。所以很多事情,比如布局和渲染,并不是在需要的时候发生的。这就是为什么我们称之为"setNeedsLayout",而不是布局子视图。每一帧布局可能会改变多次。但是,iOS会尝试每一帧只调用
layoutSubviews
一次。而不是setNeedsLayout
可能被调用的10次。然而,CPU上发生了很多事情(布局、
-drawRect:
等),那么这一切是如何结合在一起的呢?每个UIView都可以看作是一个位图,一个图像/GPU纹理。当屏幕渲染时,GPU将视图层次结构合成为我们看到的结果帧。它合成视图,将先前视图顶部的子视图纹理渲染为我们在屏幕上看到的最终渲染(类似于游戏)。
这就是为什么iOS能拥有如此流畅、轻松的动画界面。要在屏幕上为一个视图制作动画,它不需要重新渲染任何东西。在下一帧,该视图的纹理只是在屏幕上与之前略有不同的位置合成。无论是它,还是它所在的视图,都不需要重新渲染它们的内容。
在过去,一个常见的性能技巧是通过完全在
drawRect:
中渲染表视图单元格来减少视图层次结构中的视图数量。这个技巧是为了使早期iOS设备上的GPU合成步骤更快。然而,GPU在现代iOS设备上的速度非常快,现在这不再是非常担心的问题。布局子视图和绘制矩形
-setNeedsLayout
使视图当前布局无效,并将其标记为需要布局。如果视图没有有效的布局,
-layoutIfNeeded
将重新布局视图-setNeedsDisplay
将标记需要重绘的视图。我们前面说过,每个视图都被渲染为视图的纹理/图像,GPU可以在不需要重绘的情况下移动和操作视图。这将触发它重绘。绘图是通过在CPU上调用-drawRect:
完成的,因此比依赖GPU要慢,GPU可以处理大多数帧。需要注意的是这些方法没有做什么。layout方法不做任何可视化的事情。尽管如果视图
contentMode
被设置为redraw
,改变视图框架可能会使视图渲染无效(触发-setNeedsDisplay
)。您可以全天尝试以下操作,但似乎不起作用:
从我们所了解到的情况来看,答案应该很明显,为什么这在现在不起作用。
view.layoutIfNeeded()
只重新计算其子视图的帧。view.setNeedsDisplay()
只是将视图标记为下次UIKit扫过视图层次结构时需要重绘,更新视图纹理以发送到GPU。但是,这不会影响您尝试添加的子视图。在您的示例中,
view.addItemsA()
添加了100个子视图。这些是GPU上独立的不相关图层/纹理,直到GPU将它们合成到下一个帧缓冲区中。唯一的例外是CALayer将shouldRasterize
设置为true。在这种情况下,它会为视图及其子视图创建单独的纹理并进行渲染(在GPU上)视图及其子视图合并到一个纹理中,有效地缓存了每帧所需的合成。这具有无需每帧合成所有子视图的性能优势。然而,如果视图或其子视图频繁地改变(如在动画期间),则这将是性能损失,因为这将使高速缓存的纹理无效,从而频繁地要求其被重绘(类似于频繁地调用X1 M30 N1 X)。现在,任何游戏工程师都会这么做...
事实上,这似乎奏效了。
但为什么它会起作用呢?
现在
-setNeedsLayout
和-setNeedsDisplay
不会触发重新绘制或重绘,而是将视图标记为需要它。当UIKit准备渲染下一帧时,它会触发具有无效纹理或布局的视图进行重绘或重新绘制。一切就绪后,它会发送通知GPU合成并显示新帧。所以UIKit中的主运行循环可能看起来像这样。
回到你原来的代码。
那么,为什么所有3个变化在3秒后立即显示,而不是间隔1秒一次显示一个呢?
如果这段代码是由于按下按钮或类似操作而运行的,它会同步执行,从而阻塞主线程(UIKit线程需要更改UI),因此会阻塞第1行的run循环,即偶数处理部分。实际上,您正在使runloop方法的第一行需要3秒才能返回。
然而,我们已经确定布局直到第3行才更新,各个视图直到第4行才呈现,并且直到runloop方法的最后一行(第5行)屏幕上才实际出现更改。
手动提取runloop的原因是因为您实际上是在插入对
runloop()
方法的调用,您的方法是作为从runloop函数内部调用的结果而运行的这是可行的,只要它不经常使用,因为这是使用递归,如果它经常使用,每次调用
-runloop
可能触发另一个导致失控递归。结束
下面这一点只是澄清。
关于这里发生的事情的额外信息
CADisplayLink和NSRunLoop显示链接和运行循环
如果我没有弄错的话,KH似乎从根本上你相信“运行循环”(即:这一个:运行循环。当前)是CADisplayLink。
runloop和CADisplayLink不是一回事,但是CADisplayLink连接到runloop才能工作。
我之前说的NSRunLoop在每一个节拍都调用CADisplayLink的时候说错了,它没有。据我所知,NSRunLoop基本上是一个while(1)循环,它的工作是保持线程活动,处理事件等...为了避免失误,我将尝试广泛引用苹果自己的文档来解释下面的内容。
run循环与其名称听起来非常相似。它是线程进入并用于运行事件处理程序以响应传入事件的循环。代码提供用于实现run循环的实际循环部分的控制语句-换句话说,代码提供驱动run循环的
while
或for
循环。在循环中,使用runloop对象来“运行”接收事件并调用已安装的处理程序的事件处理代码。Anatomy of a Run Loop - Threading Programming Guide - developer.apple.com
CADisplayLink
使用NSRunLoop
,需要添加到一个不同的文件中。引用CADisplayLink头文件:除非暂停,否则它将激发每个垂直同步,直到删除为止。
出发地:
func add(to runloop: RunLoop, forMode mode: RunLoopMode)
以及
preferredFramesPerSecond
属性文档中的内容。默认值为零,表示显示链接将以显示硬件的本机节奏触发。
...
例如,如果屏幕的最大刷新率是每秒60帧,则这也是显示链接设置为实际帧速率的最高帧速率。
因此,如果你想做任何事情定时屏幕刷新
CADisplayLink
(与默认设置)是你想使用的。介绍渲染服务器
如果您碰巧阻塞了一个线程,这与UIKit的工作方式无关。
不完全是。我们被要求只从主线程接触UIView的原因是因为UIKit不是线程安全的,它在主线程上运行。如果你阻塞了主线程,你已经阻塞了线程UIKit运行。
UIKit是否工作“就像你说的”{...“发送一条消息停止视频帧。做我们所有的工作!发送另一条消息重新开始视频!"}
我不是这个意思.
或者它是否工作“就像我说的”{......也就是说,像正常的编程“尽你所能,直到帧即将结束-哦,不,它的结束!-等到下一帧!做更多......"}
这不是UIKit的工作方式,如果不从根本上改变它的架构,我不知道它怎么能做到。它是如何观看帧结束的?
正如我的答案中的“UIKit布局和渲染概述”部分所讨论的那样,UIKit不尝试做任何前期工作。
-setNeedsLayout
和-setNeedsDisplay
可以在每帧中调用多次。它们只会使布局和视图渲染无效,如果已经使该帧无效,则第二调用不做任何事情。这意味着如果10次更改都使视图布局无效,UIKit仍然只需要支付一次重新计算布局的成本(除非您在-setNeedsLayout
调用之间使用了-layoutIfNeeded
)。-setNeedsDisplay
也是如此。尽管如前所述,这两个变量都与屏幕上显示的内容无关。layoutIfNeeded
更新视图帧,displayIfNeeded
更新视图渲染纹理,但这与屏幕上显示的内容无关。假设每个UIView都有一个UIImage变量,表示其后备存储(它实际上在CALayer中,或者更低,并且不是一个UIImage。但是这是一个插图)。重绘该视图只是更新UIImage。但是UIImage仍然只是数据,而不是屏幕上的图形,直到它被某个东西绘制到屏幕上。那么如何在屏幕上绘制UIView呢?
早些时候我写了伪代码UIKit的主渲染运行循环,到目前为止,我的回答忽略了UIKit的一个重要部分,并不是所有的UIKit都在你的进程中运行。2与显示内容相关的UIKit的惊人数量实际上发生在呈现服务器进程中,而不是你的应用程序进程中。3呈现服务器/窗口服务器是SpringBoard(主屏幕UI),直到iOS 6(从那时起,BackBoard和FrontBoard吸收了很多SpringBoards更核心的操作系统相关特性,让它更专注于成为主要的操作系统UI。主屏幕/锁屏/通知中心/控制中心/应用切换器/等等...)。
UIKit的主渲染运行循环的伪代码可能更接近于此。同样,请记住UIKit的架构被设计为尽可能少地工作,因此它每帧只会做一次这些事情(不像网络调用或主运行循环还可能管理的任何其他东西)。
这是有道理的,冻结一个iOS应用程序不应该冻结整个设备。事实上,我们可以在两个应用程序并排运行的iPad上演示这一点。当我们冻结一个应用程序时,另一个应用程序不受影响。
这是我创建并粘贴了相同代码的两个空应用程序模板。两个模板都应该在屏幕中间的标签中显示当前时间。当我按下freeze时,它调用
sleep(1)
并冻结应用程序。一切都停止了。但iOS作为一个整体是好的。其他应用程序,控制中心,通知中心等...都不受影响。UIKit是否工作"就像你说的"{..."发送一条消息停止视频帧。做我们所有的工作!发送另一条消息重新开始视频!"}
应用中没有UIKit
stop video frames
命令,因为应用根本无法控制屏幕。屏幕将使用窗口服务器提供的任何帧以60FPS的速度更新。窗口服务器将使用应用提供的最后已知位置、纹理和图层树以60FPS的速度合成新的显示帧。冻结应用中的主线程时,最后运行的
CoreAnimation.commitLayersAnimationsToWindowServer()
行(在昂贵的add lots of views
代码之后)将被阻塞,无法运行。因此,即使有更改,窗口服务器也尚未发送更改,因此将继续使用上次发送给应用的状态。动画是UIKit的另一个部分,在窗口服务器中运行进程外。如果在示例应用中的
sleep(1)
之前,我们首先启动UIView动画,我们将看到它启动,然后标签将冻结并停止更新(因为sleep()
已运行)。然而,即使应用主线程冻结,动画仍将继续。这就是结果:
事实上,我们可以做得更好。
如果我们把
freezePressed()
方法改成这样。现在如果没有
sleep(2)
调用,动画将运行0.2秒,然后它将被取消,视图将被移动到屏幕的另一个部分,颜色不同。但是,sleep调用会阻塞主线程2秒,这意味着这些更改都不会发送到窗口服务器,直到动画的大部分时间。这里只是为了确认注解掉
sleep()
行后的结果。这应该可以解释发生了什么。这些变化就像你在问题中添加的UIView一样。它们排队等待包含在下一次更新中,但是因为你一次发送这么多信息阻塞了主线程,所以你停止了发送消息,这将使它们包含在下一帧中。下一帧没有被阻塞,iOS将生成一个新框架,显示从SpringBoard和其他iOS应用收到的所有更新。但由于您的应用仍在阻止其主线程,iOS尚未从您的应用收到任何更新,因此不会显示任何更改(除非它有变化,如动画,已经排队在窗口服务器上)。
ilmyapht2#
窗口服务器对屏幕上显示的内容具有最终控制权。iOS仅在提交当前
CATransaction
时才向窗口服务器发送更新。为了在需要时执行此操作,iOS在主线程的运行循环中为.beforeWaiting
活动注册CFRunLoopObserver
。处理事件后(大概是通过调用你的代码),运行循环在等待下一个事件到达之前调用观察器。观察器提交当前事务(如果有)。提交事务包括运行布局通道,显示过程(在其中调用drawRect
方法),并将更新的布局和内容发送到窗口服务器。调用
layoutIfNeeded
执行布局(如果需要),但不调用显示通道或向窗口服务器发送任何内容。如果您希望iOS向窗口服务器发送更新,则必须提交当前事务。一种方法是调用
CATransaction.flush()
。使用CATransaction.flush()
的一个合理的情况是当你想把一个新的CALayer
放到屏幕上,并且你想让它立即有一个动画。新的CALayer
在事务提交之前不会被发送到窗口服务器,并且你不能在它出现在屏幕上之前给它添加动画。所以,将层添加到层层次结构,调用CATransaction.flush()
,然后将动画添加到层。你可以使用
CATransaction.flush
来获得你想要的效果。我不推荐这样做,但是代码如下:结果是这样的:
上述解决方案的问题在于,它通过调用
Thread.sleep
来阻塞主线程。如果主线程不响应事件,用户不仅会感到沮丧(因为应用不响应她的触摸),而且最终iOS会判定应用挂起并杀死它。更好的办法是,简单地安排好每个视图的添加时间。你声称"这不是工程设计",但你错了,你给出的理由毫无意义。iOS通常每16毫秒更新一次屏幕(除非你的应用处理事件的时间比这个长)。只要你想要的延迟至少有那么长,你可以只安排一个程序块在延迟后运行以添加下一个视图。2如果你希望延迟小于16毫秒,一般来说你不能这样做。
因此,下面是添加子视图的更好、推荐的方法:
下面是(相同的)结果:
rsaldnfx3#
这是一种解决方案,但不是工程学。
事实上,是的,通过增加延迟,你正在做你说你想做的事情:您允许运行循环完成并执行布局,并在完成后立即重新进入主线程。实际上,这是我对
delay
的一个主要用途。(您甚至可以使用为零的delay
。)ugmeyewa4#
下面有3种方法可以使用。第一种方法是如果一个子视图添加了控制器,如果它不是直接在视图控制器中,我也可以使用它。第二种方法是自定义视图:)在我看来,你似乎想知道layoutSubviews在视图上什么时候完成。这个连续的过程是什么冻结了显示,因为1000个子视图按顺序增加。根据你的情况,你可以添加一个childviewcontroller视图,并在viewDidLayoutSubviews()完成时发布一个通知,但我不知道这是否适合你的用例。我在添加的viewcontroller视图上测试了1000个子视图,它工作正常。在这种情况下,延迟为0将完全满足你的需要。下面是一个工作示例。
然后在你的主视图控制器中,你可以添加子视图。
或者还有一个更疯狂的方法,可能是更好的方法。创建自己的视图,在某种意义上保持自己的时间,并在不掉帧的情况下回叫。这是未经打磨的,但会起作用。
或者最后,您可以在viewDidAppear中执行回调或通知,但似乎也需要 Package 在回调上执行的任何代码,以便从viewDidAppear回调中及时执行。
2vuwiymt5#
使用
NSNotification
达到所需效果。首先-向主视图注册一个观察器并创建观察器处理程序。
然后,在单独的线程(例如,后台)中初始化所有这些A、B、C...对象,例如,
self performSelectorInBackground
然后- post来自子视图的通知,最后-
performSelectorOnMainThread
以所需的延迟按所需顺序添加子视图。为了回答评论中的问题,假设你有一个UIViewController显示在屏幕上。这个对象-不是讨论的重点,你可以决定把代码放在哪里,控制视图外观。代码是UIViewController对象的(所以,它是自己的)。View -一些UIView对象,被认为是父视图。ViewN -子视图之一。它可以在以后缩放。
这注册了一个观察者-需要在线程之间通信。ViewN * V1 = [[ViewN alloc] init];此处-可以分配子视图-尚未显示。
此处理程序允许接收消息并将
UIView
对象放置到父视图中。看起来很奇怪,但关键是-您需要在主线程上执行addSubview方法才能生效。performSelectorOnMainThread
允许在主线程上开始添加子视图,而不会阻止应用程序执行。现在,我们创建一个将子视图放置到屏幕上的方法。
此方法将发布来自任何线程的通知,并发送一个名为ViewArrived的NSDictionary项对象。
最后-必须添加3秒延迟的视图:
这不是唯一的解决方案,也可以通过计算
NSArray
subviews
属性来控制父视图的子视图。在任何情况下,您都可以随时运行
initViews
方法,甚至在后台线程中运行,它允许控制子视图外观,performSelector
机制允许避免执行线程阻塞。