GOTK3遍历ListStore并更新条目时,TreeModel.ForEach与IterNext的内存泄漏及排序异常问题求助
哎,我之前用GOTK3写GTK3应用的时候也踩过类似的坑!你遇到的排序时遍历漏/重复处理的问题,还有内存泄漏,大概率是两个核心原因:GTK严格的单线程模型限制,以及混淆了排序包装模型和原始ListStore的遍历。我给你拆解下具体的解决方案:
先解决排序时遍历乱跳的问题
如果你的MainListStore是被TreeModelSort包裹的(也就是UI上显示的是排序后的列表),直接遍历这个排序后的模型就会出问题——因为你修改的字段刚好是排序依据,修改后模型会立即重新排序,导致TreeIter的位置跟着变动,自然会出现漏处理或者重复处理条目的情况。
正确的做法是:跳过排序包装层,直接遍历原始的ListStore。你可以通过TreeModelSort.GetModel()拿到底层的原始ListStore,这样不管上层怎么排序,底层数据的遍历顺序是固定的,完全不会受排序影响:
// 先判断当前模型是不是排序后的包装模型 var rawListStore *gtk.ListStore if sortedModel, ok := MainListStore.(*gtk.TreeModelSort); ok { // 获取底层原始的ListStore underlyingModel, _ := sortedModel.GetModel() rawListStore, _ = underlyingModel.(*gtk.ListStore) } else { // 如果本来就是原始ListStore,直接用 rawListStore = MainListStore.(*gtk.ListStore) }
之后所有的遍历和更新操作都用这个rawListStore来做,就不会出现遍历位置乱跳的问题了。
再解决内存泄漏的问题
GOTK3的内存泄漏,90%以上的概率是在非主线程操作GTK对象导致的!GTK是严格单线程模型,所有UI相关的操作(包括ListStore的读写、TreeIter的操作)必须在Gtk主循环所在的主线程执行。如果你的这段遍历代码是在goroutine里跑的,那不仅会有线程安全问题,还会导致C侧的GTK对象引用计数混乱,进而出现内存泄漏。
解决方法是把所有UI操作通过glib.IdleAdd提交到主线程执行,同时补充必要的错误处理(你之前的代码忽略了很多错误,这也可能埋下隐患)。优化后的完整代码大概是这样:
// 把遍历更新的逻辑放到主线程执行 glib.IdleAdd(func() bool { // 先拿到原始的ListStore var rawListStore *gtk.ListStore if sortedModel, ok := MainListStore.(*gtk.TreeModelSort); ok { underlyingModel, err := sortedModel.GetModel() if err != nil { return false // 出错就终止 } var ok bool rawListStore, ok = underlyingModel.(*gtk.ListStore) if !ok { return false } } else { var ok bool rawListStore, ok = MainListStore.(*gtk.ListStore) if !ok { return false } } // 用IterNext遍历原始ListStore treeIter, iterOk := rawListStore.GetIterFirst() for iterOk { // 读取索引值,增加错误处理 value, err := rawListStore.GetValue(treeIter, MCOL_INPORT) if err != nil { iterOk = rawListStore.IterNext(treeIter) continue } goValue, err := value.GoValue() if err != nil { iterOk = rawListStore.IterNext(treeIter) continue } key, ok := goValue.(int) if !ok { iterOk = rawListStore.IterNext(treeIter) continue } // 读取Statmap,Mutex保护还是需要的(如果其他goroutine会修改它) Mutex.Lock() stat, ok := Statmap[key] if ok && len(stat) >= 3 { // 确保stat的长度足够,避免越界 // 更新ListStore,这里在主线程执行,完全符合GTK的线程要求 rawListStore.Set(treeIter, []int{MCOL_STATPIX, MCOL_STATINT, MCOL_SPDIN, MCOL_SPDOUT}, []interface{}{Pixes[stat[0]], stat[0], stat[1], stat[2]}) } Mutex.Unlock() // 移动到下一个条目 iterOk = rawListStore.IterNext(treeIter) } // 返回false表示这个任务只执行一次 return false })
额外的注意事项
- 永远不要在goroutine里直接操作GTK对象,哪怕是读操作也不行!所有和UI相关的操作都要通过
glib.IdleAdd或glib.TimeoutAdd放到主线程。 - 如果你必须遍历排序后的视图(比如只需要处理当前显示的条目),那绝对不要在遍历过程中修改影响排序的字段,否则必然会出现遍历异常。
- 可以用Go的
pprof工具排查内存泄漏:如果是C侧的GTK对象泄漏,基本就是线程问题导致的引用计数错误;如果是Go侧的对象泄漏,就检查是不是有循环引用或者长期持有不必要的对象。
内容来源于stack exchange

