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)
}
}