Loading...
墨滴

anhoder

2021/05/14  阅读:88  主题:萌绿

musicfox 2.0——golang重写版

musicfox 2.0——golang重写版

musicfox 2.0终于完成了,没有太多的功能变更,主要使用golang进行重写,优化了用户体验(主要是Windows下)。

存在的问题与方案

musicfox最开始是用Dart写的,工作原理大致是主进程fork一个mpg123的子进程,主进程处理UI渲染、用户交互及网络请求等,然后通过进程间管道通信,来控制mpg123子进程的音乐播放、暂停以及播放器的音量大小等。

这种模式下,会存在以下这些问题:

  • 依赖mpg123,需要用户手动安装mpg123
  • mpg123只能播放mp3音乐,网易云无损音质是flac格式,无法播放
  • 进程间需要通过管道进行通信,Windows下不容易处理
  • Dart大多数三方包都是基于Flutter的,原生包不多
  • ...

为了解决这些问题,于是开始采用golang进行重写,优势在于:

  • golang支持更多平台的二进制编译
  • 拥有丰富的第三方包,例如音频播放的beep、TUI渲染的 bubbletea、本地数据库的bolt等等
  • 不需要借助其他进程,所以不存在进程通信
  • 支持mp3、flac、wav等格式音频文件的解码播放
  • goroutine + chan

另外对我自己来说可以深入熟悉golang...

预览

安装

Mac

提供两种方式安装:

使用brew安装

brew tap anhoder/go-musicfox && brew install go-musicfox

如果你之前安装过musicfox,需要使用下列命令重新链接:

brew unlink musicfox && brew link --overwrite go-musicfox

直接下载

下载Mac可执行文件,在iTerm或Terminal中打开

Linux

身边没有Linux设备,而播放音乐依赖CGO,无法进行交叉编译,所以暂时没有可用的二进制文件

Windows

下载Windows可执行文件,在命令行中运行即可。

推荐使用Windows Terminal,UI及体验会好很多

快捷键

按键 作用 备注
h/H/LEFT
l/L/RIGHT
k/K/UP
j/J/DOWN
q/Q 退出
space 暂停/播放
[ 上一曲
] 下一曲
- 减小音量
= 加大音量
n/N/ENTER 进入选中的菜单
b/B/ESC 返回上级菜单
w/W 退出并退出登录
p 切换播放方式
P 心动模式(仅在歌单中时有效)
r/R 重新渲染UI Windows调整窗口大小后,没有事件触发,可以使用该方法手动重新渲染
, 喜欢当前播放歌曲
< 喜欢当前选中歌曲
. 当前播放歌曲移除出喜欢
> 当前选中歌曲移除出喜欢
/ 标记当前播放歌曲为不喜欢
? 标记当前选中歌曲为不喜欢

TODO

  • [x] 我的歌单
  • [x] 每日推荐歌曲
  • [x] 每日推荐歌单
  • [x] 私人FM
  • [x] 歌词显示
  • [x] 欢迎界面
  • [x] 搜索
    • [x] 按歌曲
    • [x] 按歌手
    • [x] 按歌词
    • [x] 按歌单
    • [x] 按专辑
    • [x] 按用户
  • [x] 排行榜
  • [x] 精选歌单
  • [x] 最新专辑
  • [x] 热门歌手
  • [x] 云盘
  • [x] 播放方式切换
  • [x] 喜欢/取消喜欢
  • [x] 心动模式/智能模式
  • [x] 音乐电台
  • [ ] 配置文件
  • [ ] 通知功能

使用go时遇到的问题

协程泄漏

在使用golang过程中遇到比较严重的问题就是协程泄漏。例如下面的代码:

package main

import (
 "fmt"
 "runtime"
)

func main() {
 c1 := make(chan struct{})
 for i := 0; i < 3; i++ {
  go test(c1)
  fmt.Println(runtime.NumGoroutine())
 }
}

func test(c chan<- struct{}) {
 c <- struct{}{}
}

执行后会输出:

2
3
4
5
6
7
8
9
10
11

可见,协程数一直在增加,这是因为没有消费者消费chan中的数据,所有子协程都阻塞在写chan的操作。

类似这种情况就会导致协程数不断增长,出现协程泄漏,内存占用不断上升。

如何尽量避免协程泄漏

  1. 确保chan都会有相应的消费者、生产者,且生产速度和消费速度差不多
  2. 如果需要往chan中压数据或读数据,但又不确定是否有消费者或生产者,可以使用select结构避免阻塞
  3. 使用goroutine + chan处理,避免创建大量协程,例如:
package main

import (
 "fmt"
 "runtime"
 "time"
)

func main() {
 var data [100]string
 // 获取data逻辑
 for _, v := range data {
  go test(v)
 }

 fmt.Println(runtime.NumGoroutine())
}

func test(s string) {
 // 假装业务逻辑
 time.Sleep(time.Millisecond)
}

输出为: 101,也就是新开了100个协程,如果数据更多时,会产生更多协程。这种情况可以考虑创建固定数量的若干个协程,然后通过chan将数据投递给子协程进行处理,避免大量协程占用内存,如:

package main

import (
 "fmt"
 "runtime"
 "time"
)

func main() {
 var data [100]string
 c := make(chan string)

 for i := 0; i < 5; i++ {
  go test(c)
 }

 for _, v := range data {
  c <- v
 }

 fmt.Println(runtime.NumGoroutine())
}

func test(c <-chan string) {
 for _ = range c {
        // 业务逻辑
  time.Sleep(time.Millisecond)
 }
}

协程泄漏排查

  1. runtime.NumGoroutine()查看协程数
  2. 使用gops排查,可以查看当前协程数、调用栈等

anhoder

2021/05/14  阅读:88  主题:萌绿

作者介绍

anhoder