好幾個月沒寫東西了,今天有空寫點iOS的(我發現自己是非常不專註,安卓沒搞好,又轉而搞iOS了)。
我的程式中有一個擷取使用者當前位置地址的功能。我寫了一個定位的輔助類LocationHelper,在這個類裡調用CLLocationManager,接管didUpdateToLocation事件擷取經緯度座標,然後再向後台發送座標請求返回地址。使用時,我在某ViewController裡建立一個LocationHelper類,將ViewController當成locHandler的Delegate傳給過去,當獲得到座標時立即停止定位功能,並調後台請求返回地址,得到地址後再回調locHandler的方法,完成定位地址過程。在這過程中,程式將顯示定位進度條不允許使用者操作,直到定位完成擷取地址。LocationHelper將被ViewController一直保持,直到ViewController釋放。
LocationHelper的大概類定義如下:
@implementation LocationHelper@synthesize locHandler;- (id)initLocationHelper:(id<MyLocationDelegate>)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 + 1861 libobjc.A.dylib 0x342d41e5 objc_exception_throw + 322 CoreFoundation 0x34087acb -[NSObject doesNotRecognizeSelector:] + 1743 CoreFoundation 0x34086945 ___forwarding___ + 3004 CoreFoundation 0x33fe1680 _CF_forwarding_prep_0 + 485 Foundation 0x359ce1b7 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 2666 Foundation 0x359cde49 -[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 1367 AutoTraffic2012 0x001232db -[GeoAddressHelper OnNetReqFinished:] + 5028 Foundation 0x359ce1b7 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 2669 Foundation 0x359cde49 -[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 13610 AutoTraffic2012 0x0011170f -[NetReqOperation OnNetReqFinished:] + 10611 CoreFoundation 0x33fe322b -[NSObject performSelector:withObject:] + 4212 Foundation 0x35a6e757 __NSThreadPerformPerform + 35013 CoreFoundation 0x34058b03 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 1414 CoreFoundation 0x340582cf __CFRunLoopDoSources0 + 21415 CoreFoundation 0x34057075 __CFRunLoopRun + 65216 CoreFoundation 0x33fda4dd CFRunLoopRunSpecific + 30017 CoreFoundation 0x33fda3a5 CFRunLoopRunInMode + 10418 GraphicsServices 0x3085efcd GSEventRunModal + 15619 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的引用計數防止它自動釋放呢?其實最開始我也是這麼乾的,但這樣又導致了另一個問題,所以後來才改成不加引用的。具體我在下一篇文章再解釋。