记一次桌面歌词插件开发

晚上没睡觉 过几天随缘更新创作心路历程

设置里面的那个选项目前还没有实现,所以就是可以理解为开箱即用没有设置

已知问题:由于网易云不知道干了什么会导致启动的时候把第二首歌的歌词载入缓存导致识别错误,虽然通过代码已经尽我所能的避免这个问题了但是如果遇到了这个问题的话,切一下歌曲吧,我不知道怎么修太菜了

附上屏幕截图一张

// 就知道没人用传错包了都没人发现
wiki.qaq.NMLrc_1.1_iphoneos-arm.deb (173.8 KB)

记一次桌面歌词插件开发

这个坑,其实挺早就想着是不是改做了,今天来记录一下实现的方法。首先是思路。我们hook网易设置歌词的方法,推送数据给SpringBoard,再在SpringBoard上面绘制歌词窗口。

那么首先是获取ipa用于分析。使用Frida-iOS-dump可以轻松的提取砸壳完成的网易云音乐。由于我们开发的插件并不需要很多调试内容,选择MonkeyDev创建logos语法的hook项目即可。创建MonkeyApp调试App会带来很多额外的问题,比如不能登录之类的App自身限制。

砸壳以后,找到位于Payload内的Executable文件放入Hopper开始分析。与此同时,我们可以使用FLEX插件来仔细看看网易云歌词的界面实现。盲猜是一个TableView。FLEXing插件位于BigBoss源内,通过长按状态栏实现调用。点击Select的小箭头再点击歌词界面,可以看到用于显示歌词的UILabel。打开Views选项,便能看到一个叫做NMLyricCell的Class。这个名字是我们在Hopper/IDA中需要重点关注的对象。

打开Hopper,检查这个对象。我们发现了一个方法,名字叫做setLyric。这个名字既然在这里,那么很大的可能开发人员在其他地方也会使用这个名字。比如说在更新歌词的时候会Call一个Wrapper,他的名字极有可能也是setLyric。不妨搜索看看?

果然,这是一个通用的方法名。

这里有两个地方引起了我的注意,第一个是类名字:NMLyricObject,第二个还是类的名字:NMPlayerManager。第二个类名引起我的注意是因为一个叫做setLyricsArray的方法。很有可能,setLyricsArray是NMPlayerManager类自动生成的Setter。不过这个无所谓了,反正都是要hook哒!

接下来我猜在播放器这个shared类里面会有一个歌词高亮的wrapper,比如setLyricIndex之类的。在搜索“NMPlayerManager index”之后,果然找到了一个方法:

-[NMPlayerManager setHighlightedLyricIndex:]

同时还有 -[NMPlayerManager highlightedLyricIndex]。这说明HighlightedLyricIndex很有可能是NMPlayerManager的一个属性。

于是我们有了思路:

  • Hook setHighlightedLyricIndex 来获取应该显示的歌词的位置
  • Hook NMPlayerManager 任意方法来缓存一个self指针
  • 通过obj-C runtime和缓存的self获取歌词内容
  • 通过特定方法将数据发送给SpringBoard
  • 在SpringBoard上绘制窗口更新歌词
  • 当歌词超过6秒没有更新,隐藏绘制

上代码!

这里,由于网易云初始化的缓存包含两首歌的歌词,就很**。通过上述代码可以规避这个问题,但是觉得很不安全。如果有更好的点子欢迎留言。其中注意,setLyricsArray会设置一个NMLyricObject的Array,并非直接储存NSString。这样一来我们需要使用obj-C runtime去取这个歌词的内容。你可以缓存这个内容,也可以通过NMPlayerManager的属性取出这个Array。

// 导出水果私有方法 objc_retainAutoreleaseReturnValue
// 真的不知道为什么不是 objc_retainAutoReleaseReturnValue
OBJC_EXPORT id objc_retainAutoreleaseReturnValue(id obj) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0);

// 获取歌词数组 
    SEL _sel1 = NSSelectorFromString(@"lyricsArray");
    id _voucher1 = objc_msgSend(manager, _sel1); // NMPlayerManager*
    NSArray *lrcOA = objc_retainAutoreleaseReturnValue(_voucher1);
    id targetObject = [lrcOA objectAtIndex: index];
// 获取原歌词
    SEL _sel2 = NSSelectorFromString(@"lyric");
    id _voucher2 = objc_msgSend(targetObject, _sel2);
    NSString *_lrc = objc_retainAutoreleaseReturnValue(_voucher2);
// 获取翻译词
    SEL _sel3 = NSSelectorFromString(@"translatedLyric");
    id _voucher3 = objc_msgSend(targetObject, _sel3);
    NSString *_lrctrnd = objc_retainAutoreleaseReturnValue(_voucher3);

接下来就需要把歌词数据发送给SpringBoard。由于进程间通讯非常麻烦,UIPasteBoard不知道为啥被block了获取不到数据,我决定使用GCDWebSocket进行操作。:man_shrugging:

这个就挺好玩,去SpringBoard开一个服务器😂

不过在这之前,我们先把这个桌面歌词的绘制处理一下。

接下来开端口,处理更新。划重点,UI更新必须在主线程,不然会崩掉SpringBoard。

那我们respond写什么呢?嘿嘿

        return [GCDWebServerDataResponse responseWithHTML:@"花Q"];

至此,插件编写已经完成了。如果遇到undefinedSymbols的话去添加一下框架,也可以动态查找。下面这张图是手动链接需要的Framework,有一些是GCDWebServer需要使用的。

项目地址: https://lab.qaq.wiki/Lakr233/ilrcoverlay.git

此致,全体起立!

2019年冬 -> 2020年春
https://www.qaq.wiki/?p=621

4 Likes

那么iPad Pro去哪里领呢

1 Like

这个插件让我想起了远古时期的iPad多窗口插件quasar……

1 Like

你已经是一个成熟的论坛会员了,要学会自己缓解尴尬。

1 Like

起立

1 Like

老师好,老师再见

1 Like

我的歌词悬浮窗是时候发帖了哈哈

1 Like

我发现qq音乐也不知道干了什么启动时也会把第二首歌的歌词载入,*了狗了

1 Like

那有没有兴趣pr一下呢 qq我搞不动了他歌词是字符级的。。

它每个句子有时间的,获取当前播放进度判断一下应该放哪个歌词就好了。另外,应该怎么PR呢,没有找到PR的地方。。

我给推送到GitHub了 你pr完了我去手动合并到gitlab跑CI

提交了~实现的不好,应该还可以改进改进

最近有一些用户疯狂催更 啥窗口拖动 触控穿透 等我高考完了我会补上的!

CI已经更新~

大佬,你的悬浮歌词那里可以下载?