GeoCoordinateWatcher 的诡异问题

前两天在做 WP7 开发,需要用到 GPS 定位,这里会用到一个 GeoCoordinateWatcher 类。

写了一段代码后发现自己写错了,但是它竟然可以运行!再仔细一推敲,发现了很多坑。

 

我在 stackoverflow 上也报告了这个问题:传送门

问题代码如下:

private void button1_Click(object sender, RoutedEventArgs e)
{
    GeoCoordinateWatcher watcher = new GeoCoordinateWatcher();
    watcher.PositionChanged += new EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>>(watcher_PositionChanged);
    watcher.Start();
}

void watcher_PositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
{
    Debug.WriteLine(e.Position.Timestamp.ToString());
}

上述代码中,理论上 watcher 实例在按钮点击事件后应该会被销毁,所以下面的 PositionChanged 事件应该也不会被触发。

一开始这么写,是我的错误,但是看到这个问题后,我才发现我的程序是可以正常运作的…

调试后才发现,这个对象永远不会被销毁,每按一次就会多一个实例,并且你找不到它!

GPS 位置每改变一次,所有的实例就会输出调试信息。

网友的解答

发帖后,很多老外给予我帮助,一个人说:每次绑定事件,这个实例的 引用计数 就会加一,所以除非取消事件绑定,否则它不会被销毁。

嗯… 初看很有道理,但是其实有很多问题:

  1. C# 中用的是垃圾回收,而不是引用计数,两者是不同的。具体可以看看这个:传送门
  2. 如果非要说是事件的原因,那也应该是实例引用了委托,然后委托再引用了事件的提供这吧?

 

我将信将疑地发了第二个问题:传送门

这时,一个在我上个问题中回答过我的人(当时他回答地太简单)看不下去了。

这位老外指出了另一个人的错误,也开始道出了自己的猜想:

一定是 GeoCoordinateWatcher 在执行 start() 方法的时候将自己放置在了某个地方,保持被引用状态,所以才不会被销毁!

根本没有引用计数这个说法!

 

反编译后终得结果

为了寻找最后的结果,决定反编译一下这个类:

public override bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
{
    if (!this.IsStarted)
    {
        this.IsStarted = true;
        if (this.Status != GeoPositionStatus.Ready || this.m_position.Location.IsUnknown)
        {
            this.m_eventGetLocDone = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(GetInitialLocationData, suppressPermissionPrompt);
            if (timeout != TimeSpan.Zero && !this.m_eventGetLocDone.WaitOne(timeout))
            {
                this.Stop();
            }
        }
        else
        {
            this.OnPositionChanged(new GeoPositionChangedEventArgs<GeoCoordinate>(this.m_position));
        }
    }
    return this.IsStarted;
}

原来,这个 start() 函数把实例内部的另外一个方法加入了线程池,并保持运行。

**所以,就算就算实例看上去被销毁了,实际上它还是被线程池引用着。

 

解决方法

既然找到了原因,就需要去避免它。在上面这段代码中,有两种方式可以避免:

  1. 在触发 PositionChanged 事件后对实例执行 stop() 函数,这样可以让这个线程停下来。
  2. 在父实例中,保持对 watcher 的引用,这样做更好,可以随时开关,也不用但是产生多余的实例。

 

其他类似的类

由于这个问题,我又想到了很多别的类,也会有同样的用法,那的类会有怎么样的表现呢?

我这里测试了一下 WebClient

private void Form1_Load(object sender, EventArgs e)
{
    for (var k = 0; k < 10000; k++)
    {
        StartWebClient();
    }
}
private void StartWebClient()
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += client_DownloadStringCompleted;
    client.DownloadStringAsync(new Uri("http://www.dozer.cc"));
}
int count = 0;
public void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    count++;
    label1.Text = count.ToString();
}

测试后发现,WebClient 也和上面的一样!DownloadStringCompleted 事件根本也会被触发!

执行完上述代码后,程序会保持对 10000 个实例的引用,内存占用高达 100MB。

explorer

但是还好,一般情况下也不会用那么多实例,而且 WebClient 不像前面那个,WebClient 执行一次后就停止了,然后就会被回收了。

 

总结

最后,发现了问题以后肯定要避免以下了。

首先像 WebClient 这样只会出发一次事件的,可以不去管它,最终也会被回收。没必要外父实例内保持引用。

但是,像 GeoCoordinateWatcher 这样持续触发事件的,一定不能这么些了!否则你会多出很多多余的实例!

具体怎么做就需要根据不同的类进行不同的处理了。总之一定要注意!

本作品由 Dozer 创作,采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。