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-无广告版哔哩哔哩

相关推荐
2501_916008895 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
忆江南5 小时前
Flutter深度全解析
ios
山水域5 小时前
Swift 6 严格并发检查:@Sendable 与 Actor 隔离的深度解析
ios
楚轩努力变强6 小时前
iOS 自动化环境配置指南 (Appium + WebDriverAgent)
javascript·学习·macos·ios·appium·自动化
游戏开发爱好者81 天前
日常开发与测试的 App 测试方法、查看设备状态、实时日志、应用数据
android·ios·小程序·https·uni-app·iphone·webview
黑码哥1 天前
ViewHolder设计模式深度剖析:iOS开发者掌握Android列表性能优化的实战指南
android·ios·性能优化·跨平台开发·viewholder
2501_915106321 天前
app 上架过程,安装包准备、证书与描述文件管理、安装测试、上传
android·ios·小程序·https·uni-app·iphone·webview
2501_915106321 天前
使用 Sniffmaster TCP 抓包和 Wireshark 网络分析
网络协议·tcp/ip·ios·小程序·uni-app·wireshark·iphone
熊猫钓鱼>_>1 天前
移动端开发技术选型报告:三足鼎立时代的开发者指南(2026年2月)
android·人工智能·ios·app·鸿蒙·cpu·移动端
徐同保2 天前
通过ip访问nginx的服务时,被第一个server重定向了,通过设置default_server解决这个问题
ios·iphone