iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景
前言
作为哔哩哔哩的重度用户,我一直期待官方支持 3 倍速播放,但该功能迟迟未上线。于是,我利用 iOS 逆向工程知识,为 B 站 App 添加这一功能。
修改前:最高仅支持 2.0 倍速。
修改后:成功添加 3.0 倍速选项
本系列分为多篇,本文聚焦 竖屏视频·全屏播放 场景下的 3 倍速实现。
系列回顾:
场景说明
本文分析的具体场景为:竖屏视频全屏播放。
开发环境
分析
1. 播放速度组件定位
通过 Lookin 分析 UI 层级可以发现,播放速度面板对应的视图组件为:
VKSettingView.TabContent
该组件内部持有一个 VKSettingView.TabModel,用于描述播放速度相关的数据模型。
import Foundation
class VKSettingView.TabContent: VKSettingView.BaseContent {
/* fields */
var model: VKSettingView.TabModel ?
var lazy selecter: VKSettingView.VKSelectControl ?
}
2. TabModel 结构分析
从 Mach-O 中导出的 Swift 文件可以确认,VKSettingView.TabModel 中包含一个 items 属性,其类型为 [String],极有可能即为 播放速度数组。
进一步在 IDA 中查看该类的方法实现,可以发现 items 对应的 setter 方法为:sub_10D8B5FA8
class VKSettingView.TabModel: VKSettingView.BaseModel {
/* fields */
var icon: String
var itemsSize: __C.CGSize
var items: [String]
var selectedIndex: Int
var dynamicSelectedString: String?
var enableRepeatSelect: Swift.Bool
var selectChangeCallback: ((_:))?
/* methods */
func sub_10d8b5bc4 // getter (instance)
func sub_10d8b5c80 // setter (instance)
func sub_10d8b5cdc // modify (instance)
func sub_10d8b5d64 // getter (instance)
func sub_10d8b5dfc // setter (instance)
func sub_10d8b5e50 // modify (instance)
func sub_10d8b5efc // getter (instance)
func sub_10d8b5fa8 // setter (instance)
func sub_10d8b5ff8 // modify (instance)
...
}
3. items 赋值来源追踪
- 尝试直接对
sub_10D8B5FA8添加符号断点并未触发,因此推断该属性可能通过 Objective-C Runtime 间接调用。
- 结合
Swift / Objective-C混编特性,对-[TabModel setItems:]添加断点后成功捕获调用。
-
通过 LLDB 打印参数内容可以确认:
(0.5, 0.75, 1.0, 1.25, 1.5, 2.0)该数组正是当前
UI中显示的播放速度列表。
LLDB:
(lldb) register read x2
x2 = 0x000000028044efc0
(lldb) p (id)0x000000028044efc0
(__NSArrayI *) 0x000000028044efc0 @"6 elements"
(lldb) po (id)0x000000028044efc0
<__NSArrayI 0x28044efc0>(
0.5,
0.75,
1.0,
1.25,
1.5,
2.0
)
4. 调用栈分析
查看调用栈可以发现,items 的赋值逻辑来自:
-[BBPlayerPlaySettingWidgetV2 playbackRate:]
说明 播放速度数组是在该方法中被构造并传入 TabModel 的。
调用堆栈:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 10.1
* frame #0: 0x000000011084df44 bili-universal`-[TabModel setItems:]
frame #1: 0x0000000128d2ba78 BiliBiliTweak.dylib`_logos_method$App$VKSettingViewTabModel$setItems$(self=0x00000002821d6220, _cmd="setItems:", items=6 elements) at NJDetailPlayerAd.xm:418:5
frame #2: 0x000000011062db8c bili-universal`-[BBPlayerPlaySettingWidgetV2 playbackRate:] + 488
frame #3: 0x000000011062c17c bili-universal`sub_10D69410C + 112
frame #4: 0x000000018ff78ce0 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 16
frame #5: 0x000000018fe7cc0c CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 192
...
5. 伪代码验证
在 IDA 中分析 -[BBPlayerPlaySettingWidgetV2 playbackRate:] 的伪代码,可以清晰看到播放速度数组是通过如下方式写死创建的:
v25[0] = CFSTR("0.5");
v25[1] = CFSTR("0.75");
v25[2] = CFSTR("1.0");
v25[3] = CFSTR("1.25");
v25[4] = CFSTR("1.5");
v25[5] = CFSTR("2.0");
至此可以确认:竖屏全屏场景下的播放速度选项并非动态配置,而是硬编码在该方法中。
伪代码:
id __cdecl -[BBPlayerPlaySettingWidgetV2 playbackRate:](BBPlayerPlaySettingWidgetV2 *self, SEL a2, id a3)
{
...
v4 = objc_retain(a3);
v5 = objc_retainAutoreleasedReturnValue(-[BBPlayerObject context](self, "context"));
v6 = objc_retainAutoreleasedReturnValue(-[BBPlayerContext status](v5, "status"));
v7 = -[BBPlayerStatus isVerticalScreen](v6, "isVerticalScreen");
objc_release(v6);
objc_release(v5);
if ( v7 )
{
v25[0] = CFSTR("0.5");
v25[1] = CFSTR("0.75");
v25[2] = CFSTR("1.0");
v25[3] = CFSTR("1.25");
v25[4] = CFSTR("1.5");
v25[5] = CFSTR("2.0");
v8 = objc_retainAutoreleasedReturnValue(+[NSArray arrayWithObjects:count:](&OBJC_CLASS___NSArray, "arrayWithObjects:count:", v25, 6LL));
v21 = 0LL;
v22 = &v21;
...
通用解决方案
由于播放速度数组是通过 +[NSArray arrayWithObjects:count:] 构造的,因此可以采用 全局 Hook 的方式,对该方法进行拦截与替换。
核心思路如下:
- 判断
count == 6 - 校验原始数组内容是否与默认倍速数组一致
- 在满足条件时返回包含
3.0的新数组
Hook 实现代码
%hook NSArray
+ (instancetype)arrayWithObjects:(id *)objects count:(NSUInteger)cnt {
if (cnt != 6) {
return %orig;
}
NSArray *origArr = %orig(objects, cnt);
// 用 __autoreleasing 修饰数组元素
__autoreleasing id oldRates[] = {
@"0.5",
@"0.75",
@"1.0",
@"1.25",
@"1.5",
@"2.0"
};
NSUInteger oldRatesCount = sizeof(oldRates) / sizeof(oldRates[0]);
// 传数组名即可,数组名会退化为指针类型 __autoreleasing id *
NSArray *oldRatesArr = %orig(oldRates, oldRatesCount);
if (cnt == 6 && [origArr isEqualToArray:oldRatesArr]) {
__autoreleasing id newRates[] = {
@"0.5",
@"1.0",
@"1.25",
@"1.5",
@"2.0",
@"3.0"
};
NSUInteger newRatesCount = sizeof(newRates) / sizeof(newRates[0]);
NSArray *newRatesArr = %orig(newRates, newRatesCount);
return newRatesArr;
}
return origArr;
}
%end
最终效果
竖屏视频全屏播放场景下,播放速度列表成功新增 3.0 倍速,且不影响其他播放场景与现有功能逻辑。
总结
本文通过 UI 分析、调用栈追踪与伪代码验证,完整定位了 竖屏视频全屏播放场景 下播放速度数组的生成位置,并给出了一个 稳定、通用且侵入性较低 的 Hook 方案。
该思路同样适用于其他存在硬编码配置的功能修改场景。