普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月18日首页

iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景

作者 TouchWorld
2025年12月17日 07:49

Xnip2025-12-16_11-22-40.jpg

前言

作为哔哩哔哩的重度用户,我一直期待官方支持 3 倍速播放,但该功能迟迟未上线。于是,我利用 iOS 逆向工程知识,为 B 站 App 添加这一功能。

修改前:最高仅支持 2.0 倍速。

Screenshot 2025-12-11 at 07.26.05.png

修改后:成功添加 3.0 倍速选项

Screenshot 2025-12-11 at 07.22.57.png

本系列分为多篇,本文聚焦 竖屏视频·全屏播放 场景下的 3 倍速实现。

系列回顾

场景说明

本文分析的具体场景为:竖屏视频全屏播放

499416D4-1488-4417-89CD-E42861795807.png

开发环境

  • 哔哩哔哩版本:8.41.0

  • 逆向框架:MonkeyDev

  • 反汇编工具:IDA Professional 9.0

  • IDA插件:patching

  • UI 调试工具:Lookin

分析

1. 播放速度组件定位

通过 Lookin 分析 UI 层级可以发现,播放速度面板对应的视图组件为:

VKSettingView.TabContent

该组件内部持有一个 VKSettingView.TabModel,用于描述播放速度相关的数据模型。

157920C7-792E-4FEC-AF23-1AA783CBC33D.png

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)
...
}

41E4F621-7F23-4445-A24D-0347CB125655.png

3. items 赋值来源追踪

  • 尝试直接对 sub_10D8B5FA8 添加符号断点并未触发,因此推断该属性可能通过 Objective-C Runtime 间接调用。

818F4D98-BD7D-4C7F-A134-A566E41E6D99.png

  • 结合 Swift / Objective-C 混编特性,对 -[TabModel setItems:] 添加断点后成功捕获调用。

C1BA5A52-C023-45FE-8436-EB592F188CF1.png

  • 通过 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 的方式,对该方法进行拦截与替换。

核心思路如下:

  1. 判断 count == 6
  2. 校验原始数组内容是否与默认倍速数组一致
  3. 在满足条件时返回包含 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 倍速,且不影响其他播放场景与现有功能逻辑。

Screenshot 2025-12-16 at 10.18.43.png

总结

本文通过 UI 分析、调用栈追踪与伪代码验证,完整定位了 竖屏视频全屏播放场景 下播放速度数组的生成位置,并给出了一个 稳定、通用且侵入性较低Hook 方案。

该思路同样适用于其他存在硬编码配置的功能修改场景。

代码

BiliBiliMApp-无广告版哔哩哔哩

❌
❌