Swift Accelerometer 不断与 ScrollView 崩溃

Swift Accelerometer keeps crashing with ScrollView

编辑: 我简化了代码并添加了对 NSThread.isMainThread() 的调用,以查看这是否是问题所在。在下面查看更广泛的编辑

我正在开发一个相当简单的应用程序,以在暑假期间协助一位教授进行研究。该应用程序旨在根据 iPad 中的加速度计确定句子中的单词难度。

本质上,用户将倾斜 iPad,从而产生非零加速度,并且位于 UILabelscrollView 内的文本将滚动因此。

这在 99% 的时间里都非常有效。在我们几乎所有的测试中,它都能完美运行,它可以毫无问题地遍历整个文本,并且没有发生任何不好的事情。然而很少见,它只是中断,抛出 EXC_BAD_ACCESS 的错误。我想强调的是,在极少数情况下它确实会中断,没有明显的模式,它有时会发生在滚动的中间、接近尾声或开始时。

显然,我希望该应用程序没有错误,这是一个相当重要的问题,我只是想不通,所以如果您能提供任何帮助,我们将不胜感激。

这是我的 ScrollingLabel Class 的总代码(错误总是发生在 startScrolling class 的末尾)。

import Foundation
import UIKit
import QuartzCore
import CoreMotion


public class ScrollingLabel {

//Instantiation of scroll view and label
var baseTextLabel:UILabel!
var baseScrollView:UIScrollView!
var frame:CGRect!


//Instantiation of accelerometer materials
var motionManager=CMMotionManager()
var queue=NSOperationQueue()
//To rectify the issue, I have changed this to:
//var queue=NSOperationQueue.mainQueue(), SEE EDIT BELOW

init(frame:CGRect) {
    /*Initializes the object by calling 3 private setup functions,
    each dealing one with a specific feature of the final label, and
    finally calling the scroll function to activate the accelerometer
    control*/
    setupFrame(frame)
    setupLabel()
    setupScroll()
    startScrolling()
}



private func startScrolling() {
    //The main accelerometer control of the label
    println(NSThread.isMainQueue) //THIS RETURNS TRUE

    //Allows the start orientation to become default
    var firstOrientation:Bool
    var timeElapsed:Double=0
    if letUserCreateDefaultOrientation {firstOrientation=true}
    else {firstOrientation=false}
    var standardAccel:Double=0

    //Begins taking updates from the accelerometer
    if motionManager.accelerometerAvailable{
        motionManager.accelerometerUpdateInterval=updateTimeInterval
        motionManager.startAccelerometerUpdatesToQueue(self.queue, withHandler: { (accelerometerData, error:NSError!) -> Void in
            println(NSThread.isMainQueue) //THIS RETURNS FALSE
            //Changes the input of acceleration depending on constant control variables

            var accel:Double
            if self.timerStarted {
                timeElapsed+=Double(self.updateTimeInterval)
            }
            if !self.upDownTilt {
                if self.invertTextMotion {accel = -accelerometerData.acceleration.y}
                else {accel = accelerometerData.acceleration.y}
            }
            else {
                if self.invertTextMotion {accel = -accelerometerData.acceleration.x}
                else {accel = accelerometerData.acceleration.x}
            }

            //Changes default acceleration if allowed
            if firstOrientation {
                standardAccel=accel
                firstOrientation=false
            }
            accel=accel-standardAccel

            //Sets the bounds of the label to prevent nil unwrapping
            var minXOffset:CGFloat=0
            var maxXOffset=self.baseScrollView.contentSize.width-self.baseScrollView.frame.size.width

            //If accel is greater than minimum, and label is not paused begin updates
            if !self.pauseScrolling && fabs(accel)>=self.minTiltRequired {

                //If the timer has not started, and accel is positive, begin the timer
                if !self.timerStarted&&accel<0{
                    self.stopwatch.start()
                    self.timerStarted=true
                }

                //Stores the data, and moves the scrollview depending on acceleration and constant speed
                if self.collectData {self.storeIndexAccelValues(accel,timeElapsed: timeElapsed)}
                var targetX:CGFloat=self.baseScrollView.contentOffset.x-(CGFloat(accel) * self.speed)
                if targetX>maxXOffset {targetX=maxXOffset;self.stopwatch.stop();self.doneWithText=true}
                else if targetX<minXOffset {targetX=minXOffset}
                self.baseScrollView.setContentOffset(CGPointMake(targetX,0),animated:true)
                if self.baseScrollView.contentOffset.x>minXOffset&&self.baseScrollView.contentOffset.x<maxXOffset {
                    if self.PRIVATEDEBUG {
                        println(self.baseScrollView.contentOffset)
                    }
                }
            }
        })
    }
}

当它确实崩溃时,它发生在 startScrolling 的末尾,当时我将内容偏移量设置为目标 X。如果您需要更多信息,我很乐意提供,但随着错误的发生所以很少我没有什么可说的,特别是什么时候发生或类似的事情......它似乎是随机的。

编辑: 我已将代码简化为仅相关部分,并添加了我调用 NSThread.isMainQueue() 的两个位置。在 startScrolling 的第一行调用时,.isMainQueue() returns TRUE,但在 motionManager.startAccelerometerUpdatesToQueue 内部调用时 returns 错误

为了纠正这个问题,我将 self.queue 从 NSOperationQueue() 更改为 NSOperationQueue.mainQueue(),并且在进行此切换后,第二个 .isMainThread() 调用(motionManager.startAccelerometerUpdatesToQueue) 现在 returns TRUE 正如我们所希望的那样。

代码太多了。我在基于 targetX 设置内容偏移量的行中没有看到任何内容。

有两种可能性

baseScrollView 正在被释放并且是一个僵尸(不太可能,因为它看起来像您将其定义为常规(强)实例变量。)

您正在从后台线程调用 startScrolling。如果您从后台线程更新 UI 个对象,则所有赌注都会关闭。您可以使用 NSThread.isMainThread() 检查它。将该代码放入您的 startScrolling 方法中,如果它 returns 为假,那就是您的问题。

编辑:

根据您在下面回复我的回答的评论,您正在从后台线程调用startScrolling

确实很有可能是这个问题。编辑您的 post 以显示从后台线程调用的代码,包括上下文。 (您的“加速度计更新周期”代码)。

您无法从后台线程操作 UIKit 对象,因此您可能需要在调用 dispatch_async 在主线程上运行。

编辑#2:

您终于 post 获得了足够的信息,我们可以为您提供帮助。您的原始代码让您在后台队列中接收加速度计更新。您正在从该代码执行 UIKit 调用。这是一个禁忌,这样做的结果是不确定的。它们的范围从更新永远持续到根本不发生,再到崩溃。

更改您的代码以使用 NSOperationQueue.mainQueue() 作为处理更新的队列应该可以解决问题。

如果您需要在处理程序中进行耗时的加速度计更新处理,那么您可以继续使用之前使用的后台队列,但将您的 UIKit 调用包装在 dispatch_async:

dispatch_async(dispatch_get_main_queue())
{
  //UIKit code here
}

这样您耗时的加速度计更新代码就不会使主线程陷入困境,但 UI 更新仍然在主线程上完成。