普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月24日iOS

mach_msg_header_t详解

作者 iOS在入门
2026年1月23日 18:01

借助AI能力分析。

mach_msg_header_t - Mach 消息头

作用

这是 Mach 消息的头部结构,用于在 macOS/iOS 的进程间(或线程间)传递消息。

6个字段详解

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;

形象比喻(信封):

字段 对应信封上的 说明
msgh_remote_port 收件人地址 消息发往哪个端口
msgh_local_port 回信地址 如果需要回复,发到这里
msgh_size 信件大小 包括信封和内容
msgh_bits 邮寄方式 挂号信、平信等
msgh_id 信件编号 用于区分不同类型的信
msgh_voucher_port 追踪单号 用于追踪和调试

在 RunLoop 中的使用

1. 发送唤醒消息(CFRunLoopWakeUp)

// 构造消息头
mach_msg_header_t header;
header.msgh_remote_port = rl->_wakeUpPort;  // 发往唤醒端口
header.msgh_local_port = MACH_PORT_NULL;    // 不需要回复
header.msgh_size = sizeof(mach_msg_header_t); // 只有头,无内容
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_id = 0;

// 发送(唤醒 RunLoop)
mach_msg(&header, MACH_SEND_MSG, ...);

2. 接收消息(RunLoop 休眠)

// 准备缓冲区
uint8_t buffer[3 * 1024];
mach_msg_header_t *msg = (mach_msg_header_t *)buffer;

msg->msgh_local_port = waitSet;  // 在哪个端口等待
msg->msgh_size = sizeof(buffer);  // 缓冲区大小

// 阻塞等待(线程休眠)
mach_msg(msg, MACH_RCV_MSG, ...);

// 被唤醒后,检查消息来源
if (msg->msgh_local_port == _wakeUpPort) {
    // 手动唤醒
} else if (msg->msgh_local_port == _timerPort) {
    // 定时器到期
}

关键理解

mach_msg_header_t 是 Mach IPC 的核心

  1. 通信基础:所有 Mach 消息都以这个头开始
  2. 路由信息:指明消息的来源和去向
  3. RunLoop 休眠/唤醒:通过接收/发送消息实现

完整消息结构

┌──────────────────────┐
│ mach_msg_header_t    │ ← 消息头(必需)
├──────────────────────┤
│ 消息体(可选)        │ ← 实际数据
├──────────────────────┤
│ trailer(可选)       │ ← 附加信息
└──────────────────────┘

RunLoop 的简化消息:只有头部,无消息体(称为 "trivial message"),足以唤醒线程。

objc_msgSend(obj, @selector(foo)); 到底发生了什么?

作者 汉秋
2026年1月23日 17:53

objc_msgSend(obj, @selector(foo)); 到底发生了什么?

在 Objective-C 的世界里,有一句话几乎是底层原教旨主义

Objective-C 是一门基于消息发送(Message Sending)的语言,而不是函数调用。

而这一切,都浓缩在一行看似普通、却极其核心的代码中:

objc_msgSend(obj, @selector(foo));

本文将从语法糖 → 运行时 → 完整调用链,一步一步拆解:

  • 这行代码到底在“发什么”
  • 消息是如何被找到并执行的
  • 如果找不到方法,Runtime 又做了什么

一、从表面看:它等价于什么?

这行代码:

objc_msgSend(obj, @selector(foo));

在语义上 等价于

[obj foo];

也就是说:

给对象 obj 发送一条名为 foo 的消息

[obj foo] 只是编译器提供的语法糖,真正执行的永远是 objc_msgSend。


二、谁是发送者?谁是接收者?

很多初学者会卡在这个问题上:

到底是谁“调用”了谁?

正确理解方式

  • 接收者(receiver) :obj

  • 消息(selector) :foo

  • 发送动作的发起者:当前代码位置(不重要)

Objective-C 不关心调用栈的“谁” ,只关心:

👉 这条消息发给谁

所以永远用这句话来理解:

给 obj 发送 foo 消息


三、Runtime 真正发生的 6 个步骤(核心)

下面是你在 Xcode 里写下一行 [obj foo] 后,Runtime 在背后真实发生的完整流程


步骤 1️⃣:取得接收者obj

id obj = ...;
objc_msgSend(obj, @selector(foo));
  • obj 是一个对象指针
  • 本质上指向一块内存
  • 内存布局的第一个成员,就是 isa 指针
obj
 ├─ isa → Class
 ├─ ivar1
 ├─ ivar2

如果 obj == nil:

  • 整个流程直接结束
  • 返回 0 / nil
  • 不会崩溃(OC 的著名特性)

步骤 2️⃣:通过isa找到 Class

Class cls = obj->isa;
  • isa 指向对象所属的类

  • 这是 所有方法查找的起点

示例:

@interface Person : NSObject
- (void)foo;
@end
obj (Person 实例)
  └─ isa → Person

步骤 3️⃣:在方法缓存(cache)中查找

Runtime 首先查 cache,而不是方法列表

Class Person
 ├─ cache      ← ① 先查这里
 ├─ methodList ← ② 再查这里
 └─ superclass
  • cache 是一个哈希表:SEL → IMP

  • 命中 cache = 极快(接近 C 函数调用)

如果在 cache 中找到了 foo:

IMP imp = cache[foo];
imp(obj, @selector(foo));

流程结束****


步骤 4️⃣:在方法列表 & 父类中查找

如果 cache 未命中:

  1. 查 Person 的方法列表
  2. 找不到 → superclass
  3. 一直向上查,直到 NSObject
Person
  ↓
NSObject

如果在某个类中找到:

  • 将 SEL → IMP 放入 cache(下次更快)
  • 立即执行 IMP

步骤 5️⃣:动态方法解析(resolve)

如果 整个继承链都没找到 foo

Runtime 会给你一次**“临时补救”的机会**。

+ (BOOL)resolveInstanceMethod:(SEL)sel;

示例:动态添加方法

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod(self, sel, (IMP)fooIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的 foo 被调用了");
}

@end

如果返回 YES:

  • Runtime 重新从步骤 3 开始查找

步骤 6️⃣:消息转发(Message Forwarding)

如果你没有动态添加方法,Runtime 进入 消息转发三连

6.1 快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return otherObj;
    }
    return [super forwardingTargetForSelector:aSelector];
}

等价于:

[otherObj foo];

6.2 完整转发(NSInvocation)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:otherObj];
}

6.3 最终失败 → 崩溃

如果以上都没处理:

-[Person foo]: unrecognized selector sent to instance

应用直接崩溃 💥


四、完整流程总览(记住这个顺序)

objc_msgSend
  ↓
isa
  ↓
cache
  ↓
method list
  ↓
superclass
  ↓
resolveInstanceMethod
  ↓
forwardingTargetForSelector
  ↓
forwardInvocation
  ↓
crash

五、为什么 objc_msgSend 这么重要?

  • KVC / KVO

  • 方法交换(Method Swizzling)

  • AOP / Hook

  • 崩溃防护

  • 热修复(早期方案)

全都建立在它之上。

理解 objc_msgSend,

才算真正“入门” Objective-C Runtime。


六、结语

当你再看到这行代码时:

objc_msgSend(obj, @selector(foo));

请在脑中自动展开:

cache → superclass → resolve → forwarding → crash

那一刻,你已经不是在“写 OC”,而是在和 Runtime 对话

Flutter 最新xyz

作者 忆江南
2026年1月23日 16:19

包含 55+ 道xyz,覆盖基础、原理、性能优化、复杂场景和高难度题目


一、Dart 语言基础xyz(15题)

1. Dart 是值传递还是引用传递?

答案

类型 传递方式 示例
基本类型(int、double、bool、String) 值传递 修改不影响原值
对象和集合(List、Map、Set、自定义类) 引用传递 修改会影响原对象
void modifyInt(int value) {
  value = 100; // 不影响原值
}

void modifyList(List<int> list) {
  list.add(4); // 会影响原列表
}

2. constfinal 的区别?

答案

特性 const final
赋值时机 编译时确定 运行时确定
是否可用于类成员 需要 static const 可以
对象创建 共享同一对象 每次创建新对象
嵌套要求 所有成员必须是 const 无要求
const int a = 10;                    // ✓ 编译期常量
final int b = DateTime.now().year;   // ✓ 运行时常量
const DateTime c = DateTime.now();   // ✗ 报错,编译时无法确定

3. vardynamicObject 的区别?

答案

关键字 类型检查时机 类型能否改变 使用场景
var 编译时 一旦确定不可改变 类型推断
dynamic 运行时 可随时改变 动态类型、JSON解析
Object 编译时 只能调用 Object 方法 需要类型安全的通用类型
var x = 'hello';    // x 被推断为 String
x = 123;            // ✗ 报错

dynamic y = 'hello';
y = 123;            // ✓ 可以

Object z = 'hello';
z.length;           // ✗ 报错,Object 没有 length

4. .. 级联操作符与 . 的区别?

答案

操作符 返回值 用途
. 方法的返回值 普通方法调用
.. this(当前对象) 链式调用配置
var paint = Paint()
  ..color = Colors.red
  ..strokeWidth = 5.0
  ..style = PaintingStyle.stroke;

5. Dart 的空安全(Null Safety)是什么?

答案

Dart 2.12+ 引入空安全,区分可空类型非空类型

String name = 'Flutter';      // 非空,不能赋值 null
String? nickname = null;       // 可空,可以赋值 null

// 空安全操作符
String? text = null;
int length = text?.length ?? 0;  // 安全访问 + 默认值
String value = text!;            // 断言非空(危险!)

6. late 关键字的作用?

答案

late 用于延迟初始化非空变量:

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late AnimationController controller; // 延迟初始化

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
  }
}

使用场景

  • 需要在构造函数之后初始化的非空变量
  • 惰性计算的变量

7. Mixin 是什么?与继承的区别?

答案

Mixin 用于代码复用,不同于继承:

特性 继承(extends) 混入(with)
数量 单继承 可多个
构造函数 可以有 不能有
代码复用
类型关系 is-a has-ability
mixin Flyable {
  void fly() => print('Flying!');
}

mixin Swimmable {
  void swim() => print('Swimming!');
}

class Duck extends Animal with Flyable, Swimmable {
  // Duck 同时拥有 fly() 和 swim()
}

8. extendswithimplements 的执行顺序?

答案

顺序为:extends → with → implements

class Child extends Parent with Mixin1, Mixin2 implements Interface {
  // 1. 首先继承 Parent
  // 2. 然后混入 Mixin1, Mixin2(后者覆盖前者的同名方法)
  // 3. 最后实现 Interface
}

方法查找顺序(从右到左): Child → Mixin2 → Mixin1 → Parent → Object


9. Dart 中的泛型是什么?

答案

泛型用于类型安全代码复用

// 泛型类
class Box<T> {
  T value;
  Box(this.value);
}

// 泛型方法
T first<T>(List<T> items) {
  return items[0];
}

// 泛型约束
class NumberBox<T extends num> {
  T value;
  NumberBox(this.value);

  double toDouble() => value.toDouble();
}

10. Dart 的 typedef 是什么?

答案

typedef 用于定义函数类型别名

// 定义函数类型
typedef Compare<T> = int Function(T a, T b);

// 使用
int sort(int a, int b) => a - b;
Compare<int> comparator = sort;

// 新语法(Dart 2.13+)
typedef IntList = List<int>;
typedef StringCallback = void Function(String);

11. Dart 的 extension 扩展方法是什么?

答案

extension 用于给现有类添加方法,无需继承:

extension StringExtension on String {
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }

  bool get isEmail => contains('@');
}

// 使用
'hello'.capitalize();  // 'Hello'
'a@b.com'.isEmail;     // true

12. Dart 的 factory 构造函数是什么?

答案

factory 构造函数可以返回已有实例子类实例

class Logger {
  static final Logger _instance = Logger._internal();

  // 工厂构造函数
  factory Logger() {
    return _instance; // 返回单例
  }

  Logger._internal();
}

// 使用
var l1 = Logger();
var l2 = Logger();
print(l1 == l2); // true,同一个实例

13. Dart 3 的 Records(记录类型)是什么?

答案

Records 是 Dart 3 引入的匿名复合类型

// 位置记录
(int, String) getUserInfo() => (1, 'John');

var info = getUserInfo();
print(info.$1); // 1
print(info.$2); // 'John'

// 命名记录
({int id, String name}) getUser() => (id: 1, name: 'John');

var user = getUser();
print(user.id);   // 1
print(user.name); // 'John'

14. Dart 3 的 Pattern Matching(模式匹配)是什么?

答案

模式匹配用于解构和条件匹配

// switch 表达式
String describe(Object obj) => switch (obj) {
  int n when n > 0 => 'Positive number: $n',
  int n when n < 0 => 'Negative number: $n',
  String s => 'String: $s',
  _ => 'Unknown type',
};

// 解构
var (x, y) = (1, 2);
var {'name': name, 'age': age} = {'name': 'John', 'age': 30};

// if-case
if (json case {'name': String name, 'age': int age}) {
  print('Name: $name, Age: $age');
}

15. Dart 3 的 Sealed Class 是什么?

答案

sealed 类用于限制子类,实现穷尽式 switch:

sealed class Shape {}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
}

// 编译器会检查是否穷尽所有子类
double area(Shape shape) => switch (shape) {
  Circle(radius: var r) => 3.14 * r * r,
  Rectangle(width: var w, height: var h) => w * h,
};

二、Flutter 核心原理xyz(15题)

16. Flutter 的三棵树是什么?各自职责是什么?

答案

类型 职责 特点
Widget Tree 配置层 描述 UI 结构 不可变、轻量、频繁重建
Element Tree 连接层 管理生命周期、持有 State 可变、持久化
RenderObject Tree 渲染层 布局、绘制、事件处理 重量级、存储几何信息

创建流程

Widget.createElement() → Element
Element.createRenderObject() → RenderObject

为什么需要三棵树?

  • Widget 频繁重建成本低
  • Element 复用避免重复创建
  • RenderObject 只在必要时更新

17. Flutter 完整渲染流程是什么?

答案

┌─────────────────────────────────────────────┐
│                   UI 线程                    │
├─────────────────────────────────────────────┤
│ 1. Build(构建)                             │
│    - 从脏 Element 开始重建                   │
│    - 调用 build() 方法                       │
│    - 生成新的 Widget 树                      │
├─────────────────────────────────────────────┤
│ 2. Layout(布局)                            │
│    - 约束从父向子传递                        │
│    - 几何信息从子向父返回                    │
│    - 计算大小和位置                          │
├─────────────────────────────────────────────┤
│ 3. Paint(绘制)                             │
│    - 生成绘制指令                            │
│    - 构建 Layer Tree                         │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│               光栅线程(Raster)              │
├─────────────────────────────────────────────┤
│ 4. Composite(合成)                         │
│    - 图层合成                                │
│    - Skia/Impeller 栅格化                    │
│    - 提交给 GPU                              │
└─────────────────────────────────────────────┘
                    ↓
                显示到屏幕

性能标准

  • 60fps:每帧 ≤ 16ms
  • 120fps:每帧 ≤ 8.3ms

18. setState() 的底层原理是什么?

答案

void setState(VoidCallback fn) {
  // 1. 执行回调函数,修改状态
  fn();

  // 2. 标记当前 Element 为脏
  _element!.markNeedsBuild();
}

// markNeedsBuild() 的实现
void markNeedsBuild() {
  // 标记为脏
  _dirty = true;

  // 加入脏 Element 列表
  owner!.scheduleBuildFor(this);
}

流程

  1. 执行回调更新状态
  2. 标记 Element 为脏
  3. 注册到 BuildOwner 的脏列表
  4. 下一帧触发重建
  5. 只重建脏 Element 及其子树

19. Flutter 的约束(Constraints)系统是什么?

答案

约束是父节点向子节点传递的布局信息

class BoxConstraints {
  final double minWidth;   // 最小宽度
  final double maxWidth;   // 最大宽度
  final double minHeight;  // 最小高度
  final double maxHeight;  // 最大高度
}

布局算法

1. 父节点传递约束给子节点
2. 子节点选择约束范围内的大小
3. 子节点返回实际大小给父节点
4. 父节点确定子节点位置

严格约束(Tight Constraints):

  • minWidth == maxWidthminHeight == maxHeight
  • 子节点无法改变大小
  • 父节点可直接定位而无需重新布局子节点

20. Key 的作用是什么?有哪些类型?

答案

作用:帮助 Flutter 在 Widget 树重建时正确匹配和复用 Element

Key 类型 作用域 使用场景
GlobalKey 整个应用唯一 跨组件访问 State、保持状态
LocalKey 局部唯一 列表项复用
ValueKey 基于值 数据驱动列表
ObjectKey 基于对象引用 对象唯一性
UniqueKey 随机唯一 强制重建
// GlobalKey 示例
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
formKey.currentState?.validate();

// ValueKey 示例
ListView(
  children: items.map((item) =>
    ListTile(key: ValueKey(item.id), title: Text(item.name))
  ).toList(),
)

21. BuildContext 是什么?

答案

BuildContext 是 Widget 在 Widget 树中的位置引用,本质是 Element 对象

// 向上查找
Theme.of(context);           // 获取主题
Navigator.of(context);       // 获取导航器
MediaQuery.of(context);      // 获取媒体查询
Scaffold.of(context);        // 获取 Scaffold

// InheritedWidget 查找
MyInheritedWidget.of(context);

注意事项

  • initState() 中不能使用 context(Element 未完全挂载)
  • 异步操作后需检查 mounted 状态

22. Widget 有哪些分类?

答案

类型 代表类 作用
组合类 StatelessWidget、StatefulWidget 组合其他 Widget
代理类 InheritedWidget、ParentDataWidget 状态共享、数据传递
绘制类 RenderObjectWidget 真正的布局和绘制

RenderObject 三个子类

  • LeafRenderObjectWidget:叶子节点(无子节点)
  • SingleChildRenderObjectWidget:单子节点
  • MultiChildRenderObjectWidget:多子节点

23. StatefulWidget 的完整生命周期?

答案

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState(); // 1. 创建 State
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {                    // 2. 初始化(只调用一次)
    super.initState();
  }

  @override
  void didChangeDependencies() {         // 3. 依赖变化
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {   // 4. 构建 UI
    return Container();
  }

  @override
  void didUpdateWidget(MyWidget old) {   // 5. Widget 更新
    super.didUpdateWidget(old);
  }

  @override
  void reassemble() {                    // 6. 热重载时调用
    super.reassemble();
  }

  @override
  void deactivate() {                    // 7. 暂时移除
    super.deactivate();
  }

  @override
  void dispose() {                       // 8. 永久销毁
    super.dispose();
  }
}

生命周期图

createState → initState → didChangeDependencies → build
                                    ↓
                          [setState/父Widget更新]
                                    ↓
                          didUpdateWidget → build
                                    ↓
                          deactivate → dispose

24. InheritedWidget 的原理是什么?

答案

InheritedWidget 用于数据向下传递,避免多层传参:

class ThemeProvider extends InheritedWidget {
  final Color color;

  ThemeProvider({required this.color, required Widget child})
    : super(child: child);

  static ThemeProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
  }

  @override
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return color != oldWidget.color;
  }
}

性能优化原理

  • Element 维护 InheritedWidget 哈希表
  • 查找时间复杂度 O(1)
  • 避免遍历父链(O(N))

25. 热重载(Hot Reload)的原理是什么?

答案

流程

  1. 代码修改保存
  2. IDE 发送变更到 Dart VM
  3. VM 增量编译新代码
  4. 新代码注入到 VM(保留旧实例)
  5. 调用 reassemble()
  6. 触发完整的 build 流程

不支持热重载的场景

  • ❌ 修改 main() 函数
  • ❌ 修改 initState() 方法
  • ❌ 修改全局变量初始化
  • ❌ 修改枚举类型
  • ❌ 修改泛型类型

26. Flutter 与原生如何通信?

答案

三种 Channel

Channel 用途 数据流向
MethodChannel 方法调用 双向请求/响应
EventChannel 事件流 原生 → Flutter
BasicMessageChannel 消息传递 双向自定义编解码
// MethodChannel 示例
const platform = MethodChannel('com.example/battery');

Future<int> getBatteryLevel() async {
  try {
    return await platform.invokeMethod('getBatteryLevel');
  } on PlatformException catch (e) {
    return -1;
  }
}

// EventChannel 示例
const eventChannel = EventChannel('com.example/sensor');
Stream<dynamic> get sensorStream => eventChannel.receiveBroadcastStream();

27. Impeller 与 Skia 的区别?

答案

特性 Skia Impeller
平台 全平台 iOS(默认)、Android(预览)
着色器编译 运行时 预编译
首帧卡顿
Emoji 渲染 可能卡顿 流畅
GPU 内存管理 一般 优化

28. Flutter 的 Layer Tree 是什么?

答案

Layer Tree 是绘制阶段生成的图层树

Layer Tree 结构:
├── TransformLayer(变换层)
├── ClipRectLayer(裁剪层)
├── OpacityLayer(透明度层)
├── PictureLayer(绘制层)
└── ...

用途

  • 优化重绘(只重绘变化的图层)
  • 支持合成效果(透明度、变换等)
  • 提交给 GPU 合成

29. RepaintBoundary 的作用是什么?

答案

RepaintBoundary 用于隔离重绘区域

// 场景:动画只影响一小块区域
Stack(
  children: [
    StaticBackground(),  // 不需要重绘
    RepaintBoundary(
      child: AnimatedWidget(), // 动画只在此区域重绘
    ),
  ],
)

原理

  • 创建独立的绘制边界
  • 子树重绘不影响外部
  • 外部重绘不影响子树

30. Flutter 架构分层是什么?

答案

┌─────────────────────────────────────────┐
│           应用层(Your App)              │
├─────────────────────────────────────────┤
│        Framework 层(Dart)               │
│  ┌─────────────────────────────────────┐ │
│  │ Material / Cupertino Widgets        │ │
│  ├─────────────────────────────────────┤ │
│  │ Widgets Layer                       │ │
│  ├─────────────────────────────────────┤ │
│  │ Rendering Layer                     │ │
│  ├─────────────────────────────────────┤ │
│  │ Foundation / Animation / Gesture    │ │
│  └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│          Engine 层(C++)                 │
│  Skia / Impeller / Dart VM / Text       │
├─────────────────────────────────────────┤
│        Embedder 层(平台适配)             │
│  Android / iOS / Web / Desktop          │
└─────────────────────────────────────────┘

三、异步编程xyz(10题)

31. Dart 事件循环是怎样的?

答案

main() {
  print('1. main start');           // 同步

  Future(() => print('4. event'));  // 事件队列

  scheduleMicrotask(              // 微任务队列
    () => print('3. microtask')
  );

  print('2. main end');             // 同步
}

// 输出顺序:1 → 2 → 3 → 4

优先级:同步代码 > 微任务队列 > 事件队列


32. Future 和 Stream 的区别?

答案

特性 Future Stream
返回值次数 一次 多次
使用场景 网络请求、文件读取 按钮点击、WebSocket
订阅方式 .then() / await .listen()
取消 不可取消 可取消
// Future
Future<String> fetchData() async {
  return await http.get(url);
}

// Stream
Stream<int> countStream() async* {
  for (int i = 0; i < 10; i++) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

33. Stream 的两种订阅模式?

答案

模式 特点 使用场景
单订阅 只能有一个监听者 文件读取、HTTP 响应
广播 多个监听者 按钮点击、状态变化
// 单订阅(默认)
stream.listen((data) => print(data));

// 转为广播
Stream broadcastStream = stream.asBroadcastStream();
broadcastStream.listen((data) => print('1: $data'));
broadcastStream.listen((data) => print('2: $data'));

34. Isolate 是什么?如何使用?

答案

Isolate 是 Dart 的并发模型,拥有独立的内存和事件循环:

// 方法1:Isolate.run()(推荐)
Future<List<Photo>> loadPhotos() async {
  final jsonString = await rootBundle.loadString('assets/photos.json');

  return await Isolate.run(() {
    final data = jsonDecode(jsonString) as List;
    return data.map((e) => Photo.fromJson(e)).toList();
  });
}

// 方法2:compute()
final result = await compute(parseJson, jsonString);

使用场景

  • JSON 解析(大文件)
  • 图片处理
  • 复杂计算
  • 加密解密

35. async/await 的执行顺序?

答案

Future<void> test() async {
  print('1');
  await Future.delayed(Duration.zero);  // 让出执行权
  print('2');
}

main() {
  print('a');
  test();
  print('b');
}

// 输出:a → 1 → b → 2

原理await 之前同步执行,之后加入微任务队列


36. Future.wait 和 Future.any 的区别?

答案

// Future.wait:等待所有完成
final results = await Future.wait([
  fetchUser(),
  fetchPosts(),
  fetchComments(),
]);
// results = [user, posts, comments]

// Future.any:返回最先完成的
final fastest = await Future.any([
  fetchFromServer1(),
  fetchFromServer2(),
]);
// fastest = 最快返回的结果

37. StreamController 的使用?

答案

class EventBus {
  final _controller = StreamController<Event>.broadcast();

  Stream<Event> get stream => _controller.stream;

  void emit(Event event) {
    _controller.add(event);
  }

  void dispose() {
    _controller.close();
  }
}

// 使用
final bus = EventBus();
bus.stream.listen((event) => print(event));
bus.emit(LoginEvent());

38. FutureBuilder 和 StreamBuilder 的区别?

答案

Widget 数据源 使用场景
FutureBuilder Future(一次性) 网络请求
StreamBuilder Stream(持续) 实时数据
// FutureBuilder
FutureBuilder<User>(
  future: fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.hasData) return UserWidget(snapshot.data!);
    if (snapshot.hasError) return ErrorWidget(snapshot.error!);
    return CircularProgressIndicator();
  },
)

// StreamBuilder
StreamBuilder<int>(
  stream: countStream(),
  builder: (context, snapshot) {
    return Text('Count: ${snapshot.data ?? 0}');
  },
)

39. async* 和 sync* 生成器的区别?

答案

// sync*:同步生成器,返回 Iterable
Iterable<int> syncGenerator() sync* {
  yield 1;
  yield 2;
  yield 3;
}

// async*:异步生成器,返回 Stream
Stream<int> asyncGenerator() async* {
  for (int i = 0; i < 3; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

40. Completer 的作用?

答案

Completer 用于手动完成 Future

Future<String> fetchWithTimeout() {
  final completer = Completer<String>();

  // 设置超时
  Future.delayed(Duration(seconds: 5), () {
    if (!completer.isCompleted) {
      completer.completeError(TimeoutException('Timeout'));
    }
  });

  // 模拟网络请求
  http.get(url).then((response) {
    if (!completer.isCompleted) {
      completer.complete(response.body);
    }
  });

  return completer.future;
}

四、性能优化xyz(10题)

41. 如何减少 Widget 重建?

答案

// 1. 使用 const Widget
const Text('Hello');
const MyWidget();

// 2. 拆分 Widget
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveWidget(),  // 不会重建
        DynamicWidget(),           // 可能重建
      ],
    );
  }
}

// 3. 使用 Consumer 精确订阅
Consumer<CounterProvider>(
  builder: (context, counter, child) {
    return Text('${counter.value}');
  },
  child: const ExpensiveChild(), // 不会重建
)

// 4. 使用 Selector 订阅单个字段
Selector<AppState, String>(
  selector: (context, state) => state.userName,
  builder: (context, userName, child) {
    return Text(userName);
  },
)

42. 如何优化 ListView 性能?

答案

ListView.builder(
  // 1. 指定固定高度(避免高度计算)
  itemExtent: 80,

  // 2. 设置缓存范围
  cacheExtent: 500,

  // 3. 使用懒加载
  itemCount: items.length,
  itemBuilder: (context, index) {
    // 4. 使用 RepaintBoundary 隔离重绘
    return RepaintBoundary(
      // 5. 使用 const
      child: ListItemWidget(item: items[index]),
    );
  },
)

// 6. 使用 AutomaticKeepAliveClientMixin 保持状态
class _ItemState extends State<Item> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ...;
  }
}

43. 如何避免 saveLayer 导致的性能问题?

答案

saveLayer 是昂贵操作,以下 Widget 会触发:

Widget 替代方案
Opacity 直接设置颜色透明度
ShaderMask 简化效果
ColorFilter 直接应用到 Image
Clip.antiAliasWithSaveLayer 使用 Clip.hardEdge
// ❌ 触发 saveLayer
Opacity(
  opacity: 0.5,
  child: Container(color: Colors.blue),
)

// ✓ 直接设置透明度
Container(
  color: Colors.blue.withOpacity(0.5),
)

44. 如何优化图片加载?

答案

// 1. 设置缓存尺寸
Image.network(
  url,
  cacheWidth: 200,
  cacheHeight: 200,
)

// 2. 预加载图片
precacheImage(NetworkImage(url), context);

// 3. 使用渐进式加载
FadeInImage.memoryNetwork(
  placeholder: kTransparentImage,
  image: url,
)

// 4. 使用缓存库
CachedNetworkImage(
  imageUrl: url,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

// 5. 及时释放
@override
void dispose() {
  imageProvider.evict();
  super.dispose();
}

45. 如何优化动画性能?

答案

// 1. 使用 AnimatedBuilder 而非 setState
AnimatedBuilder(
  animation: controller,
  builder: (context, child) {
    return Transform.rotate(
      angle: controller.value * 2 * pi,
      child: child, // child 不重建
    );
  },
  child: const ExpensiveWidget(),
)

// 2. 使用 RepaintBoundary 隔离重绘
RepaintBoundary(
  child: AnimatedWidget(),
)

// 3. 使用 Transform 而非改变布局
// ❌ 触发布局
Container(
  margin: EdgeInsets.only(left: animation.value),
  child: widget,
)

// ✓ 只触发绘制
Transform.translate(
  offset: Offset(animation.value, 0),
  child: widget,
)

// 4. 使用 vsync
AnimationController(
  vsync: this, // 与屏幕刷新率同步
  duration: Duration(seconds: 1),
)

46. 如何检测和解决内存泄漏?

答案

常见泄漏场景

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription? subscription;
  Timer? timer;
  AnimationController? controller;
  TextEditingController? textController;

  @override
  void initState() {
    super.initState();
    subscription = stream.listen((_) {});
    timer = Timer.periodic(duration, (_) {});
    controller = AnimationController(vsync: this);
    textController = TextEditingController();
  }

  @override
  void dispose() {
    // ✓ 必须释放所有资源
    subscription?.cancel();
    timer?.cancel();
    controller?.dispose();
    textController?.dispose();
    super.dispose();
  }
}

异步回调中的安全检查

Future<void> loadData() async {
  final data = await fetchData();

  // ✓ 检查 mounted 状态
  if (!mounted) return;

  setState(() => this.data = data);
}

47. 如何优化启动性能?

答案

// 1. 延迟初始化非关键服务
void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // 只初始化必需的
  initCriticalServices();

  runApp(MyApp());

  // 延迟初始化其他服务
  Future.delayed(Duration(seconds: 1), () {
    initNonCriticalServices();
  });
}

// 2. 使用懒加载
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FutureBuilder(
        future: loadInitialData(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return SplashScreen();
          }
          return HomeScreen(data: snapshot.data);
        },
      ),
    );
  }
}

// 3. 使用 deferred loading(代码分割)
import 'package:heavy_module/heavy_module.dart' deferred as heavy;

Future<void> loadHeavyModule() async {
  await heavy.loadLibrary();
  heavy.doSomething();
}

48. 如何使用 DevTools 进行性能分析?

答案

1. Performance 视图

  • Flutter Frames Chart:查看每帧的 UI/Raster 时间
  • Frame Analysis:自动检测性能问题
  • Timeline Events:详细追踪事件

2. 关键指标

✓ 绿色帧:< 16ms(正常)
✗ 红色帧:> 16ms(卡顿)

UI Thread:构建和布局时间
Raster Thread:绘制和合成时间

3. 常见优化建议

  • 避免在 build 中创建对象
  • 使用 const Widget
  • 减少 Widget 深度
  • 使用 RepaintBoundary

49. Flutter 3.24+ 性能优化新特性?

答案

1. Impeller 渲染引擎优化

  • 预编译着色器,消除首帧卡顿
  • Emoji 滚动更流畅
  • GPU 内存管理改进

2. 新的 Sliver 组件

CustomScrollView(
  slivers: [
    SliverFloatingHeader(...),     // 浮动头部
    PinnedHeaderSliver(...),       // 固定头部
    SliverResizingHeader(...),     // 可调整大小头部
  ],
)

3. 增强的 Performance 视图

  • 着色器编译追踪
  • 更详细的帧分析
  • 自动性能建议

50. 如何实现高性能的无限滚动列表?

答案

class InfiniteScrollList extends StatefulWidget {
  @override
  State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}

class _InfiniteScrollListState extends State<InfiniteScrollList> {
  final List<Item> items = [];
  final ScrollController controller = ScrollController();
  bool isLoading = false;
  bool hasMore = true;
  int page = 1;

  @override
  void initState() {
    super.initState();
    controller.addListener(_onScroll);
    _loadMore();
  }

  void _onScroll() {
    if (controller.position.pixels >=
        controller.position.maxScrollExtent - 200) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (isLoading || !hasMore) return;

    setState(() => isLoading = true);

    try {
      final newItems = await fetchItems(page: page);
      setState(() {
        items.addAll(newItems);
        page++;
        hasMore = newItems.length >= 20;
        isLoading = false;
      });
    } catch (e) {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: controller,
      itemExtent: 80,                    // 固定高度
      cacheExtent: 500,                  // 缓存范围
      itemCount: items.length + (hasMore ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == items.length) {
          return Center(child: CircularProgressIndicator());
        }
        return RepaintBoundary(          // 隔离重绘
          child: ItemWidget(item: items[index]),
        );
      },
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

五、复杂场景xyz(5题)

51. 如何实现自定义 RenderObject?

答案

class CustomProgressBar extends LeafRenderObjectWidget {
  final double progress;
  final Color color;

  const CustomProgressBar({
    required this.progress,
    required this.color,
  });

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderCustomProgressBar(
      progress: progress,
      color: color,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderCustomProgressBar renderObject,
  ) {
    renderObject
      ..progress = progress
      ..color = color;
  }
}

class RenderCustomProgressBar extends RenderBox {
  double _progress;
  Color _color;

  RenderCustomProgressBar({
    required double progress,
    required Color color,
  })  : _progress = progress,
        _color = color;

  set progress(double value) {
    if (_progress != value) {
      _progress = value;
      markNeedsPaint();  // 触发重绘
    }
  }

  set color(Color value) {
    if (_color != value) {
      _color = value;
      markNeedsPaint();
    }
  }

  @override
  void performLayout() {
    size = constraints.constrain(Size(300, 20));
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final canvas = context.canvas;

    // 背景
    canvas.drawRect(
      Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
      Paint()..color = Colors.grey[300]!,
    );

    // 进度
    canvas.drawRect(
      Rect.fromLTWH(offset.dx, offset.dy, size.width * _progress, size.height),
      Paint()..color = _color,
    );
  }
}

52. 状态管理方案如何选择?

答案

方案 复杂度 适用场景 特点
setState 简单组件 最基础
InheritedWidget 数据传递 Flutter 原生
Provider 中小型应用 官方推荐
Riverpod 现代应用 类型安全、可测试
Bloc 大型应用 事件驱动、清晰分层
GetX 快速开发 轻量、功能全

53. 如何实现国际化(i18n)?

答案

// 1. 定义翻译
class AppLocalizations {
  static Map<String, Map<String, String>> _localizedValues = {
    'en': {'hello': 'Hello', 'world': 'World'},
    'zh': {'hello': '你好', 'world': '世界'},
  };

  static String translate(BuildContext context, String key) {
    Locale locale = Localizations.localeOf(context);
    return _localizedValues[locale.languageCode]?[key] ?? key;
  }
}

// 2. 配置 MaterialApp
MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', 'US'),
    Locale('zh', 'CN'),
  ],
)

// 3. 使用
Text(AppLocalizations.translate(context, 'hello'))

54. 如何实现复杂的表单验证?

答案

class FormValidator {
  static String? validateEmail(String? value) {
    if (value?.isEmpty ?? true) return '邮箱不能为空';
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
      return '邮箱格式错误';
    }
    return null;
  }

  static String? validatePassword(String? value) {
    if (value?.isEmpty ?? true) return '密码不能为空';
    if (value!.length < 6) return '密码至少6位';
    if (!value.contains(RegExp(r'[A-Z]'))) return '需要包含大写字母';
    return null;
  }
}

class LoginForm extends StatefulWidget {
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => _isLoading = true);

    try {
      await login(_emailController.text, _passwordController.text);
      if (!mounted) return;
      Navigator.pushReplacementNamed(context, '/home');
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('登录失败: $e')),
      );
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            validator: FormValidator.validateEmail,
            decoration: InputDecoration(labelText: '邮箱'),
          ),
          TextFormField(
            controller: _passwordController,
            validator: FormValidator.validatePassword,
            obscureText: true,
            decoration: InputDecoration(labelText: '密码'),
          ),
          ElevatedButton(
            onPressed: _isLoading ? null : _submit,
            child: _isLoading
              ? CircularProgressIndicator()
              : Text('登录'),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

55. Flutter 3.24+ 最新特性有哪些?

答案

1. 新的 Sliver 组件

  • SliverFloatingHeader:浮动头部
  • PinnedHeaderSliver:固定头部
  • SliverResizingHeader:可调整大小头部

2. CarouselView(轮播)

CarouselView(
  itemCount: 10,
  itemBuilder: (context, index, realIndex) {
    return Container(color: Colors.primaries[index % 10]);
  },
)

3. TreeView(树形视图)

TreeView(
  nodes: [
    TreeViewNode(title: Text('Parent'), children: [...]),
  ],
)

4. AnimationStatus 增强

if (status.isRunning) { ... }
if (status.isForwardOrCompleted) { ... }

5. Flutter GPU(预览)

  • 直接渲染 3D 图形

6. Web 热重载支持


总结表

分类 核心知识点 题目数
Dart 基础 语法特性、空安全、泛型、扩展方法 15
Flutter 原理 三棵树、渲染流程、生命周期、Key 15
异步编程 Event Loop、Future/Stream、Isolate 10
性能优化 Widget 重建、列表优化、内存管理 10
复杂场景 自定义渲染、状态管理、表单验证 5

掌握这 55 道xyz,可以应对 99% 的 Flutter 面试!🚀

App Groups in iOS

作者 songgeb
2026年1月23日 14:41

参考:developer.apple.com/documentati…

一、什么是 App Group

  • App Group 允许同一开发者团队(Team)下的多个 App访问一个或多个共享空间(Shared Container)
  • 默认情况下(未使用 App Group):
    • 每个 App 都运行在独立进程
    • 拥有独立 沙盒
    • 无法进行数据共享
    • 无法进行 进程间通信
  • 对于 iOS 应用
    • 即使开启了 App Group
    • 只能实现多 App / App Extension 之间的数据或空间共享
    • 无法实现真正的跨进程通信( IPC
  • 对于 macOS 应用
    • App Group 可以放宽沙盒边界
    • 允许通过 Mach IPC、UNIX domain socket 等机制实现 IPC

⚠️ App Group 在 iOS 与 macOS 上的能力存在显著差异

二、App Group 的历史背景

  • App Group 是在 WWDC 2014 中提出的能力
  • iOS 8(以及 OS X 10.10 Yosemite)一起发布
  • 设计初衷是配合 App Extension 的出现:
    • 主 App
    • Widget
    • Share / Action Extension
    • 等多个进程之间的安全数据共享

三、App Group 的基本规则与限制

1. 数量限制

  • 一个开发者账号 最多可以注册 1000 个 App Group
  • 一个 App:
    • 可以不使用 App Group
    • 也可以属于一个或多个 App Group

2. 使用范围

  • 以下组合都可以使用 App Group:
    • App ↔ App Extension
    • App ↔ App
    • App ↔ App Clip

3. Container ID 规则

  • 创建 App Group 时需要设置一个 Container ID
  • Container ID 用于标识共享空间
  • 当 App Group 包含 iOS App(而非 macOS App)时
    • Container ID 必须以 group. 作为前缀

示例:

group.com.company.shared

四、iOS 中 App Group 能做什么,不能做什么

4.1 能做的事情

  • 多进程( App / Extension/App Clip)共享数据

4.2 不能做的事情

  • ❌ 不支持进程间通信(IPC)
  • ❌ 不支持 Mach IPC、socket、shared memory 等机制
  • ❌ 不能假设共享目录的真实路径
  • ❌ 不能假设共享目录一定长期存在

在 iOS 中,App Group 的本质是: 共享存储权限,而不是通信权限

五、iOS App 使用 App Group 共享空间的方式

系统提供了三种主要方式:

5.1 通过 UserDefaults 共享数据

  • 必须使用 init(suiteName:) 初始化

let defaults = UserDefaults(suiteName: "group.com.company.shared") ``defaults?.set("value", forKey: "key")

适用于:

  • 配置项
  • 功能开关
  • 小体量状态数据

5.2 通过共享容器路径读写文件

  • 使用 containerURL(forSecurityApplicationGroupIdentifier:) 获取共享空间 URL

let containerURL = FileManager.default.containerURL( ``forSecurityApplicationGroupIdentifier: "group.com.company.shared" )

说明:

  • 系统只会自动创建 Library/Caches 目录
  • 其他目录需要自行创建
  • 适合存储:
    • JSON
    • SQLite
    • 缓存文件

5.3 App Extension 使用 Background URL Session

  • 对于 App Extension:
    • 使用 URLSessionConfiguration.background
    • 设置 sharedContainerIdentifier
  • 下载的数据会直接存储在 App Group 的共享空间中

适用于:

  • 后台下载
  • Extension 与主 App 共享下载结果

六、工程实践注意事项

  • 不要写死 App Group 的磁盘路径
  • 不要假设共享容器一定存在
    • 当设备上属于同一个app group中的所有应用都卸载后,共享容器也会被删除
  • 多个 App / Extension 需要:
    • 统一目录结构
    • 统一数据格式

七、总结

  • App Group 是 iOS 8 引入的一项共享容器能力
  • 在 iOS 平台上:
    • 它解决的是数据共享问题
    • 而不是进程间通信 问题
  • 合理使用 App Group,可以安全地协调多个 App / Extension 之间的状态与资源
昨天 — 2026年1月23日iOS

Maintaining shadow branches for GitHub PRs

作者 MaskRay
2026年1月22日 16:00

I've created pr-shadow with vibecoding, a tool that maintains a shadow branch for GitHub pull requests(PR) that never requires force-pushing. This addresses pain points Idescribed in Reflectionson LLVM's switch to GitHub pull requests#Patch evolution.

The problem

GitHub structures pull requests around branches, enforcing abranch-centric workflow. There are multiple problems when you force-pusha branch after a rebase:

  • The UI displays "force-pushed the BB branch from X to Y". Clicking"compare" shows git diff X..Y, which includes unrelatedupstream commits—not the actual patch difference. For a project likeLLVM with 100+ commits daily, this makes the comparison essentiallyuseless.
  • Inline comments may become "outdated" or misplaced after forcepushes.
  • If your commit message references an issue or another PR, each forcepush creates a new link on the referenced page, cluttering it withduplicate mentions. (Adding backticks around the link text works aroundthis, but it's not ideal.)

These difficulties lead to recommendations favoring less flexibleworkflows that only append commits (including merge commits) anddiscourage rebases. However, this means working with an outdated base,and switching between the main branch and PR branches causes numerousrebuilds-especially painful for large repositories likellvm-project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git switch main; git pull; ninja -C build

# Switching to a feature branch with an outdated base requires numerous rebuilds.
git switch feature0
git merge origin/main # I prefer `git rebase main` to remove merge commits, which clutter the history
ninja -C out/release

# Switching to another feature branch with an outdated base requires numerous rebuilds.
git switch feature1
git merge origin/main
ninja -C out/release

# Listing fixup commits ignoring upstream merges requires the clumsy --first-parent.
git log --first-parent

In a large repository, avoiding rebases isn't realistic—other commitsfrequently modify nearby lines, and rebasing is often the only way todiscover that your patch needs adjustments due to interactions withother landed changes.

In 2022, GitHub introduced "Pull request title and description" forsquash merging. This means updating the final commit message requiresediting via the web UI. I prefer editing the local commit message andsyncing the PR description from it.

The solution

After updating my main branch, before switching to afeature branch, I always run

1
git rebase main feature

to minimize the number of modified files. To avoid the force-pushproblems, I use pr-shadow to maintain a shadow PR branch (e.g.,pr/feature) that only receives fast-forward commits(including merge commits).

I work freely on my local branch (rebase, amend, squash), then syncto the PR branch using git commit-tree to create a commitwith the same tree but parented to the previous PR HEAD.

1
2
3
4
5
6
Local branch (feature)     PR branch (pr/feature)
A A (init)
| |
B (amend) C1 "Fix bug"
| |
C (rebase) C2 "Address review"

Reviewers see clean diffs between C1 and C2, even though theunderlying commits were rewritten.

When a rebase is detected (git merge-base withmain/master changed), the new PR commit is created as a merge commitwith the new merge-base as the second parent. GitHub displays these as"condensed" merges, preserving the diff view for reviewers.

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Initialize and create PR
git switch -c feature
edit && git commit -m feature

# Set `git merge-base origin/main feature` as the initial base. Push to pr/feature and open a GitHub PR.
prs init
# Same but create a draft PR. Repeated `init`s are rejected.
prs init --draft

# Work locally (rebase, amend, etc.)
git fetch origin main:main
git rebase main
git commit --amend

# Sync to PR
prs push "Rebase and fix bug"
# Force push if remote diverged due to messing with pr/feature directly.
prs push --force "Rewrite"

# Update PR title/body from local commit message.
prs desc

# Run gh commands on the PR.
prs gh view
prs gh checks

The tool supports both fork-based workflows (pushing to your fork)and same-repo workflows (for branches likeuser/<name>/feature). It also works with GitHubEnterprise, auto-detecting the host from the repository URL.

Related work

The name "prs" is a tribute to spr, which implements asimilar shadow branch concept. However, spr pushes user branches to themain repository rather than a personal fork. While necessary for stackedpull requests, this approach is discouraged for single PRs as itclutters the upstream repository. pr-shadow avoids this by pushing toyour fork by default.

I owe an apology to folks who receiveusers/MaskRay/feature branches (if they use the defaultfetch = +refs/heads/*:refs/remotes/origin/* to receive userbranches). I had been abusing spr for a long time after LLVM'sGitHub transition to avoid unnecessary rebuilds when switchingbetween the main branch and PR branches.

Additionally, spr embeds a PR URL in commit messages (e.g.,Pull Request: https://github.com/llvm/llvm-project/pull/150816),which can cause downstream forks to add unwanted backlinks to theoriginal PR.

If I need stacked pull requests, I will probably use pr-shadow withthe base patch and just rebase stacked ones - it's unclear how sprhandles stacked PRs.

❌
❌