普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月13日掘金 iOS

🚫求求你别再手动改类名了!Swift 自动混淆脚本上线,4.3 头发保卫战正式开始!

作者 StarkCoder
2025年12月12日 18:50
🚫求求你别再手动改类名了!Swift 自动混淆脚本上线,4.3 头发保卫战正式开始! 最近又被苹果爸爸 4.3 拿捏了吗? 是不是已经习惯了以下这些「灵魂折磨」: 为了上架不得不手动画几个类名 ——
昨天以前掘金 iOS

Kingfisher 深度指南:Swift 生态下的高性能图片处理艺术

作者 sweet丶
2025年12月11日 23:22
引言:为什么需要专门的图片加载库? 在移动应用开发中,图片加载是影响用户体验的核心环节之一。一个优秀的图片加载库需要解决多个复杂问题:异步下载、内存缓存、磁盘缓存、图片解码、动画支持、列表优化等。在

iOS逆向-哔哩哔哩增加3倍速(1)-最大播放速度

作者 TouchWorld
2025年12月11日 08:02

前言

作为一名 哔哩哔哩的重度用户,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮‍💨。

修改前效果: Screenshot 2025-12-11 at 07.26.05.png

刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。

修改后效果: Screenshot 2025-12-11 at 07.22.57.png

由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力

场景

哔哩哔哩的视频播放页面

8C0CA5C5-C2D9-4E60-9BB0-0FF3B83A03DA-5346442.png

开发环境

  • 哔哩哔哩版本:8.41.0
  • MonkeyDev
  • IDA Professional 9.0
  • 安装IDA插件:patching
  • Lookin

目标

视频最大播放速度改为4倍速播放

分析

  • 我们知道哔哩哔哩开源了他们的视频播放器ijkplayer,我们可以从中了解到设置播放速度的方法,是IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate
@interface IJKFFMoviePlayerController : NSObject 

@property (nonatomic) float playbackRate;

@end

  • Mach-O文件导出的IJKFFMoviePlayerControllerOC头文件可以知道,它有一个maxPlaybackRate属性,应该是最大播放速度
// IJKFFMoviePlayerController.h
@interface IJKFFMoviePlayerController : NSObject  {
    /* instance variables */
    id  _player;
}

...
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
  • 我们hook IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate,添加断点并打印一些日志,其中就有打印maxPlaybackRate
    • inputPlaybackRate:要设置的播放速度
    • changedPlaybackRate:更改后的播放速度
    • realPlaybackRate:真实播放速度
    • maxPlaybackRate:最大播放速度
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 每次设置播放速度,IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate的断点都会触发,我们从日志中可以看到,最大播放速度为3.0
cxzcxz:IJKFFMoviePlayerController-0x280519220-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:1.500000-changedPlaybackRate:1.500000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
  • 因为倍速面板无法选择超过2.0的速度,所以我们就设置,如果倍速面板选择的是2.0,我们就改成4.0,看是否会限制播放速度到3.0
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 倍速面板选择2.0,从日志中可以看到,播放速度(changedPlaybackRate)改成了3.0而不是4.0,这也证明maxPlaybackRate就是最大播放速度
cxzcxz:IJKFFMoviePlayerController-0x281304380-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
  • 我们尝试hook maxPlaybackRategetter方法,看能不能修改最大播放速度?
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

- (float)maxPlaybackRate {
    return 4.0;
}

%end
  • 从日志可以看到,虽然maxPlaybackRate的输出值改成了4.0,但是播放速度(changedPlaybackRate)还是3.0,所以修改无效
cxzcxz:IJKFFMoviePlayerController-0x2808e2a40-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000
  • 我们从IDA中查看IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现调用的是[self->_player setPlaybackRate:]方法
void __cdecl -[IJKFFMoviePlayerController setPlaybackRate:](IJKFFMoviePlayerController *self, SEL a2, float a3)
{
  -[IJKMediaPlayback setPlaybackRate:](self->_player, "setPlaybackRate:");
}
  • 我们给IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate,添加断点,看看self->_player的值是什么?

C72ACE3B-EA9B-4684-A7AB-71C162A27F1E.png

  • 断点触发,发现self->_player的类型是IJKFFMoviePlayerControllerFFPlay
(lldb) po self->_player

  • Mach-O文件导出的IJKFFMoviePlayerControllerFFPlayOC头文件可以知道,IJKFFMoviePlayerControllerFFPlay是一个视频播放器,看来IJKFFMoviePlayerController是基于IJKFFMoviePlayerControllerFFPlay实现的
@interface IJKFFMoviePlayerControllerFFPlay : NSObject  {
    /* instance variables */
    struct IjkMediaPlayer * _mediaPlayer;
...
@property (weak, nonatomic) IJKMediaPlayerItem *item;
@property (retain, nonatomic) id  fileOpenDelegate;
@property (retain, nonatomic) id  segmentOpenDelegate;
@property (readonly, nonatomic) double fpsInMeta;
@property (readonly, nonatomic) double fpsAtOutput;
@property (nonatomic) _Bool shouldShowHudView;
@property (readonly, nonatomic) long long numberOfBytesTransferred;
@property (nonatomic) _Bool allowsMediaAirPlay;
@property (nonatomic) _Bool isDanmakuMediaAirPlay;
@property (readonly, nonatomic) _Bool airPlayMediaActive;
@property (readonly, nonatomic) int isSeekBuffering;
@property (readonly, nonatomic) int isAudioSync;
@property (readonly, nonatomic) int isVideoSync;
@property (readonly, nonatomic) int currentVideoIdentifier;
@property (readonly, nonatomic) int currentAudioIdentifier;
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
  • 我们从IDA中查看IJKFFMoviePlayerControllerFFPlay- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现跟开源版本的IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate是一致的
// 哔哩哔哩版本
void __cdecl -[IJKFFMoviePlayerControllerFFPlay setPlaybackRate:](
        IJKFFMoviePlayerControllerFFPlay *self,
        SEL a2,
        float a3)
{
  sub_10F3F5768(
    0LL,
    32LL,
    "IJKFFMoviePlayerControllerFFPlay: setPlaybackRate ts = %lld, playbackRate = %f\n",
    +[IJKFFUtils getIjkTickHR](&OBJC_CLASS___IJKFFUtils, "getIjkTickHR"),
    a3);
  if ( self->_mediaPlayer )
    sub_10F0BAFD4(a3);
}
// 开源版本
- (void)setPlaybackRate:(float)playbackRate
{
    if (!_mediaPlayer)
        return;

    return ijkmp_set_playback_rate(_mediaPlayer, playbackRate);
}
  • 我们从IDA中查看sub_10F0BAFD4的伪代码实现,发现跟开源版本的ijkmp_set_playback_rate的实现是一致的
// 哔哩哔哩版本
__int64 __fastcall sub_10F0BAFD4(__int64 a1, float a2)
{
  printf("%s(%f)\n", "ijkmp_set_playback_rate", a2);
  pthread_mutex_lock((pthread_mutex_t *)(a1 + 8));
  sub_10F0A70B4(*(_QWORD *)(a1 + 136), a2);
  pthread_mutex_unlock((pthread_mutex_t *)(a1 + 8));
  return printf("%s()=void\n", "ijkmp_set_playback_rate");
}
// 开源版本
void ijkmp_set_playback_rate(IjkMediaPlayer *mp, float rate)
{
    assert(mp);

    MPTRACE("%s(%f)\n", __func__, rate);
    pthread_mutex_lock(&mp->mutex);
    ffp_set_playback_rate(mp->ffplayer, rate);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("%s()=void\n", __func__);
}
  • 我们从IDA中查看sub_10F0A70B4的伪代码实现,可以知道是从sub_10F101034获取最大播放速度的
__int64 __fastcall sub_10F0A70B4(__int64 result, float a2)
{
  __int64 v3; // x19
  float v4; // s0
  __int64 v5; // x8
  float v6; // s1
  float v7; // [xsp+1Ch] [xbp-24h] BYREF

  if ( result )
  {
    v3 = result;
    v7 = 0.0;
    sub_10F101034(10LL, &v7, 4LL);
    if ( v7 < a2 )
    {
      sub_10F3F5768(0LL, 32LL, "%s: origin %f, new %f \n", "adjust_playback_rate", a2, v7);
      a2 = v7;
    }
    sub_10F3F5768((__int64 *)v3, 32LL, "Playback rate: %f\n", a2);
    if ( a2 == 0.0 )
      v4 = 1.0;
    else
      v4 = a2;
    if ( v4 != 1.0 )
      *(_DWORD *)(v3 + 884) = 1;
    v5 = *(_QWORD *)(v3 + 8);
    if ( v5 )
    {
      v6 = *(float *)(v3 + 876);
      if ( v6 != v4 )
        *(double *)(v5 + 8040) = vabds_f32(v6, v4);
    }
    if ( v4 > *(float *)(v3 + 6724) )
      *(float *)(v3 + 6724) = v4;
    *(float *)(v3 + 876) = v4;
    *(_DWORD *)(v3 + 880) = 1;
    return sub_10F100F98(8LL, v3 + 9272);
  }
  return result;
}
  • 我们从IDA中查看sub_10F101034的伪代码实现,再根据sub_10F101034(10LL, &v7, 4LL); 知道参数a1=10,a3=4,将伪代码和参数交给chatgpt分析,得出下面几个结论:
    • sub_10F10449C(a9) 计算最大播放速度
    • 该函数内部会将计算结果放入某个寄存器(例如 d0
    • 解码器将其反编译成 v14
    • LABEL_38 → v13 = v14
    • 最终写入:*(float*)a2 = v13
void __fastcall sub_10F101034(
        int a1,
        double *a2,
        __int64 a3,
        __int64 a4,
        __int64 a5,
        __int64 a6,
        __int64 a7,
        __int64 a8,
        _QWORD *a9,
        __int64 a10,
        unsigned int a11,
        __int64 a12)
{
  float v13; // s0
  double v14; // d0
  __int64 v15; // x20
  int v16; // w0
  __int64 *v17; // [xsp+18h] [xbp-68h] BYREF
  __int64 v18; // [xsp+20h] [xbp-60h]
  __int64 v19; // [xsp+30h] [xbp-50h]
  unsigned int *v20; // [xsp+58h] [xbp-28h]

  if ( a2 && a3 )
  {
    switch ( a1 )
    {
...
      case 10:
        if ( a3 == 4 )
        {
          v17 = &a10;
          sub_10F10449C((unsigned int)a9);
          goto LABEL_38;
        }
        break;
...
LABEL_38:
            v13 = v14;
          }
LABEL_44:
          *(float *)a2 = v13;
        }
        break;
...
  • 我们从IDA中查看sub_10F10449C的伪代码实现,发现有可能返回两个值,一个是2.0,一个是全局变量qword_117084280的值
double __fastcall sub_10F10449C(__int64 a1)
{
  __int64 v2; // x0
  double result; // d0
  double v4[6]; // [xsp+0h] [xbp-40h] BYREF

  v2 = sub_10F104600();
  sub_10F104B7C(v4, v2, a1);
  result = 2.0;
  if ( v4[0] < 50.0 )
    return *(double *)&qword_117084280;
  return result;
}
  • 我们查看全局变量qword_117084280的值,发现是3.0,而3.0就是最大播放速度
    • qword_11708428016进制值

      0000000117084280  00 00 00 00 00 00 08 40  00 00 00 00 00 00 00 00
      
    • 关键是前 8 字节:

      00 00 00 00 00 00 08 40
      
    • ARM64Mach-Odouble 存储方式都是 little-endian,所以按小端序解析:

      40 08 00 00 00 00 00 00
      
    • IEEE-754 双精度格式:

      0x40080000000000003.0double
    • 所以qword_117084280 = 3.0

  • 我们添加符号断点sub_10F10449C,看它的返回值多少

C291B52E-537E-494A-A8C4-8332634DEC40.png

  • sub_10F10449C断点触发,返回值发现是3.0,这样就验证了全局变量qword_117084280的值是3.0
(lldb) register read d0
      d0 = 3
  • 总结:哔哩哔哩的最大播放速度方法是sub_10F10449C,最大播放速度有可能是2倍速,也可能是3倍速,猜测跟视频有关。

越狱解决方案

我们hook sub_10F10449C,将最大值改为4.0

// 视频最大播放速度
public let maxPlaybackRateValue = 4.0

// 声明原函数类型
public typealias orig_get_max_playback_rate_type = @convention(c) (_ a1: Int64) -> Double

// 定义全局函数指针变量,并绑定一个 C 名字
@_silgen_name("orig_get_max_playback_rate")
nonisolated(unsafe) public var orig_get_max_playback_rate: orig_get_max_playback_rate_type? = nil

// 获取最大播放速度方法
@_cdecl("my_get_max_playback_rate")
func my_get_max_playback_rate(a1: Int64) -> Double {
    return maxPlaybackRateValue
}
// 获取最大播放速度方法
long long get_max_playback_rate_address = g_slide+0x10F10449C;
NSLog(@"[%@] cal func get_max_playback_rate address:0x%llx", nj_logPrefix, get_max_playback_rate_address);
MSHookFunction((void *)get_max_playback_rate_address,
   (void*)my_get_max_playback_rate,
   (void**)&orig_get_max_playback_rate);
  • 我们再hook IJKFFMoviePlayerControllerFFPlay- (void)setPlaybackRate:(float)playbackRate方法,用以打印日志
%hook IJKFFMoviePlayerControllerFFPlay

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 倍速面板选择2.0,从日志中可以看到,最大播放速度(maxPlaybackRate)改成了4.0,播放速度(changedPlaybackRate)改成了4.0,👍
cxzcxz:IJKFFMoviePlayerControllerFFPlay-0x136428000-_logos_method$App$IJKFFMoviePlayerControllerFFPlay$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:4.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000

非越狱解决方案

修改Mach-O文件的汇编指令

  • sub_10F10449C方法的伪代码可知,sub_10F10449C方法返回的就是最大播放速度
double __fastcall sub_10F10449C(__int64 a1)
{
  __int64 v2; // x0
  double result; // d0
  double v4[6]; // [xsp+0h] [xbp-40h] BYREF

  v2 = sub_10F104600();
  sub_10F104B7C(v4, v2, a1);
  result = 2.0;
  if ( v4[0] < 50.0 )
    return *(double *)&qword_117084280;
  return result;
}
  • 修改sub_10F10449C方法的实现,改为类似下面的伪代码

    return 4.0;
    
    • 对应的汇编指令就是

      FMOV            D0, #4.0
      RET
      
  • 我们修改sub_10F10449C方法的前两条汇编指令,改为下面

FMOV            D0, #4.0
RET
  • 示例

    修改第一条汇编指令

    • 鼠标点击000000010F10449C

      __text:000000010F10449C                 SUB             SP, SP, #0x50
      
    • 右键选择Assemble F5144010-91BD-48E8-8E73-75223FF2C980.png

    • 改成

      FMOV    D0, #4.0
      

    3428FB04-36CC-43E8-B362-89CF02D7FFC6.png

    • 点击enter
  • 全部修改结果

F808A0ED-9C78-4589-80A0-76BD2FCF1F8A.png

  • 保存到Mach-O文件中

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

    542DF73E-FA15-41B7-A251-31E30F37EB38.png

    0D71933B-BB78-48A6-AEA2-A12AC7261586.png

  • 保存后,底部会显示log

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

F1E26F23-6F74-4DEC-B1CD-1256372F5FBE.png

代码

BiliBiliMApp-无广告版哔哩哔哩

相关链接

哔哩哔哩的视频播放器:ijkplayer

SDWebImage深度解析:高效图片加载背后的架构设计与卓越实践

作者 sweet丶
2025年12月10日 23:48

在移动应用开发中,图片加载和缓存是影响用户体验的关键环节。SDWebImage作为iOS平台上最受欢迎的图片加载库,以其高性能、丰富的功能和稳定的表现赢得了全球开发者的信赖。本文将深入探讨SDWebImage的核心原理、架构设计,并解答实际使用中的常见问题。

一、整体架构设计:模块化与职责分离

为了让你能迅速抓住核心,我们先用一张图,从宏观视角看懂它的工作原理与核心流程。

deepseek_mermaid_20251210_9277be.png

SDWebImage采用了清晰的分层架构设计,将复杂的图片加载过程分解为独立的模块,各司其职:

1. 核心协调者:SDWebImageManager

这是SDWebImage的"大脑",负责协调缓存查找、下载和图片处理流程。它使用组合模式将SDImageCacheSDWebImageDownloader组合在一起,实现了对图片加载全生命周期的管理。

2. 缓存模块:SDImageCache 负责内存和磁盘缓存的双层存储。内存缓存基于NSCache实现,提供快速访问;磁盘缓存将图片持久化存储到文件系统中,支持自定义缓存策略。

3. 下载模块:SDWebImageDownloader 基于NSURLSession构建的异步下载器,支持并发控制、请求优先级设置和身份验证等高级功能。它通过操作队列管理下载任务,确保高效利用网络资源。

4. 解码与处理模块 负责图片解码、缩放、裁剪等后处理操作。SDWebImage在后台线程执行这些操作,避免阻塞主线程。

5. 视图扩展:UIImageView+WebCache 为UIKit组件提供的便捷接口,开发者可以通过一行代码实现图片的异步加载和缓存管理。

这种模块化设计不仅使代码结构清晰,还提高了库的可扩展性和可维护性。

二、图片解码机制:后台线程的高效处理

iOS系统在渲染图片时需要将其解码为位图格式,这个过程默认在主线程进行,可能导致界面卡顿。SDWebImage对此进行了重要优化:

解码时机与线程策略 SDWebImage在图片从磁盘加载或网络下载完成后,立即在后台线程进行解码。解码器使用专门的NSOperationQueue,避免了解码任务阻塞主线程。

空间换时间的缓存策略 解码后的位图数据会被缓存到内存中。当同一图片再次请求时,可以直接使用缓存的解码结果,无需重复解码,显著提升了性能。

渐进式解码支持 对于网络下载的大图片,SDWebImage支持渐进式解码。图片在下载过程中逐步显示,用户可以更快地看到图片内容,提升等待体验。

三、缓存机制:智能的双层存储系统

SDWebImage的缓存系统是其高性能的核心保障:

1. 内存缓存(Memory Cache) 基于NSCache实现,具有自动清理机制。当系统内存紧张时,NSCache会自动释放部分缓存。默认情况下,SDWebImage不限制内存缓存大小,但支持通过totalCostLimitcountLimit进行自定义限制。

2. 磁盘缓存(Disk Cache) 图片以文件形式存储在Cache目录中,文件名经过MD5哈希处理,确保唯一性和安全性。

3. 默认最大缓存大小 可设置SDWebImage的磁盘缓存大小为100MB。当缓存超过此限制时,SDWebImage会基于文件的最后访问时间进行清理,优先移除最久未访问的图片。这一设置可以在SDImageCacheConfig中自定义。

四、缓存清理机制:灵活的资源管理

SDWebImage提供了多种缓存清理方式,满足不同场景的需求:

1. 自动清理机制

  • 基于时间的清理:默认配置下,SDWebImage会自动清理超过一周的缓存文件
  • 基于大小的清理:当缓存超过设定的大小时,自动清理最旧的图片文件

2. 手动清理接口

// 清理所有内存缓存
[[SDImageCache sharedImageCache] clearMemory];

// 清理所有磁盘缓存(异步)
[[SDImageCache sharedImageCache] clearDiskOnCompletion:nil];

// 清理过期的磁盘缓存(异步)
[[SDImageCache sharedImageCache] deleteOldFilesWithCompletionBlock:nil];

3. 细粒度控制 开发者可以针对特定URL或key清理缓存,实现更精准的缓存管理。

五、动态图支持:从GIF到现代动画格式

SDWebImage对动态图的支持经历了显著的演进:

早期方案 早期版本通过将GIF分解为帧序列,使用UIImage的动画API播放,这种方式内存占用高且功能有限。

现代方案:SDAnimatedImage协议 从SDWebImage 5.0开始,引入了SDAnimatedImage协议,提供了统一的动画图片接口,支持多种格式:

  • GIF:完整的解码和播放支持
  • APNG:Apple原生支持的动画格式
  • WebP:Google的高效图片格式(需要额外编码器)
  • HEIC:高效的现代图片格式

内存优化 SDAnimatedImageView采用惰性解码策略,仅解码当前显示和预加载的帧,大幅降低了内存占用。开发者还可以通过maxBufferSize属性控制缓冲帧数,在流畅度和内存消耗之间找到平衡。

六、视图可见性触发加载:按需加载的智能策略

在列表等场景中,实现"视图出现在屏幕才开始加载图片"是提升性能的关键。SDWebImage与UIKit的协同工作实现了这一目标:

UITableView/UICollectionView的优化加载

// 在cellForRowAtIndexPath中设置图片
- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@&#34;Cell&#34;];
    
    // 获取图片URL
    NSURL *imageURL = [self imageURLForIndexPath:indexPath];
    
    // 使用SDWebImage加载图片
    [cell.imageView sd_setImageWithURL:imageURL
                      placeholderImage:[UIImage imageNamed:@&#34;placeholder&#34;]
                             completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
        // 加载完成处理
    }];
    
    return cell;
}

// 在prepareForReuse中取消未完成的加载
- (void)prepareForReuse {
    [super prepareForReuse];
    [self.imageView sd_cancelCurrentImageLoad];
}

工作原理

  1. 当cell准备显示时,tableView:cellForRowAtIndexPath:被调用,开始图片加载
  2. 如果cell滚出屏幕,prepareForReuse会被调用,取消未完成的图片加载
  3. SDWebImage内部会管理加载队列,优先处理可见cell的请求

高级优化技巧

// 1. 预加载:提前加载即将显示的图片
- (void)tableView:(UITableView *)tableView 
  willDisplayCell:(UITableViewCell *)cell 
forRowAtIndexPath:(NSIndexPath *)indexPath {
    // 预加载下一批图片
    if (indexPath.row + 5 < [self.dataSource count]) {
        NSURL *preloadURL = [self imageURLForIndexPath:[NSIndexPath indexPathForRow:indexPath.row + 5 inSection:indexPath.section]];
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[preloadURL]];
    }
}

// 2. 设置不同的加载优先级
SDWebImageOptions options = SDWebImageLowPriority | SDWebImageProgressiveLoad;
[cell.imageView sd_setImageWithURL:imageURL placeholderImage:nil options:options];

七、静态图与动态图的选择策略

在实际开发中,我们经常面临选择:使用UIImageView还是SDAnimatedImageView?以下是明确的指导原则:

1. 静态图片场景

  • 使用标准的UIImageView
  • 通过sd_setImageWithURL:方法加载
  • 性能最佳,内存占用最低

2. 动态图片场景

  • 使用SDAnimatedImageView
  • 通过sd_setImageWithURL:方法加载(SDWebImage会自动检测图片类型)
  • 支持GIF、APNG、WebP等多种动态格式

3. 未知图片类型的处理策略 当不确定图片是静态还是动态时,推荐采用以下方案:

// 方案1:统一使用SDAnimatedImageView(推荐)
// 优点:自动适应所有图片类型,代码简洁
// 缺点:对静态图有轻微性能开销
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
[imageView sd_setImageWithURL:imageURL];

// 方案2:根据URL或响应头动态选择
// 在知道图片类型的情况下优化性能
if ([url.pathExtension isEqualToString:@&#34;gif&#34;] || 
    [url.absoluteString containsString:@&#34;animated&#34;]) {
    // 使用SDAnimatedImageView
    SDAnimatedImageView *animatedImageView = [SDAnimatedImageView new];
    [animatedImageView sd_setImageWithURL:url];
} else {
    // 使用普通UIImageView
    UIImageView *staticImageView = [UIImageView new];
    [staticImageView sd_setImageWithURL:url];
}

// 方案3:使用SDWebImage的自动检测功能
// SDWebImage 5.0+ 可以自动检测图片类型并选择合适的视图
UIImageView *imageView = [UIImageView new];
SDWebImageOptions options = SDWebImageAutoHandleAnimatedImage;
[imageView sd_setImageWithURL:url options:options];

性能对比与建议

场景 推荐视图 内存占用 CPU使用 兼容性
已知静态图 UIImageView 最佳
已知动态图 SDAnimatedImageView 中等 中等 最佳
未知类型 SDAnimatedImageView 中等 中等 最佳
大量静态图列表 UIImageView 最佳

最佳实践建议

  1. 对于图片社交应用(如Instagram),用户上传内容类型未知,建议统一使用SDAnimatedImageView
  2. 对于电商应用,商品主图大多是静态图,使用UIImageView即可
  3. 在性能敏感的场景(如大规模图片列表),可以考虑先获取图片元信息再决定视图类型

八、高级功能与定制扩展

自定义缓存策略

// 创建自定义缓存配置
SDImageCacheConfig *config = [SDImageCacheConfig defaultCacheConfig];
config.maxDiskAge = 7 * 24 * 60 * 60; // 一周
config.maxDiskSize = 200 * 1024 * 1024; // 200MB
config.maxMemoryCost = 100 * 1024 * 1024; // 100MB内存缓存
config.diskCacheExpireType = SDImageCacheConfigExpireTypeAccessDate; // 按访问时间过期

// 创建自定义缓存实例
SDImageCache *customCache = [[SDImageCache alloc] initWithNamespace:@&#34;Custom&#34; diskCacheDirectory:customPath config:config];

图片转换器

// 创建圆角图片转换器
SDImageRoundCornerTransformer *transformer = [SDImageRoundCornerTransformer transformerWithRadius:10 corners:UIRectCornerAllCorners borderWidth:1 borderColor:[UIColor whiteColor]];

// 加载时应用转换器
[imageView sd_setImageWithURL:url placeholderImage:nil options:0 context:@{SDWebImageContextImageTransformer: transformer}];

九、性能监控与调试

内存使用监控

// 获取缓存统计信息
NSUInteger memCost = [[SDImageCache sharedImageCache] totalMemoryCost];
NSUInteger diskCount = [[SDImageCache sharedImageCache] totalDiskCount];
NSUInteger diskSize = [[SDImageCache sharedImageCache] totalDiskSize];

// 监控图片加载性能
[SDWebImageManager.sharedManager setCacheKeyFilter:^NSString * _Nullable(NSURL * _Nullable url) {
    // 记录加载时间
    CFTimeInterval startTime = CACurrentMediaTime();
    return [url absoluteString];
}];

十、总结与展望

SDWebImage通过其精良的架构设计,在图片加载的各个关键环节都做了深度优化。从后台解码到智能缓存,从动态图支持到可见性触发加载,每一个设计决策都体现了对性能与用户体验的极致追求。

随着iOS开发技术的演进,SDWebImage也在不断发展。未来,我们期待看到:

  1. 对Swift Concurrency的更好支持
  2. 与SwiftUI的更深度集成
  3. 对新型图片格式的更快适配
  4. 更智能的缓存预取和淘汰算法

无论你是刚刚接触SDWebImage的新手,还是希望深入优化图片加载性能的资深开发者,理解SDWebImage的核心原理都将帮助你构建更流畅、更高效的iOS应用。

Swift 迭代三巨头(下集):Sequence、Collection 与 Iterator 深度狂飙

2025年12月10日 10:12

在这里插入图片描述

🛡️ 第一重防线:破解 AsyncSequence 的错误恢复漏洞

异步序列的 “错误恢复漏洞”,本质是没搞懂AsyncSequence的错误传播规则 —— 就像 F1 赛车的刹车系统没校准,一踩就抱死,一松就失控。

next()抛出错误时,默认会直接终止迭代,可如果粗暴重试,又会导致 “重复接收元素”;如果不重试,又会丢失关键数据。

在本堂F1调教课中,您将学到如下内容:

  • 🛡️ 第一重防线:破解 AsyncSequence 的错误恢复漏洞
  • 错误传播的 “底层逻辑”
  • 精准重试:给迭代器加 “记忆功能”
  • 实战效果:再也不重复,再也不丢失
  • 🛡️ 第二重防线:给 SafeRingBuffer 加 “数据校验锁”
  • 校验逻辑:哈希值是 “数据身份证”
  • 升级后的 SafeRingBuffer(核心校验代码)
  • 效果:“迭代异常兽” 的篡改彻底失效
  • 🏆 终局:组合拳粉碎 “迭代异常兽”
  • 完整流程代码(终局方案)
  • 剧情收尾:赛道恢复平静,迭代真相揭晓
  • 📝 终极总结:Swift 迭代的 “黄金法则”

艾拉和杰西要做的,就是找到 “精准重试” 的平衡点。

在这里插入图片描述


错误传播的 “底层逻辑”

先搞懂一个关键:AsyncSequence的错误是 “终止性的”—— 一旦next()抛出错误,整个迭代就会停止,就像赛车引擎爆缸后再也没法前进。比如传感器断连抛出SensorDisconnectErrorfor try await循环会立刻跳出,进入catch块,后续的元素再也接收不到。

看这个 “踩坑示例”:

// 错误示范:粗暴重试导致重复数据
Task {
    do {
        for try await data in SensorSequence() {
            sensorBuffer.enqueue(data)
            print("接收数据:\(data)")
        }
    } catch is SensorDisconnectError {
        // 断连后直接重试,却没记录“已接收的元素ID”
        print("传感器断连,重试中...")
        await retrySensorSequence() // 重试时会重新接收之前已处理的元素
    }
}

这段代码的问题在于:重试时会生成全新的异步迭代器,它不知道之前已经接收过哪些元素,导致 “旧数据重复入队”—— 这正是 “迭代异常兽” 想要的结果。

精准重试:给迭代器加 “记忆功能”

杰西的解决方案是:自定义一个RetryableSensorSequence,给异步迭代器加 “元素 ID 记忆”,重试时跳过已处理的元素,就像赛车在维修后重回赛道,能精准接上之前的位置继续跑。

在这里插入图片描述

// 带“记忆功能”的可重试异步序列
struct RetryableSensorSequence: AsyncSequence {
    typealias Element = SensorData // 传感器数据(含唯一ID和校验码)
    typealias AsyncIterator = RetryableSensorIterator
    
    private let baseSequence: SensorSequence // 原始传感器序列
    private var processedIDs: Set<String> = [] // 记录已处理的元素ID(防重复)
    private let maxRetries: Int = 3 // 最大重试次数(避免无限循环)
    private var currentRetry: Int = 0 // 当前重试次数

    init(baseSequence: SensorSequence) {
        self.baseSequence = baseSequence
    }

    func makeAsyncIterator() -> RetryableSensorIterator {
        RetryableSensorIterator(
            baseIterator: baseSequence.makeAsyncIterator(),
            processedIDs: &processedIDs,
            maxRetries: maxRetries,
            currentRetry: &currentRetry
        )
    }

    // 带记忆功能的异步迭代器
    struct RetryableSensorIterator: AsyncIteratorProtocol {
        typealias Element = SensorData
        
        private var baseIterator: SensorSequence.AsyncIterator
        private var processedIDs: inout Set<String> // 引用外部的已处理ID集合
        private let maxRetries: Int
        private var currentRetry: inout Int

        mutating func next() async throws -> SensorData? {
            do {
                guard let data = try await baseIterator.next() else {
                    return nil // 序列正常结束
                }
                // 关键:检查元素ID是否已处理,避免重复
                guard !processedIDs.contains(data.id) else {
                    return try await next() // 跳过重复元素,继续获取下一个
                }
                processedIDs.insert(data.id) // 记录已处理的ID
                return data
            } catch is SensorDisconnectError {
                // 达到最大重试次数,抛出最终错误
                guard currentRetry < maxRetries else {
                    currentRetry = 0 // 重置重试次数,方便后续复用
                    throw error // 重试失败,终止迭代
                }
                currentRetry += 1
                print("第\(currentRetry)次重试传感器连接...")
                // 重建基础迭代器(重新连接传感器)
                self.baseIterator = SensorSequence().makeAsyncIterator()
                // 递归调用next(),继续迭代(不重复已处理元素)
                return try await next()
            } catch {
                // 其他错误(比如数据格式错误),直接抛出
                throw error
            }
        }
    }
}

实战效果:再也不重复,再也不丢失

艾拉把这个 “带记忆的序列” 集成到系统后,效果立竿见影:

// 正确姿势:用可重试序列消费传感器数据
Task {
    do {
        let retryableSequence = RetryableSensorSequence(baseSequence: SensorSequence())
        for try await data in retryableSequence {
            sensorBuffer.enqueue(data)
            print("安全接收数据:\(data)(ID:\(data.id))")
        }
    } catch {
        print("最终失败:\(error),已触发备用传感器")
    }
}

当传感器断连时,序列会自动重试(最多 3 次),且重试后绝不会重复接收旧数据 —— 因为processedIDs会牢牢记住 “哪些数据已经处理过”。就像赛车在维修区快速换胎后,能精准回到赛道的正确位置,既不落后,也不跑偏。

在这里插入图片描述

🛡️ 第二重防线:给 SafeRingBuffer 加 “数据校验锁”

解决了重复问题,下一个目标是 “数据篡改”——“迭代异常兽” 会修改传感器数据的校验码,让错误数据混入缓冲区。杰西的方案是:给SafeRingBuffer升级,在 “入队” 和 “访问” 时双重校验数据完整性,就像给赛车装 “指纹锁”,不是自己人的数据,一概不让进。

校验逻辑:哈希值是 “数据身份证”

在这里插入图片描述

传感器数据会自带一个checksum(哈希值),计算规则是 “数据内容 + 时间戳” 的 MD5 值。SafeRingBuffer在入队时要验证这个哈希值,不匹配就拒绝入队;在访问时再二次校验,确保数据没被篡改。

升级后的 SafeRingBuffer(核心校验代码)

struct SafeRingBuffer<Element: DataVerifiable>: Collection {
    // 新增约束:Element必须遵守DataVerifiable协议(有校验能力)
    private var storage: [Element?]
    private var head = 0
    private var tail = 0
    private(set) var count = 0
    private let lock = NSLock()

    // 入队时校验:篡改的数据直接拒之门外
    mutating func enqueue(_ element: Element) throws {
        lock.lock()
        defer { lock.unlock() }

        // 关键:验证数据哈希值,不匹配则抛出“数据篡改错误”
        guard element.verifyChecksum() else {
            throw DataTamperingError.invalidChecksum(
                "数据校验失败,ID:\(element.id),可能被篡改"
            )
        }

        storage[tail] = element
        tail = (tail + 1) % storage.count

        if count == storage.count {
            head = (head + 1) % storage.count
        } else {
            count += 1
        }
    }

    // 下标访问时二次校验:防止缓冲区内部数据被篡改
    subscript(position: Index) -> Element {
        lock.lock()
        defer { lock.unlock() }

        precondition((0..<count).contains(position), "索引超出范围")
        let actualPosition = (head + position) % storage.count
        guard let element = storage[actualPosition] else {
            preconditionFailure("缓冲区数据丢失,位置:\(actualPosition)")
        }

        // 二次校验:确保数据在缓冲区中没被篡改
        precondition(element.verifyChecksum(), "缓冲区数据被篡改,ID:\(element.id)")
        return element
    }
}

// 数据校验协议:所有需要校验的数据都要遵守
protocol DataVerifiable {
    var id: String { get } // 唯一ID
    var checksum: String { get } // 数据哈希值
    // 校验方法:计算当前数据的哈希值,和自带的checksum对比
    func verifyChecksum() -> Bool
}

// 传感器数据实现校验协议
extension SensorData: DataVerifiable {
    func verifyChecksum() -> Bool {
        // 计算“内容+时间戳”的MD5哈希值(真实项目中建议用更安全的SHA256)
        let calculatedChecksum = "\(content)-\(timestamp)".md5()
        return calculatedChecksum == self.checksum
    }
}

// 自定义错误:数据篡改错误
enum DataTamperingError: Error {
    case invalidChecksum(String)
}

在这里插入图片描述

效果:“迭代异常兽” 的篡改彻底失效

当 “迭代异常兽” 试图把篡改后的传感器数据(校验码不匹配)入队时,enqueue会直接抛出DataTamperingError,错误数据连缓冲区的门都进不了;就算它想偷偷修改缓冲区里的数据,subscript访问时的二次校验也会触发preconditionFailure,立刻暴露问题。

艾拉测试时故意注入一条篡改数据,系统瞬间弹出警告:“DataTamperingError:数据校验失败,ID:sensor_123,可能被篡改”—— 就像赛车的防盗系统检测到非法入侵,立刻锁死引擎,让 “小偷” 无从下手。

🏆 终局:组合拳粉碎 “迭代异常兽”

解决了错误恢复和数据篡改,艾拉和杰西打出最后一套 “组合拳”:用RetryableSensorSequence处理异步错误,用SafeRingBuffer做数据缓存和校验,再配合一个 “监控 Task” 实时监控序列状态 —— 三者联动,形成无死角的防御网。

在这里插入图片描述

完整流程代码(终局方案)

// 1. 创建带校验的环形缓冲区(容量10,只存合法数据)
var verifiedBuffer = SafeRingBuffer<SensorData>(capacity: 10)

// 2. 创建可重试的传感器序列(防断连、防重复)
let sensorSequence = SensorSequence()
let retryableSequence = RetryableSensorSequence(baseSequence: sensorSequence)

// 3. 主Task:消费序列,存入缓冲区
let mainTask = Task {
    do {
        for try await data in retryableSequence {
            do {
                try verifiedBuffer.enqueue(data)
                print("成功入队:ID=\(data.id),转速=\(data.engineRPM)转")
                // 实时更新仪表盘(只传合法数据)
                await dashboard.update(with: verifiedBuffer[verifiedBuffer.count - 1])
            } catch DataTamperingError.invalidChecksum(let message) {
                print("拦截篡改数据:\(message)")
                // 触发警报,记录日志
                await alertSystem.triggerLevel(.high, message: message)
            }
        }
    } catch {
        print("迭代终止:\(error)")
        // 重试失败,切换到备用传感器
        await switchToBackupSensor()
    }
}

// 4. 监控Task:实时检查缓冲区状态,防止异常
let monitorTask = Task {
    while !Task.isCancelled {
        guard verifiedBuffer.count > 0 else {
            print("警告:缓冲区为空,可能传感器无数据")
            await alertSystem.triggerLevel(.low, message: "缓冲区空")
            try await Task.sleep(nanoseconds: 1_000_000_000)
            continue
        }
        // 每1秒检查一次最新数据的时效性(防止数据过期)
        let latestData = verifiedBuffer[verifiedBuffer.count - 1]
        if Date().timeIntervalSince(latestData.timestamp) > 5 {
            print("警告:最新数据已过期,可能序列卡顿")
            await alertSystem.triggerLevel(.medium, message: "数据过期")
        }
        try await Task.sleep(nanoseconds: 1_000_000_000)
    }
}

剧情收尾:赛道恢复平静,迭代真相揭晓

当这套组合拳部署完成后,赛车仪表盘的转速数据瞬间稳定下来 ——10000 转、10020 转、9980 转,每一个数字都精准跳动,控制室的警报声渐渐平息。艾拉看着屏幕上 “所有传感器正常” 的绿色提示,长舒一口气:“‘迭代异常兽’被打跑了?”

杰西笑着点开日志,里面全是 “拦截篡改数据”“重试成功” 的记录:“不是打跑,是它再也没法钻漏洞了。你看 ——AsyncSequence 的核心是‘错误可控’,Collection 的核心是‘数据可信’,迭代器的核心是‘状态独立’,只要守住这三个核心,再狡猾的问题也能解决。”

在这里插入图片描述

突然,仪表盘弹出一条新消息:“检测到外部干扰源已断开连接”——“迭代异常兽” 彻底消失了。

赛道上,F1 赛车重新加速,引擎的轰鸣声再次变得均匀有力;屏幕前,艾拉和杰西相视一笑,他们不仅修复了系统,更摸清了 Swift 迭代的 “底层逻辑”。

📝 终极总结:Swift 迭代的 “黄金法则”

  1. Sequence 是 “入门契约”:只承诺 “能迭代一次”,适合懒加载、生成器场景,像 F1 的 “练习赛”—— 灵活但不追求稳定。
  2. Collection 是 “进阶契约”:多轮迭代、索引访问、数据稳定,适合需要反复操作的数据,像 F1 的 “正赛”—— 稳定且高效。
  3. AsyncSequence 是 “异步契约”:支持暂停、抛错,适合数据流场景,但要注意 “错误终止性” 和 “重试防重复”,像 F1 的 “夜间赛”—— 更复杂,但有专属的应对策略。
  4. 迭代器的 “值语义优先”:尽量用 struct 实现迭代器,避免共享可变状态,就像赛车的 “独立操控系统”—— 互不干扰,安全可控。

在这里插入图片描述

最后记住:Swift 的迭代看似简单,实则是 “协议驱动” 的精妙设计。当你写下for item in list时,背后是 Sequence、Collection、Iterator 的协同作战 —— 就像 F1 赛车的引擎、刹车、底盘完美配合,才能跑出最快的速度,也才能写出最稳定、最高效的代码。

在这里插入图片描述

那么,宝子们看到这里学到了吗?

感谢观赏,下次我们再会吧!8-)

Swift 迭代三巨头(上集):Sequence、Collection 与 Iterator 深度狂飙

2025年12月10日 10:08
🏁 引子:赛道惊魂!迭代引擎的致命故障 赛道上的引擎轰鸣震耳欲聋,天才 Swift 工程师艾拉紧盯着赛车数据面板,额角的冷汗浸透了队服 —— 连续三次,实时处理赛车传感器数据的系统在迭代时突然宕机,就
❌
❌