iOS下CLLocationManager多次定位引起内存访问错误的问题解决

好几个月没写东西了,今天有空写点iOS的(我发现自己是非常不专注,安卓没搞好,又转而搞iOS了)。

我的程序中有一个获取用户当前位置地址的功能。我写了一个定位的辅助类LocationHelper,在这个类里调用CLLocationManager,接管didUpdateToLocation事件获取经纬度坐标,然后再向后台发送坐标请求返回地址。使用时,我在某ViewController里创建一个LocationHelper类,将ViewController当成locHandler的Delegate传给过去,当获得到坐标时立即停止定位功能,并调后台请求返回地址,得到地址后再回调locHandler的方法,完成定位地址过程。在这过程中,程序将显示定位进度条不允许用户操作,直到定位完成获取地址。LocationHelper将被ViewController一直保持,直到ViewController释放。

LocationHelper的大概类定义如下:

@implementation LocationHelper
@synthesize locHandler;

- (id)initLocationHelper:(id)handler{
    self=[super init];
    self.locHandler=handler;
    locationMan=[[CLLocationManager alloc]init];
    locationMan.delegate=self;
    [locationMan startUpdatingLocation];
    return self;
}

- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation{
    // 取得经纬度
    CLLocationCoordinate2D coordinate = newLocation.coordinate;
    CLLocationDegrees latitude = coordinate.latitude;
    CLLocationDegrees longitude = coordinate.longitude;
    
    [locationMan stopUpdatingLocation];
    
    GeoAddressHelper * gah=[[GeoAddressHelper alloc] initWithGeoX:longitude andGeoY:latitude andResultDelegate:self];
    self.curGah=gah;
    [gah release];
}

- (void)OnGeoAddressFound:(NSObject*)res{
    [locHandler locationHelperFoundAddress:res];   
}

@end


其中GeoAddressHelper通过网络请求,把经纬度转成中文地址:

@implementation GeoAddressHelper
@synthesize eventDelegate,geoX,geoY;

-(void)initWithGeoX:(double)x andGeoY:(double)y andResultDelegate:(NSObject*)evtDlg{
    self.eventDelegate=evtDlg;
    
    geoX=x;
    geoY=y;
    
    NSMutableString * url=(NSMutableString*)[MyApp getServerHttpUrl:@"opId=7100017"];
    [url appendFormat:@"&x=%.6f&y=%.6f",x,y];
    
    NetReqOperation * req=[[NetReqOperation alloc] initWithURL:url withDelegate:self];
    [[MyApp netReqQueue] addOperation:req];
    [req release];
}

- (void)OnNetReqFinished:(NSObject *)res{
    [eventDelegate performSelectorOnMainThread:@selector(OnGeoAddressFound:) withObject:res waitUntilDone:YES];
}

@end

 


但是,就这么看似简单的两个类,居然时不时会出现内存地址错误(无法识别的selector之类的),导致程序闪退。闪退时错误堆栈如下:

-[__NSCFSet OnGeoAddressFound:]: unrecognized selector sent to instance 0xf678640
(null)
(
	0   CoreFoundation    0x340848d7 __exceptionPreprocess + 186
	1   libobjc.A.dylib   0x342d41e5 objc_exception_throw + 32
	2   CoreFoundation    0x34087acb -[NSObject doesNotRecognizeSelector:] + 174
	3   CoreFoundation    0x34086945 ___forwarding___ + 300
	4   CoreFoundation    0x33fe1680 _CF_forwarding_prep_0 + 48
	5   Foundation        0x359ce1b7 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 266
	6   Foundation        0x359cde49 -[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 136
	7   AutoTraffic2012   0x001232db -[GeoAddressHelper OnNetReqFinished:] + 502
	8   Foundation        0x359ce1b7 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 266
	9   Foundation        0x359cde49 -[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 136
	10  AutoTraffic2012   0x0011170f -[NetReqOperation OnNetReqFinished:] + 106
	11  CoreFoundation    0x33fe322b -[NSObject performSelector:withObject:] + 42
	12  Foundation        0x35a6e757 __NSThreadPerformPerform + 350
	13  CoreFoundation    0x34058b03 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14
	14  CoreFoundation    0x340582cf __CFRunLoopDoSources0 + 214
	15  CoreFoundation    0x34057075 __CFRunLoopRun + 652
	16  CoreFoundation    0x33fda4dd CFRunLoopRunSpecific + 300
	17  CoreFoundation    0x33fda3a5 CFRunLoopRunInMode + 104
	18  GraphicsServices  0x3085efcd GSEventRunModal + 156
	19  UIKit             0x3745b743 UIApplicationMain + 1090
)


 

从错误堆栈上看,显然LocationHelper这时被释放了。但由于LocationHelper被ViewController引用,而ViewController在这段时间内正在显示定位进度条不允许操作退出,因此理论上是不可能被提前释放的。这个错误不容易重现,研究了很久代码,也没找出哪里写法有问题。

后来经过大量测试,发现有这么个规律:如果经常使用程序,这个错误不容易出现;操作慢一些也不容易出错;但如果把手机闲置一段时间再来使用,并且界面切换的操作速度快一些,则第一次使用时很容易出现这个错误。

闲置一段时间后,程序使用上跟平时有什么区别呢?我感觉可能跟第一次定位有关。在操作地图时,首次定位经常会先出现一个粗略定位,过一会再出现一个更精确的纠正的定位,这样可能会连续触发两次didUpdateToLocation事件,导致出错;而后续的定位可能就都是一次定位。我在代码中,第一次定位成功时,已经立即调stopUpdatingLocation停止定位扫描了,理论上是不会再触发定位了,因此我一直没往这上面想。

但实际情况似乎跟想像的不一样,从现象看很可能didUpdateToLocation触发了两次。于是我在LocationHelper中加了个locFired标识,只要触发一次,立即将此标识置为YES,下次不再处理。经过这么处理,错误果然消失了。

原来,stopUpdatingLocation并不一定能立即停止定位。在我第一次获得经纬度坐标完成地址查询后,ViewController上的定位进度条消失允许操作;这时LocatipnHelper没有完全停止扫描,didUpdateToLocation事件再次触发,又发起了坐标转换地址请求;恰恰在这个时候,ViewController在用户操作快时可能会立即被pop关闭并释放,同时LocationHelper也被释放,导致GeoAddressHelper在请求完成时回调LocationHelper产生内存访问错误。通过设置标识位阻止其第二次触发,问题就解决了。

那有人又会说了,既然GeoAddressHelper引用了LocationHelper,为何不增加LocationHelper的引用计数防止它自动释放呢?其实最开始我也是这么干的,但这样又导致了另一个问题,所以后来才改成不加引用的。具体我在下一篇文章再解释。

 

你可能感兴趣的:(iOS)