阅读视图

发现新文章,点击刷新页面。

带可可学数学

可可三年级,前段老师说她数学成绩不好,需要在家加强一下。

这段时间我每天晚上给她讲一点点数学,都是课本上的内容,然后我再稍稍发挥一下。几次之后,我发现最大的问题是她觉得数学很无聊。

她似乎比较抗拒学新的知识,更喜欢用熟悉的方法。去年我发现她计算能力有问题,每天给她做加减法练习,总算不再用更早年我教她的 +1 法算加法了:即计算 7+8 的时候,算 8 次 +1 ,也就是数数。二年级学了乘法,乘法表也背了,但现在做应用题,本该用乘法的场合,她还是习惯连续算加法,一旦乘数太大就会出错。要用除法的时候就更混乱了,并不是用减法,而是靠猜测来试。大脑里完全没有建立乘除的概念。乘法表更像是独立的有背诵任务的诗词,还没古诗那么有趣。

我说:数学其实是这个世界上最有趣的东西。

她说:为什么呢?

我说:这个世界上有趣的东西很多,但数学是性价比最高的。一本数学书很便宜,但可以让你读很久。从中发现有那么多有趣的事实。原来解决不了的问题,知道方法突然就明白了。如果是自己找到的方法,就更让人兴奋了。

她说:我还是觉得数学没意思。

我说:慢慢来,不着急。首先不要排斥它。学数学其实不需要硬背那么多东西,我小时候最不喜欢背书了,所以才喜欢数学的。因为数学是最不需要背的,只要你从原理出发,一步步理解,最后什么问题都能解决,只是速度慢一点。多练习就快了。那些需要记住的知识,用的多就自动记住了,不需要专门背。


还好她不排斥我给她讲数学。前段还给我说,为什么只有你给我讲数学我才懂呢?

就这么,我每天(半个小时左右)给她讲一点点。我觉得其实也讲不了什么,聊胜于无。有一点我觉得还不错,她有数学作业不会做,会主动来问,不需要等我检查作业。一点拨就懂了,但第二天又会有新问题,依旧靠自己解决不了。不过,知道自己不懂算是个好的开始吧?

昨天晚上,她说今天学的直线、射线和线段。这个好简单,之前的都好难。

我说,我出道题目吧。在纸上画五个点,你看看一共可以连出多少条线段。

她说,我去拿张纸来。

画了半天,数错了。可可说,我脑子好乱啊。

我说,我来教你方法。我们先把你画的点标上数字编号,从 1 标到 5 。然后你每次连一条线段,就把两端的数字记在图案下方,顺着写整齐。最后,数下面的记号。

这次她算对了。但是,好麻烦啊,为什么要用这么麻烦的方法。我想偷懒就会搞错。

我说,其实,你不需要画图,只用写数字就可以了。但是不要随便连线。1 号点先和 2 号点连,再和 3 号点连…… 顺着数字从小到大,这样就不会搞错。不需要真的把连线图画在纸上,在脑子里相像就可以了,只需要在纸上写下 1 号点能和几个点连成线段就可以了。这里,你写个 4 。然后再看 2 号点。

她画了一点时间,终于发现了规律,列出了算式 4 + 3 + 2 + 1 = 10 。

我说看吧,其实这不是一道绘图题,用数学方法,它转化为一道计算题了。

可可好像有点兴趣了,说我现在可厉害了,我能算 10 个点的问题。我说你试试。可可的悟性没我相像的那么高,并没有直接写出 9 到 1 的等差数列,每个数字都想了一会,直到 4 以后才有点把握后面应该是 3 2 1 。但是,面对长达 10 个数字的加法算式,可可说,我知道方法了,这个算起来太麻烦了,我不算了。

我说,你不是学了乘法吗?其实,这个问题不一定要用加法做,我告诉你怎么用乘法解决这个问题,就不用算这么麻烦的算式了。你看,刚才你连线的时候,把 1 号和 2 号连起来之后,从 2 号做起点就不连 1 号了。因为 1 到 2 和 2 到 1 是同一条线段。两个点之间只可以连一次。但是,如果我们每两个点都连两次,那么这 10 个点,每个都可以连出去 9 条(射线)。

最后,总共画了 10 组,每组 9 条射线,一共是 10 * 9 = 90 条。因为我们每条线段都计算了 2 次,所以答案就是 90 / 2 = 45 条线段。

有了这个方法,100 个点的问题你也能算出来了吧。

可可问,家里有没有有趣一点的数学书,我要看。我翻出了前年带着云豆读过的《我的第一本数学书》。可可翻了一下说,怎么这么多字啊。我说,周末我带着你读。


早上起床的时候,可可问,今天我能不能把昨天那本书带到学校去?

希望她能发现一点点数学的乐趣。

iOS逆向-哔哩哔哩增加3倍速播放(3)-[横屏视频-全屏播放]场景

前言

作为哔哩哔哩的重度用户,我一直期待官方支持 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 倍速实现。

系列回顾

场景

E260DF27-B08D-4BD1-ADED-A6DD94AAE1A3.png

开发环境

分析

1. 定位倍速面板控件

  • 通过 Lookin 查看层级,播放速度浮层为 BBPlayerFloatingWidgetView,其包含 _rootWidget 属性(类型为 BBPlayerNavigationWidget)。

308375D5-0802-4726-902D-EAB1CB96CABA.png

@interface BBPlayerFloatingWidgetView : UIView {
    /* instance variables */
    BBPlayerContext *_context;
    BBPlayerWidget *_rootWidget;
}

/* instance methods */
- (id)initWithContext:(id)context rootWidget:(id)widget;
- (id)hitTest:(struct CGPoint { double x0; double x1; })test withEvent:(id)event;

@end

2. 追踪倍速列表

  • 我们在Xcode添加符号断点-[BBPlayerFloatingWidgetView initWithContext:rootWidget:],看看rootWidget的值是什么?

412EE2D5-DBAE-4F6B-892D-E1C957715D7F.png

  • -[BBPlayerFloatingWidgetView initWithContext:rootWidget:]断点触发,我们打印参数的值,知道rootWidget的类型是BBPlayerNavigationWidget
(lldb) p (id)$x3
(BBPlayerNavigationWidget *) 0x00000002827e2be0
  • Mach-O文件导出的BBPlayerNavigationWidgetOC头文件可以知道,BBPlayerNavigationWidget继承自BBPlayerObjectBBPlayerObjectsubWidgets属性
@interface BBPlayerNavigationWidget : BBPlayerFloatingWidget <CAAnimationDelegate, UIGestureRecognizerDelegate> {
    /* instance variables */
    NSMutableArray *_widgets;
    UIView *_view;
}

@property (readonly, nonatomic) BBPlayerFloatingWidget *rootWidget;
...
/* instance methods */
- (id)initWithContext:(id)context rootWidget:(id)widget;
- (id)view;
- (void)pushWidget:(id)widget animated:(_Bool)animated;
- (void)pushWidget:(id)widget animated:(_Bool)animated completion:(id /* block */)completion;
...

@end
@interface BBPlayerWidget : BBPlayerObject <BBPlayerWidgetDelegate> 

@property (nonatomic) long long lifecycleFlag;
@property (readonly, nonatomic) UIView *view;
@property (readonly, weak, nonatomic) BBPlayerWidget *superWidget;
@property (readonly, copy, nonatomic) NSArray *subWidgets;
...
- (void)willAppear:(_Bool)appear;
- (void)willLayoutSubWidgets;
- (void)didLayoutSubWidgets;
- (void)didAppear:(_Bool)appear;
- (void)willDisappear:(_Bool)disappear;
- (void)didDisappear:(_Bool)disappear;
...

@end
  • 我们在Xcode尝试添加符号断点-[BBPlayerNavigationWidget didLayoutSubWidgets],但是添加不成功

9088873D-45A0-4B91-A181-AD273DD35DE2.png

  • 我们hook BBPlayerNavigationWidgetdidLayoutSubWidgets方法,并添加断点
%hook BBPlayerNavigationWidget

- (void)didLayoutSubWidgets {
    %orig;
    NSLog(@"%@:%@-%p-%s", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__);
}

%end
  • BBPlayerNavigationWidgetdidLayoutSubWidgets方法的断点触发,我们打印subWidgets属性,发现为空
(lldb) po [self subWidgets]
nil
  • 我们知道BBPlayerNavigationWidget有个_widgets成员变量,我们打印它的值,发现为空
@interface BBPlayerNavigationWidget : BBPlayerFloatingWidget <CAAnimationDelegate, UIGestureRecognizerDelegate> {
    /* instance variables */
    NSMutableArray *_widgets;
    UIView *_view;
}
(lldb) po ((BBPlayerNavigationWidget *)self)->_widgets
<__NSArrayM 0x280448600>(
)
  • 我们知道BBPlayerNavigationWidget有个- (void)pushWidget:(id)widget animated:(_Bool)animated completion:(id)completion方法,我们添加断点,看widget的值为多少
%hook BBPlayerNavigationWidget

- (void)didLayoutSubWidgets {
    %orig;
    NSLog(@"%@:%@-%p-%s", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__);
}

- (void)pushWidget:(id)widget animated:(_Bool)animated completion:(id)completion {
    %orig;
}

- (void)addWidget:(id)widget animated:(_Bool)animated completion:(id)completion {
    %orig;
}

%end
  • - (void)pushWidget:(id)widget animated:(_Bool)animated completion:(id)completion方法的断点触发,发现widget的类型是BBPlayerPlaybackRateListWidget
(lldb) p widget
(BBPlayerPlaybackRateListWidget?) 0x0000000282437b60
  • 而且- (void)pushWidget:(id)widget animated:(_Bool)animated completion:(id)completion方法执行完后,_widgets属性有值了,subWidgets属性也有值了,类型是BBPlayerPlaybackRateListWidget
(lldb) po ((BBPlayerNavigationWidget *)self)->_widgets
<__NSArrayM 0x2804a05d0>(
<BBPlayerPlaybackRateListWidget: 0x282437b60>

)

(lldb) po [self subWidgets]
<__NSSingleObjectArrayI 0x2817c38e0>(
<BBPlayerPlaybackRateListWidget: 0x282437b60>
)
  • Mach-O文件导出的BBPlayerCommonSwift.BBPlayerPlaybackRateListWidgetswift文件可以知道,它有一个rateArray属性,猜测它是播放速度数组,rateArray属性只读,getter方法叫做sub_10D829128
class BBPlayerCommonSwift.BBPlayerPlaybackRateListWidget: BBPlayerFloatingWidget {
  /* fields */
    var lazy tableView: UITableView?
    var lazy playbackRate: (private) ?
    var lazy rateArray: Array -> BBPlayerCommonSwift.BBPlayerPlayerRateModel ?
    var lazy playbackRateProxy: BBPlayerPlaybackRateValueService??
  /* methods */
    func tableView.getter.sub_10d828e50
    // <stripped> func tableView.setter 
    // <stripped> func tableView.modify 
    func playbackRate.getter.sub_10d829074
    // <stripped> func playbackRate.setter 
    // <stripped> func playbackRate.modify 
    func rateArray.getter.sub_10d829128
    // <stripped> func rateArray.setter 
    // <stripped> func rateArray.modify 
...
}
  • 我们添加一个符号断点sub_10D829128,看看rateArray的值是多少?

8CA3E563-CE85-4CB9-BADB-BC848632374B.png

  • sub_10D829128断点触发,打印它的返回值,发现是播放速度,这也证明了rateArray属性是播放速度数组
(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStorageC19BBPlayerCommonSwift23BBPlayerPlayerRateModel_$ 0x2835d09b0>(
<BBPlayerPlayerRateModel: 0x28107fb20; value = 2.000000; text = 2.0>,
<BBPlayerPlayerRateModel: 0x28107fae0; value = 1.500000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x28107dc40; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x28107fc60; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x28107e880; value = 0.750000; text = 0.75>,
<BBPlayerPlayerRateModel: 0x28107e660; value = 0.500000; text = 0.5>
)

3. 找到数组创建函数

  • IDA查看sub_10D829128的伪代码,分析后猜测是由sub_10D82E870方法创建的播放速度数组
__int64 sub_10D829128()
{
...
  v1 = OBJC_IVAR___BBPlayerPlaybackRateListWidget____lazy_storage___rateArray;
  v2 = *(_QWORD *)(v0 + OBJC_IVAR___BBPlayerPlaybackRateListWidget____lazy_storage___rateArray);
  if ( v2 )
  {
    v3 = *(_QWORD *)(v0 + OBJC_IVAR___BBPlayerPlaybackRateListWidget____lazy_storage___rateArray);
  }
  else
  {
    v4 = sub_10D82E870();
    v3 = sub_10D82918C(v4);
    v5 = *(_QWORD *)(v0 + v1);
    *(_QWORD *)(v0 + v1) = v3;
    ((void (*)(void))swift_bridgeObjectRetain)();
    swift_bridgeObjectRelease(v5);
    v2 = 0LL;
  }
  swift_bridgeObjectRetain(v2);
  return v3;
}
  • 查看sub_10D82E870方法的伪代码,知道它返回一个包含 6 个 BBPlayerPlayerRateModel 的数组。
__int64 sub_10D82E870()
{
...
  sub_10D7DBF44();
  v0 = swift_allocObject();
  *(_OWORD *)(v0 + 16) = xmmword_110613B30;
  v1 = (objc_class *)type metadata accessor for BBPlayerPlayerRateModel();
  v2 = (char *)objc_msgSend(objc_allocWithZone(v1), "init");
  v3 = &v2[OBJC_IVAR___BBPlayerPlayerRateModel_value];
  swift_beginAccess(&v2[OBJC_IVAR___BBPlayerPlayerRateModel_value], v38, 1LL, 0LL);
  *(_QWORD *)v3 = 0x3FE0000000000000LL;
  v4 = &v2[OBJC_IVAR___BBPlayerPlayerRateModel_text];
  swift_beginAccess(&v2[OBJC_IVAR___BBPlayerPlayerRateModel_text], v37, 1LL, 0LL);
  v5 = *((_QWORD *)v4 + 1);
  *(_QWORD *)v4 = 3485232LL;
  *((_QWORD *)v4 + 1) = 0xE300000000000000LL;
  swift_bridgeObjectRelease(v5);
  *(_QWORD *)(v0 + 32) = v2;
  v6 = (char *)objc_msgSend(objc_allocWithZone(v1), "init");
  v7 = &v6[OBJC_IVAR___BBPlayerPlayerRateModel_value];
  swift_beginAccess(&v6[OBJC_IVAR___BBPlayerPlayerRateModel_value], v36, 1LL, 0LL);
  *(_QWORD *)v7 = 0x3FE8000000000000LL;
  v8 = &v6[OBJC_IVAR___BBPlayerPlayerRateModel_text];
  swift_beginAccess(&v6[OBJC_IVAR___BBPlayerPlayerRateModel_text], v35, 1LL, 0LL);
  v9 = *((_QWORD *)v8 + 1);
  *(_QWORD *)v8 = 892808752LL;
  *((_QWORD *)v8 + 1) = 0xE400000000000000LL;
  swift_bridgeObjectRelease(v9);
  *(_QWORD *)(v0 + 40) = v6;
  v10 = (char *)objc_msgSend(objc_allocWithZone(v1), "init");
  v11 = &v10[OBJC_IVAR___BBPlayerPlayerRateModel_value];
  swift_beginAccess(&v10[OBJC_IVAR___BBPlayerPlayerRateModel_value], v34, 1LL, 0LL);
  *(_QWORD *)v11 = 0x3FF0000000000000LL;
  v12 = &v10[OBJC_IVAR___BBPlayerPlayerRateModel_text];
  swift_beginAccess(&v10[OBJC_IVAR___BBPlayerPlayerRateModel_text], v33, 1LL, 0LL);
  v13 = *((_QWORD *)v12 + 1);
  *(_QWORD *)v12 = 3157553LL;
  *((_QWORD *)v12 + 1) = 0xE300000000000000LL;
  swift_bridgeObjectRelease(v13);
  *(_QWORD *)(v0 + 48) = v10;
...
  return v0;
}
  • 我们添加一个符号断点sub_10D82E870,看看方法返回值多少?

5A6089F4-393E-4D9C-8C53-DAF4C10C7226.png

  • sub_10D82E870断点触发,打印它的返回值,发现是播放速度数组[0.5、0.75、1.0、1.25、1.5、2.0],这也证明了是由sub_10D82E870方法创建的播放速度数组
(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStorageC19BBPlayerCommonSwift23BBPlayerPlayerRateModel_$ 0x2834b2da0>(
<BBPlayerPlayerRateModel: 0x281164280; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x281165760; value = 0.750000; text = 0.75>,
<BBPlayerPlayerRateModel: 0x2811655c0; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x281164840; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x281165500; value = 1.500000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x281164a40; value = 2.000000; text = 2.0>
)

越狱解决方案

MSHookFunction

MSHookFunction 来自 Cydia Substrate(MobileSubstrate),用于 直接 Hook 一个函数地址,而不是 Objective-C 方法。

函数原型

void MSHookFunction(
    void *symbol,
    void *replace,
    void **result
);

参数说明

参数 说明
symbol 原始函数地址
replace 你的替换函数
result 保存原始函数指针(可选)

方案

  • 使用 MSHookFunction 直接 hook sub_10D82E870,修改返回数组的 valuetext 属性。

核心代码:

#import "NJChangePlaybackRateTool.h"
#import "NJCommonDefine.h"

static const double NJChangePlaybackRateFlag = 3.0;

@interface NJChangePlaybackRateTool ()

/// 播放速度
@property (nonatomic, strong) NSArray<NSString *> *playbackRates;

@end

@implementation NJChangePlaybackRateTool

#pragma mark - Life Cycle Methods

- (instancetype)init {
    self = [super init];
    if (self) {
        [self doInit];
    }
    return self;
}

#pragma mark - Do Init

- (void)doInit {
    self.playbackRates = @[@"0.5", @"1.0", @"1.25", @"1.5", @"2.0", @"3.0"];
}

#pragma mark - Override Methods

#pragma mark - Public Methods

- (NSArray *)changePlaybackRateWithRateArray:(NSArray *)rateArray {
    if (![self shouldChange:rateArray]) {
        return rateArray;
    }
//    NSLog(@"[%@] change before rateArray = %@, class:%@", nj_logPrefix, rateArray, NSStringFromClass([rateArray class]));
    NSInteger rateCount = rateArray.count;
    NSInteger newRateCount = self.playbackRates.count;
    NSInteger count = MIN(rateCount, newRateCount);
    for (NSInteger i = 0; i < count; i++) {
        BBPlayerPlayerRateModel *rateModel = rateArray[i];
        NSString *newRateStr = self.playbackRates[i];
        rateModel.value = newRateStr.doubleValue;
        rateModel.text = newRateStr;
    }
//    NSLog(@"[%@] change after rateArray = %@, class:%@", nj_logPrefix, rateArray, NSStringFromClass([rateArray class]));
    return rateArray;
}

#pragma mark - Private Methods

- (BOOL)shouldChange:(NSArray *)rateArray {
    BOOL flag = YES;
    for (BBPlayerPlayerRateModel *item in rateArray) {
        if (item.value == NJChangePlaybackRateFlag) {
            flag = NO;
            break;
        }
    }
    return flag;
}

@end

Hook 实现

// 声明原函数类型
public typealias orig_landscapeVideo_fullScreenPlayback_RateModelArr_type = @convention(c) () -> Int64

// 定义全局函数指针变量,并绑定一个 C 名字
@_silgen_name("orig_landscapeVideo_fullScreenPlayback_RateModelArr")
nonisolated(unsafe) public var orig_landscapeVideo_fullScreenPlayback_RateModelArr: orig_landscapeVideo_fullScreenPlayback_RateModelArr_type? = nil
// [横屏视频-全屏播放]播放速度数组
@_cdecl("my_landscapeVideo_fullScreenPlayback_RateModelArr")
func my_landscapeVideo_fullScreenPlayback_RateModelArr() -> Int64 {
    if let orig_landscapeVideo_fullScreenPlayback_RateModelArr {
        let origPtr = orig_landscapeVideo_fullScreenPlayback_RateModelArr()
        let origArr = unsafeBitCast(origPtr, to: [NSObject].self)
        let tool = NJChangePlaybackRateTool()
        tool.changePlaybackRate(withRateArray: origArr)
        let retPtr = unsafeBitCast(origArr, to: Int64.self)
        return retPtr
    }
    return 0
}

// __int64 sub_10D82E870()
// [横屏视频-全屏播放]播放速度数组
long long landscapeVideo_fullScreenPlayback_RateModelArr_address = g_slide+0x10D82E870;
NSLog(@"[%@] cal func landscapeVideo_fullScreenPlayback_RateModelArr_address address:0x%llx", nj_logPrefix, landscapeVideo_fullScreenPlayback_RateModelArr_address);
MSHookFunction((void *)landscapeVideo_fullScreenPlayback_RateModelArr_address,
   (void*)my_landscapeVideo_fullScreenPlayback_RateModelArr,
   (void**)&orig_landscapeVideo_fullScreenPlayback_RateModelArr);

非越狱解决方案

通过 IDA Pro 修改汇编指令,直接硬编码新倍速。

分析

  • 我们知道sub_10D82E870方法返回的是BBPlayerPlayerRateModel数组,而BBPlayerPlayerRateModel的结构如下,所以我们要修改BBPlayerPlayerRateModelvaluetext的值
@interface BBPlayerPlayerRateModel : NSObject // (Swift)

@property (nonatomic) double value;
@property (nonatomic, copy) NSString *text;

/* instance methods */
- (id)initWithValue:(double)value text:(id)text;
- (id)init;

@end

修改value

chatgpt帮忙分析sub_10D82E870方法的汇编指令,给我们赋值value的地址,给出如下表格:

元素 index 地址 旧值: 旧汇编指令 新值: 新汇编指令
0 0x10D82E8FC "0.5"MOV X8, #0x3FE0000000000000 不变
1 0x10D82E970 "0.75"MOV X8, #0x3FE8000000000000 "1.0"MOV X8, #0x3FF0000000000000
2 0x10D82E9E0 "1.0"MOV X8, #0x3FF0000000000000 "1.25"MOV X8, #0x3FF4000000000000
3 0x10D82EA4C "1.25"MOV X8, #0x3FF4000000000000 "1.5"MOV X8, #0x3FF8000000000000
4 0x10D82EAB8 "1.5"MOV X8, #0x3FF8000000000000 "2.0"MOV X8, #0x4000000000000000
5 0x10D82EB28 "2.0"MOV X8, #0x4000000000000000 "3.0"MOV X8, #0x4008000000000000

示例

  • 我们修改000000010D82E970的指令

    __text:000000010D82E970                 MOV             X8, #0x3FE8000000000000
    
    • 鼠标点击000000010D82E970
    __text:000000010D82E970                 MOV             X8, #0x3FE8000000000000
    
    • 右键选择Assemble CBAD6A1D-ABAA-46E3-9855-87E9CC816B2F.png
    • 改成
    MOV     X8, #0x3FF0000000000000
    

    70A070E9-EA12-4482-BDF0-636E77B7C97B.png

    • 点击enter
  • 保存到Mach-O文件中

    • IDA->Edit->Patch program->Apply patches to input file->Apply patches
12636991-9898-4A7D-B2A1-D400B420039F.png

06857153-3C9D-4018-82FA-55C53C1C6516.png

  • 保存后:底部会显示log

    Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal
    

DE3F7FC3-798A-4A23-9C34-FF6AF7F24BC7.png

  • 验证修改结果

    • 将修改后的Mach-O文件放入.app文件中
    • Xcode添加符号断点sub_10D82E870

    4B068744-8DF9-4C12-99BF-DC96A954A2EC.png

    • sub_10D82E870断点触发,打印它的返回值,发现元素1value已经从0.75改为了1.0,验证修改成功。
    (lldb) po (id)$x0
    <_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x303adb570>(
    <BBPlayerPlayerRateModel: 0x301d18f60; value = 0.500000; text = 0.5>,
    <BBPlayerPlayerRateModel: 0x301d19880; value = 1.000000; text = 0.75>,
    <BBPlayerPlayerRateModel: 0x301d197e0; value = 1.000000; text = 1.0>,
    <BBPlayerPlayerRateModel: 0x301d19840; value = 1.250000; text = 1.25>,
    <BBPlayerPlayerRateModel: 0x301d19780; value = 1.500000; text = 1.5>,
    <BBPlayerPlayerRateModel: 0x301d19860; value = 2.000000; text = 2.0>
    )
    
  • 全部修改结果

(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x3009c9360>(
<BBPlayerPlayerRateModel: 0x3028a4cc0; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x3028a5660; value = 1.000000; text = 0.75>,
<BBPlayerPlayerRateModel: 0x3028a49a0; value = 1.250000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x3028a4900; value = 1.500000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x3028a48a0; value = 2.000000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x3028a4400; value = 3.000000; text = 2.0>
)

修改text

chatgpt分析sub_10D82E870方法的汇编指令,给我们赋值text的地址,给出如下表格:

元素 index 地址 旧值: 旧汇编指令
0 0x10D82E928 0.5MOV W8, #0x352E30
1 0x10D82E998 0.75MOV W8, #0x35372E30
2 0x10D82EA08 1.0MOV W28, #0x302E31
3 0x10D82EA74 1.25MOV W8, #0x35322E31
4 0x10D82EAE0 1.5MOV W8, #0x352E31
5 0x10D82EB50 2.0ADD X8, X28, #1

分析

5个元素的0x10D82EB50的指令是ADD X8, X28, #1,而x28寄存器只在__text:000000010D82EA08 MOV W28, #0x302E31赋过值,而#0x302E31的值是1.0,所以ADD X8, X28, #1相当于是X8 = X28 + 1 = 1.0 + 1 = 2.0 ,所以第5个元素显示的是2.0

所以方案就是,1.0要使用MOV W28, #0x302E312.0使用ADD X8, X28, #13.02.0一致。

下面在元素1中,给出具体的调试过程,其他元素类似。


元素0

元素0不需要修改,就是0.5

元素1

  • 原本0x10D82E998MOV W8, #0x35372E30(0.75),占两个指令空间,要改成MOV W28, #0x302E31(1.0)

    • 修改前
    __text:000000010D82E998                 MOV             W8, #0x35372E30
    
    • 修改后
    __text:000000010D82E998                 MOV             W28, #0x302E31
    
伪指令

伪指令Pseudo-Instruction)不是 CPU 真实存在的机器指令,而是编译器/汇编器提供的“方便写法”。

最终汇编器会把伪指令转换成 一条或多条真实的机器指令。

看个例子:

  • 鼠标点击000000010D82E998
__text:000000010D82E998                 MOV             W8, #0x35372E30
  • 查看000000010D82E99816进制视图,发现指令占用了8 bytes,也就是两个指令的空间,所以MOV W8, #0x35372E30是一个伪指令

DA6DF5E1-0397-46BD-A893-A8AB6F9456BC.png

  • chatgptMOV W8, #0x35372E30可以拆分成什么指令?回答如下
MOVZ    W8, #0x2E30
MOVK    W8, #0x3537, LSL #16
获取伪指令的真实指令
  • chatgptMOV W28, #0x302E31可以拆分成:
MOVZ    W28, #0x2E31            // 写入低 16 位
MOVK    W28, #0x0030, LSL #16   // 写入高 16 位
获取汇编指令的机器码
  • main.S文件
.global _main
_main:
    MOVZ    W28, #0x2E31            // 写入低 16 位
    MOVK    W28, #0x0030, LSL #16   // 写入高 16 位
    ret
  • 编译main.S文件
clang -arch arm64 main.S -o main

BCD67C3E-D7D5-4282-A0CF-8A886CC9D27B.png

  • 使用MachOView查看main文件(Mach-O文件)的机器码

image-20251215150438720.png

  • 所以MOV W28, #0x302E31的机器码是
3C C6 85 52 1C 06 A0 72
修改指令的机器码
  • 鼠标点击000000010D82E998
  • IDA->Edit->Patch program->Change byte

7055BC7A-0B60-49DF-AA40-6444A3240912.png

  • 显示Patch Bytes弹框

459F522D-9842-4D5F-AFC8-F87A82FC8865.png

  • Origin value
    • 08 C6 85 52 E8 A6 A6 72 1B 80 FC D2 C8 6E 00 A9
  • 修改 Values 为:
    • 3C C6 85 52 1C 06 A0 72 1B 80 FC D2 DC 6A 00 A9
  • 点击OK,真正修改

image-20251215151448578.png

保存
  • 保存到Mach-O文件中

    • IDA->Edit->Patch program->Apply patches to input file->Apply patches

12636991-9898-4A7D-B2A1-D400B420039F-5765270.png

06857153-3C9D-4018-82FA-55C53C1C6516-5765282.png

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

DE3F7FC3-798A-4A23-9C34-FF6AF7F24BC7-5765291.png

验证失败
  • 将修改后的Mach-O文件放入.app文件中
  • Xcode添加符号断点sub_10D82E870

4B068744-8DF9-4C12-99BF-DC96A954A2EC-5765304.png

  • sub_10D82E870断点触发,打印它的返回值,发现元素1text已经从"0.75"改为了"1.0",但是打印的数组显示不全,而且倍速列表显示的是1.0,不是1.0X,所以还是有问题。
(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x3036c0190>(
<BBPlayerPlayerRateModel: 0x301058d80; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x30105b380; value = 1.000000; text = 1.0

4F4E4F3B-2414-42BE-81EF-1C90EDB6362C.png

处理问题
  • 我们知道0x10D82EA08MOV W28, #0x302E31的值是1.0,它的下一条指令是STP X28, X26, [X22]
__text:000000010D82EA08                 MOV             W28, #0x302E31
__text:000000010D82EA10                 STP             X28, X26, [X22]
  • 而修改后的000000010D82E998MOV W28, #0x302E31,它的下一条指令是STP X8, X27, [X22]
__text:000000010D82E998                 MOV             W28, #0x302E31
__text:000000010D82E9A4                 STP             X8, X27, [X22]
  • 我们尝试将0x10D82E9A4STP X8, X27, [X22],改为STP X28, X26, [X22]

image-20251215152153278.png

再次验证
  • sub_10D82E870断点触发,打印它的返回值,发现元素1text已经从"0.75"改为了"1.0",而且数组全部打印了,倍速列表显示的是1.0X,修改成功。
(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x301b6b8e0>(
<BBPlayerPlayerRateModel: 0x303bbd1c0; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x303bbd200; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x303bbcf80; value = 1.250000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x303bbcf40; value = 1.500000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x303bbcb20; value = 2.000000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x303bbc860; value = 3.000000; text = 2.0>
)

9103463C-D49B-4FE6-BD66-E79B0172F047.png

元素2

  • 原本0x10D82EA08MOV W28, #0x302E31(1.0),占两个指令空间,要改成MOV W8, #0x35322E31(1.25)

    • 修改前
    __text:000000010D82EA08                 MOV             W28, #0x302E31
    
    • 修改后
    __text:000000010D82EA08                 MOV             W8, #0x35322E31
    
  • MOV W8, #0x35322E31拆分成:

MOVZ    W8, #0x2E31              // 写低 16 bit
MOVK    W8, #0x3532, LSL #16     // 写高 16 bit
  • MOV W8, #0x35322E31的机器码
28 C6 85 52 48 A6 A6 72
  • 还需要将0x10D82EA10STP X28, X26, [X22],占一个指令空间,改成STP X8, X27, [X22]

    • 修改前
    __text:000000010D82EA10                 STP             X28, X26, [X22]
    
    • 修改后
    __text:000000010D82EA10                 STP             X8, X27, [X22]
    
  • 验证

(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x3022f5f90>(
<BBPlayerPlayerRateModel: 0x30057cca0; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x30057f360; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x30057ccc0; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x30057f3c0; value = 1.500000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x30057c960; value = 2.000000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x30057ca00; value = 3.000000; text = 2.0>
)

元素3

  • 原本0x10D82EA74MOV W8, #0x35322E31(1.25),占两个指令空间,要改成MOV W8, #0x352E31(1.5)

    • 修改前
    __text:000000010D82EA74                 MOV             W8, #0x35322E31
    
    • 修改后
    __text:000000010D82EA74                 MOV             W8, #0x352E31
    
  • MOV W8, #0x352E31拆分成

MOVZ    W8, #0x2E31              // 写低16位
MOVK    W8, #0x0035, LSL #16     // 写高16位
  • MOV W8, #0x352E31的机器码
28 C6 85 52 A8 06 A0 72
  • 还需要将0x10D82EA7CSTP X8, X27, [X22],占一个指令空间,改成STP X8, X26, [X22]

    • 修改前
    __text:000000010D82EA7C                 STP             X8, X27, [X22]
    
    • 修改后
    __text:000000010D82EA7C                 STP             X8, X26, [X22]
    
  • 验证

(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x300d97610>(
<BBPlayerPlayerRateModel: 0x302ac6ac0; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x302af3760; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x302af3420; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x302af3b40; value = 1.500000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x302af2de0; value = 2.000000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x302af3d00; value = 3.000000; text = 2.0>
)

元素4

  • 原本0x10D82EAE0MOV W8, #0x352E31(1.5)占三个指令空间),要改成MOV W8, #0x302E32(2.0)(占两个指令空间) + 一个NOP(占一个指令空间)

    • 修改前
    __text:000000010D82EAE0                 MOV             W8, #0x352E31
    
    • 修改后
    __text:000000010D82EAE0                 MOV             W8, #0x302E32
    __text:000000010D82EAE8                 NOP
    
  • MOV W8, #0x302E32拆分成

MOVZ    W8, #0x2E32            // 写低 16 位
MOVK    W8, #0x0030, LSL #16   // 写高 16 位
  • MOV W8, #0x302E32的机器码
48 C6 85 52 08 06 A0 72
  • NOP的机器码
1F 20 03 D5
  • 总的机器码
48 C6 85 52 08 06 A0 72 1F 20 03 D5
  • 验证
(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x303fe4f00>(
<BBPlayerPlayerRateModel: 0x3018ffe60; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x3018ffe80; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x3018ffa20; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x3018ffb20; value = 1.500000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x3018ffb00; value = 2.000000; text = 2.0>,
<BBPlayerPlayerRateModel: 0x3018ff860; value = 3.000000; text = 2.0>
)

元素5

  • 原本0x10D82EB50ADD X8, X28, #1(2.0),占一个指令空间,要改成ADD X8, X28, #2(3.0)

    • 修改前
    __text:000000010D82EB50                 ADD             X8, X28, #1
    
    • 修改后
    __text:000000010D82EB50                 ADD             X8, X28, #2
    
  • 验证

(lldb) po (id)$x0
<_TtGCs23_ContiguousArrayStoragePs9AnyObject__$ 0x301eb6f80>(
<BBPlayerPlayerRateModel: 0x30393ac20; value = 0.500000; text = 0.5>,
<BBPlayerPlayerRateModel: 0x30393ac40; value = 1.000000; text = 1.0>,
<BBPlayerPlayerRateModel: 0x30393ac60; value = 1.250000; text = 1.25>,
<BBPlayerPlayerRateModel: 0x30393ac80; value = 1.500000; text = 1.5>,
<BBPlayerPlayerRateModel: 0x30393aca0; value = 2.000000; text = 2.0>,
<BBPlayerPlayerRateModel: 0x30393acc0; value = 3.000000; text = 3.0>
)

效果

横屏全屏模式下,倍速菜单成功显示并支持 3.0 倍速播放。

Screenshot 2025-12-15 at 10.07.00.png

总结

  • 横屏全屏场景下,播放倍速列表由 BBPlayerPlaybackRateListWidgetrateArray 生成,最终创建函数为 sub_10D82E870
  • 通过 越狱 Hook非越狱 Mach-O Patch,即可稳定新增 3.0x 倍速。

代码

BiliBiliMApp-无广告版哔哩哔哩

周日小插曲 -- 肘子的 Swift 周报 #115

issue115.webp

周日小插曲

周日下午,我正准备周一的周报。起身的瞬间,右手小指似乎碰到了什么,然后它就伸不直了。

有些诡异——不痛、不肿,但就是无法伸直。急诊医生初步判断是小指伸肌腱损伤,约了周一做进一步检查,很可能需要手术修复。

这才意识到,平时存在感不强的小指,原来如此重要。现在戴着固定指套,打字变得比较困难。好在应该不是什么大问题,只是接下来一段时间会减少打字量。

人生无常,微笑对待。

周一更新:今天进一步检查后,发现没有需要复位的骨折,只是单纯的肌腱断裂。这样就不用手术了,只需要戴指套固定 6 周就基本能恢复了。

本期内容 | 前一期内容 | 全部周报列表

🚀 《肘子的 Swift 周报》

每周为你精选最值得关注的 Swift、SwiftUI 技术动态

近期推荐

从 YaoYao 到 Tooboo:watchOS 开发避坑与实战

作为知名 watchOS 应用 YaoYao、Tooboo、DunDun 的作者,Haozes 分享了 watchOS 开发中关于版本兼容、App 唤起通信、数据同步、重启恢复、内存泄露和电量优化等高质量实战经验。这篇文章涵盖了从 HealthKit 到 WCSession、从 HKWorkoutSession 到 TimelineSchedule 的完整开发避坑与性能调优指南,对于正在开发或计划开发 Apple Watch 应用的开发者具有极高参考价值。

作为一个长期的 Apple Watch 用户和 Swift 博主,我常感叹 watchOS 开发的“神秘”——文档之外细节繁多,且网络上真正有深度的实战文章寥寥无几。为此,我特别邀请了 watchOS 领域的头部独立开发者 Haozes,来分享他的开发体会。


Swift 6 严格并发迁移实战 (My journey to Swift 6 and Strict Concurrency)

一次“只在真实用户设备上才会出现”的崩溃,迫使 Irving Popovetsky 将一整个已上线的 iOS 应用迁移到 Swift 6 + Strict Concurrency。这篇文章完整记录了从迁移之初的 76 个 error、238 个 warning 开始,到逐步引入 @preconcurrencyactor@MainActorSendablenonisolated(unsafe),再到真机 + Thread Sanitizer 验证的全过程。Irving 指出,Swift 6 的运行时会在闭包被调用的瞬间验证 actor 隔离,哪怕你在闭包内部再 Task { @MainActor in } 也可能为时已晚。文章中对这一行为给出了清晰、可复用的修复模式。


用 MetricKit 监控应用性能 (Monitoring app performance with MetricKit)

MetricKit 是 Apple 在 iOS 13 引入的系统级性能诊断框架,用于向开发者提供来自真实用户设备的应用性能与稳定性数据。它只在真机环境中工作,数据来源于系统在后台长期采样并聚合的结果,因此能够更真实地反映应用在实际使用场景下的行为。

Majid Jabrayilov 在本文中展示了如何通过 MXMetricManager 订阅系统级性能数据,从后台异常退出、CPU / 内存资源限制,到崩溃诊断信息,逐步提取关键指标并上报,从而构建一个更完整、可追溯的应用性能监控面板。Majid 也特别提醒,MetricKit 的数据通常按天聚合下发,而非实时回调,实际分析时需要结合时间戳理解其所覆盖的时间区间。


用 Claude Code 重构 MistKit (Rebuilding MistKit with Claude Code - From CloudKit Docs to Type-Safe Swift)

Leo Dion 在重建自己停更多年的 CloudKit 服务端库 MistKit 时,发现 swift-openapi-generator 能显著降低重构复杂度:只要将 CloudKit 的 REST 文档转化为 OpenAPI 规范,就可以自动生成大规模、类型安全的 Swift 客户端代码。为此,他引入 Claude Code,专门负责完成“从 Apple 文档到 OpenAPI YAML”的翻译工作。

文章最有价值的地方,并不在于“AI 生成了多少代码”,而在于 Leo 如何使用 AI。他并没有让 Claude 直接实现 MistKit,而是将其定位为“文档翻译与模式放大器”,而真正复杂、需要工程判断与架构取舍的部分,始终由作者亲自把控。与其让 AI 直接“写代码”,不如让它把复杂问题变成可生成、可校验的问题——这正是这次 MistKit 重建过程中最值得借鉴的地方。


ChatGPT 记忆系统逆向分析 (I Reverse Engineered ChatGPT's Memory System, and Here's What I Found!)

Manthan Gupta 通过大量对话实验,对 ChatGPT 的“记忆”行为进行了逆向分析,发现其实现方式远比想象中简单:没有向量数据库、没有跨历史对话的 RAG,而是由四层上下文共同构成——一次性注入的 Session Metadata、显式存储的长期用户记忆、近期对话的轻量摘要,以及当前会话的滑动窗口。Manthan 认为,这种设计在性能、延迟和 token 成本上的优势非常明显:通过预计算摘要和显式事实注入,换取“足够好”的连续性,而不是昂贵的全量历史检索。Manthan 还有另一篇针对 Claude 记忆系统的研究

在使用 ChatGPT 的过程中,我时常会对它对我的了解程度感到惊讶。尽管我清楚,这种“熟悉感”来自其记忆与上下文机制,但这也正是我长期偏向使用官方客户端的重要原因之一。当然,如果你对隐私问题格外敏感,那么这种能力本身,可能正是你不愿接受的那一部分。


Swift Configuration 1.0 released

Honza Dvorsky 在 Swift 官方博客中宣布 Swift Configuration 1.0 正式发布。该库为 Swift 应用与库提供了一套统一、类型安全的配置读取抽象,其关键并不在于支持多少配置格式,而是彻底分离「配置如何读取」与「配置来自哪里」。这一设计对库作者尤为重要——库可以接受配置而不绑定具体来源,从而在不同部署环境中保持良好的可组合性。随着 1.0 发布,Swift Configuration 的 API 已进入稳定阶段,并开始被 Vapor、Hummingbird 等项目探索集成。

乍看之下,为配置管理引入这样一个抽象层似乎显得有些“重量级”,但一旦配置来源不再局限于单一文件,或者代码需要在不同环境与项目中复用,其带来的结构清晰度与扩展性优势就会迅速显现。


TCA 架构:一个被美化的反模式? (TCA Architecture: A Glorified Antipattern)

这是一篇立场极其鲜明的长文。Lazar Otasevic 从函数式编程与 SwiftUI 的运行模型出发,系统性地反对了 TCA 以 Action enum + Reducer 为核心的设计,认为这是一种“将行为编码为数据”的经典反模式,并且不必要地夺走了 SwiftUI 原本应当拥有的状态所有权。与许多停留在情绪或偏好层面的“反 TCA”讨论不同,本文给出了一套完整、可运行的替代思路:将 State 视为纯数据、Logic 视为无状态的实现细节,并通过闭包(capabilities)暴露行为,从而在保持可测试性与可组合性的同时,更贴合 SwiftUI 的声明式数据流模型。

考虑到 TCA 刚出现的时期,很多开发者仍难以应对 ObservableObject 响应颗粒度不足、缺乏 @MainActorTask 等工具、异步调用高度依赖 Combine 的现实处境,TCA 确实有效解决了当时的一系列痛点。但随着 Swift 与 SwiftUI 的不断演进,TCA 中一些曾经突出的优势已不再明显,而其可组合性模型对不少开发者而言,使用与理解成本也依然不低。即便如此,TCA 仍然是许多开发者和团队已经适应并喜爱的开发范式——归根结底,适合自己的,才是最好的。

工具

FluidAudio: 为 Apple 平台打造的本地化音频 AI 工具包

FluidAudio 是一个专为 Apple 平台设计的 Swift 音频 AI SDK,提供完全本地化的语音识别、说话人分离、语音活动检测等功能,由 Brandon WengAlex Weng 主导开发。所有推理都在 Apple Neural Engine (ANE) 上运行,实现了低延迟、低功耗的音频处理能力。

核心特性:

  • 完全本地化:所有模型在设备端运行,无需网络连接,充分保护用户隐私
  • ANE 优化:充分利用 Apple Neural Engine,避免使用 GPU/MPS,降低 CPU 使用率和功耗
  • 开源透明:基于 MIT/Apache 2.0 许可的开源模型,可在 Hugging Face 获取
  • 易于集成:通过 Swift Package Manager 安装,API 设计简洁直观
  • 生产就绪:已被 Voice Ink、Spokenly、Slipbox、BoltAI 等知名应用采用

FluidAudio 支持自动语音识别(Parakeet TDT v3,25 种欧洲语言)、说话人分离(离线/在线双模式)、语音活动检测(Silero VAD)以及文字转语音(Beta)。在 M4 Pro 上,语音识别的实时系数可达约 190x,处理 1 小时音频仅需 19 秒。

如果你正在开发涉及语音处理的 iOS/macOS 应用,FluidAudio 绝对值得尝试。


Navigable: 统一 SwiftUI 导航管理

SwiftUI 中的一大痛点,在于如何将多级路由、sheet、full-screen cover 以及 alert 等不同导航与展示机制统一管理。由 Corey Davis 开发的 Navigable 提供了一种更集中、可测试的解决方案:它将所有导航行为统一收敛到一个可观察的 NavigationState 中,作为唯一的状态来源。

通过这种方式,Navigable 能够在保持 type-safe 与现代 Swift(@Observable、Swift 6) 特性的同时,将导航逻辑从视图中剥离出来,支持复杂流程、深链以及单元测试。同时,它既支持声明式的视图构建,也允许通过命令式 API(如 push / pop)驱动状态变化,在复杂场景下更接近传统路由系统的可控性。

该库仍处在开发早期,但其实现思路值得持续关注。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

❌