数组在被枚举时发生了变异 - Swift


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 米时将公交车停靠点添加到地图中,然后在公交车停靠点超过此高度时将其移除而不会收到此错误。

怕"被枚举时变异"不是你最大的问题。您还会快速连续地触发新线程,因为mapView:regionDidChange:可以在缩放/平移期间快速连续调用。从类引用:

每当当前显示的地图区域时,都会调用此方法 变化。在滚动过程中,可能会多次调用此方法以 报告地图位置的更新。

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

您的算法应如下所示。请注意,这只是一个草图,它不处理删除,甚至可能不是有效的 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;
    }
}

该问题涉及线程安全。您的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
});

如果要更细致入微,可以将队列设置为并发队列,并对busStops数组的所有读取使用 dispatch_async,对所有写入使用 dispatch_barrier_async。后者实质上使队列暂时表现为串行队列。

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

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

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, ^{});

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

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

因此,在加载线程中,一旦加载了所有总线站,就会触发通知消息。

接收例程中,在收到通知之前,您什么都不做(没有缩放或其他任何操作)。

您可以像其他人提到的那样同步对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 = []
                    }
                }
            }
        }
    }
}

最新更新