UIView becomes twice slower after suspend/resume

I've found that UIView becomes twice slower in terms of performing UIImage.drawAt after suspend/resume. Its ability in terms of String.drawAt or filling rectangles stays the same, which makes it less likely to be a mistake of mine. Recreating view fixes the problem, but one can't expect all developers recreating their views hierarchy after resume. I see the problem on all devices and simulators on iOS 9-13 (did not check older ones).


Code sample below requires an image asset called card (mine was 112x145 PNG with transparency, but I don't think it matters). Just add it to x1, x2, x3 to make sure that a native density is present (otherwise upscaling becomes the slowest operation instead of drawing). If your test runs at maximum 60 FPS, increase max constant in the last method of example.


After pressing Home or Power or other ways of suspending, observed FPS is twice lower.


import UIKit

@UIApplicationMain
class AppDelegate: UIResponder,UIApplicationDelegate
{
  var window: UIWindow?

  func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)->Bool
  {
    self.window=UIWindow(frame: UIScreen.main.bounds)
    window!.makeKeyAndVisible()
    let testViewController=TestViewController()
    window!.rootViewController=testViewController
    Runner._instance.testViewController=testViewController
    return true
  }

  func applicationDidEnterBackground(_ application: UIApplication)
  {
    Runner._instance.stop()
  }

  func applicationDidBecomeActive(_ application: UIApplication)
  {
    Runner._instance.start()
  }

  func applicationWillTerminate(_ application: UIApplication)
  {
    Runner._instance.stop()
  }
}

class Runner: NSObject
{
  static let _instance=Runner()
  var testViewController: TestViewController!
  var displayLink: CADisplayLink!
  var fps=0
  var lastFpsCounterTime=Date().timeIntervalSince1970
  var fpsCounter=0

  func start()
  {
    if displayLink == nil
    {
      displayLink=CADisplayLink(target: self,selector: #selector(Runner.run))
      displayLink.add(to: RunLoop.main,forMode: RunLoop.Mode.common)
    }
  }

  func stop()
  {
    displayLink?.invalidate()
    displayLink=nil
  }

  @objc func run()
  {
    if lastFpsCounterTime+1<Date().timeIntervalSince1970
    {
      fps=fpsCounter
      lastFpsCounterTime=Date().timeIntervalSince1970
      fpsCounter=0
    }
    fpsCounter+=1
    testViewController.view.setNeedsDisplay()
  }
}

class TestViewController: UIViewController
{
  required init?(coder aDecoder: NSCoder)
  {
    super.init(coder: aDecoder)
    loadView()
  }

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
  {
    super.init(nibName: nibNameOrNil,bundle: nibBundleOrNil)
    loadView()
  }

  override func loadView()
  {
    view=TestView(frame: UIScreen.main.bounds)
  }
}

class TestView: UIView
{
  let image=UIImage(named: "card")!

  required init?(coder: NSCoder)
  {
    super.init(coder: coder)
  }

  override init(frame: CGRect)
  {
    super.init(frame: frame)
    isOpaque=true
  }

  override func draw(_ _rect: CGRect)
  {
    let context=UIGraphicsGetCurrentContext()!
    context.setFillColor(red: 1, green: 1, blue: 1, alpha: 1)
    context.fill(bounds)
    let max=50 //Choose max high enough to get less than 60 FPS
    let time=Date().timeIntervalSince1970
    for i in 1...max
    {
      image.draw(at: CGPoint(x: (image.size.width+bounds.width)*CGFloat(time.truncatingRemainder(dividingBy: Double(1+i))/Double(1+i))-image.size.width,y: bounds.height*CGFloat(i)/CGFloat(max)))
    }
    let font=UIFont(name: "Arial", size: 15)!
    let textFontAttributes=[NSAttributedString.Key.font: font,
    NSAttributedString.Key.foregroundColor: UIColor(red: 1, green: 0, blue: 0, alpha: 1),
    NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle]
    "FPS: \(Runner._instance.fps)".draw(at: CGPoint(x: 2,y: 30),withAttributes: textFontAttributes)
  }
}

Replies

Before going into details:


an image asset called card (mine was 112x145 PNG with transparency, but I don't think it matters)


If I remember well, it does matter, because the drawing computation is more complex.

Could you test without transparency ?


Nevertheless, that does not explain the slowdown after resume.


Questions:

- does the slow down occurs at first drawing only or every time after resume ?

If only first time, that may mean that assets were purged out of memory, and that's the time needed to reload ?


- how did you measure slow down ? Can you give figures ?

In my example it is continuous repainting of the screen. I measure FPS and display it on the screen. The "game" moves 50 pics across the screen at stable 40 fps for as long as I want, but after suspend/resume, it is down to 20 fps. Also for as long as I want. Actual numbers depend on pic and phone model, but it is always about a half of what it was after first suspend/resume. Every phone, every simulator.


It could be different during the first few seconds, but I do not really care about the first repaint. A long-term effect is what I worry about. I can check with a no transparency pic, but the most pics we show over backgrounds have transparency one way or another.


If you are worried about the way I measure FPS, the effect is also noticeable by less smooth movement of pictures. And, again, when I had String.drawAt, instead of UIImage.drawAt, everything went smooth. Amount of drawn strings affected fps, but it was not different before and after suspend/resume.

And yes, the problem also accurs with opaque image, but the drop is not 50%. In my case FPS went from 39 to 25 after suspend/resume.

Thanks for the details.


Unfortunately, I have no further idea of what's causing the problem.


If it is critical for your app (and I understand it may be), you could use a Developer Technical Support ticket (2 of them free per year for developer).


Good luck.

It is not critical, I just recreated a view to get back initial performance. But I thought to bring it to Apple attention. Is not it a right forum? Should I use Feedback Assistant instead or Feedback Assistant + Technical Support Incident? Sorry for questions, I spend more time programming for another OS.

Yes, you should use Feedback assistant. Forum is not an official communication channel to Apple, even though some engineers contribute.


And don't forget to close the thread once bug reported through feedback.