编译优化-观测

“无法度量就无法优化”。在开始任何编译优化动作之前,必须先建立一套可重复、可对比的观测手段,否则改动的真实收益无从谈起。本文介绍 iOS 编译耗时观测的主要工具链和原理。 观测目标分层 不同层次的观测工具回答不同的问题: flowchart TD A[编译耗时观测] --> B[整体耗时] A --> C[阶段耗时] A --> D[任务级耗时] A --> E[函数/表达式级] B --> B1[xcodebuild 总耗时] C --> C1[Build Timing Summary] C --> C2[Pod Install 阶段计时] D --> D1[Build Timeline] D --> D2[XCLogParser] E --> E1[-debug-time-compilation] E --> E2[-warn-long-expression] 观测层次 典型问题 工具 整体 一次构建花了多久? time xcodebuild、MetricKit 阶段 哪个阶段最慢? -showBuildTimingSummary、Build Timing Summary 任务 哪个文件/目标最慢? Xcode Build Timeline、XCLogParser 函数级 哪个函数/表达式让前端卡住? -debug-time-function-bodies、-warn-long-expression-type-checking 整体耗时 xcodebuild 命令行计时 最简单也最稳定的方式是直接给 xcodebuild 加 time: ...

May 5, 2026

编译优化-Explicit Modules

Apple 从 Xcode 15 开始在 Swift 上引入 Explicit Modules,Xcode 16 在 C/C++/Objective-C 上全面铺开,Xcode 17 进一步默认启用并与细粒度依赖追踪结合。这是近五年 Apple 构建系统最重要的一次变革,直接影响到大部分项目的编译模型。 模块(Module)基础 为什么需要模块 C 家族语言的 #include 机制有两个致命问题: 文本替换:头文件是纯文本替换,每个源文件都要重新解析一遍所有包含的头文件 宏污染:先引入的头文件宏会影响后续头文件的行为,没有隔离 Clang 在 2012 年引入 Clang Modules,用一个预编译的二进制模块(.pcm)替代文本包含,同一 module 在一次构建里只解析一次。Swift 的 .swiftmodule 从设计之初就是模块化的。 flowchart LR A["源文件"] -->|"include 头文件"| B["Foundation.h 文本"]; B --> C["每次都重新解析"]; D["源文件"] -->|"@import Foundation"| E["Foundation.pcm 预编译"]; E --> F["一次解析,多次复用"]; 模块的组成 类型 载体 描述 Clang Module .pcm C/OC 模块的序列化 AST Swift Module .swiftmodule Swift 模块的接口 + SIL Swift Interface .swiftinterface 可被不同 Swift 版本解析的文本接口 Module Map module.modulemap 描述哪些头文件构成某个 module Implicit Modules 的问题 原理 在 Xcode 16 之前,Clang Modules 以 隐式 方式工作: ...

May 5, 2026

死锁(Deadlock)原理、常见场景与治理

死锁是 iOS 多线程开发中最隐蔽、也最致命的一类稳定性问题。它不像越界、空指针那样"立刻崩溃给你看",而是表现为主线程"卡住"——用户能感受到界面无响应、无法交互,最终被 iOS watchdog 强杀(0x8BADF00D),落在 Apple 崩溃日志里归为 EXC_CRASH (SIGKILL) 或卡死(Hang)。 本文系统梳理死锁的四个必要条件、iOS 生产环境中常见的发生场景,并结合 Apple 官方工具(TSan、Main Thread Checker、MetricKit)与业界最新实践(Sentry App Hangs、字节 Heimdallr、Swift Concurrency 的反思)讨论如何检测与治理。 一、死锁是什么 死锁(Deadlock)指两个或多个线程因争夺资源(锁、队列、信号量等)而互相等待,导致所有相关线程永久阻塞、无法推进的状态。 与死锁容易混淆的几个概念: 名称 核心特征 典型例子 死锁 Deadlock 多个线程循环等待对方持有的资源,永久阻塞 线程 A 持 lockA 等 lockB;线程 B 持 lockB 等 lockA 活锁 Livelock 线程不阻塞但也不推进,不停重试互相让步 两个线程都检测到冲突就回退重试,永远撞在一起 饥饿 Starvation 低优先级线程长期分不到资源 高优先级线程持续抢占 CPU,导致低优先级线程持锁永远运行不完 优先级反转 Priority Inversion 低优先级持锁 + 高优先级自旋等锁 + 中优先级抢 CPU,高优先级被间接阻塞 OSSpinLock 被废弃的根本原因 死循环 Busy Loop 单线程进入无限循环,CPU 占用 100% while(true) 忘记 break 卡顿 Hang 主线程执行时间超过阈值(几百 ms~几秒) 主线程做网络、解压大图、Core Data 大量 fetch 卡死 Watchdog 主线程卡住数秒(启动约 20s、前后台切换约 10s),触发系统强杀 死锁导致的卡死是最严重的一种 关键区别:死循环线程 CPU 占用高、处于 RUNNING 态;死锁线程 CPU 占用为 0、处于 WAITING/BLOCKED 态并被换出。这一点正是线上死锁自动判定的核心依据(见"检测"章节)。 ...

May 4, 2026

崩溃

崩溃是影响App稳定性的最严重问题。当应用发生崩溃时,用户会被强制退出应用,严重影响用户体验和产品口碑。 本系列文章系统性地介绍iOS崩溃的原理、采集方案以及治理策略。 什么是崩溃 崩溃(Crash)是指应用程序因为异常或错误而被强制终止的现象。从技术角度看,崩溃可以分为以下几类: 崩溃类型 触发原因 典型场景 Mach异常 底层硬件/内核异常 访问无效内存、除零错误 Unix信号 系统信号导致进程终止 SIGABRT、SIGSEGV、SIGBUS NSException OC层未捕获的异常 数组越界、unrecognized selector 内存问题 OOM或内存损坏 内存不足、野指针 Watchdog 系统监控超时 启动超时、后台任务超时 崩溃率指标 崩溃率是衡量App稳定性的核心指标: 崩溃率计算方式: 1. 崩溃用户率(推荐) 崩溃用户率 = 发生崩溃的用户数 / 总活跃用户数 × 100% 2. 崩溃次数率 崩溃次数率 = 崩溃次数 / 启动次数 × 100% 3. 崩溃Session率 崩溃Session率 = 崩溃Session数 / 总Session数 × 100% 崩溃处理的整体架构 ┌─────────────────────────────────────────────────────────────┐ │ 崩溃处理架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃发生 │ │ │ │ Mach异常 / Unix信号 / NSException / OOM │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃捕获 │ │ │ │ • Mach异常处理 │ │ │ │ • Signal Handler │ │ │ │ • NSSetUncaughtExceptionHandler │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 信息采集 │ │ │ │ • 堆栈信息 │ │ │ │ • 设备信息 │ │ │ │ • 用户上下文 │ │ │ │ • 崩溃现场 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 本地存储 │ │ │ │ • 写入文件(需要异步安全) │ │ │ │ • 避免使用OC/Swift API │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 上报分析 │ │ │ │ • 下次启动时上报 │ │ │ │ • 符号化解析 │ │ │ │ • 聚合分析 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 4, 2026

Swift 宏

Swift 5.9(WWDC 2023)引入的宏(Macro)是近几年语言层最大的一次能力扩展。它让 Swift 第一次具备了编译期代码生成的官方机制——你可以在源码层面写一段"能生成代码的代码",编译器会在类型检查前把它展开成真正的 Swift 源码,再走正常的编译流程。SwiftUI 的 #Preview、Observation 框架的 @Observable、SwiftData 的 @Model、Swift Testing 的 @Test / #expect、FoundationModels 的 @Generable / @Tool,全都是宏。 一、为什么要有宏 1.1 Swift 之前能做什么,又差在哪 在宏之前,Swift 已经有一整套"元编程"手段,但每一种都有明显的边界: 机制 做什么 局限 @propertyWrapper 给属性注入行为(如 @Published、@State) 只能包属性,不能生成方法、不能生成类型、不能访问其他成员 @resultBuilder 把一段 DSL 收集成表达式(如 SwiftUI 的 ViewBuilder) 只作用于闭包里,无法改类型定义 KeyPath + Mirror 运行时反射读写属性 只能读,类型安全差,性能差(见 iOS反射) Codable 等编译器魔法 自动合成 init(from:) / encode(to:) 只有编译器内部能写,开发者写不出同等能力的东西 Sourcery / SwiftGen 文本级代码生成 游离于编译器之外,无类型信息、无法访问 AST Objective-C #define 纯文本替换 没有作用域、没有类型、没有卫生(hygiene),对调试器和 IDE 极不友好 宏就是要一次性补齐这块能力:在保留类型安全与 IDE 体验的前提下,把"只有 Swift 编译器团队能写的代码生成"开放给所有开发者。 ...

May 4, 2026

计算机网络

网络分层模型 OSI七层模型 OSI(Open Systems Interconnection)是国际标准化组织提出的网络通信参考模型: 层级 名称 功能 协议/设备举例 7 应用层 为应用程序提供网络服务 HTTP, FTP, DNS, SMTP 6 表示层 数据格式转换、加密/解密 SSL/TLS, JPEG, ASCII 5 会话层 建立、管理和终止会话 RPC, SQL 4 传输层 端到端的可靠数据传输 TCP, UDP 3 网络层 路由选择与IP寻址 IP, ICMP, ARP 2 数据链路层 帧的封装与MAC寻址 Ethernet, Wi-Fi 1 物理层 比特流的物理传输 光纤, 双绞线 TCP/IP四层模型 实际工程中更常用的是TCP/IP四层模型,它将OSI的上三层合并为应用层,下两层合并为网络接口层: graph TB subgraph "TCP/IP四层模型" A["应用层HTTP, DNS, FTP, SMTP"] B["传输层TCP, UDP"] C["网络层IP, ICMP, ARP"] D["网络接口层Ethernet, Wi-Fi"] end subgraph "OSI七层模型" E["应用层"] F["表示层"] G["会话层"] H["传输层"] I["网络层"] J["数据链路层"] K["物理层"] end A --- E A --- F A --- G B --- H C --- I D --- J D --- K 数据封装与解封装 数据在发送方从上到下逐层封装,每一层都将上层传来的数据视为载荷(Payload),在其前面添加本层的头部信息(部分层还会添加尾部信息),然后交给下一层处理。接收方则从下到上逐层解封装,每一层剥离本层头部后将载荷交给上层。 ...

May 3, 2026

static关键字详解

基础概念 static 是 C 语言定义的存储类说明符(Storage Class Specifier),Objective-C 和 Swift 中均有使用,但语义有所扩展。它的核心作用可以归纳为两点: 改变存储位置:将局部变量从栈区移到数据段,使其在函数返回后依然存活 限制链接可见性:将全局变量或函数的链接属性从 external 改为 internal,使其仅在当前编译单元(.m 文件)内可见 flowchart TD A["static 关键字"] --> B["修饰局部变量"] A --> C["修饰全局变量"] A --> D["修饰函数"] A --> E["Swift 中的 static"] B --> B1["存储位置:栈 → 数据段"] B --> B2["生命周期:函数作用域 → 整个进程"] B --> B3["作用域不变:仍限于函数内"] C --> C1["存储位置不变:仍在数据段"] C --> C2["链接属性:external → internal"] C --> C3["作用域缩小:全局 → 当前文件"] D --> D1["存储位置不变:仍在代码段"] D --> D2["链接属性:external → internal"] D --> D3["作用域缩小:全局 → 当前文件"] 一、静态局部变量 基本用法 在函数或方法内部用 static 修饰局部变量,使其只初始化一次,后续调用保留上次的值。 ...

May 3, 2026

崩溃-Mach异常

本文深入介绍Mach异常的底层实现细节,包括端口操作、消息机制以及如何实现自定义的Mach异常处理器。 关于Mach异常的基础概念(Task、Thread、Port、Message)和异常端口的查找顺序,请参考 崩溃-原理。 Mach端口详解 端口权限 端口权限类型: ┌─────────────────────────────────────────────────────────────┐ │ 端口权限 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ MACH_PORT_RIGHT_SEND │ │ ├── 发送权限 │ │ ├── 可以向端口发送消息 │ │ └── 可以复制给其他任务 │ │ │ │ MACH_PORT_RIGHT_RECEIVE │ │ ├── 接收权限 │ │ ├── 可以从端口接收消息 │ │ ├── 只能有一个持有者 │ │ └── 持有者销毁时端口销毁 │ │ │ │ MACH_PORT_RIGHT_SEND_ONCE │ │ ├── 一次性发送权限 │ │ └── 发送一次后自动失效 │ │ │ │ MACH_PORT_RIGHT_PORT_SET │ │ ├── 端口集合权限 │ │ └── 可以同时监听多个端口 │ │ │ │ MACH_PORT_RIGHT_DEAD_NAME │ │ ├── 死名权限 │ │ └── 端口已销毁但名字仍存在 │ │ │ └─────────────────────────────────────────────────────────────┘ 端口操作 #include <mach/mach.h> // 分配新端口 mach_port_t port; kern_return_t kr = mach_port_allocate( mach_task_self(), // 当前任务 MACH_PORT_RIGHT_RECEIVE, // 接收权限 &port // 输出端口名 ); // 添加发送权限 kr = mach_port_insert_right( mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND // 创建发送权限 ); // 销毁端口 kr = mach_port_destroy(mach_task_self(), port); // 释放端口权限 kr = mach_port_deallocate(mach_task_self(), port); Mach消息 消息结构 // 消息头部 typedef struct { mach_msg_bits_t msgh_bits; // 消息类型和权限 mach_msg_size_t msgh_size; // 消息大小 mach_port_t msgh_remote_port; // 目标端口 mach_port_t msgh_local_port; // 本地端口(用于回复) mach_port_name_t msgh_voucher_port;// 凭证端口 mach_msg_id_t msgh_id; // 消息ID } mach_msg_header_t; // 完整消息示例 typedef struct { mach_msg_header_t header; // 可选的消息体 mach_msg_body_t body; // 可选的端口描述符 mach_msg_port_descriptor_t port; // 可选的数据 char data[256]; } MyMessage; 消息发送与接收 // 发送消息 kern_return_t send_message(mach_port_t port, const char *data, size_t len) { struct { mach_msg_header_t header; char data[256]; } message; message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); message.header.msgh_size = sizeof(message); message.header.msgh_remote_port = port; message.header.msgh_local_port = MACH_PORT_NULL; message.header.msgh_id = 0; size_t copy_len = len < sizeof(message.data) ? len : sizeof(message.data); memcpy(message.data, data, copy_len); return mach_msg( &message.header, MACH_SEND_MSG, // 发送 sizeof(message), // 发送大小 0, // 接收大小 MACH_PORT_NULL, // 接收端口 MACH_MSG_TIMEOUT_NONE, // 超时 MACH_PORT_NULL // 通知端口 ); } // 接收消息 kern_return_t receive_message(mach_port_t port) { struct { mach_msg_header_t header; char data[256]; } message; kern_return_t kr = mach_msg( &message.header, MACH_RCV_MSG, // 接收 0, // 发送大小 sizeof(message), // 接收大小 port, // 接收端口 MACH_MSG_TIMEOUT_NONE, // 超时 MACH_PORT_NULL // 通知端口 ); if (kr == MACH_MSG_SUCCESS) { // 处理消息 printf("Received: %s\n", message.data); } return kr; } 异常端口操作 设置异常端口 #include <mach/mach.h> // 设置任务级异常端口 kern_return_t setup_task_exception_port(mach_port_t exception_port) { // 异常类型掩码 exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT | EXC_MASK_SOFTWARE; return task_set_exception_ports( mach_task_self(), mask, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); } // 获取当前异常端口 kern_return_t get_exception_ports(void) { exception_mask_t masks[EXC_TYPES_COUNT]; mach_msg_type_number_t count = EXC_TYPES_COUNT; mach_port_t ports[EXC_TYPES_COUNT]; exception_behavior_t behaviors[EXC_TYPES_COUNT]; thread_state_flavor_t flavors[EXC_TYPES_COUNT]; return task_get_exception_ports( mach_task_self(), EXC_MASK_ALL, masks, &count, ports, behaviors, flavors ); } 异常处理流程 完整的异常处理流程 flowchart TD A["1. 硬件检测到异常CPU执行非法操作,触发硬件中断"] --> B["2. 陷入内核CPU切换到内核态,执行异常处理程序"] B --> C["3. 内核构造异常消息创建包含异常信息的Mach消息"] C --> D["4. 发送到异常端口按照 Thread → Task → Host 顺序查找端口"] D --> E["5. 等待回复内核暂停线程,等待异常处理程序的回复"] E --> F{"6. 处理回复"} F -->|KERN_SUCCESS| G["恢复线程执行"] F -->|其他| H["转换为Unix信号或终止进程"] 异常消息格式 // 异常消息结构 typedef struct { mach_msg_header_t Head; // 消息体 mach_msg_body_t msgh_body; mach_msg_port_descriptor_t thread; // 发生异常的线程 mach_msg_port_descriptor_t task; // 发生异常的任务 // 异常信息 NDR_record_t NDR; exception_type_t exception; // 异常类型 mach_msg_type_number_t codeCnt; // 代码数量 int64_t code[2]; // 异常代码 } __Request__mach_exception_raise_t; // 异常回复消息 typedef struct { mach_msg_header_t Head; NDR_record_t NDR; kern_return_t RetCode; // 返回码 } __Reply__mach_exception_raise_t; 实现Mach异常处理器 完整示例 #include <mach/mach.h> #include <pthread.h> // 异常处理线程 static pthread_t exception_thread; static mach_port_t exception_port; // 保存原始异常端口 static mach_port_t original_exception_port; static exception_behavior_t original_behavior; static thread_state_flavor_t original_flavor; // 异常处理函数 static kern_return_t handle_exception( mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t code_count ) { // 获取线程状态(ARM64架构) // 注意:如需支持x86_64模拟器,需要使用x86_thread_state64_t和X86_THREAD_STATE64 #if defined(__arm64__) arm_thread_state64_t state; mach_msg_type_number_t state_count = ARM_THREAD_STATE64_COUNT; thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, &state_count); // 记录崩溃信息 printf("Exception: %d\n", exception); printf("Code: 0x%llx, 0x%llx\n", code[0], code[1]); printf("PC: 0x%llx\n", (unsigned long long)state.__pc); printf("LR: 0x%llx\n", (unsigned long long)state.__lr); #endif // 采集堆栈(需要自行实现) // collect_backtrace(thread); // 保存崩溃日志(需要自行实现) // save_crash_log(); // 返回失败,让系统继续处理(转换为信号) return KERN_FAILURE; } // 异常处理线程主函数 static void *exception_handler_thread(void *arg) { kern_return_t kr; while (1) { // 接收异常消息 struct { mach_msg_header_t head; mach_msg_body_t body; mach_msg_port_descriptor_t thread; mach_msg_port_descriptor_t task; NDR_record_t NDR; exception_type_t exception; mach_msg_type_number_t code_count; int64_t code[2]; int flavor; mach_msg_type_number_t state_count; natural_t state[224]; } request; struct { mach_msg_header_t head; NDR_record_t NDR; kern_return_t ret_code; } reply; // 接收消息 kr = mach_msg( &request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); if (kr != MACH_MSG_SUCCESS) { continue; } // 处理异常 kr = handle_exception( exception_port, request.thread.name, request.task.name, request.exception, request.code, request.code_count ); // 发送回复 reply.head.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND_ONCE, 0); reply.head.msgh_size = sizeof(reply); reply.head.msgh_remote_port = request.head.msgh_remote_port; reply.head.msgh_local_port = MACH_PORT_NULL; // Mach IPC惯例:回复消息ID = 请求消息ID + 100 reply.head.msgh_id = request.head.msgh_id + 100; reply.NDR = NDR_record; reply.ret_code = kr; mach_msg( &reply.head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); } return NULL; } // 安装异常处理器 kern_return_t install_exception_handler(void) { kern_return_t kr; // 创建异常端口 kr = mach_port_allocate( mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port ); if (kr != KERN_SUCCESS) return kr; // 添加发送权限 kr = mach_port_insert_right( mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND ); if (kr != KERN_SUCCESS) return kr; // 保存原始异常端口 exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT; // 用于接收查询结果的数组 exception_mask_t masks_out[EXC_TYPES_COUNT]; mach_port_t ports_out[EXC_TYPES_COUNT]; exception_behavior_t behaviors_out[EXC_TYPES_COUNT]; thread_state_flavor_t flavors_out[EXC_TYPES_COUNT]; mach_msg_type_number_t count = EXC_TYPES_COUNT; kr = task_get_exception_ports( mach_task_self(), mask, // 输入:要查询的异常类型掩码 masks_out, // 输出:实际的掩码数组 &count, // 输入输出:数组大小 ports_out, // 输出:端口数组 behaviors_out, // 输出:行为数组 flavors_out // 输出:flavor数组 ); // 保存第一个有效的原始端口(简化处理) if (count > 0) { original_exception_port = ports_out[0]; original_behavior = behaviors_out[0]; original_flavor = flavors_out[0]; } // 设置新的异常端口 kr = task_set_exception_ports( mach_task_self(), mask, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); if (kr != KERN_SUCCESS) return kr; // 创建异常处理线程 pthread_create(&exception_thread, NULL, exception_handler_thread, NULL); return KERN_SUCCESS; } 调试器检测 调试器(如LLDB)通过在Task(任务/进程)级别注册异常端口来优先拦截异常。了解这一机制有助于理解为什么调试时某些崩溃行为会有所不同。 ...

May 3, 2026

JetsamEvent 日志解读

JetsamEvent 是 iOS 唯一由系统官方生成、且专门记录"因内存原因终止进程"的日志。它不记录线程堆栈,却完整刻画了发生强杀那一刻整机的内存分布——这使它成为 OOM 归因、阈值建模、后台生命周期治理最权威的数据源。 本文聚焦 JetsamEvent 日志本身:存放位置、文件结构、字段含义、reason 类型、案例解读、自动化批量解析脚本,以及机型内存上限/iOS 版本差异等实战要点。OOM 监控治理的整体方法论见 崩溃-治理。 1. Jetsam 与 JetsamEvent 是什么 iOS 通过 XNU 内核的 memorystatus 子系统(俗称 Jetsam)做整机内存管理: 前台/后台进程按 Jetsam 优先级排序; 当系统整体可用物理页(vm_page_free_count)或单个进程 phys_footprint 触发阈值时,按优先级杀掉进程; 被杀时,内核在 /var/mobile/Library/Logs/CrashReporter/ 生成 JetsamEvent-yyyy-MM-dd-HHmmss.ips 文件,并通过 DiagnosticsReporting 暴露给 设置 → 隐私与安全性 → 分析与改进 → 分析数据。 需要理解两个关键结论: JetsamEvent 不是单个进程的崩溃日志,而是整机一次 Jetsam 事件的快照。 一个文件里往往包含几十个进程条目,只有其中一个(或少数)被真正杀掉;其余进程是作为"当时整机内存画像"被附带记录。 JetsamEvent 不含线程堆栈。 它的价值在于告诉你"谁占用了多少内存"和"为什么被杀",而不是"在哪一行代码被杀"。堆栈需要由 App 自己的 Memory Dump / MetricKit 补齐。 2. 获取方式 场景 路径 真机设置页查看 设置 → 隐私与安全性 → 分析与改进 → 分析数据,文件名 JetsamEvent-*.ips Xcode 连接真机 Window → Devices and Simulators → 选中设备 → View Device Logs sysdiagnose 包 解压后位于 crashes_and_spins/*.ips 越狱设备 /var/mobile/Library/Logs/CrashReporter/JetsamEvent-*.ips 自动上报 App 下次启动时扫描沙盒外的 Apple 日志目录不可行,只能依赖 MetricKit 的 MXCrashDiagnostic/MXAppExitMetric(iOS 14+) ⚠️ iOS 沙盒下 App 无法直接读取 /var/mobile/Library/Logs/ 的原始 ips 文件,线上规模化采集必须走 MetricKit。JetsamEvent 原始日志主要用于研发期抓真机、或用户反馈场景的手工分析。 ...

May 3, 2026

包瘦身

包体积(IPA大小)是iOS应用的重要质量指标。App Store对蜂窝网络下载有200MB的提示阈值,超过这个大小会弹窗询问用户是否使用流量下载(iOS 13之前是强制要求Wi-Fi)。这个弹窗会增加用户的决策成本,影响下载转化率。因此,包体积优化对于用户获取和留存都有重要影响。 包格式介绍 iOS开发中常见的包格式有三种: 格式 扩展名 说明 .app 目录 应用程序包,是一个包含可执行文件和资源的目录结构 .xcarchive 目录 Xcode归档包,包含.app、dSYM符号文件、构建信息等 .ipa 文件 安装包,本质是ZIP压缩的.app,用于分发和安装 日常使用场景: 开发调试:Xcode直接将.app安装到模拟器或真机 测试分发:使用.ipa通过TestFlight、蒲公英、fir.im等平台分发给测试人员 App Store上架:通过Xcode或Transporter将.xcarchive导出并上传到App Store Connect(实际上传的是从xcarchive导出的IPA或直接上传xcarchive中的内容) .app(Application Bundle) .app是macOS/iOS应用的标准格式,实际上是一个目录(Finder中显示为单个文件)。包含: 可执行文件(Mach-O格式) Info.plist配置文件 资源文件(图片、音频、本地化文件等) Frameworks目录(动态库) PlugIns目录(App Extension) .xcarchive(Xcode Archive) .xcarchive是Xcode的归档格式,用于保存完整的构建产物。目录结构: YourApp.xcarchive/ ├── Products/ │ └── Applications/ │ └── YourApp.app # 应用程序包 ├── dSYMs/ │ └── YourApp.app.dSYM # 符号文件(用于崩溃解析) ├── Info.plist └── SCMBlueprint/ # 源码管理信息 主要用途: 保存dSYM符号文件,用于线上崩溃日志符号化 导出不同分发渠道的IPA(App Store、Ad Hoc、Enterprise) 上传到App Store Connect .ipa(iOS App Store Package) .ipa是iOS应用的安装包格式,本质是一个ZIP压缩文件。结构: ...

May 2, 2026