前言
作为一名 哔哩哔哩的重度用户 ,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮💨。
修改前效果: 
刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。
修改后效果: 
由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析 ,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力。
场景
**横屏视频-半屏播放**\]的播放页面

## 开发环境
* 哔哩哔哩版本:`8.41.0`
* [MonkeyDev](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FAloneMonkey%2FMonkeyDev "https://github.com/AloneMonkey/MonkeyDev")
* `IDA Professional 9.0`
* 安装IDA插件:`patching`
* `Lookin`
## 目标
\[**横屏视频-半屏播放** \]增加**三倍速**播放
## 分析
* 从`Lookin`可以知道,播放速度组件叫做`VKSettingView.SelectContent`

* 从`Mach-O`文件导出的`VKSettingView.SelectContent`的`swift`文件可以知道,它的`model`叫做`VKSettingView.SelectModel`
```swift
class VKSettingView.SelectContent: VKSettingView.TitleBaseContent {
/* fields */
var model: VKSettingView.SelectModel ?
var lazy selecter: VKSettingView.VKSelectControl ?
}
```
* `VKSettingView.SelectModel`有个`items`属性,有可能是播放速度数组。我们从`IDA`依次查看方法的实现,找到`items`的`setter`方法叫做`sub_10D8ACB88`
```swift
import Foundation
class VKSettingView.SelectModel: VKSettingView.BaseModel {
/* fields */
var icon: String
var items: [String]
var reports: [String]
var selectedIndex: Int
var dynamicSelectedString: String?
var enableRepeatSelect: Swift.Bool
var selectChangeCallback: ((_:_:))?
var preferScrollPosition: VKSettingView.VKSelectControlScrollPosition
/* methods */
func sub_10d8aca08 // getter (instance)
func sub_10d8acac4 // setter (instance)
func sub_10d8acb20 // modify (instance)
func sub_10d8acb70 // getter (instance)
func sub_10d8acb88 // setter (instance)
func sub_10d8acb94 // modify (instance)
func sub_10d8acc48 // getter (instance)
func sub_10d8acd10 // setter (instance)
func sub_10d8acd68 // modify (instance)
func sub_10d8acf6c // getter (instance)
func sub_10d8acff8 // setter (instance)
func sub_10d8ad040 // modify (instance)
func sub_10d8ad138 // getter (instance)
func sub_10d8ad234 // setter (instance)
func sub_10d8ad2a0 // modify (instance)
func sub_10d8ad328 // getter (instance)
func sub_10d8ad3b4 // setter (instance)
func sub_10d8ad3fc // modify (instance)
}
```

* 我们在`Xcode`添加符号断点`sub_10D8ACB88`,看到底谁设置了`items`的值

* `sub_10D8ACB88`断点触发,我们打印参数的值,证明`items`确实是播放速度数组
```objc
(lldb) p (id)$x0
(_TtGCs23_ContiguousArrayStorageSS_$ *) 0x00000001179c8370
(lldb) expr -l Swift -- unsafeBitCast(0x00000001179c8370, to: Array.self)
([String]) $R4 = 6 values {
[0] = "0.5"
[1] = "0.75"
[2] = "1.0"
[3] = "1.25"
[4] = "1.5"
[5] = "2.0"
}
```
* 我们打印方法的调用堆栈,发现是`sub_10A993E14`修改了`items`的值
```objc
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x000000010de78b88 bili-universal`sub_10D8ACB88
frame #1: 0x000000010af5fea0 bili-universal`sub_10A993E14 + 140
frame #2: 0x000000010af5f15c bili-universal`sub_10A992320 + 3644
frame #3: 0x000000010af5db20 bili-universal`sub_10A9916B4 + 1132
frame #4: 0x000000010af6714c bili-universal`sub_10A99B130 + 28
frame #5: 0x000000010af6859c bili-universal`sub_10A99C1A0 + 1020
frame #6: 0x000000010af67128 bili-universal`sub_10A99B118 + 16
...
```
* 我们从`IDA`看下`sub_10A993E14`的伪代码实现
```objc
_QWORD *__fastcall sub_10A993E14(void *a1, id a2)
{
...
v3 = a2;
if ( a2 && (v4 = v2, v6 = type metadata accessor for SelectModel(0LL), (v7 = swift_dynamicCastClass(v3, v6)) != 0) )
{
v9 = (_QWORD *)v7;
v10 = sub_107C8B79C(&unk_116BB42E8, v8);
inited = swift_initStaticObject(v10, &unk_116E60370);
v12 = *(void (__fastcall **)(__int64))((swift_isaMask & *v9) + 0x1C0LL);
v13 = objc_retain(v3);
...
```
* 我们直接搜索`sub_10A993E14`的伪代码,看是否有直接调用`sub_10D8ACB88`,很遗憾并没有
* 我们添加`sub_10A993E14`符号断点,断点触发后打印方法的参数,发现`x1`的值是`_TtC13VKSettingView11SelectModel`,也就是`VKSettingView.SelectModel`
```objc
(lldb) p (id)$x0
(BAPIPlayersharedSettingItem *) 0x0000000282c0f880
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
```
* 我们打印`x1`(`VKSettingView.SelectModel`)(`0x0000000283b51790`)的`items`的值,发现是个**空数组**
```objc
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs19__EmptyArrayStorage *) 0x00000001dd92e978
(lldb) expr -l Swift -- unsafeBitCast(0x00000001dd92e978, to: Array.self)
([String]) $R2 = 0 values {}
```
* 我们在`sub_10A993E14`方法返回之前添加一个断点,看下`x1`(`VKSettingView.SelectModel`)(`0x0000000283b51790`)的`items`的值

```objc
(lldb) register read
General Purpose Registers:
x0 = 0x0000000283b51790
x1 = 0x00000002819eb700
x2 = 0x0000000000000003
...
x23 = 0x0000000283b51790
x24 = 0x0000000283b51790
x25 = 0x0000000116e17f28 (void *)0x00000001173e6b88: OBJC_METACLASS_$__TtC16BBUGCVideoDetail13VDUGCMoreBloc
x26 = 0x00000001142906d8 bili-universal`type_metadata_for_ToolCell + 784
x27 = 0x000000010a552534 bili-universal`sub_109F86534
x28 = 0x0000000116718000 "badge_control"
fp = 0x000000016f832610
lr = 0x000000010af5f15c bili-universal`sub_10A992320 + 3644
sp = 0x000000016f832510
pc = 0x000000010af5ffe4 bili-universal`sub_10A993E14 + 464
cpsr = 0x60000000
```
* 因为`x0`的值是`0x0000000283b51790`,所以打印`x0`的值,看到`x0`(`VKSettingView.SelectModel`)(`0x0000000283b51790`)的`items`有值了,就是播放速度数组,这也证明`sub_10A993E14`修改了`VKSettingView.SelectModel`的`items`的值。`x0`通常拿来存放函数的返回值。
```objc
(lldb) p (id)$x0
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs22__SwiftDeferredNSArray *) 0x0000000280743120 6 values
(lldb) po 0x0000000280743120
(
0.5,
0.75,
1.0,
1.25,
1.5,
2.0
)
```
* 我们将`sub_10A993E14`的伪代码,参数`a1`的类型是`BAPIPlayersharedSettingItem`,`a2`的类型是`VKSettingView.SelectModel`一起给`chatgpt`分析,`chatgpt`叫我们查看 `swift_initStaticObject` 的参数 `&unk_116E60370`的值是什么。
* 如果`chatgpt`的分析结果没用,我们就自己打断点,看是哪些汇编代码更改了`items`的值,再看汇编代码对应的伪代码是怎样的。
```objc
检查 inited = swift_initStaticObject(...) 对象
inited 很可能是 SelectModel 或其内部配置对象(例如某个静态配置结构体或 Swift 字典/数组)被初始化。你可以在反汇编中查看 swift_initStaticObject 的参数 &unk_116E60370 看看该静态对象是什么,它可能携带 items 的初始数据。若你在数据段或只读段中找到与 "items" 相关的字符串数组、常量字符串列表、NSStringPointer 等,那可能就是 items 的来源。
```
* 查看`&unk_116E60370`的值,发现是在数据段(`__data`)中

* 查看`&unk_116E60370`的值的`16`进制视图,发现它旁边的地址存放着播放速度,所以`&unk_116E60370`保存着播放速度数组

* 我们知道数据段(`__data`)存放着全局变量,所以播放速度数组应该是放在一个全局变量里面,类似:
```objc
var playbackRates = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]
```
## 说明
比如`0000000116E789B0`,保存的值是`0.75`
```objc
0000000116E789B0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
```
各个字节的解析如下,**特别是最后一个字节`E4`,代表要读取`4`个字节的数据,如果是`E3`代表要读取`3`个字节的数据**
```objc
30 : 0
2E : .
37 : 7
35 : 5
E4 : 读取四个字节的数据
```
## 越狱解决方案
* 修改下面地址存储的值
```objc
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
```
* 具体代码
```objc
/// 将速度写入到内存地址
/// - Parameters:
/// - dest_addr: 目标内存地址
/// - str: 速度字符串,比如"1.0"
static int write_rate_string_to_address(uintptr_t dest_addr, NSString *str) {
if (str == nil) {
return -1;
}
// UTF8 字符串
const char *utf8Str = [str UTF8String];
size_t strLength = strlen(utf8Str); // 字符数(不含 \0)
if (strLength > (NJ_RATE_BLOCK_SIZE - 1)) {
// 只能容纳前15字节 + 最后一字节用于 E0+strLength
strLength = NJ_RATE_BLOCK_SIZE - 1;
}
uint8_t block[NJ_RATE_BLOCK_SIZE];
memset(block, 0, NJ_RATE_BLOCK_SIZE);
// 前 strLength 字节写入字符串
memcpy(block, utf8Str, strLength);
// 最后一个字节写入:E0 + 长度
block[NJ_RATE_BLOCK_SIZE - 1] = 0xE0 + (uint8_t)strLength;
// 将 block 写到目标地址
memcpy((void *)dest_addr, block, NJ_RATE_BLOCK_SIZE);
return 0;
}
/// 将速度写入到内存地址
/// - Parameter baseAddress: 起始内存地址
static void write_rate_to_address(uintptr_t baseAddress) {
NSArray *playbackRates = @[@"0.5", @"1.0", @"1.25", @"1.5", @"2.0", @"3.0"];
NSInteger count = playbackRates.count;
for (NSInteger i = 0; i < count; i++) {
uintptr_t currentAddress = baseAddress + i * NJ_RATE_BLOCK_SIZE;
write_rate_string_to_address(currentAddress, playbackRates[i]);
}
}
// [横屏视频-半屏播放]的播放速度
static void changePlaybackRates_LandscapeVideo_HalfScreenPlayback() {
/*
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
*/
uintptr_t baseAddress = g_slide + 0x116E60390;
write_rate_to_address(baseAddress);
}
```
## 非越狱解决方案
修改`Mach-O`文件的汇编指令
### 目标
* 修改下面地址存储的值
```objc
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
```
### 示例
比如修改`0000000116E603A0`
```objc
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
```
* 鼠标点击`0000000116E603A0`
* `IDA`-\>`Edit`-\>`Patch program`-\>`Change byte`

* 显示`Patch Bytes`弹框

* 从`Origin value`:
* **30 2E 37 35** 00 00 00 00 00 00 00 00 00 00 00 **E4**
* 修改 Values 为:
* **31 2E 30 00** 00 00 00 00 00 00 00 00 00 00 00 **E3**
* 点击`OK`,真正修改
### 修改结果
* 当前的值:
```objc
0.5 → 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
0.75 → 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.0 → 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.25 → 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.5 → 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
2.0 → 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
```
* 新的播放速度对应的值:
```objc
0.5 → 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.0 → 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.25 → 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.5 → 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
2.0 → 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
3.0 → 33 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
```
* 全部修改完后
```objc
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603B0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603C0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603D0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
0000000116E603E0 33 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 3.0.............
```
### 保存
保存到`Mach-O文件`中
* `IDA`-\>`Edit`-\>`Patch program`-\>`Apply patches to input file`-\>`OK`


* 保存后,底部会显示`log`:
```objc
Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal
```

## 效果

## 代码
[BiliBiliMApp-无广告版哔哩哔哩](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FTouchFriend%2FBiliBiliMApp "https://github.com/TouchFriend/BiliBiliMApp")