阅读视图

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

🚀 纯前端离线局域网P2P大文件断点传输:别让你的照片墙崩了

前言:小明和他的照片墙危机

想象一下,你是小明,一个热爱摄影的程序员。周末去爬山拍了一堆4K高清照片,总共3GB,准备发给老婆分享。微信传?算了,压缩后画质渣得像像素风。网盘?离线状态下连不上。蓝牙?慢得像蜗牛在跑步。你急得团团转,突然灵机一动:用电脑直接传啊!可是怎么在浏览器里实现局域网P2P大文件传输,还得支持断点续传?别急,今天咱们就聊聊这个技术方案。

技术背景:P2P不是什么新鲜玩意儿

P2P(点对点)传输在局域网里其实挺常见的,BT下载就是经典案例。但咱们今天聊的是纯前端实现,意思是完全不用后端服务器,用户A直接把文件传给用户B。核心技术是WebRTC(Web Real-Time Communication),这货本来是用来视频聊天的,但咱们可以拿来传文件。

为什么选WebRTC?因为它支持数据通道(DataChannel),可以直接在浏览器间建立连接。加上File API和Blob,咱们就能把大文件切成小块,边传边收,断点续传自然就出来了。

核心实现:分块传输 + 断点续传

1. 文件切块:大象塞冰箱,得先切成块

浏览器处理大文件有个硬伤:内存限制。如果直接把3GB文件读进内存,Chrome得哭爹喊娘。所以咱们用FileReader分块读:

// 文件分块函数
function chunkFile(file, chunkSize = 1024 * 1024) { // 1MB每块
  const chunks = [];
  let offset = 0;
  
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    chunks.push(chunk);
    offset += chunkSize;
  }
  
  return chunks;
}

这里用file.slice()切块,每个块1MB。为什么要1MB?平衡传输效率和内存占用,太小网络开销大,太大浏览器卡。

2. WebRTC连接:建立地下通道

WebRTC连接需要信令服务器(用来交换连接信息),但咱们是离线局域网,所以可以用WebSocket或者直接用浏览器本地存储交换SDP(会话描述协议)。

// 创建RTCPeerConnection
const pc = new RTCPeerConnection({
  iceServers: [] // 局域网不需要STUN服务器
});

// 创建数据通道
const dataChannel = pc.createDataChannel('file-transfer');

// 监听连接事件
dataChannel.onopen = () => console.log('通道开了,可以传文件了');
dataChannel.onmessage = handleMessage;

3. 断点续传:从中断处继续

断点续传的关键是记录已传块的进度。用IndexedDB或者localStorage存进度:

// 发送文件块
async function sendFileChunks(file, dataChannel) {
  const chunks = chunkFile(file);
  const progress = loadProgress(file.name) || 0; // 从本地加载进度
  
  for (let i = progress; i < chunks.length; i++) {
    const chunk = chunks[i];
    const arrayBuffer = await chunk.arrayBuffer();
    
    // 发送块数据,带上索引
    dataChannel.send(JSON.stringify({
      type: 'chunk',
      index: i,
      data: arrayBuffer
    }));
    
    saveProgress(file.name, i + 1); // 保存进度
  }
  
  // 发送结束信号
  dataChannel.send(JSON.stringify({ type: 'end' }));
}

接收端收到块后,先存到临时数组,收到'end'信号再合并成完整文件。

浏览器限制:那些坑爹的现实

1. 文件大小限制:Chrome说不行就不行

Chrome对单个文件上传有限制,默认是2GB。有些版本甚至更低。遇到大文件怎么办?继续分块,但块数太多会影响性能。

解决方案:用File System Access API(Chrome 86+),可以直接操作本地文件系统,绕过内存限制。

// 使用File System Access API
const fileHandle = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const writableStream = await fileHandle.createWritable();

// 分块写入
for (const chunk of chunks) {
  await writableStream.write(chunk);
}
await writableStream.close();

2. 内存泄漏:传着传着浏览器崩了

大文件传输时,如果不及时释放Blob对象,内存会爆。解决方案:用stream API边读边传:

// 用ReadableStream处理大文件
const stream = file.stream();
const reader = stream.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  // 直接发送value(Uint8Array)
  dataChannel.send(value);
}

3. 网络限制:局域网防火墙挡道

公司局域网可能有防火墙,WebRTC的UDP连接会被挡。解决方案:降级到WebSocket,或者用TURN服务器中转(但这就不纯前端了)。

实战案例:三个真实场景

案例1:照片分享应用

我之前做的一个家庭相册App,用这个技术实现了局域网照片同步。妈妈在客厅电脑上传相册,爸爸在卧室就能直接收到,不用连路由器。

关键代码:进度条显示 + 错误重试

// 进度显示
dataChannel.onmessage = (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'progress') {
    updateProgressBar(message.percent);
  }
};

案例2:游戏存档同步

做游戏开发时,团队成员在局域网同步大存档文件(几GB)。用断点续传,网络断了重连后从断点继续,省去了重传的时间。

案例3:视频剪辑素材传输

剪辑师在局域网传4K视频素材。传统方法用U盘慢,用这个方案直接浏览器传,还能显示传输速度和剩余时间。

其他方案:当WebRTC不够用时

方案1:WebTorrent

基于WebRTC的BitTorrent实现,支持多对多传输,更适合大文件群发。

import WebTorrent from 'webtorrent';

const client = new WebTorrent();
client.seed(file, (torrent) => {
  console.log('种子创建成功:', torrent.magnetURI);
});

优点:多人同时下载快。缺点:需要种子文件管理。

方案2:Socket.IO + 二进制传输

用WebSocket传二进制数据,配合socket.io实现断点续传。

const socket = io();
socket.emit('send-file-chunk', { chunk, index });

socket.on('chunk-received', (index) => {
  // 继续发下一块
});

优点:兼容性好。缺点:需要服务器中转。

方案3:Electron应用

如果纯浏览器限制太多,可以做个Electron桌面应用,用Node.js的fs模块直接操作文件系统,结合WebRTC。

总结与展望

纯前端P2P大文件断点传输,听起来高大上,其实就是把文件切块 + WebRTC传数据 + 本地存储进度。浏览器限制是客观存在的,但通过File System API和内存管理,大部分场景都能搞定。

未来,随着WebTransport协议的普及(基于HTTP/3),传输效率会更高。5G和WiFi 6普及后,局域网传输速度会飞起。

下次老婆让你传照片,别再抱怨网速了,直接用浏览器P2P传吧!有什么问题,评论区见。🚀

Ollama 本地部署完整指南

1. 简介

1.1 什么是 Ollama?

Ollama 是一个开源的大模型运行工具,支持在本地运行 Llama 3、Qwen2.5、DeepSeek、Mistral 等上百款大语言模型。它通过命令行操作,简单高效,特别适合开发者快速部署和测试各类 AI 模型。

1.2 核心特性

特性 说明
一键部署 一行命令完成安装和模型启动
API 兼容 自带 OpenAI 格式 API,现有项目可直接迁移
跨平台支持 macOS、Linux、Windows 全平台覆盖
模型丰富 内置 Qwen2.5、DeepSeek-V3、Llama 3 等上百款模型
安全可靠 支持密钥认证,修复已知安全漏洞

1.3 硬件要求

模型规模 显存要求 内存要求 推荐场景
3B(轻量) 3GB+ 8GB+ 低配设备、快速测试
7B(推荐) 4-6GB 16GB+ 日常开发、个人使用
13B(进阶) 10-12GB 32GB+ 专业应用、团队协作
30B+(专业) 24GB+ 64GB+ 企业部署、复杂任务

2. 安装指南

2.1 macOS 安装

# 方法一:一键安装脚本(推荐)
curl -fsSL https://ollama.com/install.sh | sh

# 方法二:使用 Homebrew
brew install ollama

# 验证安装
ollama --version

2.2 Linux 安装

# 一键安装脚本
curl -fsSL https://ollama.com/install.sh | sh

# 验证安装
ollama --version

2.3 Windows 安装

  1. 访问官网下载:ollama.com/download
  2. 下载 OllamaSetup.exe
  3. 运行安装程序,务必勾选「Add to PATH」
  4. 打开 PowerShell 或 CMD,验证安装:
ollama --version

2.4 验证安装成功

安装完成后,运行以下命令验证:

ollama --version
# 输出示例:ollama version is 0.12.0

3. 启动服务(重要)

3.1 启动 Ollama 服务

安装完成后,必须先启动 Ollama 服务才能下载模型或进行对话。

方法一:启动服务(推荐)

ollama serve

保持这个终端窗口运行,然后新开一个终端窗口执行其他命令。


方法二:后台运行(macOS/Linux)

# 后台启动服务
ollama serve &

# 然后直接执行其他命令
ollama pull qwen2.5:7b

Windows 用户:

确保 Ollama 应用已经在运行(在系统托盘查看 Ollama 图标),或在 PowerShell/CMD 中执行:

ollama serve

3.2 验证服务运行状态

# 测试服务是否正常运行
curl http://localhost:11434/api/tags

# 或查看已安装模型
ollama list

3.3 服务未响应处理

如果遇到 Error: ollama server not responding 错误:

  1. 确认服务已启动:运行 ollama serve
  2. 检查端口占用lsof -i :11434(macOS/Linux)或 netstat -ano | findstr :11434(Windows)
  3. 重启服务:关闭当前终端,重新执行 ollama serve
  4. 检查安装:运行 ollama --version 确认正确安装

4. 国内加速配置(必做)

4.1 设置国内镜像

由于 Ollama 官方模型库在国内访问较慢,建议配置国内镜像加速:

# macOS / Linux
export OLLAMA_MODEL_SERVER=https://mirror.ollama.com

# Windows(PowerShell)
$env:OLLAMA_MODEL_SERVER="https://mirror.ollama.com"

4.2 永久配置镜像

macOS / Linux(推荐):

# 编辑配置文件
nano ~/.bashrc  # 或 ~/.zshrc

# 添加以下内容
export OLLAMA_MODEL_SERVER=https://mirror.ollama.com

# 保存后重新加载配置
source ~/.bashrc  # 或 source ~/.zshrc

Windows:

  1. 右键「此电脑」→「属性」→「高级系统设置」→「环境变量」
  2. 在「用户变量」中新建:
    • 变量名:OLLAMA_MODEL_SERVER
    • 变量值:https://mirror.ollama.com

5. 安全配置(重要)

5.1 设置 API 密钥

为防止未授权访问,建议设置访问密钥:

# 设置密钥
export OLLAMA_API_KEY=your_strong_password123

# Windows PowerShell
$env:OLLAMA_API_KEY="your_strong_password123"

5.2 限制本地访问

仅允许本地访问,避免暴露到公网:

# 绑定到本地回环地址
export OLLAMA_HOST=127.0.0.1:11434

# Windows PowerShell
$env:OLLAMA_HOST="127.0.0.1:11434"

6. 模型管理

6.1 搜索可用模型

访问 Ollama 官方模型库:ollama.com/library

常用中文模型推荐:

模型 说明 显存占用
qwen2.5:7b 通义千问 2.5,中文效果优异 ~4.5GB
qwen2.5:14b 更强中文能力,适合专业场景 ~9GB
deepseek-r1:7b DeepSeek 推理模型 ~4.5GB
gemma2:9b Google 开源模型 ~5.5GB
llama3.1:8b Meta Llama 3.1 ~5GB

6.2 下载模型

# 下载通义千问 7B(中文推荐)
ollama pull qwen2.5:7b

# 下载 INT4 量化版本(显存优化)
ollama pull qwen2.5:7b-chat-q4_0

# 下载 Llama 3.1
ollama pull llama3.1:8b

# 下载 DeepSeek 推理模型
ollama pull deepseek-r1:7b

6.3 查看已安装模型

ollama list

输出示例:

NAME                    ID              SIZE    MODIFIED
qwen2.5:7b              846a0b7e        4.7GB   2 hours ago
llama3.1:8b             a7872503        4.9GB   1 day ago

6.4 删除模型

# 删除指定模型
ollama rm qwen2.5:7b

# 删除多个模型
ollama rm llama3.1:8b gemma2:9b

7. 使用指南

7.1 命令行对话

# 启动交互式对话
ollama run qwen2.5:7b

# 直接提问(非交互模式)
ollama run qwen2.5:7b "请用 Python 写一个快速排序"

# 从文件读取提示
ollama run qwen2.5:7b "$(cat prompt.txt)"

7.2 启动 API 服务

# 启动服务(默认端口 11434)
ollama serve

# 自定义端口
export OLLAMA_HOST=0.0.0.0:8080
ollama serve

7.3 测试 API 服务

# 测试健康检查
curl http://localhost:11434/api/tags

# 测试对话接口
curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b",
  "prompt": "你好,请介绍一下你自己"
}'

8. 编程调用

8.1 浏览器环境(原生 Fetch)

// 流式响应示例
async function chatWithOllama(prompt) {
  const response = await fetch("http://localhost:11434/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer your_api_key"
    },
    body: JSON.stringify({
      model: "qwen2.5:7b",
      messages: [{ role: "user", content: prompt }],
      stream: true
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let result = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split("\n").filter(line => line.trim());

    for (const line of lines) {
      if (line.startsWith("data: ")) {
        const data = line.slice(6);
        if (data === "[DONE]") continue;
        try {
          const json = JSON.parse(data);
          const content = json.choices?.[0]?.delta?.content;
          if (content) {
            result += content;
            console.log(content);
          }
        } catch (e) {
          // 忽略解析错误
        }
      }
    }
  }
  return result;
}

// 使用
chatWithOllama("写一个 Python 快速排序");

8.2 Node.js 环境

const axios = require("axios");

async function chatWithOllama(prompt) {
  const response = await axios.post(
    "http://localhost:11434/v1/chat/completions",
    {
      model: "qwen2.5:7b",
      messages: [{ role: "user", content: prompt }],
      temperature: 0.7,
      max_tokens: 2000
    },
    {
      headers: {
        "Authorization": "Bearer your_api_key"
      }
    }
  );

  return response.data.choices[0].message.content;
}

// 使用
chatWithOllama("写一个 Python 快速排序").then(console.log);

9. 常见问题

9.1 下载速度慢

解决方案:

# 配置国内镜像
export OLLAMA_MODEL_SERVER=https://mirror.ollama.com

9.2 显存不足

解决方案:

# 选择 INT4 量化版本
ollama pull qwen2.5:7b-chat-q4_0

# 或选择更小的模型
ollama pull qwen2.5:3b

9.3 Windows 启动失败

解决方案:

下载并安装 Microsoft C++ 生成工具: visualstudio.microsoft.com/visual-cpp-…

9.4 服务未响应

检查清单:

# 1. 确认服务正在运行
ollama serve

# 2. 检查端口是否被占用
lsof -i :11434  # macOS/Linux
netstat -ano | findstr :11434  # Windows

# 3. 检查防火墙设置

# 4. 验证 API 密钥
echo $OLLAMA_API_KEY

9.5 模型加载慢

优化方案:

  • 将模型存储在 SSD 硬盘
  • 关闭其他占用 GPU 的程序
  • 选择量化版本的模型

10. 进阶配置

10.1 自定义模型

创建 Modelfile 自定义模型:

FROM qwen2.5:7b

# 设置系统提示
SYSTEM You are a helpful AI assistant specialized in Python programming.

# 设置参数
PARAMETER temperature 0.7
PARAMETER num_ctx 4096

构建自定义模型:

ollama create my-python-assistant -f Modelfile
ollama run my-python-assistant

10.2 并发设置

调整并发请求数:

export OLLAMA_NUM_PARALLEL=4

10.3 日志级别

# 设置日志级别(debug/info/warn/error)
export OLLAMA_DEBUG=1
export OLLAMA_LOG_LEVEL=info

11. 更新与卸载

11.1 更新 Ollama

# macOS / Linux
ollama update

# Windows:重新下载安装包覆盖安装

11.2 卸载 Ollama

macOS:

brew uninstall ollama
rm -rf ~/.ollama

Linux:

sudo systemctl stop ollama
sudo systemctl disable ollama
rm -rf /usr/local/bin/ollama ~/.ollama

Windows:

  1. 控制面板 → 程序和功能 → 卸载 Ollama
  2. 删除用户目录下的 .ollama 文件夹

mach_msg_header_t详解

借助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)); 到底发生了什么?

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

包含 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

参考: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 之间的状态与资源

CSS魔法:对话生成器与奔驰骏马的创意实现

最近逛GitHub时,发现一个很有意思的项目——一个纯前端实现的对话生成器,效果非常精致。推荐给感兴趣的朋友,如果想深入钻研CSS,这个项目也很值得研究。另外,马上就要到马年了,顺便也分享一个之前收藏的用CSS实现的动态奔跑骏马效果。这两个项目都展现了CSS的巧妙运用,既适合学习借鉴,也很有趣味性。

👩聊天生成器

对话生成器:一个简单好玩的在线聊天制作工具

github地址:github.com/zixiwangluo…

在线地址:zixiwangluo.github.io/wxdh/

🌠功能简介

这是一个可以在线生成微信风格聊天截图的工具,支持以下功能:

  • 自定义苹果或安卓手机状态栏(时间、电量、信号等)
  • 设置对话双方的头像和昵称
  • 发送文字、语音、红包、转账等聊天元素
  • 自由编辑对话内容,生成高度仿真的微信聊天效果
  • 无论是制作搞笑对话、剧情截图,还是用于演示与分享等轻松场景,这个工具都能带来不少乐趣。

该项目完全基于HTML、CSS和JavaScript实现,非常适合前端学习者参考。如果想直接使用,可将源码下载到本地,打开index.html即可运行;如果有服务器,也可以直接部署到Nginx等环境中,使用非常简单,这里就不多做介绍了。

平时写文章或做教程时如果需要聊天素材,也可以用它来快速生成。效果示例如下:

🐎纯css实现的奔驰的骏马

随着马年临近,这个奔跑的骏马效果格外应景。最初在某博客中发现这个创意实现,视觉效果流畅自然,于是特别部署了在线演示方便体验。

演示地址1:h5.xiuji.mynatapp.cc/horse/

演示地址2:aa51f2d3.pinit.eth.limo/

效果如下:

horse-run.gif

如果你想获取源码,可以通过浏览器开发者工具(F12)查看并复制相关代码。代码就两文件,一个html,一个css。

👽总结

这两个项目虽然功能不同,但都体现了前端开发的创意与技巧:

  1. 微信对话生成器展示了如何通过前端技术模拟复杂UI,实现高度可定制的交互工具,对学习CSS布局和JavaScript DOM操作很有帮助。
  2. CSS奔驰骏马则纯粹依靠样式表创造流畅动画,是学习CSS动画、关键帧和性能优化的优秀案例。

无论是想要寻找实用工具,还是希望深入学习前端技术,这两个项目都值得收藏研究。它们证明了,即使不使用复杂框架和库,纯前端技术也能创造出既美观又实用的效果。

🐟今日摸鱼小贴士:从入门到“入厕”新境界

领导眼皮底下吨吨灌水,洗手间里频繁“打卡”

主打一个尿喝白,电充绿,事干黄

业绩虽暂时躺平,但新陈代谢已实现遥遥领先

❗❗❗特别声明

此项目来自互联网公开资源,仅供学习交流使用,切勿用于非法途径,由此产生任何纠纷由使用者本人自己承担,如有侵权,请及时联系删除❗❗❗

《实时渲染》第2章-图形渲染管线-2.4光栅化

实时渲染

2. 图形渲染管线

2.4 光栅化

顶点及其关联的着色数据(全部来自几何处理阶段)在进行变换和投影后,下一阶段的目标是找到所有像素(图片元素的缩写),这些像素位于要渲染的图元内部,例如三角形。我们将此过程称为光栅化,它分为两个功能子阶段:三角形设置(也称为图元组装)和三角形遍历。它们显示在图2.8的左侧。请注意,这些也可以处理点和线,但由于三角形最常见,因此子阶段的名称中带有“三角形”。因此,光栅化也称为扫描转换,是将屏幕空间中的二维顶点(每个顶点具有z值(深度值)和与每个顶点关联的各种着色信息)转换为屏幕上的像素。光栅化也可以被认为是几何处理和像素处理之间的同步点,因为在这里三角形由三个顶点形成并最终发送到像素处理。

图2.8. 左:光栅化分为两个功能阶段,称为三角形设置和三角形遍历。右:像素处理分为两个功能阶段,即像素着色和合并。

是否认为三角形与像素重叠取决于您如何设置GPU的管线。例如,您可以使用点抽样来确定“内部性”。最简单的情况是在每个像素的中心使用单点样本,因此如果该中心点在三角形内部,那么相应的像素也被视为三角形内部。您还可以使用超级采样或多重采样抗锯齿技术(第5.4.2节)为每个像素使用多个样本。另一种方法是使用保守光栅化,其中的定义是,如果像素的至少一部分与三角形重叠,则像素位于三角形“内部”(第23.1.2节)。

2.4.1 三角形设置

在这个阶段,计算三角形的微分、边方程和其他数据。这些数据可用于三角形遍历(第2.4.2节),以及用于几何阶段产生的各种着色数据的插值。这个任务通常是硬件的固定功能。

2.4.2 三角形遍历

在这个阶段,会检查每个像素中心(或样本)被三角形覆盖的位置,并为与三角形重叠的像素部分生成片元。更详细的抽样方法可以在第5.4节中找到。查找三角形内的样本或像素通常称为三角形遍历。每个三角形片元的属性是使用在三个三角形顶点之间插入的数据生成的(第5章)。这些属性包括片段的深度,以及来自几何阶段的任何着色数据。McCormack等人在文献[1162]中提到了有关三角形遍历的更多信息。也是在这个阶段,对三角形执行透视校正插值[694](第23.1.1节)。然后将图元内的所有像素或样本发送到像素处理阶段,这个下述章节会进行论述。

宝洁第二财季净销售额222亿美元,同比增长1%

当地时间1月22日,宝洁公司发布2026财年第二季度(2025年10-12月)财报。财报显示,第二财季净销售额为222亿美元,同比增长1%;摊薄后每股净收益为1.78美元,同比下降5%。该公司维持2026财年总销售额同比增长1%至5%的指引区间,将2026财年摊薄后每股净收益增长指引从原先的3%-9%调整至1%-6%。(界面)

天合光能:将剩余募集资金17亿元用于新项目“分布式智慧光伏电站建设项目”

36氪获悉,天合光能公告,公司拟缩减“年产35GW直拉单晶项目”的募集资金投资金额,不再实施二期15GW项目,并将剩余募集资金17亿元用于新项目“分布式智慧光伏电站建设项目”。新项目预计2026年底建成并投入运营。公司表示,本次变更有利于提高募集资金使用效率,合理优化资源配置,巩固市场地位,不会对公司正常经营产生不利影响。该事项尚需提交公司股东会和债券持有人会议审议通过后方可实施。

晶升股份:拟购买为准智能100%股份,交易作价8.57亿元

36氪获悉,晶升股份公告,公司拟通过发行股份及支付现金的方式购买本尚科技等10名交易对方持有的为准智能100%股份,同时募集配套资金。为准智能100%股份的交易作价为8.57亿元。本次交易构成关联交易和重大资产重组,但不构成重组上市。交易完成后,为准智能将成为上市公司控股子公司。

美股大型科技股盘前涨跌不一,Arm涨超2%

36氪获悉,美股大型科技股盘前涨跌不一,截至发稿,Arm涨超2%,英伟达涨超1%,谷歌涨0.1%,微软涨0.06%;特斯拉跌0.17%,奈飞跌0.11%,亚马逊跌0.07%,苹果跌0.03%。

四连板*ST长药:收到终止上市事先告知书

36氪获悉,*ST长药公告,公司于2026年1月23日收到深圳证券交易所下发的《事先告知书》,拟决定终止公司股票上市交易。公司2021年至2023年年度报告信息披露存在虚假记载,触及创业板股票上市规则规定的股票终止上市情形。公司有权在规定期限内申请听证或提出书面陈述和申辩。若未在规定期限内提出听证申请,上市委员会将在规定期限后审议是否终止上市。公司股票自1月26日起停牌,退市整理期为十五个交易日,整理期届满次一交易日摘牌。

三连板ST新华锦:预计2025年净亏损9500万元-1.42亿元

36氪获悉,ST新华锦公告,预计2025年度实现归属于母公司所有者的净利润-1.42亿元到-9500万元,将出现亏损。海正石墨因尚未办理矿区用地、未取得安全生产许可证和环评批复文件等,未达到矿山企业开工生产条件,短期内无法进行矿山开采。

龙韵股份:筹划购买愚恒影业58%股权,股票停牌

36氪获悉,龙韵股份公告,公司拟筹划以发行股份的方式购买新疆愚恒影业集团有限公司58%股权。本次交易完成后,愚恒影业将成为公司的全资子公司。本次交易预计构成重大资产重组,构成关联交易。本次交易不会导致公司实际控制人的变更,不构成重组上市。公司股票自2026年1月26日起停牌,预计不超过10个交易日。

深交所:本周共对326起证券异常交易行为采取了自律监管措施

36氪获悉,深交所本周共对326起证券异常交易行为采取了自律监管措施,涉及盘中拉抬打压、虚假申报等异常交易情形;对因涉嫌财务数据存在虚假记载被中国证监会立案的“*ST立方”“*ST长药”进行重点监控;共对6起上市公司重大事项进行核查,并上报证监会1起涉嫌违法违规案件线索。近期“锋龙股份”股价出现严重异常波动,公司停牌核查并多次发布风险提示公告,复牌后股价连续涨停,部分投资者在交易该股过程中存在影响股票交易正常秩序的异常交易行为,本所依规对相关投资者采取了暂停交易等自律监管措施。
❌