数组在被枚举时发生了变异 - 斯威夫特

时间:2014-12-03 13:40:33

标签: ios xcode swift xcode6

func mapView(mapView: MKMapView!, regionDidChangeAnimated animated: Bool) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        if (self.busStops.count > 0) {
            if (mapView.camera!.altitude <= 1000) {
                for (var i = 0; i < self.busStops.count; i++) {
                    if (MKMapRectContainsPoint(mapView.visibleMapRect, MKMapPointForCoordinate(self.busStops[i].position))) {
                        let stop = BusAnno()
                        stop.setCoordinate(self.busStops[i].position)
                        stop.type = "stop"
                        stop.title = self.busStops[i].name
                        stop.subtitle = self.busStops[i].street
                        self.activeStops.append(stop)
                    }
                }
                dispatch_async(dispatch_get_main_queue()) {
                    self.mapView.addAnnotations(self.activeStops)
                }
            } else if (self.activeStops.count > 0) {
                mapView.removeAnnotations(self.activeStops)
                self.activeStops = []
            }
        }
    }
}

上面的代码目前给了我:

Terminating app due to uncaught exception 'NSGenericException',   
reason: '*** Collection <__NSArrayM: 0x1775edd0> was mutated while being enumerated.'

发生这种情况的原因是因为如果用户在应用程序仍在添加公交车站点时快速缩小,则会提交“被枚举时发生变异”错误。问题是,我不知道如何解决这个问题,我基本上需要检查应用程序是否已经完成添加公交车站点然后将其删除。

此代码的目的是在放大小于1000米时向地图添加公交车站,然后在高于此高度时移除公交车站而不会出现此错误。

5 个答案:

答案 0 :(得分:6)

该问题涉及线程安全。您的busStops数组正在被2个线程同时修改。

因此,您需要同步对它的访问,即确保您对busStops数组的更新是连续发生的(一个接一个),而不是同时发生(同时发生)。一种方法是将该数组的所有修改分配给您创建的串行队列。

dispatch_get_global_queue将上述逻辑分派给全局共享并发队列。而不是使用全局队列,创建自己的队列并将其存储在类的实例变量中。

_queue = dispatch_queue_create("com.app.serialQueue", DISPATCH_QUEUE_SERIAL);

然后根据需要向其发送工作:

dispatch_async(_queue, ^{

    // Work, i.e. modifications to busStops array

});

如果您希望获得更多细微差别,可以将队列设置为并发队列,并使用dispatch_asyncbusStops阵列进行所有读取,并对所有队列使用dispatch_barrier_async写道。后者实质上使队列暂时表现为串行队列。

答案 1 :(得分:5)

我担心“被点名时变异”并不是你最大的问题。您也快速连续启动新线程,因为mapView:regionDidChange:可以在缩放/平移期间快速连续调用。来自class reference

  

只要当前显示的地图区域,就会调用此方法   变化。在滚动期间,可以多次调用此方法   报告地图位置的更新。

因此,您还要多次向地图添加相同的注释。

您的算法应该类似于以下内容。请注意,这只是一个草图,它不处理删除,它可能甚至不是有效的Swift,我从来没有使用该语言编程:

func mapView(mapView: MKMapView!, regionDidChangeAnimated animated: Bool) {
    if (isUpdating) {
        return;
    isUpdating = true;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        var currentRect;
        do {
            currentRect = mapView.visibleMapRect;    
            var toAdd = [];            
            for (var i = 0; i < self.busStops.count; i++) {
                if (!self.busStops[i].isOnMap && MKMapRectContainsPoint(currentRect, MKMapPointForCoordinate(self.busStops[i].position))) {
                    // create stop
                    toAdd.append(stop);
                    self.busStops[i].isOnMap = true;
                }
            }
            dispatch_async(dispatch_get_main_queue()) {
                self.mapView.addAnnotations(toAdd)
            }
        } while (mapView.visibleMapRect != currentRect);
        isUpdating = false;
    }
}

答案 2 :(得分:1)

问题是由于多个线程同时修改了同一个对象。

有两种方法可以解决此问题:

1)方法1

使用:

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))

而不是:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))

但我不推荐这种方式,因为它可以冻结主线程


2)方法2

使用dispatch_barrier_async自定义串行队列并为阵列实现自定义getter和setter。

声明一个属性:

// For handling the read-write problem
@property (nonatomic, strong) dispatch_queue_t myQueue;

viewDidLoad初始化它:

_myQueue = dispatch_queue_create("com.midhun.mp.myQueue", DISPATCH_QUEUE_CONCURRENT);

并实施您的方法,如:

func mapView(mapView: MKMapView!, regionDidChangeAnimated animated: Bool)
{
    dispatch_barrier_async(_myQueue, ^{
        if (self.busStops.count > 0)
        {
            if (mapView.camera!.altitude <= 1000)
            {
                for (var i = 0; i < self.busStops.count; i++)
                {
                    if (MKMapRectContainsPoint(mapView.visibleMapRect, MKMapPointForCoordinate(self.busStops[i].position)))
                    {
                        let stop = BusAnno()
                        stop.setCoordinate(self.busStops[i].position)
                        stop.type = "stop"
                        stop.title = self.busStops[i].name
                        stop.subtitle = self.busStops[i].street
                        self.activeStops.append(stop)
                    }
                }
                dispatch_async(dispatch_get_main_queue())
                {
                    self.mapView.addAnnotations(self.activeStops)
                }
            }
            else if (self.activeStops.count > 0)
            {
                mapView.removeAnnotations(self.activeStops)
                self.activeStops = []
            }
        }
    }
}

更改数据删除方法也使用dispatch_barrier_async(_myQueue, ^{});

答案 3 :(得分:0)

当我只看问题时,这是线程安全问题。

您可以使用线程间通信让一个线程知道另一个线程的状态。例如。通过通知系统设置变量。

因此,在加载线程中,只要加载了所有总线停止,就会触发通知消息。

在接收例程中,在收到通知之前,您什么都不做(没有缩放或者什么)。

答案 4 :(得分:0)

您可以像其他人提到的那样同步访问self.activeStops,但这也是另一个简单的解决方案:

您可以将BusAnno对象添加到线程内的临时数组中(我假设迭代是代码中最昂贵的部分)然后将它们刷回到主线程上的self.activeStops变量(只需将现有对象添加到另一个数组即可。)

这种方式你只是在主线程上触及self.activeStops,但仍然可以获得更新线程的好处。

我也是这样做的,因此线程中没有触及地图视图,因为它是一个UI元素

注意:我没有Swift专家所以这可能需要一些调整

func mapView(mapView: MKMapView!, regionDidChangeAnimated animated: Bool) {

    // Take a snapshot of the altitude and visibleMapRect so we're not accessing the map view in the thread
    let altitude = mapView.camera!.altitude
    let visibleMapRect = mapView.visibleMapRect

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        if (self.busStops.count > 0) {
            if (altitude <= 1000) {
                // New bus stops added here in the thread
                var newActiveStops = [BusAnno]()
                for (var i = 0; i < self.busStops.count; i++) {
                    if (MKMapRectContainsPoint(visibleMapRect, MKMapPointForCoordinate(self.busStops[i].position))) {
                        let stop = BusAnno()
                        stop.setCoordinate(self.busStops[i].position)
                        stop.type = "stop"
                        stop.title = self.busStops[i].name
                        stop.subtitle = self.busStops[i].street
                        newActiveStops.append(stop)
                    }
                }
                dispatch_async(dispatch_get_main_queue()) {
                    // Add to active stops on main thread
                    self.activeStops += newActiveStops
                    self.mapView.addAnnotations(self.activeStops)
                }
            } else {
                dispatch_async(dispatch_get_main_queue()) {
                    if (self.activeStops.count > 0) {
                        // Also moved all map updates to the main thread (it's a UI element)
                        mapView.removeAnnotations(self.activeStops)
                        self.activeStops = []
                    }
                }
            }
        }
    }
}