让 iPhone 上显示学期周数(三) — — 定位通知中心目标方法

这篇文章继续定位方法的工作,本次的目标是通知中心显示农历处。

反编译二进制文件

这次我们的目标位于通知中心,它是 SpringBoard 的一部分。SpringBoard 负责管理 iOS 设备的主屏,它本质上是一个 App,而非 framework,不过处理方法大同小异,首先还是找到它的二进制文件。

在设备的 /System/Library/CoreServices/SpringBoard.app/ 目录下找到二进制文件 SpringBoard,将它拷贝至 Mac。下面采取一种新的办法 — — 反编译二进制文件。

打开 Hopper Disassembler,将 SpringBoard 文件拖拽进窗口,Hopper 会对其中的方法进行分析,我们可以直接对方法进行搜索。根据经验,这次我们先使用 DateLabel 作为关键词。

可以马上发现,搜索结果中 SBTodayTableHeaderView 这个类中有多个含有 lunar 字样的方法,高度可疑,所以我们把关键词换成 SBTodayTableHeaderView,直接来看这个类的所有方法。

容易发现 lunarDateHeaderString 这个方法的命名与目标一致,初步确定它作为目标方法。用 class-dump 提取出头文件,找到该方法的定义 — (id)lunarDateHeaderString; 准备进行测试。

这里之所以不直接用 class-dump,是遵从我开发的实际过程。class-dump 出头文件后,我在含有 NotificationCenter 关键词的类中尝试许久都没有奏效,最终采用在 Hopper 中直接搜索方法名的手段才成功。当然使用 grep 命令直接在头文件中搜索方法名也是可行的。除此之外,反编译方法最大的优势在于可以分析汇编代码,找出方法调用流程;还可以得到方法在内存中的地址,进而使用 lldb 进行调试。不过在这个项目中暂时就不涉及了。

使用 Cycript 测试目标方法

谁也不能保证目标方法一次就能找对,所以相比于每找一次就编写插件、编译、安装来测试,还有一种更简单的方法 — — 使用 Cycript

Cycript 是由 Cydia 的作者 Saurik 开发的一款脚本语言,你可以在 Cydia 下载它。它可以以 JavaScript-like 的方式动态执行 Objective-C 语句,用于测试可以大幅提高效率。

SSH 到设备上后,使用如下命令让 Cycript 动态注入 SpringBoard 的进程。

~ cycript -p "SpringBoard" 
cy#

在 Cycript 中,可以使用 choose 命令获取当前进程中指定的类的实例。我们之前已经确定目标方法在类 SBTodayTableHeaderView 中,所以命令如下。

cy# choose(SBTodayTableHeaderView) 
[#"<SBTodayTableHeaderView: 0x13a148c60; frame = (0 0; 375 103);opaque = NO; autoresize = W+BM; layer = <CALayer: 0x1361492a0>>"]

果然找到了它(实际上返回了一个列表,不过当前进程中只有一个该类实例)并且返回了其内存地址,我们使用 # 操作符获取这个对象,然后直接调用目标方法,看看返回值是什么。

cy# headerView = #0x13a148c60 
#"<SBTodayTableHeaderView: 0x13a148c60; frame = (0 0; 375 103); opaque = NO; autoresize = W+BM; layer = <CALayer: 0x1361492a0>>"
cy# [headerView lunarDateHeaderString]
@"\xe4\xb8\x99\xe7\x94\xb3\xe5\xb9\xb4\xe4\xb8\x83\xe6\x9c\x88\xe5\x88\x9d\xe5\x9b\x9b"

返回的是一串中文字符,不过由于编码的原因无法正确显示。我们简单粗暴地请 Python2 的 print 关键字帮忙处理编码问题。

~ python 
Python 2.7.10 (default, Oct 23 2015, 19:19:21) [GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin Type "help", "copyright", "credits" or "license" for more information.
>>> lunarText = '\xe4\xb8\x99\xe7\x94\xb3\xe5\xb9\xb4\xe4\xb8\x83\xe6\x9c\x88\xe5\x88\x9d\xe5\x9b\x9b'
>>> print lunarText
丙申年七月初四
>>>

果然就是你!接下来只要编写插件修改这个方法的返回值即可。

非中文环境的处理

注意到 SBTodayTableHeaderView 类中还有一个 — (_Bool)showsLunarDate; 方法似与我们的插件有关。经过测试发现,在非中文环境下该方法返回 false,不会显示农历日期(想想这是显然的)。所以我们还需要再 hook 这个方法,让它恒返回 true,这样英文环境下也能正常显示了。

%hook SBTodayTableHeaderView
- (_Bool)showsLunarDate {
return true;
}
- (id)lunarDateHeaderString {
return @"WeekCount";
}
%end

效果如图。至此 WeekCount 开发中与 UI 修改相关的环节已经完全打通,下面的工作是计算出当前的周数,并且把结果放置在对应位置。