You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift 5视图控制器切换引发内存泄漏问题求助

视图控制器切换时的内存泄漏问题

我的应用哪怕在最基础设置下,仅在视图控制器间切换也会缓慢泄漏内存。我用CATransition实现了VC的淡入淡出切换效果。

反复在IntroVC和FirstVC之间切换时,内存占用会持续攀升——速度很慢,大概每20次往返涨0.1MB,但确实一直在增加,说明有资源没被释放。如果加上背景、图片等元素,每次重新加载重复VC时,内存会跳涨约0.2MB。

我觉得不太可能是push/pop本身的问题,更大概率是我的代码结构有问题。试过用Instruments找泄漏点,但看不懂输出。求解决思路。


AppDelegate代码

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var coordinator: MainCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        setupCore()
        return true
    }
    
    //MARK: - Setup methods
    private func setupCore() {
        let navController = UINavigationController()
        coordinator = MainCoordinator(navigationController: navController)
        coordinator?.start()
    
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
    }
}

MainCoordinator代码

import UIKit

class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController
    var rootIntro: IntroVC!
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
        self.navigationController.setNavigationBarHidden(true, animated: false)
    }
    
    func start() {
        navigationController.delegate = self
        rootIntro = IntroVC.instantiate()
        rootIntro.coordinator = self
        navigationController.fadeTo(rootIntro)
    }
    
    //MARK: - 从Intro加载VC
    func showFirst() {
        let vc = FirstVC.instantiate()
        vc.coordinator = self
        navigationController.fadeTo(vc)
    }

    
    // MARK: - 返回上级VC
    func returnToRoot() {
        navigationController.fadeToRoot()
    }
}

Coordinator协议代码

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    
    func start()
}

// MARK: - Storyboard
protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self  {
        let id = String(describing: self)
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        return storyboard.instantiateViewController(withIdentifier: id) as! Self
    }
}

UINavigationController扩展代码

extension UINavigationController {
    func fadeTo(_ toVC: UIViewController) {
        let transition = CATransition().fadeTransition()
        view.layer.add(transition, forKey: nil)
        pushViewController(toVC, animated: false)
    }

    func fadeToRoot() {
        let transition = CATransition().fadeTransition()
        view.layer.add(transition, forKey: nil)
        popToRootViewController(animated: false)
    }
}
extension CATransition {
    func fadeTransition() -> CATransition {
        let transition = CATransition()
        transition.duration = 0.3
        transition.type = CATransitionType.fade
        return transition
    }
}

IntroVC代码

class IntroVC: UIViewController, Storyboarded {

    weak var coordinator: MainCoordinator?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 加载视图后进行额外设置
        
        let title = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 100),
                                          size: CGSize(width: view.frame.width, height: 80)))
        title.text = "Intro VC"
        title.font = UIFont.systemFont(ofSize: 30, weight: .heavy)
        title.textAlignment = .center
        title.textColor = .lightGray
        view.addSubview(title)
        
        
        let button = UIButton(frame: CGRect(origin: CGPoint(x: (view.frame.width - 150)/2, y: (view.frame.height - 50)/2),
                                            size: CGSize(width: 150, height: 50)))
        button.backgroundColor = .green
        button.setTitleColor(.black, for: .normal)
        button.setTitle("-> First", for: .normal)
        button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
        view.addSubview(button)
    }
    
    @objc func tapped(_ button: UIButton) {
        coordinator?.showFirst()
    }
}

FirstVC代码

class FirstVC: UIViewController, Storyboarded  {

    weak var coordinator: MainCoordinator?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 加载视图后进行额外设置
        let title = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 100),
                                          size: CGSize(width: view.frame.width, height: 80)))
        title.text = "First VC"
        title.font = UIFont.systemFont(ofSize: 30, weight: .heavy)
        title.textAlignment = .center
        title.textColor = .white
        view.addSubview(title)
        
        
        let button = UIButton(frame: CGRect(origin: CGPoint(x: (view.frame.width - 150)/2, y: (view.frame.height - 50)/2),
                                            size: CGSize(width: 150, height: 50)))
        button.backgroundColor = .red
        button.setTitleColor(.black, for: .normal)
        button.setTitle("<- Intro", for: .normal)
        button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
        view.addSubview(button)
    }
    
    @objc func tapped(_ button: UIButton) {
        coordinator?.returnToRoot()
    }
}

问题排查与修复思路

1. CATransition动画未清理,导致内存累积

每次调用fadeTo或fadeToRoot时,你都给导航控制器的view.layer添加了新的CATransition,但没有在动画结束后移除旧动画,也没设置唯一key让新动画覆盖旧的。这些动画对象会一直附着在图层上,持续占用内存。

修复代码:

extension UINavigationController {
    func fadeTo(_ toVC: UIViewController) {
        // 移除之前的过渡动画,或用唯一key覆盖
        view.layer.removeAnimation(forKey: "fadeTransition")
        let transition = CATransition().fadeTransition()
        view.layer.add(transition, forKey: "fadeTransition")
        pushViewController(toVC, animated: false)
    }

    func fadeToRoot() {
        view.layer.removeAnimation(forKey: "fadeTransition")
        let transition = CATransition().fadeTransition()
        view.layer.add(transition, forKey: "fadeTransition")
        popToRootViewController(animated: false)
    }
}

2. 确认视图控制器的生命周期与子视图创建

当前VC的viewDidLoad只会在初始化时调用一次,pop返回时不会重复创建子视图,这部分没问题。但如果后续在viewWillAppear等重复调用的方法里创建视图,会导致子视图重复添加,需避免这种情况。

3. Instruments工具使用技巧

如果还是找不到泄漏点,用Instruments的Allocations工具:

  • 启动Allocations,切换VC几次后点击「Mark Generation」标记内存快照
  • 对比前后快照,查看哪些对象数量持续增加
  • 重点关注UIViewController实例、CALayer、CATransition这类对象的变化

内容的提问来源于stack exchange,提问作者GBasher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:25:55