如何,简单地等待iOS中的任何布局?

时间:2017-02-23 15:41:41

标签: ios performance runloop

在开始之前请注意,这与后台处理无关。没有"计算"涉及到一个人会背景。

仅限UIKit

view.addItemsA()
view.addItemsB()
view.addItemsC()

让我们说一下6s的iPhone

UIKit构建的每一个都需要一秒钟。

这将会发生:

one big step

他们一直出现。重复一遍,屏幕只会挂起3秒,而UIKit会做大量的工作。然后他们都出现了。

但是,让我说我希望这种情况发生:

enter image description here

他们一直在进步。当UIKit构建一个屏幕时,屏幕会暂停1秒钟。它出现。它会在构建下一个时再次挂起。它出现。等等。

(注意"一秒钟"为清晰起见,这只是一个简单的例子。有关更全面的示例,请参阅本文末尾。)

你是如何在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: .defaultRunLoopMode, before: Date())

view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()

合理。 但是我们最近的实际测试表明,在许多情况下这似乎不起作用。

(也就是说,Apple的UIKit现在已经足够精湛,足以让UIKit的工作超出那个"技巧&#34 ;.)

思考:在UIKit中,或许有一种方法可以获得一个回调,基本上,它已经绘制了你已经堆积的所有观点?还有其他解决方案吗?

一个解决方案似乎是......将子视图 放在控制器中 ,这样你就得到了一个" didAppear"回调,并跟踪那些。 这似乎是婴儿,但也许它是唯一的模式?无论如何它真的会起作用吗? (仅仅是一个问题:我没有看到任何保证,确保所有子视图都已被绘制。)

如果仍然不清楚......

日常用例示例:

•假设有七个部分

•假设每个人通常需要 0.01到0.20 才能构建UIKit(取决于您显示的信息)。

•如果你只是"让整个事情进行一次打击" 它通常会好或可接受(总时间,比如0.05到0.15)......但是。 ..

•随着"新屏幕出现",用户经常会有一个单调乏味的停顿。 ( .1到.5或更差)。

•如果你按照我要求的方式行事,它将始终平滑到屏幕上,一次一个块,每个块的最小可能时间。

4 个答案:

答案 0 :(得分:10)

窗口服务器可以最终控制屏幕上显示的内容。 iOS仅在提交当前CATransaction时向窗口服务器发送更新。为了在需要时实现这一点,iOS在主线程的运行循环中为CFRunLoopObserver活动注册.beforeWaiting。在处理事件之后(可能是通过调用代码),run循环在等待下一个事件到达之前调用观察者。如果存在,则观察者提交当前事务。提交事务包括运行布局传递,显示传递(调用drawRect方法),以及将更新的布局和内容发送到窗口服务器。

如果需要,调用layoutIfNeeded执行布局,但不调用显示传递或向窗口服务器发送任何内容。如果您希望iOS将更新发送到窗口服务器,则必须提交当前事务。

这样做的一种方法是致电CATransaction.flush()。使用CATransaction.flush()的合理情况是,您希望在屏幕上放置一个新的CALayer并希望它立即生成动画。在事务提交之前,新的CALayer将不会被发送到窗口服务器,并且您无法在屏幕上添加动画。因此,您将图层添加到图层层次结构中,调用CATransaction.flush(),然后将动画添加到图层。

您可以使用CATransaction.flush来获得所需的效果。 我不推荐这个,但这里是代码:

@IBOutlet var stackView: UIStackView!

@IBAction func buttonWasTapped(_ sender: Any) {
    stackView.subviews.forEach { $0.removeFromSuperview() }
    for _ in 0 ..< 3 {
        addSlowSubviewToStack()
        CATransaction.flush()
    }
}

func addSlowSubviewToStack() {
    let view = UIView()
    // 300 milliseconds of “work”:
    let endTime = CFAbsoluteTimeGetCurrent() + 0.3
    while CFAbsoluteTimeGetCurrent() < endTime { }
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

这是结果:

CATransaction.flush demo

上述解决方案的问题在于它通过调用Thread.sleep来阻止主线程。如果您的主线程没有响应事件,不仅用户会感到沮丧(因为您的应用没有响应她的触摸),但最终iOS将决定该应用程序挂起并将其终止。

更好的方法是在您希望每个视图显示时安排添加每个视图。你声称“这不是工程”,但你错了,你的理由没有意义。 iOS通常每16⅔毫秒更新一次屏幕(除非您的应用程序需要的时间比处理事件的时间长)。只要您想要的延迟至少是那么长,您就可以在延迟之后安排一个块来添加下一个视图。如果你想要一个小于16⅔毫秒的延迟,你通常不会有它。

所以这是添加子视图的更好,推荐的方法:

@IBOutlet var betterButton: UIButton!

@IBAction func betterButtonWasTapped(_ sender: Any) {
    betterButton.isEnabled = false
    stackView.subviews.forEach { $0.removeFromSuperview() }
    addViewsIfNeededWithoutBlocking()
}

private func addViewsIfNeededWithoutBlocking() {
    guard stackView.arrangedSubviews.count < 3 else {
        betterButton.isEnabled = true
        return
    }
    self.addSubviewToStack()
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
        self.addViewsIfNeededWithoutBlocking()
    }
}

func addSubviewToStack() {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

这是(相同的)结果:

DispatchQueue asyncAfter demo

答案 1 :(得分:1)

  

这是一种解决方案。但这不是工程设计。

实际上,是的。通过添加延迟,您正在完全您所说的想要做的事情:您允许完成runloop并执行布局,并在完成后立即重新进入主线程。事实上,这是delay主要用途之一。 (您甚至可以使用零delay。)

答案 2 :(得分:1)

下面可能有效的3种方法。第一个我可以使它工作,如果一个子视图添加控制器,如果它不是直接在视图控制器中。第二个是自定义视图:)似乎你想知道layoutSubviews何时完成对我的视图。由于1000个子视图加上顺序,这个连续过程就是冻结显示。根据您的情况,您可以添加childviewcontroller视图并在viewDidLayoutSubviews()完成时发布通知,但我不知道这是否适合您的用例。我在添加的viewcontroller视图上测试了1000个subs,并且它工作正常。在这种情况下,延迟0将完全符合您的要求。这是一个有效的例子。

 import UIKit
 class TrackingViewController: UIViewController {

    var layoutCount = 0
    override func viewDidLoad() {
        super.viewDidLoad()

          // Add a bunch of subviews
    for _ in 0...1000{
        let view = UIView(frame: self.view.bounds)
        view.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        view.backgroundColor = UIColor.green
        self.view.addSubview(view)
    }

}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("Called \(layoutCount)")
    if layoutCount == 1{
        //finished because first call was an emptyview
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }
    layoutCount += 1
} }

然后在您的主视图控制器中添加子视图,您可以执行此操作。

import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 0
    var count = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.finishedLayoutAddAnother), name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4, execute: {
            //add first view
            self.finishedLayoutAddAnother()
        })
    }

    deinit {
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    func finishedLayoutAddAnother(){
        print("We are finished with the layout of last addition and we are displaying")
        addView()
    }

    func addView(){
        // we keep adding views just to cause
        print("Fired \(Date())")
        if count < 100{
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {

                // let test = TestSubView(frame: CGRect(x: self.view.bounds.midX - 50, y: y, width: 50, height: 20))
                let trackerVC = TrackingViewController()
                trackerVC.view.frame = CGRect(x: self.view.bounds.midX - 50, y: self.y, width: 50, height: 20)
                trackerVC.view.backgroundColor = UIColor.red
                self.view.addSubview(trackerVC.view)
                trackerVC.didMove(toParentViewController: self)
                self.y += 30
                self.count += 1
            })
        }
    }
}

或者有更疯狂的方式,可能更好的方式。创建自己的视图,在某种意义上保持自己的时间,并在不丢帧时回调。这是未经抛光但可行的。

completion gif

import UIKit
class CompletionView: UIView {
    private var lastUpdate : TimeInterval = 0.0
    private var checkTimer : Timer!
    private var milliSecTimer : Timer!
    var adding = false
    private var action : (()->Void)?
    //just for testing
    private var y : CGFloat = 0
    private var x : CGFloat = 0
    //just for testing
    var randomColors = [UIColor.purple,UIColor.gray,UIColor.green,UIColor.green]


    init(frame: CGRect,targetAction:(()->Void)?) {
        super.init(frame: frame)
        action = targetAction
        adding = true
        for i in 0...999{
            if y > bounds.height - bounds.height/100{
                y -= bounds.height/100
            }

            let v = UIView(frame: CGRect(x: x, y: y, width: bounds.width/10, height: bounds.height/100))
            x += bounds.width/10
            if i % 9 == 0{
                x = 0
                y += bounds.height/100
            }

            v.backgroundColor = randomColors[Int(arc4random_uniform(4))]
            self.addSubview(v)
        }

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func milliSecCounting(){
        lastUpdate += 0.001
    }

    func checkDate(){
        //length of 1 frame
        if lastUpdate >= 0.003{
            checkTimer.invalidate()
            checkTimer = nil
            milliSecTimer.invalidate()
            milliSecTimer = nil
            print("notify \(lastUpdate)")
            adding = false
            if let _ = action{
                self.action!()
            }
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        lastUpdate = 0.0
        if checkTimer == nil && adding == true{
            checkTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(CompletionView.checkDate), userInfo: nil, repeats: true)
        }

        if milliSecTimer == nil && adding == true{
             milliSecTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(CompletionView.milliSecCounting), userInfo: nil, repeats: true)
        }
    }
}


import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 30
    override func viewDidLoad() {
        super.viewDidLoad()
        // Wait 3 seconds to give the sim time
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
            [weak self] in
            self?.addView()
        })
    }

    var count = 0
    func addView(){
        print("starting")
        if count < 20{
            let completionView = CompletionView(frame: CGRect(x: 0, y: self.y, width: 100, height: 100), targetAction: {
                [weak self] in
                self?.count += 1
                self?.addView()
                print("finished")
            })
            self.y += 105
            completionView.backgroundColor = UIColor.blue
            self.view.addSubview(completionView)
        }
    }
}

或者最后,您可以在viewDidAppear中执行回调或通知,但似乎还需要包含在回调上执行的任何代码,以便从viewDidAppear回调中及时执行。

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
  //code})

答案 3 :(得分:0)

使用NSNotification来达到所需的效果。

首先 - 将观察者注册到主视图并创建观察者处理程序。

然后,通过例如self performSelectorInBackground

在单独的线程(例如背景)中初始化所有这些A,B,C ......对象

然后 - 发布子视图和最后一次的通知 - performSelectorOnMainThread以所需的顺序添加所需的延迟的子视图。
要回答评论中的问题,假设您有一个显示在屏幕上的UIViewController。这个对象 - 不是讨论点,你可以决定在哪里放置代码,控制视图外观。代码用于UIViewController对象(因此,它是self)。视图 - 一些UIView对象,被视为父视图。 ViewN - 子视图之一。它可以稍后缩放。

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(handleNotification:) 
    name:@"ViewNotification"
    object:nil];

注册了一个观察者 - 需要在线程之间进行通信。     ViewN * V1 = [[ViewN alloc] init]; 这里 - 可以分配子视图 - 尚未显示。

- (void) handleNotification: (id) note {
    ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
    [self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}

此处理程序允许接收消息并将UIView对象放置到父视图。看起来很奇怪,但关键是 - 你需要在主线程上执行addSubview方法才能生效。 performSelectorOnMainThread允许开始在主线程上添加子视图,而不会阻止应用程序执行。

现在 - 我们制作了一个将子视图放到屏幕上的方法。

-(void) sendToScreen: (id) obj {
    NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}

此方法将从任何线程发布通知,将对象作为名为ViewArrived的NSDictionary项发送。
最后 - 必须添加3秒延迟的视图:

-(void) initViews {
    ViewN * V1 = [[ViewN alloc] init];
    ViewN * V2 = [[ViewN alloc] init];
    ViewN * V3 = [[ViewN alloc] init];
    [self performSelector: @selector(sendToScreen:) withObject: V1 afterDelay: 3.0];
    [self performSelector: @selector(sendToScreen:) withObject: V2 afterDelay: 6.0];
    [self performSelector: @selector(sendToScreen:) withObject: V3 afterDelay: 9.0];
}

这不是唯一的解决方案。也可以通过计算NSArray subviews属性来控制父视图的子视图 在任何情况下,您都可以在需要时运行initViews方法,甚至在后台线程中也可以控制子视图外观,performSelector机制允许避免执行线程阻塞。