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

前言

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

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

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

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

系列回顾

场景说明

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

开发环境

分析

1. 播放速度组件定位

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

复制代码
VKSettingView.TabContent

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

swift 复制代码
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

swift 复制代码
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 打印参数内容可以确认:

    objc 复制代码
    (0.5, 0.75, 1.0, 1.25, 1.5, 2.0)

    该数组正是当前 UI 中显示的播放速度列表。

LLDB:

objc 复制代码
(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 的赋值逻辑来自:

css 复制代码
-[BBPlayerPlaySettingWidgetV2 playbackRate:]

说明 播放速度数组是在该方法中被构造并传入 TabModel 的

调用堆栈:

objc 复制代码
(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:] 的伪代码,可以清晰看到播放速度数组是通过如下方式写死创建的:

objc 复制代码
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");

至此可以确认:竖屏全屏场景下的播放速度选项并非动态配置,而是硬编码在该方法中

伪代码:

objc 复制代码
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 实现代码

objc 复制代码
%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 方案。

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

代码

BiliBiliMApp-无广告版哔哩哔哩

相关推荐
战族狼魂33 分钟前
XCode 发起视频 和 收到视频通话邀请实现双语功能 中文和俄语
swift
Sim14802 小时前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
UXbot4 小时前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式
Digitally4 小时前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手7 小时前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero7 小时前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
ii_best7 小时前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器
用户223586218201 天前
WebKit WebPage API 的引入尝试与自研实现
ios
啦啦啦!1 天前
ChatGPT和Gemini的接入和封装
人工智能·ios·chatgpt
报错小能手1 天前
ios开发方向——swift并发进阶核心 async/await 详解
开发语言·ios·swift