包瘦身-资源优化

资源文件通常占iOS应用包体积的40-60%,是优化的重要方向。本文介绍各种资源优化手段及其原理。 图片优化 Asset Catalog原理 Asset Catalog(.xcassets)编译后生成Assets.car文件,这是一个优化过的资源容器: 编译前: Images.xcassets/ ├── AppIcon.appiconset/ │ ├── icon-60@2x.png │ └── icon-60@3x.png ├── Logo.imageset/ │ ├── logo@2x.png │ └── logo@3x.png └── Contents.json 编译后: Assets.car ← 单一文件,包含所有资源 Assets.car的优化: 图片被重新编码,使用更高效的压缩 支持App Thinning,按设备分发 运行时高效加载(内存映射) 可以使用assetutil查看Assets.car内容: assetutil --info Assets.car 图片压缩原理 PNG压缩原理: PNG使用DEFLATE无损压缩,但可以通过以下方式减小体积: 原始PNG结构: ┌────────────────┐ │ PNG Header │ ├────────────────┤ │ IHDR (图像信息) │ ├────────────────┤ │ IDAT (图像数据) │ ← 压缩的像素数据 ├────────────────┤ │ 辅助chunks │ ← 可以移除 └────────────────┘ 优化方向: 1. 移除不必要的chunks(如tEXt、iTXt) 2. 优化DEFLATE压缩参数 3. 减少颜色数量(有损) 4. 使用更高效的滤波器 有损压缩原理(pngquant): ...

May 2, 2026

包瘦身-可执行文件优化

可执行文件通常占iOS应用包体积的30-50%,是优化的重点。本文介绍各种可执行文件优化手段及其原理。 编译器优化选项 优化级别原理 编译器优化级别控制了LLVM在编译时应用的优化Pass数量和类型。 什么是优化Pass? Pass是指编译器对代码进行的一次特定优化处理过程。LLVM将优化工作分解为多个独立的Pass,每个Pass负责一种特定类型的优化: 源代码 → 前端解析 → IR(中间表示)→ Pass1 → Pass2 → Pass3 → ... → 优化后的代码 → 机器码 常见的优化Pass包括: Pass名称 作用 Dead Code Elimination 删除永远不会执行的代码 Constant Folding 编译时计算常量表达式,如3+5直接变成8 Function Inlining 将函数调用替换为函数体本身 Loop Unrolling 展开循环,减少循环控制开销 Common Subexpression Elimination 消除重复计算的表达式 不同的优化级别启用不同数量和类型的Pass: # Build Settings Optimization Level = -Os # 级别:-Os 优化大小(推荐用于Release) -O0(无优化) 不应用任何优化Pass,编译速度最快。代码按原样生成,保留所有变量和控制流,便于调试。适用于Debug构建。 -O1(基础优化) 应用不显著增加编译时间的基础优化,包括: 基本的死代码消除 简单的常量折叠 基本块合并 死代码消除: 死代码消除(Dead Code Elimination)是指编译器自动移除永远不会被执行或执行结果不会被使用的代码。注意:这里的死代码消除是编译阶段的优化,作用于函数内部;而后文提到的Dead Code Stripping是链接阶段的优化,用于移除未被引用的整个函数或类。 -O1 适合需要一定优化但对编译速度敏感的场景。 -O2(标准优化) 应用大多数优化Pass,是性能和体积的平衡点。包括: ...

May 2, 2026

包瘦身-分析工具

在进行包体积优化之前,首先需要了解如何分析包体积。本文介绍常用的分析工具和Mach-O文件结构。 查看IPA构成 解压IPA文件分析各部分大小: # 解压IPA unzip -q YourApp.ipa -d ./ipa_content # 查看各文件大小 find ./ipa_content -type f -exec ls -lh {} \; | sort -k5 -h -r | head -20 LinkMap分析 LinkMap文件记录了链接器生成可执行文件的详细信息,可以精确分析二进制大小: # Build Settings中设置 Write Link Map File = YES Path to Link Map File = $(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt LinkMap文件结构 # Path: /path/to/YourApp # Arch: arm64 # Object files: [ 0] linker synthesized [ 1] /path/to/ViewController.o [ 2] /path/to/Model.o # Sections: # Address Size Segment Section 0x100001000 0x00012345 __TEXT __text 0x100013345 0x00001234 __TEXT __stubs # Symbols: # Address Size File Name 0x100001000 0x00000100 [ 1] -[ViewController viewDidLoad] 0x100001100 0x00000050 [ 2] -[Model init] 解析脚本 可以使用脚本解析LinkMap统计各模块大小: ...

May 2, 2026

包瘦身-App Thinning

App Thinning是Apple提供的包体积优化技术,让用户只下载适合其设备的内容。本文详细介绍App Thinning的三个组成部分及其原理。 整体架构 开发者上传: ┌─────────────────────────────────────────┐ │ Universal IPA │ │ ├── arm64 代码 │ │ ├── @1x/@2x/@3x 图片 │ │ ├── iPhone/iPad 资源 │ │ └── Metal GPU资源(OpenGL ES已弃用) │ └─────────────────────────────────────────┘ ↓ App Store处理 ↓ ┌───────────┐ ┌──────────┐ ┌──────────┐ │ iPhone 12 │ │ iPhone 8 │ │ iPad Pro │ │ 变体 │ │ 变体 │ │ 变体 │ │ @3x图片 │ │ @2x图片 │ │ @2x图片 │ │ arm64 │ │ arm64 │ │ arm64 │ │ A14 GPU │ │ A11 GPU │ │ M1 GPU │ └───────────┘ └──────────┘ └──────────┘ App Thinning包含三个部分: ...

May 2, 2026

类型擦除

类型擦除(Type Erasure)是一种编程技术,用于在运行时隐藏具体的类型信息,使得不同类型可以通过统一的接口进行操作。在iOS开发中,Objective-C和Swift都有类型擦除的概念,但实现方式和应用场景有所不同。 Swift中的类型擦除 Swift是一门强类型语言,类型信息在编译时被严格检查。但在某些场景下,我们需要隐藏具体类型信息,这时就需要使用类型擦除技术。 存在类型与不透明类型 在理解类型擦除之前,需要先理解 Swift 类型系统中两个关键概念:存在类型(Existential Type) 和不透明类型(Opaque Type)。它们是协议在类型层面的两种使用方式,也是理解 any 和 some 关键字的基础。 存在类型(Existential Type) “存在类型"这个术语来自类型论中的存在量词(∃)。它表达的语义是:“存在某个类型 T 遵循了协议 P,但我不知道也不关心 T 具体是什么。” 当你把一个协议当作类型来使用时(而非泛型约束),你就在使用存在类型: protocol Animal { func makeSound() -> String } struct Dog: Animal { func makeSound() -> String { "Woof!" } } struct Cat: Animal { func makeSound() -> String { "Meow!" } } // animals 数组中的每个元素都是存在类型 // 编译器不知道每个元素的具体类型,只知道"存在某个类型遵循了 Animal" var animals: [any Animal] = [Dog(), Cat()] 存在类型的核心特征: 运行时多态:具体类型在运行时才确定,同一个变量可以在不同时刻持有不同的具体类型 有性能开销:Swift 通过存在容器(Existential Container)来存储值,方法调用需要通过见证表间接派发 类型信息被隐藏:调用方只能通过协议接口与值交互,无法访问具体类型的特有成员 在 Swift 5.6 之前,直接把协议写成类型(如 let x: Animal)就是存在类型,但语法上没有任何标记。Swift 5.6 引入 any 关键字使其变得显式,目的是提醒开发者这里存在运行时开销。Swift 5.7 起,对于带有关联类型的协议,必须使用 any 才能作为存在类型。 ...

May 2, 2026

卡顿

卡顿是影响用户体验的关键问题之一。当应用无法在一帧的时间内(通常为16.67ms)完成渲染工作时,就会出现掉帧,用户会感知到界面不流畅。 本系列文章从渲染原理入手,系统性地介绍iOS卡顿的检测与优化方法。 卡顿监控是 APM 流畅性子系的核心能力,线上指标体系、检测采集、上报策略请参考 APM 系列:指标体系、数据采集、业界方案(微信 Matrix / 字节 Slardar 卡死监控)。 什么是卡顿 iOS设备的屏幕刷新率通常为60Hz(ProMotion设备可达120Hz),这意味着每秒需要渲染60帧画面。每一帧的渲染时间窗口约为16.67ms(1000ms / 60)。 当CPU或GPU无法在这个时间窗口内完成工作时,就会发生掉帧(Frame Drop),用户会感知到卡顿。 掉帧情况 用户感知 严重程度 偶尔掉1-2帧 几乎无感知 轻微 连续掉帧 明显卡顿 中等 长时间阻塞(>250ms) 界面冻结 严重 渲染流程概述 iOS的渲染流程涉及CPU和GPU的协作,通过Core Animation Pipeline完成: flowchart TB subgraph APP["App 进程(CPU 阶段)"] direction TB Layout["Layout(布局计算)layoutSubviews、约束计算"] Display["Display(绘制显示)drawRect:、文本绘制"] Prepare["Prepare(准备)图片解码、格式转换"] Commit["Commit(提交)打包图层树"] Layout --> Display --> Prepare --> Commit end subgraph RS["Render Server 进程(GPU 阶段)"] direction TB Decode["解码图层树"] Vertex["顶点着色器"] Raster["光栅化"] Fragment["片段着色器"] Buffer["帧缓冲区写入"] Decode --> Vertex --> Raster --> Fragment --> Buffer end APP -->|"IPC 传输"| RS RS --> VSync["VSync 信号到来"] VSync --> Screen["显示到屏幕"] CPU与GPU的职责 阶段 主要工作 常见瓶颈 CPU 布局计算、视图创建、文本计算、图片解码 复杂布局、大量文本、主线程阻塞 GPU 纹理渲染、图层混合、离屏渲染 离屏渲染、大量透明图层、超大图片 卡顿的常见原因 CPU瓶颈 主线程阻塞:耗时操作在主线程执行 复杂布局:大量约束计算、频繁布局 文本计算:复杂富文本、大量文本渲染 图片解码:大图片在主线程解码 对象创建:大量对象的创建和销毁 GPU瓶颈 离屏渲染:圆角、阴影、遮罩触发离屏渲染 图层混合:大量透明图层叠加 超大图片:纹理尺寸超过GPU限制 复杂特效:模糊、滤镜等GPU密集操作 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 2, 2026

卡顿-离屏渲染

离屏渲染(Offscreen Rendering)是GPU渲染中的性能杀手。本文介绍离屏渲染的原理、触发条件以及优化方案。 什么是离屏渲染 正常渲染流程 正常情况下,GPU直接将渲染结果写入帧缓冲区(Frame Buffer): flowchart LR subgraph 正常渲染["正常渲染 On-Screen Rendering"] A[GPU渲染] --> B[Frame Buffer帧缓冲区] --> C[屏幕显示] end style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#e8f5e9 特点: 直接渲染到帧缓冲区 渲染完成即可显示 效率最高 离屏渲染流程 离屏渲染需要先渲染到离屏缓冲区,再合成到帧缓冲区: flowchart LR subgraph 离屏渲染["离屏渲染 Offscreen Rendering"] A[GPU渲染] --> B[Offscreen Buffer离屏缓冲区] B --> C[Frame Buffer帧缓冲区] C --> D[屏幕显示] end E[额外的缓冲区] -.-> B style A fill:#e1f5fe style B fill:#f48fb1 style C fill:#fff3e0 style D fill:#e8f5e9 style E fill:#f48fb1,stroke:#f44336,stroke-dasharray: 5 5 性能开销: ...

May 2, 2026

布局方法详解

本文详细介绍 iOS 中三种布局方式(Frame、Auto Layout、UIStackView)的原理和对比,视图更新的三个阶段(约束、布局、绘制),UIView 与 CALayer 的关系和区别,以及 updateConstraints、layoutSubviews、setNeedsLayout、layoutIfNeeded、setNeedsDisplay 等相关方法的作用、调用时机和区别。 iOS 布局方式 iOS 提供了三种主要的布局方式,按出现时间排列: 布局方式 引入版本 核心思想 API Frame 布局 iOS 2 直接指定视图的位置和大小 frame、bounds、center Auto Layout iOS 6 通过约束描述视图间关系,系统自动计算 frame NSLayoutConstraint、Anchor API UIStackView iOS 9 线性排列子视图,自动管理约束 UIStackView Frame 布局 直接通过设置 frame 属性来确定视图的位置和大小,是最基础的布局方式。 let label = UILabel() label.frame = CGRect(x: 20, y: 100, width: 200, height: 44) view.addSubview(label) 优点:性能最好(无需约束求解),逻辑直观,完全可控。 缺点:需要手动计算所有数值,难以适配不同屏幕尺寸和动态内容,维护成本高。 适用场景:简单固定布局、性能敏感的场景(如 UITableViewCell 内大量视图手动布局)、需要频繁更新 frame 的动画。 Auto Layout Auto Layout 是基于 约束(Constraint) 的布局系统。开发者不直接设置 frame,而是描述视图之间的关系(如"A 的左边距 B 右边 16pt"),系统通过求解约束方程组自动计算出每个视图的 frame。 ...

May 2, 2026

卡顿-图片优化

图片处理是iOS应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。 图片解码原理 为什么需要解码 图片文件(PNG、JPEG等)是压缩格式,无法直接显示。GPU需要的是位图(Bitmap)格式: flowchart LR A[压缩图片PNG/JPEG] --> B[解码DecodeCPU密集操作] B --> C[位图BitmapGPU可直接使用] 位图大小 = 宽度 × 高度 × 每像素字节数 例如:1000×1000 RGBA图片 = 1000 × 1000 × 4 = 4MB 默认解码时机 // 加载图片(此时未解码) let image = UIImage(named: "large_image") // 设置到ImageView(仍未解码) imageView.image = image // 在 CATransaction commit 的 prepare 阶段,Core Animation 会在主线程对未解码的图片执行解码 // 未提前解码的图片一定会在此阶段被解码,从而阻塞主线程引发卡顿 异步解码 基本原理 将解码工作移到后台线程: sequenceDiagram participant M as 主线程 participant B as 后台线程 M->>B: 请求加载图片 Note over M: 继续处理其他事件 B->>B: 加载压缩数据 B->>B: 解码为位图 B->>B: 创建CGImage B->>M: 返回解码后的图片 M->>M: 更新UI 实现方案1:强制解码 extension UIImage { /// 强制解码图片 func decodedImage() -> UIImage? { guard let cgImage = self.cgImage else { return nil } let width = cgImage.width let height = cgImage.height // 创建位图上下文 let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { return nil } // 绘制到上下文(触发解码) context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) // 从上下文创建新图片 guard let decodedCGImage = context.makeImage() else { return nil } return UIImage(cgImage: decodedCGImage, scale: scale, orientation: imageOrientation) } } // 异步使用 func loadImageAsync(named name: String, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = UIImage(named: name)?.decodedImage() DispatchQueue.main.async { completion(image) } } } 实现方案2:使用ImageIO import ImageIO class ImageDecoder { static func decodeImage(from url: URL) -> UIImage? { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } let options: [CFString: Any] = [ kCGImageSourceShouldCache: true, kCGImageSourceShouldCacheImmediately: true // 立即解码 ] guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else { return nil } return UIImage(cgImage: cgImage) } static func decodeImageAsync(from url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = ImageDecoder.decodeImage(from: url) DispatchQueue.main.async { completion(image) } } } } 实现方案3:使用UIGraphicsImageRenderer extension UIImage { func decodedImageUsingRenderer() -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = true format.preferredRange = .standard let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in draw(at: .zero) } } } 图片降采样 为什么需要降采样 当图片尺寸远大于显示尺寸时,加载原图是浪费: ...

May 2, 2026

iOS多线程编程

iOS多线程方案对比 方案 简介 语言 生命周期管理 pthread POSIX标准的多线程API C 手动管理 NSThread 面向对象的线程封装 OC/Swift 手动管理 GCD Grand Central Dispatch C/OC/Swift 自动管理 NSOperation 基于GCD的面向对象封装 OC/Swift 自动管理 pthread pthread是POSIX标准的多线程API,是最底层的多线程方案,使用C语言编写。 #import <pthread.h> void *threadFunction(void *param) { NSLog(@"pthread执行任务: %@", [NSThread currentThread]); return NULL; } - (void)createPthread { pthread_t thread; pthread_create(&thread, NULL, threadFunction, NULL); } 特点: 跨平台,可移植性强 使用复杂,需要手动管理线程生命周期 实际开发中很少直接使用 NSThread NSThread是苹果对pthread的面向对象封装,使用更加简单。 创建线程的方式 // 方式1:实例方法创建,需要手动启动 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doTask) object:nil]; thread.name = @"MyThread"; [thread start]; // 方式2:类方法创建,自动启动 [NSThread detachNewThreadSelector:@selector(doTask) toTarget:self withObject:nil]; // 方式3:隐式创建 [self performSelectorInBackground:@selector(doTask) withObject:nil]; 常用方法 // 获取当前线程 NSThread *currentThread = [NSThread currentThread]; // 获取主线程 NSThread *mainThread = [NSThread mainThread]; // 判断是否是主线程 BOOL isMain = [NSThread isMainThread]; // 线程休眠 [NSThread sleepForTimeInterval:2.0]; [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; // 退出当前线程 [NSThread exit]; 线程间通信 // 回到主线程执行 [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO]; // 在指定线程执行 [self performSelector:@selector(doTask) onThread:thread withObject:nil waitUntilDone:NO]; GCD(Grand Central Dispatch) GCD是苹果推出的多线程解决方案,基于C语言实现,自动管理线程的生命周期。GCD的核心概念是 队列(Queue) 和任务(Task)。 ...

May 2, 2026