内存泄漏及其在Swift中使用弱声明的解决方案

时间:2015-06-29 19:17:41

标签: ios swift memory-leaks weak

我正在使用SpriteKit制作一场3场比赛。 Explanation: http://www.raywenderlich.com/75273/make-game-like-candy-crush-with-swift-tutorial-part-2。请参阅第4页的swm93评论 这是一个教程,但它似乎在代码中有内存泄漏。任何人都可以下载这个swift项目文件并查找导致内存泄漏的原因并提供可能的解决方案吗? 本教程的制作者说“handleSwipe(swap)”方法中存在内存泄漏,我们可以通过向字段声明添加“弱”来修复它。我试着写“弱变量场景:GameScene?”但如果我这样做,它就说“场景是零”,即使我这样初始化它:“scene = GameScene(size:skView.bounds.size)”在“viewDidLoad()”函数中。 其余课程可以通过我的link here下载 即使视图控制器已经被解雇,内存使用百分比也不会降低......如果我调用GameViewController,将其解除,然后再次调用它,内存使用率是两倍。换一种说法, PreViewController(18MB)segue-> GameViewController(75MB)解雇 - > PreViewController(75MB)segue-> GameViewController(104MB)

import UIKit
import SpriteKit
import AVFoundation

class GameViewController: UIViewController {
    // The scene draws the tiles and cookie sprites, and handles swipes.
    var scene: GameScene!

    // The level contains the tiles, the cookies, and most of the gameplay logic.
    // Needs to be ! because it's not set in init() but in viewDidLoad().
    var level: Level!

    var movesLeft = 0
    var score = 0

    @IBOutlet weak var targetLabel: UILabel!
    @IBOutlet weak var movesLabel: UILabel!
    @IBOutlet weak var scoreLabel: UILabel!
    @IBOutlet weak var gameOverPanel: UIImageView!
    @IBOutlet weak var shuffleButton: UIButton!

    var tapGestureRecognizer: UITapGestureRecognizer!

    @IBAction func dismiss(sender: UIButton) {
        self.dismissViewControllerAnimated(true, completion: {})
    }

    lazy var backgroundMusic: AVAudioPlayer = {
        let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3")
        let player = AVAudioPlayer(contentsOfURL: url, error: nil)
        player.numberOfLoops = -1
        return player
        }()

    override func prefersStatusBarHidden() -> Bool {
        return true
    }

    override func shouldAutorotate() -> Bool {
        return true
    }

    override func supportedInterfaceOrientations() -> Int {
        return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configure the view.
        let skView = view as! SKView
        skView.multipleTouchEnabled = false

        // Create and configure the scene.
        scene = GameScene(size: skView.bounds.size)
        scene.scaleMode = .AspectFill

        // Load the level.
        level = Level(filename: "Level_1")
        scene.level = level
        scene.addTiles()
        scene.swipeHandler = handleSwipe

        // Hide the game over panel from the screen.
        gameOverPanel.hidden = true
        shuffleButton.hidden = true

        // Present the scene.
        skView.presentScene(scene)

        // Load and start background music.
        backgroundMusic.play()

        // Let's start the game!
        beginGame()
    }

    func beginGame() {
        movesLeft = level.maximumMoves
        score = 0
        updateLabels()

        level.resetComboMultiplier()

        scene.animateBeginGame() {
            self.shuffleButton.hidden = false
        }

        shuffle()
    }

    func shuffle() {
        // Delete the old cookie sprites, but not the tiles.
        scene.removeAllCookieSprites()

        // Fill up the level with new cookies, and create sprites for them.
        let newCookies = level.shuffle()
        scene.addSpritesForCookies(newCookies)
    }

    // This is the swipe handler. MyScene invokes this function whenever it
    // detects that the player performs a swipe.
    func handleSwipe(swap: Swap) {
        // While cookies are being matched and new cookies fall down to fill up
        // the holes, we don't want the player to tap on anything.
        view.userInteractionEnabled = false

        if level.isPossibleSwap(swap) {
            level.performSwap(swap)
            scene.animateSwap(swap, completion: handleMatches)
        } else {
            scene.animateInvalidSwap(swap) {
                self.view.userInteractionEnabled = true
            }
        }
    }

    // This is the main loop that removes any matching cookies and fills up the
    // holes with new cookies.
    func handleMatches() {
        // Detect if there are any matches left.
        let chains = level.removeMatches()

        // If there are no more matches, then the player gets to move again.
        if chains.count == 0 {
            beginNextTurn()
            return
        }

        // First, remove any matches...
        scene.animateMatchedCookies(chains) {

            // Add the new scores to the total.
            for chain in chains {
                self.score += chain.score
            }
            self.updateLabels()

            // ...then shift down any cookies that have a hole below them...
            let columns = self.level.fillHoles()
            self.scene.animateFallingCookies(columns) {

                // ...and finally, add new cookies at the top.
                let columns = self.level.topUpCookies()
                self.scene.animateNewCookies(columns) {

                    // Keep repeating this cycle until there are no more matches.
                    self.handleMatches()
                }
            }
        }
    }

    func beginNextTurn() {
       level.resetComboMultiplier()
        level.detectPossibleSwaps()
        view.userInteractionEnabled = true
        decrementMoves()
    }

    func updateLabels() {
        targetLabel.text = String(format: "%ld", level.targetScore)
        movesLabel.text = String(format: "%ld", movesLeft)
        scoreLabel.text = String(format: "%ld", score)
    }

    func decrementMoves() {
        --movesLeft
        updateLabels()

        if score >= level.targetScore {
            gameOverPanel.image = UIImage(named: "LevelComplete")
            showGameOver()
        }
        else if movesLeft == 0 {
            gameOverPanel.image = UIImage(named: "GameOver")
            showGameOver()
        }
    }

    func showGameOver() {
        gameOverPanel.hidden = false
        scene.userInteractionEnabled = false
        shuffleButton.hidden = true

        scene.animateGameOver() {
            self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver")
            self.view.addGestureRecognizer(self.tapGestureRecognizer)
        }
    }

    func hideGameOver() {
        view.removeGestureRecognizer(tapGestureRecognizer)
        tapGestureRecognizer = nil

        gameOverPanel.hidden = true
        scene.userInteractionEnabled = true

        beginGame()
    }

    @IBAction func shuffleButtonPressed(AnyObject) {
        shuffle()

        // Pressing the shuffle button costs a move.
        decrementMoves()
    }
}

import SpriteKit

class GameScene: SKScene {
    // This is marked as ! because it will not initially have a value, but pretty
    // soon after the GameScene is created it will be given a Level object, and
    // from then on it will always have one (it will never be nil again).
    var level: Level!

    var swipeHandler: ((Swap) -> ())?

    let TileWidth: CGFloat = 32.0
    let TileHeight: CGFloat = 36.0

    let gameLayer = SKNode()
    let cookiesLayer = SKNode()
    let tilesLayer = SKNode()
    let cropLayer = SKCropNode()
    let maskLayer = SKNode()

    var swipeFromColumn: Int?
    var swipeFromRow: Int?

    var selectionSprite = SKSpriteNode()

    // Pre-load the resources
    let swapSound = SKAction.playSoundFileNamed("Chomp.wav", waitForCompletion: false)
    let invalidSwapSound = SKAction.playSoundFileNamed("Error.wav", waitForCompletion: false)
    let matchSound = SKAction.playSoundFileNamed("Ka-Ching.wav", waitForCompletion: false)
    let fallingCookieSound = SKAction.playSoundFileNamed("Scrape.wav", waitForCompletion: false)
    let addCookieSound = SKAction.playSoundFileNamed("Drip.wav", waitForCompletion: false)

    // MARK: Game Setup
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder) is not used in this app")
    }

    override init(size: CGSize) {
        super.init(size: size)
        anchorPoint = CGPoint(x: 0.5, y: 0.5)

        let background = SKSpriteNode(imageNamed: "Background")
        addChild(background)

        gameLayer.hidden = true
        addChild(gameLayer)

        let layerPosition = CGPoint(
            x: -TileWidth * CGFloat(NumColumns) / 2,
            y: -TileHeight * CGFloat(NumRows) / 2)

        tilesLayer.position = layerPosition
        gameLayer.addChild(tilesLayer)

        // We use a crop layer to prevent cookies from being drawn across gaps
        // in the level design.
        gameLayer.addChild(cropLayer)

        // The mask layer determines which part of the cookiesLayer is visible.
        maskLayer.position = layerPosition
        cropLayer.maskNode = maskLayer

        // This layer holds the Cookie sprites. The positions of these sprites
        // are relative to the cookiesLayer's bottom-left corner.
        cookiesLayer.position = layerPosition
        cropLayer.addChild(cookiesLayer)

        // nil means that these properties have invalid values.
        swipeFromColumn = nil
        swipeFromRow = nil

        // Pre-load the label font so prevent delays during game play.
        SKLabelNode(fontNamed: "GillSans-BoldItalic")
    }

    func addSpritesForCookies(cookies: Set<Cookie>) {
        for cookie in cookies {
            // Create a new sprite for the cookie and add it to the cookiesLayer.
            let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
            sprite.position = pointForColumn(cookie.column, row:cookie.row)
            cookiesLayer.addChild(sprite)
            cookie.sprite = sprite

            // Give each cookie sprite a small, random delay.
            sprite.alpha = 0
            sprite.xScale = 0.5
            sprite.yScale = 0.5

            sprite.runAction(
                SKAction.sequence([
                    SKAction.waitForDuration(0.25, withRange: 0.5),
                    SKAction.group([
                        SKAction.fadeInWithDuration(0.25),
                        SKAction.scaleTo(1.0, duration: 0.25)
                        ])
                    ]))
        }
    }

    func removeAllCookieSprites() {
        cookiesLayer.removeAllChildren()
    }

    func addTiles() {
        for row in 0..<NumRows {
            for column in 0..<NumColumns {

                // If there is a tile at this position, then create a new tile
                // sprite and add it to the mask layer.
                if let tile = level.tileAtColumn(column, row: row) {
                    let tileNode = SKSpriteNode(imageNamed: "MaskTile")
                    tileNode.position = pointForColumn(column, row: row)
                    maskLayer.addChild(tileNode)
                }
            }
        }

        // The tile pattern is drawn *in between* the level tiles. That's why
        // there is an extra column and row of them.
        for row in 0...NumRows {
            for column in 0...NumColumns {

                let topLeft     = (column > 0) && (row < NumRows)
                    && level.tileAtColumn(column - 1, row: row) != nil
                let bottomLeft  = (column > 0) && (row > 0)
                    && level.tileAtColumn(column - 1, row: row - 1) != nil
                let topRight    = (column < NumColumns) && (row < NumRows)
                    && level.tileAtColumn(column, row: row) != nil
                let bottomRight = (column < NumColumns) && (row > 0)
                    && level.tileAtColumn(column, row: row - 1) != nil

                // The tiles are named from 0 to 15, according to the bitmask that is
                // made by combining these four values.
                let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3

                // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
                if value != 0 && value != 6 && value != 9 {
                    let name = String(format: "Tile_%ld", value)
                    let tileNode = SKSpriteNode(imageNamed: name)
                    var point = pointForColumn(column, row: row)
                    point.x -= TileWidth/2
                    point.y -= TileHeight/2
                    tileNode.position = point
                    tilesLayer.addChild(tileNode)
                }
            }
        }
    }

    // MARK: Conversion Routines

    // Converts a column,row pair into a CGPoint that is relative to the cookieLayer.
    func pointForColumn(column: Int, row: Int) -> CGPoint {
        return CGPoint(
            x: CGFloat(column)*TileWidth + TileWidth/2,
            y: CGFloat(row)*TileHeight + TileHeight/2)
    }

    // Converts a point relative to the cookieLayer into column and row numbers.
    func convertPoint(point: CGPoint) -> (success: Bool, column: Int, row: Int) {
        // Is this a valid location within the cookies layer? If yes,
        // calculate the corresponding row and column numbers.
        if point.x >= 0 && point.x < CGFloat(NumColumns)*TileWidth &&
            point.y >= 0 && point.y < CGFloat(NumRows)*TileHeight {
                return (true, Int(point.x / TileWidth), Int(point.y / TileHeight))
        } else {
            return (false, 0, 0)  // invalid location
        }
    }

    // MARK: Detecting Swipes

    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        // Convert the touch location to a point relative to the cookiesLayer.
        let touch = touches.first as! UITouch
        let location = touch.locationInNode(cookiesLayer)

        // If the touch is inside a square, then this might be the start of a
        // swipe motion.
        let (success, column, row) = convertPoint(location)
        if success {
            // The touch must be on a cookie, not on an empty tile.
            if let cookie = level.cookieAtColumn(column, row: row) {
                // Remember in which column and row the swipe started, so we can compare
                // them later to find the direction of the swipe. This is also the first
                // cookie that will be swapped.
                swipeFromColumn = column
                swipeFromRow = row

                showSelectionIndicatorForCookie(cookie)
            }
        }
    }

    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
        // If swipeFromColumn is nil then either the swipe began outside
        // the valid area or the game has already swapped the cookies and we need
        // to ignore the rest of the motion.
        if swipeFromColumn == nil { return }

        let touch = touches.first as! UITouch
        let location = touch.locationInNode(cookiesLayer)

        let (success, column, row) = convertPoint(location)
        if success {

            // Figure out in which direction the player swiped. Diagonal swipes
            // are not allowed.
            var horzDelta = 0, vertDelta = 0
            if column < swipeFromColumn! {          // swipe left
                horzDelta = -1
            } else if column > swipeFromColumn! {   // swipe right
                horzDelta = 1
            } else if row < swipeFromRow! {         // swipe down
                vertDelta = -1
            } else if row > swipeFromRow! {         // swipe up
                vertDelta = 1
            }

            // Only try swapping when the user swiped into a new square.
            if horzDelta != 0 || vertDelta != 0 {
                trySwapHorizontal(horzDelta, vertical: vertDelta)
                hideSelectionIndicator()

                // Ignore the rest of this swipe motion from now on.
                swipeFromColumn = nil
            }
        }
    }

    // We get here after the user performs a swipe. This sets in motion a whole
    // chain of events: 1) swap the cookies, 2) remove the matching lines, 3)
    // drop new cookies into the screen, 4) check if they create new matches,
    // and so on.
    func trySwapHorizontal(horzDelta: Int, vertical vertDelta: Int) {
        let toColumn = swipeFromColumn! + horzDelta
        let toRow = swipeFromRow! + vertDelta

        if toColumn < 0 || toColumn >= NumColumns { return }
        if toRow < 0 || toRow >= NumRows { return }

        // Can't swap if there is no cookie to swap with. This happens when the user
        // swipes into a gap where there is no tile.
        if let toCookie = level.cookieAtColumn(toColumn, row: toRow),
            let fromCookie = level.cookieAtColumn(swipeFromColumn!, row: swipeFromRow!),
            let handler = swipeHandler {

                // Communicate this swap request back to the ViewController.
                let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
                handler(swap)
        }
    }

    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
        // Remove the selection indicator with a fade-out. We only need to do this
        // when the player didn't actually swipe.
        if selectionSprite.parent != nil && swipeFromColumn != nil {
            hideSelectionIndicator()
        }

        // If the gesture ended, regardless of whether if was a valid swipe or not,
        // reset the starting column and row numbers.
        swipeFromColumn = nil
        swipeFromRow = nil
    }

    override func touchesCancelled(touches: Set<NSObject>, withEvent event: UIEvent) {
        touchesEnded(touches, withEvent: event)
    }

    // MARK: Animations

    func animateSwap(swap: Swap, completion: () -> ()) {
        let spriteA = swap.cookieA.sprite!
        let spriteB = swap.cookieB.sprite!

        // Put the cookie you started with on top.
        spriteA.zPosition = 100
        spriteB.zPosition = 90

        let Duration: NSTimeInterval = 0.3

        let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
        moveA.timingMode = .EaseOut
        spriteA.runAction(moveA, completion: completion)

        let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
        moveB.timingMode = .EaseOut
        spriteB.runAction(moveB)

        runAction(swapSound)
    }

    func animateInvalidSwap(swap: Swap, completion: () -> ()) {
        let spriteA = swap.cookieA.sprite!
        let spriteB = swap.cookieB.sprite!

        spriteA.zPosition = 100
        spriteB.zPosition = 90

        let Duration: NSTimeInterval = 0.2

        let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
        moveA.timingMode = .EaseOut

        let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
        moveB.timingMode = .EaseOut

        spriteA.runAction(SKAction.sequence([moveA, moveB]), completion: completion)
        spriteB.runAction(SKAction.sequence([moveB, moveA]))

        runAction(invalidSwapSound)
    }

    func animateMatchedCookies(chains: Set<Chain>, completion: () -> ()) {
        for chain in chains {
            animateScoreForChain(chain)
            for cookie in chain.cookies {

                // It may happen that the same Cookie object is part of two chains
                // (L-shape or T-shape match). In that case, its sprite should only be
                // removed once.
                if let sprite = cookie.sprite {
                    if sprite.actionForKey("removing") == nil {
                        let scaleAction = SKAction.scaleTo(0.1, duration: 0.3)
                        scaleAction.timingMode = .EaseOut
                        sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
                            withKey:"removing")
                    }
                }
            }
        }

        runAction(matchSound)

        runAction(SKAction.waitForDuration(0.3), completion: completion)
    }

    func animateScoreForChain(chain: Chain) {
        // Figure out what the midpoint of the chain is.
        let firstSprite = chain.firstCookie().sprite!
        let lastSprite = chain.lastCookie().sprite!
        let centerPosition = CGPoint(
            x: (firstSprite.position.x + lastSprite.position.x)/2,
            y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)

        let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
        scoreLabel.fontSize = 16
        scoreLabel.text = String(format: "%ld", chain.score)
        scoreLabel.position = centerPosition
        scoreLabel.zPosition = 300
        cookiesLayer.addChild(scoreLabel)

        let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7)
        moveAction.timingMode = .EaseOut
        scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
    }

    func animateFallingCookies(columns: [[Cookie]], completion: () -> ()) {
        var longestDuration: NSTimeInterval = 0
        for array in columns {
            for (idx, cookie) in enumerate(array) {
                let newPosition = pointForColumn(cookie.column, row: cookie.row)

                let delay = 0.05 + 0.15*NSTimeInterval(idx)

                let sprite = cookie.sprite!   

                let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1)
                longestDuration = max(longestDuration, duration + delay)

                let moveAction = SKAction.moveTo(newPosition, duration: duration)
                moveAction.timingMode = .EaseOut
                sprite.runAction(
                    SKAction.sequence([
                        SKAction.waitForDuration(delay),
                        SKAction.group([moveAction, fallingCookieSound])]))
            }
        }

        // Wait until all the cookies have fallen down before we continue.
        runAction(SKAction.waitForDuration(longestDuration), completion: completion)
    }

    func animateNewCookies(columns: [[Cookie]], completion: () -> ()) {
        // wait that amount before we trigger the completion block.
        var longestDuration: NSTimeInterval = 0

        for array in columns {

            let startRow = array[0].row + 1

            for (idx, cookie) in enumerate(array) {

                // Create a new sprite for the cookie.
                let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
                sprite.position = pointForColumn(cookie.column, row: startRow)
                cookiesLayer.addChild(sprite)
                cookie.sprite = sprite

                // fall after one another.
                let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1)

                // Calculate duration based on far the cookie has to fall.
                let duration = NSTimeInterval(startRow - cookie.row) * 0.1
                longestDuration = max(longestDuration, duration + delay)
                let newPosition = pointForColumn(cookie.column, row: cookie.row)
                let moveAction = SKAction.moveTo(newPosition, duration: duration)
                moveAction.timingMode = .EaseOut
                sprite.alpha = 0
                sprite.runAction(
                    SKAction.sequence([
                        SKAction.waitForDuration(delay),
                        SKAction.group([
                            SKAction.fadeInWithDuration(0.05),
                            moveAction,
                            addCookieSound])
                        ]))
            }
        }

        // Wait until the animations are done before we continue.
        runAction(SKAction.waitForDuration(longestDuration), completion: completion)
    }

    func animateGameOver(completion: () -> ()) {
        let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
        action.timingMode = .EaseIn
        gameLayer.runAction(action, completion: completion)
    }

    func animateBeginGame(completion: () -> ()) {
        gameLayer.hidden = false
        gameLayer.position = CGPoint(x: 0, y: size.height)
        let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
        action.timingMode = .EaseOut
        gameLayer.runAction(action, completion: completion)
    }

    // MARK: Selection Indicator

    func showSelectionIndicatorForCookie(cookie: Cookie) {
        if selectionSprite.parent != nil {
            selectionSprite.removeFromParent()
        }

        if let sprite = cookie.sprite {
            let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
            selectionSprite.size = texture.size()
            selectionSprite.runAction(SKAction.setTexture(texture))

            sprite.addChild(selectionSprite)
            selectionSprite.alpha = 1.0
        }
    }

    func hideSelectionIndicator() {
        selectionSprite.runAction(SKAction.sequence([
            SKAction.fadeOutWithDuration(0.3),
            SKAction.removeFromParent()]))
    }
}

0 个答案:

没有答案