You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fyne应用前台刷新时内存持续上涨,如何实现内存稳定?

问题

我用Go语言基于Fyne开发的应用,运行时会把日志输出到MultiLineEntry组件里。当前台界面保持显示(刷新)时,内存占用持续增长,但最小化到后台运行时内存能稳定住。我试过限制日志条目数量、清空绑定的数据,甚至把整个appendLogs函数注释掉,都没法阻止内存增长,想找到原因并让前台运行时内存稳定。

应用主函数

var conf = &Conf{}

func main() {
    go http.ListenAndServe("0.0.0.0:6060", nil)
    a := app.New()
    a.Settings().SetTheme(&myTheme{})
    w := a.NewWindow("forward")

    tab := container.NewAppTabs(
        container.NewTabItem("data1", WeatherScreen()),
        container.NewTabItem("data2", TossScreen()),
        //container.NewTabItem("per", PreferenceScreen(w)),
        container.NewTabItem("about", widget.NewLabel("Version:1.0")),
    )
    //tab.SetTabLocation(container.TabLocationLeading)
    rootVBox := container.NewVBox(tab, CreateLogs())

    w.SetContent(rootVBox)

    w.Resize(fyne.NewSize(700, 364))
    w.SetMaster()
    w.SetFixedSize(true)
    resource, _ := fyne.LoadResourceFromPath("Graphicloads-Polygon-Next-2.ico")
    a.SetIcon(resource)
    if desk, ok := a.(desktop.App); ok {
        m := fyne.NewMenu("forward",
            fyne.NewMenuItem("show", func() {
                w.Show()
            },
            ),
        )
        //desk.SetSystemTrayIcon(resource)
        desk.SetSystemTrayMenu(m)
    }
    w.SetIcon(resource)
    w.SetCloseIntercept(func() {
        //hide
        w.Hide()
    })
    w.CenterOnScreen()
    InitData()
    //longIp := float32(80)
    //shortPort := float32(50)
    conf = loadConfig()
    w.Show()
    a.Run()

}

日志处理函数

var logFlag = make(chan string, 10)
var cour = 1
var text = binding.NewString()
func CreateLogs() fyne.CanvasObject {
    logs := widget.NewEntryWithData(text)
    logs.MultiLine = true
    logs.Validator = nil
    logs.Resize(fyne.NewSize(688, 230))
        //update text
    go updateLogs(logFlag, logs)
    contain := container.NewWithoutLayout(logs)
    return contain
}
func updateLogs(l chan string, logs *widget.Entry) {
    for {
        if d, ok := <-l; ok {
            t, _ := text.Get()
            text.Set(t + d)
            logs.CursorRow = cour
            runtime.GC()
        }
    }
}
func appendLogs(name string, format string, a ...any) {
    now := time.Now()
    year, month, day := now.Date()
    str := fmt.Sprintf(format, a...)

    y := strconv.Itoa(year)
    var m string
    if int(month) >= 10 {
        m = strconv.Itoa(int(month))
    } else {
        m = "0" + strconv.Itoa(int(month))
    }
    var d string
    if day >= 10 {
        d = strconv.Itoa(day)
    } else {
        d = "0" + strconv.Itoa(day)
    }

    dateHeader := y + "/" + m + "/" + d + " "

    hour, min, sec := now.Clock()
    var h string
    if hour >= 10 {
        h = strconv.Itoa(hour)
    } else {
        h = "0" + strconv.Itoa(hour)
    }
    var mi string
    if min >= 10 {
        mi = strconv.Itoa(min)
    } else {
        mi = "0" + strconv.Itoa(min)
    }
    var s string
    if sec >= 10 {
        s = strconv.Itoa(sec)
    } else {
        s = "0" + strconv.Itoa(sec)
    }
    timeHeader := h + ":" + mi + ":" + s + " "
    header := dateHeader + timeHeader + name + " "
    str = header + str + "\n"
    cour++
    if cour > 50 {
        runtime.GC()
        cour = 1
        text.Set("")
    }
    logFlag <- str

}

问题分析与解决方案

核心原因

Fyne的MultiLineEntry是为用户输入场景设计的,前台显示时会维护渲染相关的内部缓存(比如行布局信息、文本渲染对象),即使清空绑定字符串,这些缓存也可能未被正确回收;此外手动调用runtime.GC()会打乱Go的自动GC调度,反而导致内存回收延迟。

具体修复措施

  1. 替换为更适合的日志展示组件
    放弃MultiLineEntry,改用Label+滚动容器的组合,更适配纯文本展示场景,减少渲染缓存残留:

    func CreateLogs() fyne.CanvasObject {
        logLabel := widget.NewLabel("")
        logLabel.Wrapping = fyne.TextWrapWord
        scroll := container.NewVScroll(logLabel)
        scroll.Resize(fyne.NewSize(688, 230))
        
        go func() {
            for d := range logFlag {
                current := logLabel.Text
                if len(current) > 0 {
                    logLabel.SetText(current + d)
                } else {
                    logLabel.SetText(d)
                }
                // 自动滚动到底部
                scroll.ScrollToBottom()
                // 限制日志行数,超过则截断
                lines := strings.Split(logLabel.Text, "\n")
                if len(lines) > 50 {
                    logLabel.SetText(strings.Join(lines[len(lines)-50:], "\n"))
                }
            }
        }()
        return scroll
    }
    
  2. 移除手动runtime.GC()调用
    Go的GC会自动根据内存压力调度,手动调用会破坏其优化策略,删除updateLogs和appendLogs中的runtime.GC()语句。

  3. 若坚持使用Entry,强制刷新清理缓存
    清空绑定字符串后,调用Refresh()强制组件更新,释放渲染层缓存:

    if cour > 50 {
        cour = 1
        text.Set("")
        logs.Refresh() // 强制刷新组件,清理内部缓存
    }
    
  4. 禁用Tab组件的内容缓存
    Fyne的AppTabs默认会缓存所有Tab内容,切换后未显示的Tab组件仍占用内存,可禁用缓存:

    tab := container.NewAppTabs(...)
    tab.SetCacheTab(false) // 切换Tab时销毁非当前Tab的内容
    
  5. 优化日志字符串拼接
    原代码的字符串拼接会产生大量临时对象,改用strings.Builder减少内存分配:

    func updateLogs(l chan string, logs *widget.Entry) {
        var builder strings.Builder
        for {
            if d, ok := <-l; ok {
                builder.WriteString(d)
                text.Set(builder.String())
                logs.CursorRow = cour
                // 按字符数限制日志长度
                if builder.Len() > 10000 {
                    builder.Reset()
                    text.Set("")
                    logs.Refresh()
                    cour = 1
                }
            }
        }
    }
    

验证方法

修改后运行应用,用pprof分析内存热点:

go tool pprof http://localhost:6060/debug/pprof/heap

查看内存分配情况,确认是否还有持续增长的对象。

内容的提问来源于stack exchange,提问作者MingleHeart -H

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 17:11:11