普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月10日掘金 iOS

“死了么”App荣登付费榜第一名!

作者 iOS研究院
2026年1月10日 14:48

背景

2026年初的App Store付费榜,突然杀出一匹“黑马”——一款名为 「“死了么 - 官方正版”」 的产品,以8元付费下载的模式,毫无征兆地登顶付费榜单榜首,成为今年首个现象级爆款。对于常年关注应用市场的开发者和从业者来说,这波操作堪称“惊喜与意外并存”:既好奇这款突然冒头的产品究竟有何魅力,更疑惑为何一款2025年就上架的产品,直到现在才迎来流量爆发。

f5521fe6324e395d5a821f0a3f03e95c.jpg

榜单变化

直观的榜单表现,打开App Store付费榜,“死了么 - 官方正版”赫然占据TOP1位置,远超同类付费应用,下载量和付费转化数据一路飙升。从榜单页面能清晰看到,这款产品的登顶直接打乱了原有榜单格局,不少常年盘踞前列的付费应用纷纷退居其后,足见其短期内的爆发力。

ScreenShot_2026-01-10_142553_600.png

关键词变化

从行业常规逻辑来看,应用上架后若出现名称违规、内容不符合平台规范、关键词堆砌等问题,就可能被平台下架或限制曝光(即“清词、清榜”)。“死了么 - 官方正版”大概率是在初期运营中触碰了平台规则红线,导致被限制传播。

3333.png

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

昨天以前掘金 iOS

AppStore卡审依旧存在,预计下周将逐渐恢复常态!

作者 iOS研究院
2026年1月9日 10:09

背景

圣诞节🎄虽然结束了,后劲儿依旧在。最直观的感受就是AppStore审核节奏还未恢复正常。依然存在审核时间较久或等待审核时间过长的问题。

举一个直观的例子🌰:

一座5层高的商场,每层都预备了洗手间🚾。正常情况下,足够满足整座商城客流量的需求。但是赶上了节假日高峰,并且只有3层洗手间可用。那么在常态客流量不变的情况也已经拥挤,更不要说节假日高峰期。

就第三方上架&更新趋势来看,AppStore审核节奏也将逐步正常。

非必要迭代

如果不是遇到重大线上问题或重大功能迭代,建议不更新或不上新包。避免正常产品遭遇卡审状态,导致难以定位问题或者审核员摆烂直接一手4.3a。

毕竟AppStore审核团队,刚刚经历了年关肯定积压了大量待审核的产品,多少也有些烦躁。(PS:单纯从心理角度来讲

新包、新账号和新代码,“三新原则”基本上叠满了卡审buffer。【特指中国大陆的开发者,海外账号亲测影响不大。】

重大更新

对于产品有着节前活动或市场战略布局的产品,那么也不用担心。在AppStore依然存在便捷通道:即加急审核!

常规产品,不必担心,这是官方提供的合理渠道,确实保障开发者的紧急需求【AppStore中的急诊室】。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

2026 年 Expo + React Native 项目接入微信分享完整指南

2026年1月8日 19:46

2026 年 Expo + React Native 项目接入微信分享完整指南

本文基于 Expo SDK 54 + React Native 0.81 + react-native-wechat-lib 1.1.27 的实战经验,详细记录了在 Expo 管理的 React Native 项目中接入微信分享功能的完整流程和踩坑记录。

前言

在 React Native 生态中,react-native-wechat-lib 是目前最常用的微信 SDK 封装库。但由于该库更新较慢,加上 Expo 的特殊性,接入过程中会遇到不少坑。本文将分享我们在生产项目中的完整接入方案。

技术栈

  • Expo SDK: 54.0.30
  • React Native: 0.81.5
  • react-native-wechat-lib: 1.1.27
  • 构建方式: EAS Build

整体流程

准备工作 → 安装依赖 → 创建 Expo 插件 → 配置 app.config.js → 
编写 JS 服务层 → 服务器配置 → 微信开放平台配置 → 构建测试

第一步:准备工作

1.1 微信开放平台配置

  1. 登录 微信开放平台
  2. 创建移动应用,获取 AppID
  3. 配置 iOS 应用信息:
    • Bundle ID: com.yourapp
    • Universal Link: https://yourdomain.com/open/

1.2 Apple Developer 配置

  1. 获取 Team ID(格式如 A1B2C3D4E5
  2. 确认 Bundle ID 与微信开放平台一致

第二步:安装依赖

npm install react-native-wechat-lib@1.1.27

⚠️ 注意:在 Expo 管理的项目中,不需要手动执行 pod install,EAS Build 会自动处理。

第三步:创建 Expo Config Plugin

由于 Expo 管理原生代码,我们需要通过 Config Plugin 来配置微信 SDK 所需的原生设置。

创建 plugins/withWechat.js

const { withInfoPlist, withAndroidManifest } = require("expo/config-plugins");

/**
 * 微信 SDK Expo Config Plugin
 * 自动配置 iOS 和 Android 的微信相关设置
 */
function withWechat(config, { appId, universalLink }) {
  if (!appId) {
    throw new Error("withWechat: appId is required");
  }

  // iOS 配置
  config = withInfoPlist(config, (config) => {
    // 添加微信 URL Scheme
    const urlTypes = config.modResults.CFBundleURLTypes || [];
    const wechatScheme = {
      CFBundleURLSchemes: [appId],
      CFBundleURLName: "wechat",
    };

    const hasWechatScheme = urlTypes.some(
      (type) =>
        type.CFBundleURLSchemes &&
        type.CFBundleURLSchemes.includes(appId)
    );

    if (!hasWechatScheme) {
      urlTypes.push(wechatScheme);
    }
    config.modResults.CFBundleURLTypes = urlTypes;

    // 添加 LSApplicationQueriesSchemes
    const queriesSchemes = config.modResults.LSApplicationQueriesSchemes || [];
    const wechatSchemes = ["weixin", "weixinULAPI"];
    wechatSchemes.forEach((scheme) => {
      if (!queriesSchemes.includes(scheme)) {
        queriesSchemes.push(scheme);
      }
    });
    config.modResults.LSApplicationQueriesSchemes = queriesSchemes;

    return config;
  });

  // Android 配置
  config = withAndroidManifest(config, (config) => {
    const mainApplication = config.modResults.manifest.application?.[0];
    if (!mainApplication) return config;

    const packageName = config.android?.package || "com.yourapp";
    const activities = mainApplication.activity || [];
    const wxActivityName = `${packageName}.wxapi.WXEntryActivity`;

    const hasWxActivity = activities.some(
      (activity) => activity.$?.["android:name"] === wxActivityName
    );

    if (!hasWxActivity) {
      activities.push({
        $: {
          "android:name": wxActivityName,
          "android:exported": "true",
          "android:launchMode": "singleTask",
          "android:taskAffinity": packageName,
          "android:theme": "@android:style/Theme.Translucent.NoTitleBar",
        },
      });
    }

    mainApplication.activity = activities;
    return config;
  });

  return config;
}

module.exports = withWechat;

第四步:配置 app.config.js

module.exports = {
  expo: {
    name: "你的应用名",
    slug: "your-app",
    version: "1.0.0",
    
    extra: {
      wechatAppId: "wx你的AppID", // 微信 AppID
    },
    
    ios: {
      bundleIdentifier: "com.yourapp",
      associatedDomains: [
        "applinks:yourdomain.com",
        "webcredentials:yourdomain.com",
      ],
      infoPlist: {
        LSApplicationQueriesSchemes: ["weixin", "weixinULAPI", "wechat"],
      },
    },
    
    android: {
      package: "com.yourapp",
    },
    
    plugins: [
      [
        "./plugins/withWechat",
        {
          appId: "wx你的AppID",
          universalLink: "https://yourdomain.com/open/",
        },
      ],
    ],
  },
};

第五步:编写微信服务层

创建 src/services/wechatService.ts

import { Platform, Alert } from "react-native";
import Constants from "expo-constants";

// 从 Expo 配置中获取微信 AppID
const WECHAT_APP_ID = Constants.expoConfig?.extra?.wechatAppId || "";

// 动态加载微信 SDK
let WeChat: any = null;
let sdkLoadAttempted = false;

const getWechatSDK = () => {
  if (sdkLoadAttempted) return WeChat;
  sdkLoadAttempted = true;
  
  if (Platform.OS === "web") {
    return null;
  }
  
  try {
    const module = require("react-native-wechat-lib");
    WeChat = module.default || module;
    
    if (!WeChat || typeof WeChat.registerApp !== "function") {
      WeChat = null;
    }
    
    return WeChat;
  } catch (error) {
    console.warn("微信 SDK 加载失败:", error);
    return null;
  }
};

class WechatService {
  private isRegistered = false;

  // 检查 SDK 是否可用
  isAvailable(): boolean {
    if (Platform.OS === "web") return false;
    const sdk = getWechatSDK();
    return sdk !== null && typeof sdk.registerApp === "function";
  }

  // 注册微信 SDK
  async register(): Promise<boolean> {
    if (this.isRegistered) return true;
    
    const sdk = getWechatSDK();
    if (!sdk) return false;
    
    try {
      const result = await sdk.registerApp(WECHAT_APP_ID);
      this.isRegistered = result;
      return result;
    } catch (error) {
      console.error("微信 SDK 注册失败:", error);
      return false;
    }
  }

  // 检查微信是否已安装
  async isWechatInstalled(): Promise<boolean> {
    const sdk = getWechatSDK();
    if (!sdk) return false;
    
    try {
      return await sdk.isWXAppInstalled();
    } catch (error) {
      return false;
    }
  }

  // 分享网页到微信
  async shareWebpage(params: {
    title: string;
    description: string;
    thumbImageUrl?: string;
    webpageUrl: string;
    scene?: "session" | "timeline" | "favorite";
  }): Promise<{ success: boolean; message: string }> {
    
    if (!this.isAvailable()) {
      return { 
        success: false, 
        message: Platform.OS === "web" 
          ? "Web 端暂不支持微信分享" 
          : "微信分享功能需要在正式构建版本中使用"
      };
    }

    try {
      const registered = await this.register();
      if (!registered) {
        return { success: false, message: "微信 SDK 初始化失败" };
      }

      const isInstalled = await this.isWechatInstalled();
      if (!isInstalled) {
        return { success: false, message: "请先安装微信" };
      }

      const sceneMap = {
        session: 0,   // 聊天界面
        timeline: 1,  // 朋友圈
        favorite: 2,  // 收藏
      };

      const sdk = getWechatSDK();
      await sdk.shareWebpage({
        title: params.title,
        description: params.description,
        thumbImageUrl: params.thumbImageUrl || "",
        webpageUrl: params.webpageUrl,
        scene: sceneMap[params.scene || "session"],
      });

      return { success: true, message: "分享成功" };
    } catch (error: any) {
      if (error?.errCode === -2) {
        return { success: false, message: "已取消分享" };
      }
      return { success: false, message: error?.message || "分享失败" };
    }
  }

  // 分享图片到微信
  async shareImage(params: {
    imageUrl?: string;
    imageBase64?: string;
    scene?: "session" | "timeline" | "favorite";
  }): Promise<{ success: boolean; message: string }> {
    if (!this.isAvailable()) {
      return { success: false, message: "微信分享不可用" };
    }

    try {
      await this.register();
      
      const isInstalled = await this.isWechatInstalled();
      if (!isInstalled) {
        return { success: false, message: "请先安装微信" };
      }

      const sceneMap = { session: 0, timeline: 1, favorite: 2 };
      const sdk = getWechatSDK();
      
      await sdk.shareImage({
        imageUrl: params.imageBase64 || params.imageUrl,
        scene: sceneMap[params.scene || "session"],
      });

      return { success: true, message: "分享成功" };
    } catch (error: any) {
      if (error?.errCode === -2) {
        return { success: false, message: "已取消分享" };
      }
      return { success: false, message: "分享失败" };
    }
  }
}

export const wechatService = new WechatService();

第六步:服务器配置 (Universal Link)

在你的服务器上创建 apple-app-site-association 文件。

文件路径

https://yourdomain.com/.well-known/apple-app-site-association

文件内容

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": ["TEAMID.com.yourapp"],
        "components": [
          { "/": "/open/*" },
          { "/": "/topic/*" }
        ]
      }
    ]
  },
  "webcredentials": {
    "apps": ["TEAMID.com.yourapp"]
  }
}

⚠️ 将 TEAMID 替换为你的 Apple Team ID,com.yourapp 替换为你的 Bundle ID。

服务器配置要求

  1. 必须通过 HTTPS 访问
  2. Content-Type 应为 application/json
  3. 文件名不能有 .json 后缀
  4. 不能有重定向

Nginx 配置示例

location /.well-known/apple-app-site-association {
    default_type application/json;
}

第七步:在组件中使用

import React from "react";
import { Button, Alert } from "react-native";
import { wechatService } from "@/services/wechatService";

export function ShareButton() {
  const handleShare = async () => {
    const result = await wechatService.shareWebpage({
      title: "分享标题",
      description: "分享描述",
      thumbImageUrl: "https://example.com/thumb.jpg",
      webpageUrl: "https://example.com/share-page",
      scene: "session", // 或 "timeline" 分享到朋友圈
    });

    if (result.success) {
      Alert.alert("成功", "分享成功");
    } else {
      Alert.alert("提示", result.message);
    }
  };

  return <Button title="分享到微信" onPress={handleShare} />;
}

第八步:构建和测试

使用 EAS Build

# 构建 iOS 生产版本
eas build -p ios --profile production

# 构建并自动提交到 TestFlight
eas build -p ios --profile production --auto-submit

测试注意事项

  1. Expo Go 不支持:微信 SDK 是原生模块,必须使用 EAS Build 构建的版本测试
  2. 重启手机:安装新版本后建议重启手机,让 iOS 刷新 Associated Domains 缓存
  3. 验证 Universal Link:访问 https://app-site-association.cdn-apple.com/a/v1/yourdomain.com 确认 Apple 已缓存配置

常见问题排查

问题 1:分享时微信没有被唤起

可能原因:

  • Universal Link 配置不一致(微信开放平台、App 代码、服务器三端必须完全一致)
  • apple-app-site-association 文件内容错误或无法访问
  • Apple 还未缓存你的配置

排查步骤:

  1. 确认三端域名完全一致(注意 www 和非 www 的区别)
  2. 直接访问 https://yourdomain.com/.well-known/apple-app-site-association 确认可以下载
  3. 检查 Apple CDN 缓存:https://app-site-association.cdn-apple.com/a/v1/yourdomain.com

问题 2:SDK 注册失败

可能原因:

  • AppID 配置错误
  • 在 Expo Go 中运行(不支持)

解决方案:

  • 确认 app.config.js 中的 AppID 与微信开放平台一致
  • 使用 EAS Build 构建的版本测试

问题 3:提示"请先安装微信"

可能原因:

  • LSApplicationQueriesSchemes 未正确配置

解决方案: 确认 app.config.js 中包含:

infoPlist: {
  LSApplicationQueriesSchemes: ["weixin", "weixinULAPI", "wechat"],
}

调试技巧

在开发阶段,可以添加调试弹窗来追踪问题:

const DEBUG_MODE = true;

const debugAlert = (title: string, message: string) => {
  if (DEBUG_MODE) {
    Alert.alert(`[调试] ${title}`, message);
  }
};

// 在关键步骤添加调试
debugAlert("开始分享", `AppID: ${WECHAT_APP_ID}`);
debugAlert("注册结果", `registered: ${registered}`);
debugAlert("微信安装检查", `isInstalled: ${isInstalled}`);

总结

在 Expo 项目中接入微信分享的关键点:

  1. 使用 Config Plugin 配置原生设置,而不是手动修改原生代码
  2. 三端配置一致 是成功的关键(微信开放平台、App、服务器)
  3. Universal Link 配置正确且可访问
  4. 必须使用 EAS Build 构建的版本测试,Expo Go 不支持原生模块

希望这篇文章能帮助你顺利接入微信分享功能!如有问题欢迎评论区交流。


参考资料:

Luban 2 Flutter:一行代码在 Flutter 开发中实现图片压缩功能

作者 小熊码匠
2026年1月8日 16:56

Luban 2 Flutter —— 高效简洁的 Flutter 图片压缩插件,像素级还原微信朋友圈压缩策略。

📑 目录

📖 项目描述

开源地址:Gitee | Github

目前做 App 开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要的问题。单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少,压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。

于是自然想到 App 巨头"微信"会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。

因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比!

本库是 LubanFlutter 版本,使用 TurboJPEG 进行高性能图片压缩,提供简洁易用的 API 和接近微信朋友圈的压缩效果。

📊 效果与对比

图片类型 原图(分辨率, 大小) Luban(分辨率, 大小) Wechat(分辨率, 大小)
标准拍照 3024×4032, 5.10MB 1440×1920, 305KB 1440×1920, 303KB
高清大图 4000×6000, 12.10MB 1440×2160, 318KB 1440×2160, 305KB
2K 截图 1440×3200, 2.10MB 1440×3200, 148KB 1440×3200, 256KB
超长记录 1242×22080, 6.10MB 758×13490, 290KB 744×13129, 256KB
全景横图 12000×5000, 8.10MB 1440×600, 126KB 1440×600, 123KB
设计原稿 6000×6000, 6.90MB 1440×1440, 263KB 1440×1440, 279KB

🔬 核心算法特性

本库采用自适应统一图像压缩算法 (Adaptive Unified Image Compression),通过原图的分辨率特征,动态应用差异化策略,实现画质与体积的最优平衡。

智能分辨率决策

  • 高清基准 (1440p):默认以 1440px 作为短边基准,确保在现代 2K/4K 屏幕上的视觉清晰度
  • 全景墙策略:自动识别超大全景图(长边 >10800px),锁定长边为 1440px,保留完整视野
  • 超大像素陷阱:对超过 4096万像素的超高像素图自动执行 1/4 降采样处理
  • 长图内存保护:针对超长截图建立 10.24MP 像素上限,通过等比缩放防止 OOM

自适应比特率控制

  • 极小图 (<0.5MP):几乎不进行有损压缩,防止压缩伪影
  • 高频信息图 (0.5-1MP):提高编码质量,补偿分辨率损失
  • 标准图片 (1-3MP):应用平衡系数,对标主流社交软件体验
  • 超大图/长图 (>3MP):应用高压缩率,显著减少体积

健壮性保障

  • 膨胀回退:压缩后体积大于原图时,自动透传原图,确保绝不"负优化"
  • 输入防御:妥善处理极端分辨率输入(0、负数、1px 等),防止崩溃

📦 安装

pubspec.yaml 文件中添加依赖:

dependencies:
  luban: ^2.0.1

然后运行:

flutter pub get

💻 使用

压缩单张图片

使用 File 对象

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final file = File('/path/to/image.jpg');
  final result = await Luban.compress(file);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成');
    print('原图大小: ${compressionResult.originalSizeKb} KB');
    print('压缩后大小: ${compressionResult.compressedSizeKb} KB');
    print('压缩率: ${(compressionResult.compressionRatio * 100).toStringAsFixed(1)}%');
    print('输出文件: ${compressionResult.file.path}');
  } else {
    print('压缩失败: ${result.error}');
  }
}

使用字符串路径

import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final result = await Luban.compressPath('/path/to/image.jpg');
  
  result.fold(
    (error) => print('压缩失败: $error'),
    (compressionResult) {
      print('压缩完成,大小: ${compressionResult.compressedSizeKb} KB');
      print('输出文件: ${compressionResult.file.path}');
    },
  );
}

指定输出文件

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final inputFile = File('/path/to/image.jpg');
  final outputFile = File('/path/to/output/compressed.jpg');
  
  final result = await Luban.compressToFile(inputFile, outputFile);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成,文件已保存到: ${compressionResult.file.path}');
  }
}

指定输出目录

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final inputFile = File('/path/to/image.jpg');
  final outputDir = Directory('/path/to/output');
  
  final result = await Luban.compress(inputFile, outputDir: outputDir);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成,文件已保存到: ${compressionResult.file.path}');
  }
}

批量压缩图片

批量压缩返回 Result<BatchCompressionResult>,需要先检查成功或失败状态,然后访问 BatchCompressionResult 获取所有图片的压缩结果。

使用文件列表

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final files = [
    File('/path/to/image1.jpg'),
    File('/path/to/image2.jpg'),
    File('/path/to/image3.jpg'),
  ];
  
  final result = await Luban.compressBatch(files);
  
  if (result.isSuccess) {
    final batchResult = result.value;
    print('批量压缩完成');
    print('总数: ${batchResult.total}');
    print('成功: ${batchResult.successCount}');
    print('失败: ${batchResult.failureCount}');
    
    for (final item in batchResult.items) {
      if (item.isSuccess) {
        final compressionResult = item.result.value;
        print('${item.originalPath}: ${compressionResult.compressedSizeKb} KB');
      } else {
        print('${item.originalPath}: 压缩失败 - ${item.result.error}');
      }
    }
  } else {
    print('批量压缩失败: ${result.error}');
  }
}

使用路径列表

import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final paths = [
    '/path/to/image1.jpg',
    '/path/to/image2.jpg',
    '/path/to/image3.jpg',
  ];
  
  final result = await Luban.compressBatchPaths(paths);
  
  result.fold(
    (error) => print('批量压缩失败: $error'),
    (batchResult) {
      print('批量压缩完成,成功 ${batchResult.successCount}/${batchResult.total} 张');
      
      for (final compressionResult in batchResult.successfulResults) {
        print('${compressionResult.file.path}: ${compressionResult.compressedSizeKb} KB');
      }
    },
  );
}

批量压缩并指定输出目录

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final files = [
    File('/path/to/image1.jpg'),
    File('/path/to/image2.jpg'),
  ];
  final outputDir = Directory('/path/to/output');
  
  final result = await Luban.compressBatch(files, outputDir: outputDir);
  
  if (result.isSuccess) {
    final batchResult = result.value;
    print('批量压缩完成,成功 ${batchResult.successCount} 张');
    
    for (final compressionResult in batchResult.successfulResults) {
      print('压缩文件: ${compressionResult.file.path}');
    }
  } else {
    print('批量压缩失败: ${result.error}');
  }
}

iOS应用(App)生命周期、视图控制器(UIViewController)生命周期和视图(UIView)生命周期

2026年1月7日 10:32

清晰的理解它们能帮你更好地管理应用状态和资源。

一、iOS 应用(App)生命周期

应用生命周期描述了 App 从启动到终止的整个过程,由UIApplicationDelegate(应用代理)来管理。

核心阶段与代理方法(按执行顺序)

import UIKit

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    // 1. App启动完成(最核心的入口)
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        print("应用启动完成 - didFinishLaunchingWithOptions")

        // 通常在这里初始化根视图控制器、配置全局设置等

        return true

    }


    // 2. App即将进入前台(还未激活,可做界面刷新)
    func applicationWillEnterForeground(_ application: UIApplication) {
    
        print("即将进入前台 - applicationWillEnterForeground")

    }


    // 3. App已进入前台并激活(用户可交互)
    func applicationDidBecomeActive(_ application: UIApplication) {

        print("已激活 - applicationDidBecomeActive")

        // 恢复定时器、重新开始播放音频、刷新数据等

    }

    // 4. App即将进入后台(用户按Home键/切换App)
    func applicationWillResignActive(_ application: UIApplication) {

        print("即将失活 - applicationWillResignActive")

        // 暂停定时器、保存临时数据、暂停音频播放等

    }


    // 5. App已进入后台
    func applicationDidEnterBackground(_ application: UIApplication) {

        print("已进入后台 - applicationDidEnterBackground")

        // 持久化数据、释放不必要的资源(有大约5秒时间,耗时操作需申请后台任务)

    }

    // 6. App即将终止(仅在后台时可能触发,如系统回收内存)
    func applicationWillTerminate(_ application: UIApplication) {

        print("即将终止 - applicationWillTerminate")

        // 最终的资源清理、数据保存

    }

}

关键说明

  • 启动流程:用户点击 App 图标 → 系统加载可执行文件 → 调用didFinishLaunchingWithOptions → 显示界面 → 进入活跃状态。
  • 后台与前台切换:活跃 → 失活(WillResignActive)→ 后台(DidEnterBackground)→ 前台(WillEnterForeground)→ 活跃(DidBecomeActive)。
  • 终止:后台状态下系统回收内存,触发applicationWillTerminate(若 App 在前台,直接终止,不触发此方法)。

二、UIViewController 生命周期

视图控制器是管理 UIView 的核心,其生命周期围绕视图的创建、显示、隐藏、销毁展开,是 iOS 开发中最常接触的生命周期。

核心方法(按执行顺序)

import UIKit

class ViewController: UIViewController {

    // 1. 初始化(创建VC对象)
    init?(coder: NSCoder) {
        super.init(coder: coder)
        print("1. 初始化 - init")
        // 初始化非UI相关的属性
    }

    // 2. 加载视图(首次访问view属性时触发)
    override func loadView() {
        super.loadView()
        print("2. 加载视图 - loadView")
        // 手动创建view(若不重写,系统会加载storyboard/xib的view)
        self.view = UIView(frame: UIScreen.main.bounds)
        self.view.backgroundColor = .white
    }

    // 3. 视图加载完成(view已创建完成)
    override func viewDidLoad() {
        super.viewDidLoad()
        print("3. 视图加载完成 - viewDidLoad")
        // 初始化UI控件、绑定数据、添加监听(只执行一次)
    }

    // 4. 视图即将布局子视图(view的bounds变化时触发,如旋转屏幕)
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print("4. 视图即将布局子视图 - viewWillLayoutSubviews")
        // 调整控件布局(执行多次)
    }

    // 5. 视图已布局子视图
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print("5. 视图已布局子视图 - viewDidLayoutSubviews")
        // 获取控件最终的frame(执行多次)
    }

    // 6. 视图即将显示在屏幕上(每次显示都触发,如push/pop后重新显示)
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print("6. 视图即将显示 - viewWillAppear")
        // 刷新数据、开始动画、注册通知等
    }

    // 7. 视图已显示在屏幕上
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("7. 视图已显示 - viewDidAppear")
        // 启动定时器、请求网络数据、播放视频等
    }

    // 8. 视图即将从屏幕上消失
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("8. 视图即将消失 - viewWillDisappear")
        // 暂停动画、移除通知、保存数据等
    }

    // 9. 视图已从屏幕上消失
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        print("9. 视图已消失 - viewDidDisappear")
        // 释放不必要的资源(如图片缓存)
    }

    // 10. 内存警告(系统内存不足时触发)
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        print("10. 内存警告 - didReceiveMemoryWarning")
        // 释放缓存、非必要的视图等
    }

    // 11. 视图控制器销毁(deinit)
    deinit {
        print("11. 视图控制器销毁 - deinit")
        // 最终的资源释放(如移除监听、取消网络请求)
    }
}

关键说明

  • 核心流程:初始化 → 加载视图 → 视图加载完成 → 布局子视图 → 即将显示 → 已显示 → 即将消失 → 已消失 → 销毁。
  • viewDidLoad:只执行一次,适合做一次性初始化;viewWillAppear/viewDidAppear:每次显示都执行,适合刷新动态数据。
  • 内存警告didReceiveMemoryWarning中需主动释放非必要资源,避免 App 被系统杀死。
  • deinit:只有当 VC 的引用计数为 0 时才会触发,需确保无循环引用(如闭包未捕获 self 为 weak/unowned)。

三、UIView 生命周期

UIView 的生命周期依附于视图控制器,核心是 “创建 - 布局 - 绘制 - 销毁”,重点关注布局和绘制相关方法。

核心阶段与方法

import UIKit

class CustomView: UIView {

    // 1. 初始化(创建View)
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("1. View初始化 - init(frame:)")
        // 设置默认属性(如背景色、圆角)
        self.backgroundColor = .lightGray
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        print("1. View初始化 - init(coder:)")
    }

    // 2. 准备布局(iOS 6+,替代autoresizingMask)
    override func prepareForLayout() {
        super.prepareForLayout()
        print("2. 准备布局 - prepareForLayout")
        // 布局前的准备工作(如设置约束优先级)
    }

    // 3. 布局子视图(bounds变化时触发,如frame、center修改)
    override func layoutSubviews() {
        super.layoutSubviews()
        print("3. 布局子视图 - layoutSubviews")
        // 手动调整子视图frame(若不用AutoLayout)
        for subview in self.subviews {
            subview.center = self.center
        }
    }

    // 4. 绘制内容(首次显示/setNeedsDisplay()触发)
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        print("4. 绘制内容 - draw(_:)")
        // 手动绘制图形(如绘制线条、文字)
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(UIColor.red.cgColor)
        context?.stroke(CGRect(x: 10, y: 10, width: 100, height: 100))
    }

    // 5. 即将添加到父视图
    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)
        print("5. 即将添加到父视图 - willMove(toSuperview:)")
        // 父视图变化前的处理
    }

    // 6. 已添加到父视图
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        print("6. 已添加到父视图 - didMoveToSuperview")
        // 父视图变化后的处理(如根据父视图调整自身大小)
    }

    // 7. 即将添加到窗口
    override func willMove(toWindow newWindow: UIWindow?) {
        super.willMove(toWindow: newWindow)
        print("7. 即将添加到窗口 - willMove(toWindow:)")
    }

    // 8. 已添加到窗口
    override func didMoveToWindow() {
        super.didMoveToWindow()
        print("8. 已添加到窗口 - didMoveToWindow")
        // 只有添加到window后,View才会真正显示在屏幕上
    }

    // 9. 销毁(deinit)
    deinit {
        print("9. View销毁 - deinit")
        // 释放View相关资源(如移除子视图、取消动画)
    }
}

关键说明

  • layoutSubviews:最常用的方法,每次 View 的尺寸变化都会触发,适合手动调整子视图布局(若使用 AutoLayout,系统会自动处理,无需重写)。
  • draw(_:) :仅在需要手动绘制内容时重写,避免在其中做耗时操作(会影响渲染性能);调用setNeedsDisplay()可触发重新绘制。
  • Window 关联:View 只有添加到UIWindow(应用的主窗口)后,才会被渲染并显示在屏幕上;didMoveToWindow是 View 真正 “可见” 的标志。
  • 销毁:当 View 从父视图移除且无强引用时,deinit触发,需确保子视图也被正确释放。

总结

  1. 应用生命周期:全局层面,管理 App 从启动到终止的状态,核心是UIApplicationDelegate的代理方法,关注前台 / 后台切换和资源保存。
  2. 视图控制器生命周期:页面层面,核心是viewDidLoad(一次性初始化)、viewWillAppear(每次显示刷新)、deinit(资源释放),是业务逻辑的主要载体。
  3. 视图生命周期:控件层面,依附于 VC,核心是layoutSubviews(布局)和draw(_:)(绘制),关注控件尺寸调整和视觉渲染。

三者的关联:App 启动后创建根 VC → VC 创建并加载 View → View 添加到 Window 显示 → App 进入前台活跃状态;App 进入后台时,VC 的 View 会被隐藏,资源可按需释放。

Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”

2026年1月7日 08:57

在这里插入图片描述

摘要:在 Swift 6.2 的并发江湖中,我们迎来了两项截然不同的新功能:一项是关于极度精妙的文本侦查术(SE-0448 正则表达式向后查找断言),另一项则是关于面对应用崩溃时的从容不迫(ST-0008 退出测试)。大熊猫侯佩将与阿朱、阿紫这对姐妹花,共同演绎这冰火两重天的技术奥秘。

0️⃣ 🐼 序章:雁门关前的技术难题

雁门关,数据流与现实交错的虚拟战场。

大熊猫侯佩正对着一块全息屏幕发呆,屏幕上是无数条交易记录,他正努力寻找他藏匿的竹笋基金。他用手摸了摸自己的头顶,确定了头绝对没有秃之后,才稍微心安。

他身旁站着一位温柔婉约的绿衣女子,正是阿朱。阿朱以易容术闻名江湖,擅长在纷乱的文本中寻找和伪装信息,她的心愿是天下太平,性格宽厚善良。

在这里插入图片描述

“侯大哥,”阿朱指着一堆交易记录说,“我想找到所有以 金币符号 $ 结算的价格,但我只想匹配出后面的数字,而不要把那个 $符号也匹配进去。我要用这些数字去结算账单,符号留着下次易容用。”

在本次大模型中,您将学到如下内容:

  • 0️⃣ 🐼 序章:雁门关前的技术难题
  • 1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions
  • 2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)
    • #expect(processExitsWith:) 的安全结界
  • 3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩为难地挠了挠头:“以前的 Regex(正则表达式),要么就全部匹配进去,要么就得用复杂的捕获组再分离。要想实现‘只看前因,不取前因’,简直难如登天啊!”

在这里插入图片描述


1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions

阿朱的问题,正是 SE-0448 所要解决的:向后查找断言(lookbehind assertions)

传统的正则表达式,可以轻松地实现“向前看”(Lookahead),例如 A(?=B),匹配 A,但前提是 A 后面跟着 B。

在这里插入图片描述

而现在,Swift 6.2 赋予了我们 “向后看” 的能力,即 (?<=A)B:匹配 B,但前提是 B 前面紧跟着 A。最关键的是,A(前置条件)不会被纳入最终的匹配结果中。

侯佩拿起代码卷轴,为阿朱演示了这招“庖丁解牛”般的绝技:

let string = "Buying a jacket costs $100, and buying shoes costs $59.99."

// (?<=\$): 向后查找断言,确认当前位置前面紧跟着一个 $ 符号。
// \d+     : 匹配至少一个数字(价格的整数部分)。
// (?:\.\d{2})?: 匹配可选的小数点和小数部分(?: 是非捕获组)。
let regex = /(?<=\$)\d+(?:\.\d{2})?/ 

for match in string.matches(of: regex) {
    // 最终输出的 match.output 只有数字,不包含 $ 符号
    print(match.output) 
}

// 输出:
// 100
// 59.99

“看到了吗,阿朱姑娘?”侯佩得意洋洋,“这个 (?<=$) 就是你的易容术精髓。它帮你确认了身份(前面必须是金币),但在匹配结果中,它却完美地把自己隐藏了起来,片叶不沾身!

在这里插入图片描述

阿朱喜出望外:“太妙了!这样我就可以精准地提取数据,再也不用担心多余的符号来捣乱了!”

2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)

就在侯佩和阿朱沉浸在正则表达式的精妙中时,一阵刺鼻的硫磺味突然袭来!

另一位身着紫衣的少女,阿紫,从烟雾中走了出来。阿紫的特点是心狠手辣,喜欢用毒,而且热衷于测试“极限”

在这里插入图片描述

“姐姐,你在玩这么幼稚的游戏?”阿紫轻蔑一笑,“我的任务才刺激。我要测试我最新的**‘鹤顶红’代码**,确保它能让整个应用彻底崩溃并退出!”

侯佩吓得连退三步:“你要测试崩溃?阿紫姑娘,你知道这意味着什么吗?应用崩溃,测试系统也会跟着崩溃啊!这叫一锅端!”

在这里插入图片描述

阿紫的测试目标,正是那些会触发 precondition()fatalError() 导致进程退出的代码。

struct Dice {
    // 掷骰子功能
    func roll(sides: Int) -> Int {
        // 🚨 前提条件:骰子面数必须大于零!
        // 如果 sides <= 0,程序将立即崩溃退出!
        precondition(sides > 0) 
        return Int.random(in: 1...sides)
    }
}

“以前,我们要么不能测,要么就得用各种奇技淫巧来捕获这种‘致命错误’。”侯佩擦着汗说,“但现在 Swift Testing 带来了 ST-0008:Exit Tests,让我们能优雅地‘置之死地而后生’!”

在这里插入图片描述

#expect(processExitsWith:) 的安全结界

Swift 6.2 引入了 #expect(processExitsWith:),它就像是一个安全结界,允许我们在隔离的子进程中执行可能导致崩溃的代码,然后捕获并验证这个退出行为。

@Test func invalidDiceRollsFail() async throws {
    let dice = Dice()

    // 🛡️ 关键:使用 #expect 包裹,并等待结果
    await #expect(processExitsWith: .failure) {
        // 在这里,roll(sides: 0) 会导致隔离的子进程崩溃退出
        let _ = dice.roll(sides: 0)
    }
    
    // 如果子进程如期以 .failure 状态退出,则测试通过。
    // 如果它没有崩溃,或者崩溃状态不对,则测试失败。
}

🔍 异步执行的关键:await 注意,这里必须使用 await。这是因为在幕后,测试框架必须启动一个专用的、独立的进程来执行危险代码。它会暂停当前测试,直到子进程运行完毕并返回退出状态。这才是真正的隔离测试

在这里插入图片描述

阿紫满意地拍了拍手:“现在我的毒药(代码)终于可以在实验室(测试环境)里安全地爆炸了!我不仅可以测试它会死(failure),还可以测试它死得很安详(success)或其他退出状态。”

3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩摸了摸自己的头发,确认没有被阿紫的毒气熏掉,然后问道:“阿紫姑娘,你这个毒药测试虽然厉害,但是你有没有想过一个问题?”

在这里插入图片描述

“什么问题?”阿紫挑了挑眉。

“如果这个 roll(sides: 0) 崩溃了,但它在崩溃前,生成了一个关键的调试日志文件,或者一个记录了现场数据的**‘遗物’**,你能不能把这个遗物附着到测试报告里?”

阿紫一愣:“不能。测试报告里只显示了‘崩溃了’这个结果,但我不知道崩溃前骰子(程序)到底在想什么!我需要那个遗物来分析我的毒药配方!”

在这里插入图片描述

阿朱也附和道:“是啊,侯大哥。就像我易容时,如果失败了,我希望在失败的记录旁边,能附上一张当时的照片,这样下次就知道是哪个环节出了错。”

侯佩微微一笑,从怀里掏出了一张写着 ST-0009 的秘籍:“两位姑娘,不必烦恼。下一章,Swift Testing 就能帮你们把这些日志、数据和现场文件,像附着‘随身物品’一样,直接捆绑到失败的测试报告上。这招就叫……”

在这里插入图片描述

(欲知后事如何,且看下回分解:Swift Testing: Attachments —— 如何将崩溃现场的证据(日志、截图、数据文件)直接附着到测试报告上,让 Bug 无所遁形。)

拒绝“假死”:为何上滑关闭是测试大忌?揭秘 iOS 真实 OOM 触发指南

2026年1月7日 08:53

在这里插入图片描述

☔️ 引子

在赛博都市“新硅谷”(Neo-Silicon Valley)的第 1024 层地下室里,资深 iOS 赏金猎人——老李(Old Li),正盯着全息屏幕上一行行红色的报错代码发愁。他嘴里叼着一根早已熄灭的合成电子烟,眉头皱得能夹死一只纳米苍蝇。

旁边漂浮着的 AI 助手“小白”发出了机械的合成音:“警报,内存溢出测试失败。目标 App 依然像个赖皮一样活着。”

在这里插入图片描述

老李叹了口气:“这年头的 App,一个个都练成了‘金刚不坏之身’。我想测一下后台上传功能在**低内存(Low RAM)**情况下的表现,结果这破手机内存大得像海一样,怎么都填不满。”

“老板,直接在 App Switcher(多任务切换器)里把它划掉不就行了?”小白天真地问道。

**在本篇博文中,您将学到如下内容: **

  • ☔️ 引子
  • 🕵️‍♂️ 第一章:真死还是假死?这是一个问题
  • 🔮 第二章:失传的“清内存大法”
  • 🛠️ 步骤一:召唤“假肢”(Assistive Touch)
  • 🧨 步骤二:准备“关机仪式”
  • 🩸 步骤三:致命一击(The Purge)
  • 🧟‍♂️ 第三章:为什么我们需要这种“假死”?
  • ⚖️ 第四章:技术验尸——“被杀”与“自杀”的区别
  • 🎬 终章:深藏功与名

老李冷笑一声,敲了一下小白的金属外壳:“图样图森破!手滑杀掉那是‘斩立决’,系统因内存不足杀掉那是‘自然死亡’。对于后台任务来说,这区别可大了去了。要想骗过死神,我们得用点‘阴招’。”

老李从积灰的档案袋里掏出一份绝密文档——《iOS 内存清空指南》。

在这里插入图片描述


🕵️‍♂️ 第一章:真死还是假死?这是一个问题

最近老李接了个大活儿,要为一个 App 开发 Background Uploading(后台上传)功能。这活儿最棘手的地方在于:你得确保当系统因为 RAM constraints(内存限制)或其他不可抗力把你的 App 挂起甚至杀掉时,这上传任务还得能像“借尸还魂”一样继续跑。

要想测试这个场景,最直接的办法就是清空设备的 RAM memory。但这可不像在电脑上拔掉电源那么简单。

小白不解:“不就是上划杀进程吗?”

在这里插入图片描述

“错!”老李严肃地解释道,“打开 Task Switcher 然后强行关闭 App,这在系统眼里属于‘用户主动终止’。这就像是不仅杀了人,还顺手把复活点给拆了。而我们需要的是模拟 App 被系统‘挤’出内存,这才是真正的Forced out of memory。”

简而言之,我们需要制造一场完美的“意外”,让 App 以为自己只是因为太胖被系统踢了出去,而不是被用户嫌弃。


🔮 第二章:失传的“清内存大法”

幸运的是,在 iOS 的底层代码深处,藏着一个不为人知的“秘技”。这招能像灭霸打响指一样,瞬间清空 iOS 设备的 RAM memory,让你的 App 享受到和真实内存不足时一样的“暴毙”待遇。

老李按灭了烟头,开始向小白传授这套“还我漂漂拳”:

在这里插入图片描述

🛠️ 步骤一:召唤“假肢”(Assistive Touch)

如果你的测试机是全面屏(没有 Home 键),你得先搞个虚拟的。 “去 Settings → Accessibility → Touch → Enable Assistive Touch。”老李指挥道。

在这里插入图片描述

屏幕上瞬间浮现出一个半透明的小圆球。 “这就是通往内存地狱的钥匙。”

技术批注: 对于有实体 Home 键的老古董设备,这一步可以跳过。

🧨 步骤二:准备“关机仪式”

在这里插入图片描述

这一步需要一点手速,就像是在玩格斗游戏搓大招。 “听好了:Volume Up(音量加),Volume Down(音量减),然后死死按住 Power Button(电源键)!”

在这里插入图片描述

老李的手指在机身上飞舞,直到屏幕上出现了那个熟悉的“滑动来关机”界面。

🩸 步骤三:致命一击(The Purge)

“就是现在!”老李大喝一声。

在关机界面出现后,千万别滑那个关机条。点击刚才召唤出来的 Assistive Touch 小圆球,找到里面的 Home Button(主屏幕按钮),然后——长按它

在这里插入图片描述

一直按着,直到屏幕一闪,或者突然跳回输入密码的界面。

“恭喜你,”老李擦了擦额头的汗,“你刚刚成功把这台设备的 RAM memory 洗劫一空。现在,后台那些苟延残喘的 App 已经被系统无情地踢出了内存。”

在这里插入图片描述


🧟‍♂️ 第三章:为什么我们需要这种“假死”?

小白看着屏幕上被清理得干干净净的后台,数据流终于开始正常波动了。

“这就好比演习,”老李解释道,“当我们在开发那些依赖于 Background Resuming(后台恢复)的功能时——比如后台上传、下载,或者定位服务——模拟 Out of Memory 场景简直是救命稻草。”

在这里插入图片描述

最让老李爽的一点是,这个操作完全脱离了 Xcode。 “以前还要连着线看 Debugger,现在我可以把手机扔给隔壁 QA 部门那个只会吃薯片的测试员,告诉他:‘按这个秘籍操作,如果上传断了,就是你们的问题,如果没断,就是我的功劳。’”


⚖️ 第四章:技术验尸——“被杀”与“自杀”的区别

为了防止小白以后出去乱说,老李决定再深入科普一下其中的Hardcore原理。

在这里插入图片描述

一个被 Forced out of RAM 的 App,在用户眼里并没有完全死透。它依然会出现在 App Switcher 里,就像个植物人。更重要的是,任何已经注册的 Background Processes(后台进程,比如 NSURLSession 的后台任务)依然在系统的监管下继续运行。

  • 正常死亡(Low Memory): 当用户开了个吃内存的大游戏,或者你的 App 很久没用了,系统为了腾地儿,会把你的 App 从内存里踢出去。当用户再次点击图标时,App 会经历一次 Fresh Launch(冷启动),但系统会给机会让它处理之前没干完的后台活儿。
  • 非正常死亡(Force Close): 当你在多任务界面上滑杀掉 App 时,iOS 会判定:“这刁民不想让这个 App 活了。”于是,系统会大义灭亲,禁止该 App 继续在后台搞小动作。所有的上传、下载任务会被立即 Cancelled(取消)。

在这里插入图片描述

所以,只有用老李刚才那招“清内存大法”,才能真实模拟用户在刷抖音、玩原神导致内存不足时,你的 App 在后台是否还能坚强地把文件传完。


🎬 终章:深藏功与名

测试通过,全息屏幕上显示出了令人安心的绿色 SUCCESS 字样。

在这里插入图片描述

老李站起身,伸了个懒腰,骨头发出噼里啪啦的响声。“行了,小白,打包发布。今晚不用加班修 Bug 了。”

他看了一眼窗外新硅谷那绚烂而又冰冷的霓虹灯。在这个充满 Bug 和 Patch 的世界里,有时候,你必须学会如何正确地“杀死”你的 App,才能让它更好地活下去。

在这里插入图片描述

“记住,”老李走出门口前回头对小白说,“杀进程不是目的,目的是为了验证它有没有重生的勇气。

大门缓缓关闭,只留下那个悬浮的 Assistive Touch 按钮,在黑暗中微微闪烁,仿佛一只窥探内存深处的眼睛。

在这里插入图片描述

SSE Connect 数据解析详解

2026年1月7日 03:45

前言

SSE(Server-Sent Events) 是一种基于 HTTP 的服务器单向推送技术。相比 WebSocket 的双向通信,SSE 更轻量、实现更简单,非常适合服务器向客户端持续推送数据的场景。ChatGPT、Claude 等 AI 产品都使用 SSE 来实现流式输出。

本文以 iOS 客户端实现为例,详细讲解 SSE 数据的接收与解析过程。


一、完整示例:一个 SSE 事件从发送到接收的全过程

服务器发送的数据

event: chunk
id: 1
data: {"content":"Hello"}

⚠️ 注意:最后有一个空行,这是事件结束的标志!


第一步:服务器发送,网络传输

服务器发送的原始字节流:

e v e n t :   c h u n k \n i d :   1 \n d a t a :   { . . . } \n \n

问题:网络传输时,数据可能被分成多个块到达客户端。

假设网络把数据分成了 3 块:

数据块 内容
块 1 "event: chu"
块 2 "nk\nid: 1\nda"
块 3 "ta: {\"content\":\"Hello\"}\n\n"

第二步:行解析器处理(OKGrowthUTF8LineParser)

2.1 收到块 1:"event: chu"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chu"                                         │
│                                                            │
│ 处理过程:                                                   │
│   1. 缓冲区当前为空: ""                                     │
│   2. 合并: "" + "event: chu" = "event: chu"                │
│   3. 扫描换行符: 没找到 \n                                  │
│   4. 没有完整行,全部存入缓冲区                              │
│                                                            │
│ 缓冲区: "event: chu"                                       │
│ 输出: []  ← 空数组,没有完整行                              │
└────────────────────────────────────────────────────────────┘

2.2 收到块 2:"nk\nid: 1\nda"

┌────────────────────────────────────────────────────────────┐
 输入: "nk\nid: 1\nda"                                      
                                                            
 处理过程:                                                   
   1. 缓冲区当前: "event: chu"                               
   2. 合并: "event: chu" + "nk\nid: 1\nda"                  
         = "event: chunk\nid: 1\nda"                        
   3. 扫描换行符:                                            
      - 位置 12 找到 \n  提取 "event: chunk"               
      - 位置 18 找到 \n  提取 "id: 1"                      
      - "da" 后面没有 \n,存入缓冲区                         
                                                            
 缓冲区: "da"                                               
 输出: ["event: chunk", "id: 1"]                            
└────────────────────────────────────────────────────────────┘

2.3 收到块 3:"ta: {\"content\":\"Hello\"}\n\n"

┌────────────────────────────────────────────────────────────┐
 输入: "ta: {\"content\":\"Hello\"}\n\n"                    
                                                            
 处理过程:                                                   
   1. 缓冲区当前: "da"                                       
   2. 合并: "da" + "ta: {...}\n\n"                          
         = "data: {\"content\":\"Hello\"}\n\n"              
   3. 扫描换行符:                                            
      - 位置 27 找到 \n  提取 "data: {...}"                
      - 位置 28 找到 \n  提取 ""   空行!                  
                                                            
 缓冲区: ""   清空                                         
 输出: ["data: {\"content\":\"Hello\"}", ""]                
└────────────────────────────────────────────────────────────┘

2.4 行解析器总结

经过 3 次数据块处理,行解析器依次输出:

次序 输出的完整行
块 1 后 [] (无)
块 2 后 ["event: chunk", "id: 1"]
块 3 后 ["data: {...}", ""]

合计得到 4 行: "event: chunk", "id: 1", "data: {...}", ""


第三步:事件解析器处理(OKGrowthEventParser)

SSEClient 将行解析器输出的每一行,依次传给事件解析器。

3.1 解析第 1 行:"event: chunk"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chunk"                                       │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 5                                        │
│   3. 字段名 = "event"                                      │
│   4. 字段值 = "chunk" (冒号后面,跳过空格)                   │
│   5. 字段名是 "event",存储 eventType                       │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = ""                                           │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.2 解析第 2 行:"id: 1"

┌────────────────────────────────────────────────────────────┐
│ 输入: "id: 1"                                              │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 2                                        │
│   3. 字段名 = "id"                                         │
│   4. 字段值 = "1"                                          │
│   5. 字段名是 "id",存储 eventId                            │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = "1"      ✓                                   │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.3 解析第 3 行:"data: {\"content\":\"Hello\"}"

┌────────────────────────────────────────────────────────────┐
 输入: "data: {\"content\":\"Hello\"}"                      
                                                            
 处理过程:                                                   
   1. 行长度 > 0,不是空行                                   
   2. 查找冒号位置: 4                                        
   3. 字段名 = "data"                                       
   4. 字段值 = "{\"content\":\"Hello\"}"                    
   5. 字段名是 "data",追加到 data                           
      (当前 data 为空,直接赋值)                              
                                                            
 当前状态:                                                   
   eventType = "chunk"                                     
   eventId   = "1"                                         
   data      = "{\"content\":\"Hello\"}"                   
                                                            
 动作: 继续等待下一行                                        
└────────────────────────────────────────────────────────────┘

3.4 解析第 4 行:"" (空行) ⚡

┌────────────────────────────────────────────────────────────┐
│ 输入: ""  (空行)                                           │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 == 0,是空行!                                  │
│   2. ⚡ 空行触发事件分发!                                   │
│                                                            │
│ 当前状态 (即将分发):                                        │
│   eventType = "chunk"                                      │
│   eventId   = "1"                                          │
│   data      = "{\"content\":\"Hello\"}"                    │
│                                                            │
│ 执行 dispatchEvent():                                      │
│   1. 调用回调: onEvent("chunk", "1", "{...}")              │
│   2. 重置状态:                                              │
│      eventType = ""                                        │
│      eventId   = ""                                        │
│      data      = ""                                        │
│                                                            │
│ 动作: 🎯 触发回调!准备解析下一个事件                        │
└────────────────────────────────────────────────────────────┘

第四步:事件分发,回调业务层

┌────────────────────────────────────────────────────────────┐
│                      回调链                                 │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  EventParser.dispatchEvent()                               │
│       │                                                    │
│       │  onEvent("chunk", "1", "{\"content\":\"Hello\"}")  │
│       ↓                                                    │
│  SSEClient.handleEvent()                                   │
│       │                                                    │
│       │  判断: eventType != "connected",不更新状态         │
│       │                                                    │
│       │  onTextChunk("chunk", "1", "{...}")                │
│       ↓                                                    │
│  MLNSSETool                                                │
│       │                                                    │
│       │  [onTextChunk addStringArgument:@"chunk"];         │
│       │  [onTextChunk addStringArgument:@"1"];             │
│       │  [onTextChunk addStringArgument:@"{...}"];         │
│       │  [onTextChunk callIfCan];                          │
│       ↓                                                    │
│  Lua 业务层                                                │
│       │                                                    │
│       │  onEvent("chunk", "1", '{"content":"Hello"}')      │
│       ↓                                                    │
│  业务代码处理                                               │
│       │                                                    │
│       │  local json = cjson.decode(data)                   │
│       │  print(json.content)  -- 输出: Hello               │
│       │  更新 UI 显示                                       │
│       ↓                                                    │
│  ✅ 完成!                                                  │
│                                                            │
└────────────────────────────────────────────────────────────┘

完整流程图(从服务器到 Lua)

服务器发送: "event: chunk\nid: 1\ndata: {...}\n\n"
                          
                           网络分块传输
┌─────────────────────────────────────────────────────────────┐
                     第一步:网络层                            
├─────────────────────────────────────────────────────────────┤
1: "event: chu"                                          
2: "nk\nid: 1\nda"                                       
3: "ta: {...}\n\n"                                       
└─────────────────────────────────────────────────────────────┘
                          
                           NSURLSession.didReceiveData
┌─────────────────────────────────────────────────────────────┐
                 第二步:行解析器                               
                 (UTF8LineParser)                            
├─────────────────────────────────────────────────────────────┤
1  []                                                   
2  ["event: chunk", "id: 1"]                            
3  ["data: {...}", ""]                                  
                                                             
  合计得到4行: `"event: chunk"`, `"id: 1"`, `"data: {...}"`, `""`  
└─────────────────────────────────────────────────────────────┘
                          
                           逐行传递
┌─────────────────────────────────────────────────────────────┐
                 第三步:事件解析器                             
                 (EventParser)                               
├─────────────────────────────────────────────────────────────┤
1"event: chunk"   eventType = "chunk"                 
2"id: 1"          eventId = "1"                       
3"data: {...}"    data = "{...}"                      
4""                触发 dispatchEvent()              
└─────────────────────────────────────────────────────────────┘
                          
                           onEvent 回调
┌─────────────────────────────────────────────────────────────┐
                     第四步:事件分发                           
├─────────────────────────────────────────────────────────────┤
  SSEClient.handleEvent("chunk", "1", "{...}")               
                                                            
  MLNSSETool.onTextChunk("chunk", "1", "{...}")              
                                                            
  Lua: onEvent("chunk", "1", '{"content":"Hello"}')          
                                                            
      业务代码: 解析 JSON,更新 UI                              
└─────────────────────────────────────────────────────────────┘
                          
                          
                     处理完成!


二、关键点总结

2.1 为什么需要行解析器?

网络数据分块到达,一行可能被拆成多块。行解析器用缓冲区解决这个问题。

2.2 为什么空行这么重要?

event: chunk     ← 存储 eventType,不触发
id: 1            ← 存储 eventId,不触发
data: {...}      ← 存储 data,不触发
                 ← ⚡ 只有空行才触发事件分发!

空行 = 事件结束的信号

2.3 一个事件的完整生命周期

阶段 输入 输出
网络传输 字节流 数据块
行解析器 数据块 文本行数组
事件解析器 文本行 event/id/data
空行触发 "" dispatchEvent()
回调链 event/id/data Lua onEvent

苹果开发者账号申请的痛点,包含新限制说明!

作者 iOS研究院
2026年1月6日 18:00

背景

上车AppStore必经之路,苹果开发者账号注册。简单盘点一下,申请苹果开发者痛点问题。

账号注册

正常的个人开发账号,基本上直接使用 126、163或者QQ邮箱都可以直接使用。

对于公司开发者账号来说,最近新增了限制条件:申请的邮箱必须为公司邮箱!

这一点限制是在最近申请公司开发账号遇到的问题,对于个人账号账号目前没有影响。[这里感谢粉丝贡献的情报。]

设备问题

设备问题主要是在Apple ID登录踩的坑。首当其冲的就是设备登录限制。

无解直接换新设备,不用想了。不然果子怎么卖的动新手机?

注册开发者的 Developer App,也需要更新到新版本。【有最低版本限制】不然果子怎么卖的动新手机?

9135238bf439b2f3a9611a0cfb5e7c8f.jpg

在注册开发者账号的过程中,切记不要更换设备,避免遇到各种奇奇怪怪的问题。也能最大程度的保保证,在注册流程不会被账号关联,避免提交代码就夭折。

信息验证问题

1.账号主体

对于公司层面的账号,场景最多的问题就是:

Q: 法人用个人账号注册了开发者,那么还可以用公司身份去注册么?

A: 其实是可以的,这一点已经咨询过了苹果客服。因为对于主体而言,一个是邓白氏编码对应的账号,一个是个人身份证对应的账号。所以本质上也是2个独立的主体。

2.忘记老账户

对于小部分一些人来说,可能之前注册了开发者流程,也提交的了相应信息。在最后付费环境,考虑到暂时没有产品提交又或者不知道了注册了干嘛,就把账号搁置了。

那这种情况是最头疼的,对于苹果而言信息已经被占用。如果无法使用首次注册开发者的账号,重新进行开发者验证。那么将陷入无法注册的死循环。简而言之:打苹果客服,也只能告诉你用老账户。如果忘记密码或者AppleID【也就是注册的邮箱】,那么对不起奶不回来。苹果客服没有权限获悉之前注册的任何信息。【上海端口没有这么高的权限!】

激活开发身份

如果顺顺利利的完成了,所有前置流程,并且成功支付苹果开发者的会员费¥688.00。那么恭喜你完成了90%

但是,别高兴的太早。很多支付了费用,超过30个小时依旧没有成功获取开发者资格。

这种情况,必须要主动与苹果技术支持联系。对于个人账号大概率是需要补充身份证信息,也就是身份证正反面

苹果会通过开发者邮箱,提供一个附件资料上传地址。上传成功之后,预计2~3个小时将会激活。

之所以遇到这种问题,是因为中国大陆区有些小区名称或者街道过于离谱。比如:

  • 江苏南京神马路:位于南京市栖霞区,连接马群街道与仙林地区,因谐音与网络流行语 “神马都是浮云” 契合,成为网红路名。

  • 江苏南京马桶巷:位于南京秦淮区,传说因明代此处有制作马桶的手工作坊而得名,现已更名为 “饮马巷”,但老南京人仍习惯称其旧名。

  • 江苏苏州裤裆巷:巷子分岔呈 “Y” 形,形似裤裆,故得此名,后改名 “邾长巷”,但老苏州人仍爱调侃 “穿裤衩的路”。

  • 四川成都肥猪市街:该地以前是卖猪的市场,所以取了这样的名字。同理还有草市街、羊市街等。

  • 广东揭阳普宁二奶街:因上世纪 90 年代街道售卖的衣物价格昂贵,人们调侃称只有 “二奶” 才消费得起,故而得名,如今已发展成为当地有名的人气美食夜市。

f3aa64439608fb9ef785ee0acce490ea.png

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

抖音思路复刻:iOS 卡死(ANR)监控 + 自动符号化全流程实战

作者 Daniel02
2026年1月6日 00:17

在 iOS 上,App 在启动 / 退出 / 响应系统事件等关键阶段如果长时间卡住,超过系统阈值就会触发保护机制,最终被 Watchdog 以 SIGKILL 强制终止。这类异常的共同特点是:不是进程内异常抛出,而是“进程外指令”直接结束进程,因此传统基于 signal/exception 的崩溃捕获往往覆盖不到,也就导致它在生产环境中经常“只见数据、不见堆栈”,长期被忽视。

为了解决这个盲区,我选择站在巨人的肩膀上:本文实践并复现字节跳动团队的文章《iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践》,从 原理 → 代码实现 → 候选文件保存 → dSYM/atos/脚本符号化 + Swift demangle,把一整套卡死(ANR/Watchdog)监控链路完整跑通,并沉淀成一个可复用 Demo。

文末给出完整代码链接


1. 痛点:卡死最难的不是“知道卡”,而是“知道卡在哪”

线上卡死/假死常见现象:

  • UI 不响应(点击无反应、滑动卡住),几秒后恢复或被系统杀掉
  • Crash 日志没有(不是进程内 crash),只剩用户反馈:“刚刚卡住了”
  • 拿到的堆栈是一堆 0x00000001.... 地址:不符号化就等于没有结论

所以要解决这类问题,本质是两件事:

  1. 卡死当下抓到主线程堆栈(或卡死期间持续采样)
  2. 自动符号化:把地址变成可读函数名,并尽可能 demangle Swift 符号

2. 原理:RunLoop 心跳 + 超阈值采样 = 还原卡死现场

2.1 监控“主线程卡死”的本质

主线程 RunLoop 正常情况下会不断在这些状态间流转:

  • BeforeTimers
  • BeforeSources
  • BeforeWaiting
  • AfterWaiting

当主线程执行重任务(大解析 / 同步 IO / 复杂布局 / 锁等待等),RunLoop 会长期卡在某个阶段不动,表现为 UI 无响应。

2.2 本 Demo 的检测策略(尽量向“抖音方案”靠拢)

Demo 采用的策略(参数对齐你当前实现):

  • 阈值:8 秒hangThreshold = 8
  • 检测周期:1 秒窗口tickInterval = 1
  • 超过阈值后:每秒采样一次主线程栈(从 Live Report 中提取 Thread 0
  • 最多保留 10 帧样本maxMainThreadSamples = 10,保留最近 10 次)

这套策略的意义是:
先判断“已经卡死到足够严重”(接近 Watchdog 风险),再进入“持续采样”,避免把轻微卡顿也当成卡死去抓栈/写文件。


3. Demo 实现结构

3.1 项目目录骨架

LagMonitorDemo/
├── LagMonitorDemo.xcodeproj
├── Sources/
│   └── Monitor/
│      ├── HMDANRMonitor.swift
│      ├── HMDLiveReportCapture.swift
│      ├── HMDANRRecord.swift
│      ├── HMDANRCandidateStore.swift
│      └── HMDDebugCacheCleaner.swift
├── Scripts/
│   └── hmd_anr_symbolicate.py
└── Samples/
    ├── hmd_anr_candidate.json
    └── symbolicated.txt

3.2 Monitor:HMDANRMonitor.swift(核心监控器)

3.2.1 监控目标

  • 主线程装 CFRunLoopObserver,每次回调认为 RunLoop 推进:heartbeat += 1
  • 监控线程用 1 秒观察窗口检查 heartbeat 是否变化
    • 1 秒内有推进:健康/恢复,清理候选
    • 1 秒内无推进:认为卡住,hangSeconds += 1
  • 卡住累计达到 8 秒阈值:创建 candidate
  • 超阈值后仍未恢复:每秒采样一次主线程栈,最多 10 帧

3.2.2 为什么看起来“没 sleep 1 秒”,却实现了“每秒检查一次”?

核心就在这一句:

_ = wakeSemaphore.wait(timeout: .now() + .seconds(config.tickInterval))

它等价于:

  • 最多等待 1 秒tickInterval = 1)作为一个观察窗口
  • 如果期间 RunLoop 有推进,observer 会 signal() → 监控线程会提前醒来
  • 窗口结束后比较 heartbeat:若 1 秒内完全没变,才算这一秒“卡住”

所以这里不是“固定每秒到点触发一次”,而是:

“最多等 1 秒,但只要 RunLoop 一推进就立刻醒来重置状态”
这比 Timer 的“固定周期触发”更贴合我们想观察的对象(RunLoop 推进)。

3.2.3 为什么不用 NSTimer / GCD 定时器,而用信号量?

Timer 触发本身就依赖调度与 RunLoop/线程状态,卡死时最容易抖动或延迟;信号量 + 超时是更稳定的“观察窗口”,还能被 RunLoop 推进即时唤醒。


3.3 抓栈:HMDLiveReportCapture.swift(PLCrashReporter Live Report)

抓栈使用 PLCrashReporter 的 Live Report 能力:

  • generateLiveReportAndReturnError():生成“当下全线程现场”
  • 再从文本中提取 Thread 0 作为主线程样本
import CrashReporter

/// 抓一次“全线程现场报告”
static func captureAllThreadsText() -> String? {
    let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
    guard let reporter = PLCrashReporter(configuration: config) else { return nil }

    do {
        let data = try reporter.generateLiveReportAndReturnError()
        let report = try PLCrashReport(data: data)
        return PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)
    } catch {
        print("[HMDLiveReportCapture] parse report error: \(error)")
        return nil
    }
}

这里的思路就是:
卡死现场抓“全线程”,用于兜底;超阈值后持续采“主线程”,用于定位稳定卡点。


3.4 保存:HMDANRCandidateStore.swift(hmd_anr_candidate.json)

Demo 保存的核心数据结构是 HMDANRRecord,通过 Codable 编码为 JSON:

public struct HMDANRRecord: Codable {
    public let recordID: String
    public let timestamp: Date
    public var hangSeconds: Int

    /// 超过阈值那一刻:全线程现场(PLCrash live report,文本)
    public var allThreadsReportText: String?

    /// 超阈值后:每秒采样主线程调用栈(最多保留最近 N 条)
    public var mainThreadSamples: [String]
}

保存到 Caches/hmd_anr_candidate.json 后,大致字段长这样:

{
  "recordID": "E2D0...-....",
  "timestamp": "2026-01-05T12:34:56Z",
  "hangSeconds": 9,
  "allThreadsReportText": "PLCrashReporter live report text ...",
  "mainThreadSamples": [
    "Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ...",
    "Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ..."
  ]
}

HMDANRCandidateStore卡住时把记录保存到缓存文件;一旦主线程恢复推进就立刻删除;如果进程被系统强杀来不及上报,这个文件会留到下次启动再读取导出/符号化。


4. 复现场景(Demo 内置)

Demo 里包含几种典型卡死触发方式:

stack.addArrangedSubview(makeButton("主线程 Busy 2s(轻微卡顿)") { [weak self] in self?.busy(seconds: 2) })
stack.addArrangedSubview(makeButton("主线程 Busy 20s(触发 candidate + 采样)") { [weak self] in self?.busy(seconds: 20) })
stack.addArrangedSubview(makeButton("锁竞争:子线程持锁 12s → 主线程尝试加锁") { [weak self] in self?.lockContention() })
stack.addArrangedSubview(makeButton("死锁:串行队列 sync + 主队列 sync(必卡死)") { [weak self] in self?.deadlock() })

5. 自动符号化:从 hmd_anr_candidate.json 到“可读堆栈”

我点击 “主线程 Busy 20s(触发 candidate + 采样)”,在第 10 秒手动杀掉 App,然后导出沙盒里的 hmd_anr_candidate.json

你会看到类似信息:

  • hangSeconds = 13(很明确的主线程长时间阻塞)
  • mainThreadSamples 有多次采样(证明卡住期间栈稳定)
  • 但这些堆栈仍然是地址/偏移,无法直接定位业务代码

1.jpg

因此需要做:

  • hmd_anr_candidate.json 里解析 frame
  • dSYM + atos 还原符号;并对 Swift 符号 demangle$s... → Foundation.Date.init()

本项目用脚本 hmd_anr_symbolicate.py 自动完成批量符号化:

python3 hmd_anr_symbolicate.py   --record hmd_anr_candidate.json   --app-dsym LagMonitorDemo.app.dSYM   --arch arm64   --demangle   --out symbolicated.txt

符号化后的symbolicated.txt的大致内容如下:

2.jpg

hmd_anr_symbolicate.pyGITHUB项目的Scripts文件夹下,hmd_anr_candidate.jsonsymbolicated.txt在Samples文件下

5.1 符号化后如何“从栈定位问题”?

symbolicated.txt 里抽 mainThreadSamples[0] 的关键几帧(你这次 5 次采样基本一致):

1   libsystem_c.dylib          gettimeofday
3   Foundation                 Date.init
4   LagMonitorDemo.debug.dylib ... (ViewController.swift:52)
5   LagMonitorDemo.debug.dylib ... (ViewController.swift:33)
6   LagMonitorDemo.debug.dylib ... (ViewController.swift:44)
7   UIKitCore                  ...

这说明卡死期间主线程一直在跑 ViewController.swift 的某段逻辑,并且频繁调用 Date()(最终落到 gettimeofday/clock_gettime),典型特征就是忙等/死循环式等待

对应 Demo 中的实现:

private func busy(seconds: Int) {
    let end = Date().addingTimeInterval(TimeInterval(seconds))
    while Date() < end {
        _ = 1 + 1
    }
}

这类栈顶常见现象就是:看起来“卡在 Date()”,其实根因是 while 循环让主线程一直跑
采样刚好截在 Date() 这一行,于是栈顶表现为 Date.init -> gettimeofday


6. 结语:复刻的意义,是把“文章里的方案”变成“项目里能用的工具”

大厂的稳定性方案往往更深、更体系化,但很多时候只停留在文章层面:看懂了思路,却很难在项目里直接落地。本文的目标就是把它“拆开 + 跑通”:

  • 把抖音文章里的链路拆成可运行代码(监控、采样、保存、恢复、导出)
  • 把最后一公里补齐(dSYM/atos 自动符号化 + Swift demangle)
  • 让“卡死问题”从 只有 SIGKILL 数字,变成 能指向具体业务函数/代码行 的结论

你真正需要的不是“我们检测到了卡死”,而是:

卡死那 8~20 秒内,主线程到底在跑什么?它卡在谁身上?

当你把“采样 + 保存 + 下次启动捞取 + 自动符号化 + demangle”这条链路跑通,线上卡死排查效率会明显提升。


GITHUB源码

Core Data 简化开发:NSPersistentContainer 从原理到实战

作者 JQShan
2026年1月5日 19:10

作为 iOS/macOS 开发者,本地数据存储是绕不开的话题。提起 Core Data,不少新手会皱眉头 —— 早期的 Core Data 配置繁琐,手动管理上下文、协调器这些组件很容易踩坑;而老开发者则清楚,自从 Apple 推出NSPersistentContainer后,Core Data 的使用体验直接 “起飞”。今天就跟大家聊聊,这个 “容器” 到底是什么、怎么用,以及它的那些优缺点。

一、先唠唠 Core Data 的 “老痛点”

在 iOS 10/macOS 10.12 之前,想用 Core Data 得手动搭一套 “流水线”:

  1. 加载NSManagedObjectModel(数据模型);
  2. 创建NSPersistentStoreCoordinator(持久化存储协调器),指定存储类型(比如 SQLite)和路径;
  3. 实例化NSManagedObjectContext(托管对象上下文),并关联协调器;
  4. 还要处理线程安全、上下文合并这些问题。

一套操作下来,代码又长又容易出错,光是初始化就能劝退一半新手。Apple 显然也发现了这个问题,于是NSPersistentContainer应运而生 —— 它把 Core Data 的核心组件全 “打包” 了,让我们不用再关心底层细节,专注于业务逻辑即可。

二、NSPersistentContainer:Core Data 的 “一站式工具箱”

1. 核心原理:封装了什么?

NSPersistentContainer本质是对 Core Data 三大核心组件的封装,相当于给我们准备了一个开箱即用的 “数据管理容器”,内部结构如下:

组件 作用 容器中的访问方式
NSManagedObjectModel 定义数据结构(对应.xcdatamodeld 文件) container.managedObjectModel
NSPersistentStoreCoordinator 管理数据存储(比如 SQLite 文件) container.persistentStoreCoordinator
NSManagedObjectContext 操作数据的 “工作台”(增删改查) container.viewContext(主线程)/container.newBackgroundContext()(后台)

简单说:你只需要告诉容器 “数据模型叫什么名字”,它会自动完成模型加载、协调器创建、上下文关联等所有底层工作,不用写一行冗余代码。

2. 最核心的两个上下文

容器里最常用的是两个上下文,一定要分清:

  • viewContext:默认绑定主线程,专门用于 UI 相关的操作(比如列表展示读书笔记),线程安全,直接用就行;
  • newBackgroundContext() :每次调用都会生成一个新的后台上下文,用于耗时操作(比如批量导入历史读书笔记),避免阻塞主线程导致 UI 卡顿。

三、实战:NSPersistentContainer 的基本用法

光说不练假把式,我们用一个简单的 “读书笔记管理” 示例,看看怎么用容器搞定 Core Data 的增删改查。

前置准备

  1. 创建 iOS 项目时勾选「Use Core Data」(Xcode 会自动生成基础的容器代码);

  2. 打开.xcdatamodeld文件,创建一个BookNote实体,添加三个属性:

    • bookName(String,书名);
    • content(String,笔记内容);
    • createTime(Date,创建时间,默认值可设为@now)。

1. 初始化容器(Xcode 自动生成,稍作优化)

AppDelegate 中的核心代码,我们优化下错误处理(别用 fatalError,实际项目要友好):

import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 懒加载持久化容器
    lazy var persistentContainer: NSPersistentContainer = {
        // 模型文件名要和.xcdatamodeld文件名称一致(比如我命名为BookNoteModel)
        let container = NSPersistentContainer(name: "BookNoteModel") 
        
        // 加载持久化存储(默认是SQLite文件,存储在App沙盒中)
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // 实际项目中替换为日志/弹窗提示,别直接崩溃
                print("Core Data加载失败:(error.localizedDescription)")
            }
        })
        return container
    }()

    // 封装保存上下文的方法,复用性更高
    func saveContext() {
        let context = persistentContainer.viewContext
        guard context.hasChanges else { return } // 没有修改就不保存,减少IO消耗
        
        do {
            try context.save()
            print("读书笔记保存成功✅")
        } catch {
            print("保存失败❌:(error.localizedDescription)")
        }
    }
}

2. 增删改查实战(ViewController 中)

import UIKit
import CoreData

class ViewController: UIViewController {
    // 获取容器(实际项目建议用单例/依赖注入,别直接强转AppDelegate,这里为了简化)
    private var container: NSPersistentContainer {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        return appDelegate.persistentContainer
    }

    // 1. 添加读书笔记
    @IBAction func addBookNote(_ sender: UIButton) {
        let context = container.viewContext
        // 创建BookNote对象
        let note = BookNote(context: context)
        note.bookName = "《小王子》"
        note.content = "正是你为你的玫瑰花费的时光,才使你的玫瑰变得如此重要。"
        note.createTime = Date() // 也可以依赖模型的默认值,这里手动赋值更直观
        
        // 调用AppDelegate的保存方法
        (UIApplication.shared.delegate as! AppDelegate).saveContext()
    }

    // 2. 查询所有读书笔记(可按创建时间倒序)
    func fetchAllBookNotes() {
        let context = container.viewContext
        // 创建查询请求
        let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
        
        // 按创建时间倒序排列,最新的笔记在前面
        let sortDescriptor = NSSortDescriptor(keyPath: \BookNote.createTime, ascending: false)
        fetchRequest.sortDescriptors = [sortDescriptor]
        
        do {
            let notes = try context.fetch(fetchRequest)
            notes.forEach { note in
                print("📚 书名:(note.bookName ?? "未知")")
                print("✍️ 笔记:(note.content ?? "无内容")")
                print("🕒 创建时间:(note.createTime ?? Date())\n")
            }
        } catch {
            print("查询读书笔记失败:(error.localizedDescription)")
        }
    }

    // 3. 删除读书笔记(示例:删除第一条《小王子》的笔记)
    @IBAction func deleteBookNote(_ sender: UIButton) {
        let context = container.viewContext
        let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
        
        // 增加筛选条件:只删《小王子》的笔记
        fetchRequest.predicate = NSPredicate(format: "bookName == %@", "《小王子》")
        
        do {
            if let targetNote = try context.fetch(fetchRequest).first {
                context.delete(targetNote) // 删除指定笔记对象
                (UIApplication.shared.delegate as! AppDelegate).saveContext()
                print("《小王子》的笔记已删除")
            }
        } catch {
            print("删除读书笔记失败:(error.localizedDescription)")
        }
    }

    // 4. 后台批量导入读书笔记(重点:用后台上下文,不卡UI)
    func batchImportBookNotes() {
        // 创建后台上下文
        let backgroundContext = container.newBackgroundContext()
        // 设置合并策略,避免多上下文操作冲突
        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        // 在后台线程执行批量操作,不会阻塞主线程
        backgroundContext.perform { [weak self] in
            // 模拟批量导入3本经典书籍的笔记
            let noteDatas = [
                ("《百年孤独》", "生命中真正重要的不是你遭遇了什么,而是你记住了哪些事,又是如何铭记的。"),
                ("《解忧杂货店》", "其实所有纠结做选择的人心里早就有了答案,咨询只是想得到内心所倾向的选择。"),
                ("《活着》", "人是为了活着本身而活着的,而不是为了活着之外的任何事物而活着。")
            ]
            
            // 循环创建笔记对象
            for (bookName, content) in noteDatas {
                let note = BookNote(context: backgroundContext)
                note.bookName = bookName
                note.content = content
                note.createTime = Date()
            }
            
            // 保存后台上下文的修改
            do {
                try backgroundContext.save()
                print("批量导入读书笔记完成✅")
            } catch {
                print("批量导入失败❌:(error.localizedDescription)")
            }
        }
    }
}

小提示

  • 别在主线程做批量导入 / 大量查询操作!一定要用newBackgroundContext()
  • 后台上下文的perform方法会自动在对应的后台线程执行代码,不用手动写 GCD(比如DispatchQueue.global().async);
  • 实际项目中,建议把 Core Data 操作封装成单独的工具类(比如BookNoteManager),把增删改查的逻辑抽离出来,ViewController 只负责调用,代码更整洁易维护。

四、NSPersistentContainer 的优缺点

优点:新手友好,效率拉满

  1. 极大简化配置:不用手动管理模型、协调器、上下文的关联,几行代码就能初始化 Core Data;
  2. 线程安全viewContext默认绑定主线程,避免了新手最容易踩的 “线程混乱” 坑;
  3. 易于扩展:支持自定义存储路径、存储类型(比如内存存储,适合测试),满足进阶需求;
  4. 官方维护:Apple 持续优化,兼容性和稳定性有保障。

缺点:灵活度略有妥协

  1. 底层封装过深:新手可能只知其然不知其所以然,遇到复杂问题(比如跨版本数据迁移)时,排查起来比较费劲;
  2. 自定义配置稍麻烦:如果要修改默认的存储路径、缓存大小,需要额外写代码拆解容器;
  3. 不支持跨平台(纯 Swift) :Core Data 是 Apple 专属框架,如果你想做跨平台 App(比如 iOS+Android),还是得用 Realm、SQLite.swift 等。

五、最后聊聊:什么时候用 NSPersistentContainer?

  • 推荐用:绝大多数常规 App(比如读书笔记、备忘录、待办清单类),只需要本地存储数据,不需要复杂的自定义配置;
  • 谨慎用:如果你需要深度定制 Core Data 的底层(比如自定义存储协调器、复杂的数据迁移策略),可能需要结合底层 API 使用;
  • 📌 替代方案:如果追求跨平台 / 纯 Swift,可考虑 Realm、GRDB.swift;如果数据量极小,直接用 UserDefaults 就行。

总结

  1. NSPersistentContainer是 Apple 为简化 Core Data 开发推出的 “利器”,封装了 Core Data 的核心组件,iOS 10 + 可直接用;
  2. 核心用法:初始化容器→用viewContext处理 UI 相关操作(比如展示读书笔记)→用newBackgroundContext()处理后台耗时操作(比如批量导入)→保存上下文;
  3. 它的优点是简单、安全、高效,缺点是底层封装过深,灵活度略有妥协,适合绝大多数常规 iOS/macOS 项目。

Core Data 看似复杂,但有了NSPersistentContainer这个 “帮手”,新手也能快速上手。与其纠结底层原理,不如先动手写起来,遇到问题再深入研究,毕竟实践才是最好的老师~


关键点回顾

  1. 示例场景替换为「读书笔记管理」,实体BookNote包含bookName(书名)、content(笔记内容)、createTime(创建时间)三个核心属性;
  2. 核心代码逻辑不变,但所有增删改查、批量导入的操作都围绕 “读书笔记” 展开,更贴近日常开发场景;
  3. 保留了原博客轻松的语气和完整的讲解结构,同时补充了排序、筛选等更实用的查询技巧。

2026:当 AI 隐入工作流,你准备好了吗? -- 肘子的 Swift 周报 #117

作者 东坡肘子
2026年1月6日 07:55

issue117.webp

2026:当 AI 隐入工作流,你准备好了吗?

大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。

如果说过去两年大多数人还在尝试如何与 Chat 机器人聊天,那么现在,AI 已经通过 CLI、MCP 以及各种 Slash、Skill、SubAgent,彻底打破了对话框的限制。对于有经验的开发者来说,AI 已经不再是一个外部工具,而是像插件一样,渗透进终端、编辑器乃至整个操作系统的每一个毛细血管。

在这一点上,macOS 展示了某种“无心插柳”的天然优势。借助 AppleScript 和快捷指令这些成熟的自动化工具,即便不通过复杂的 API 开发,普通用户也能让 AI 访问自己的私有数据。这种“老树发新芽”的现象,让苹果在 AI 时代拥有了新的护城河。而如果这种能力在 iOS 上通过系统级 Agent 完全释放,硬件设备的形态或许将迎来新一轮重塑。

与此同时,某些厂商的策略则更加“激进”。字节跳动的豆包手机尝试从系统底层通过屏幕读取与模拟交互来“暴力”接管一切;华为则通过 A2A(Agent to Agent)策略,试图在后台构建一套统一的代理调度机制。无论路线如何,2026 年对于普通消费者来说都标志着一个奇点的到来:AI 不再是聊天工具,而是显式或隐式地接管了我们的数字生活。

正如那句老话:当一个技术不再被反复提及,才说明它已真正融入生活,如同血液般不可或缺。

然而,越是无感,越要警惕。当 AI 深入工作流的每一个细节,隐私将成为最昂贵的奢侈品。在追求极致自动化与效率的同时,如何选择服务商、如何平衡本地与云端模型、如何保留最后一点象征性的“隐私”,将是我们在 2026 年必须面对的命题。

2026 来了,你开始将 AI 集成到自己的工作流中了吗?

本期内容 | 前一期内容 | 全部周报列表

🚀 《肘子的 Swift 周报》

每周为你精选最值得关注的 Swift、SwiftUI 技术动态

近期推荐

独立开发者的试炼:Zipic 从 0 到 1 的产品化之路

Zipic 是我一直在高频使用的图片压缩工具,我亲眼见证了这个应用如何从一个职场工作的小需求,逐渐在作者 十里 的不断打磨下成长为一个高效、精致、专注的成功产品。独立开发者往往意味着“一人成军”,时刻在策略、设计、开发、分发与推广之间来回切换。为了挖掘这背后的故事,我邀请了十里复盘了 Zipic 从 0 到 1 的全过程。全文共分三个篇章:产品设计(本文)、不依赖 Mac App Store 的分发与售卖 以及 技术细节复盘:SwiftUI 痛点与性能瓶颈


Swift vs. Rust:从内存管理的终极对决中学到的 5 个惊人事实

在开发者社区中,关于 Swift 和 Rust 性能的讨论从未停止。通常的看法是:Swift 因为自动引用计数(ARC)而相对较慢,而 Rust 则以其极致的速度和内存效率著称。但 Snow 认为,这种“快”与“慢”的简单标签往往掩盖了两者在设计哲学上的根本差异:Swift 优先开发体验和生态兼容,Rust 追求极致性能和编译时安全。

结合实际案例,文章揭示了五个真相:Rust 的所有权规则本质上是零开销的编译时工具;Swift 的真正性能包袱来自 Objective-C 兼容性而非 ARC 本身;ARC 的核心问题是性能的不可预测性;并发安全上 Swift 依赖运行时保护而 Rust 实现编译时保证;以及为何 Swift 无法“变成”Rust。


StoreKit 订阅实战指南 (StoreKit Subscriptions: A Practical Guide)

Mohammad Azam 基于多年 iOS 开发经验和真实案例,撰写了完整的 StoreKit 订阅实践教程。系列涵盖:变现模型选择(一次性购买、订阅、消耗型购买及混合策略)、付费墙策略对比(软/硬付费墙及订阅试用的权衡)、引导体验设计(从静态截图演进到 8 步交互式引导,让用户在付费前完成核心功能体验并建立情感投入)、以及完整的技术实现(App Store Connect 配置、StoreKit 集成、产品加载和购买流程的代码示例)。


Skip 2025 回顾与 2026 路线图 (Skip 2025 Retrospective and 2026 Roadmap)

在 2025 年,随着 Swift SDK for Android 在 swift.org 正式发布,Skip 通过 Skip Fuse 提供原生编译支持,解锁了数千个原生 Swift 包在 Android 上的使用。同时新增 NFC、Stripe、PostHog、Auth0、Socket.IO 等双平台框架。iOS 26 推出的 Liquid Glass 界面风格成为跨平台框架的试金石。Skip 因采用“完全原生”策略(iOS 上使用原生 SwiftUI,Android 上映射到 Jetpack Compose)而在第一天就自动支持新界面,无需重写或变通。在 2026 年 Skip 计划继续扩展集成框架、优化 Skip Fuse 工具链、提升性能和开发体验。


如何使用 Claude Code (How to use Claude Code)

这是一份 Khoa Pham 在高强度使用 Claude Code 数月后整理的实战指南。核心技巧包括各种不同模式的详细应用场景,尤其是如何合理使用 Extended Thinking 模式以避免浪费 Token。另外还涵盖了关键快捷键、上下文管理技巧、MCP 集成、VS Code 和 Chrome 扩展、GitHub Actions 集成、Git Worktrees 并行工作流、插件生态以及提示词最佳实践等众多内容。内容详实、具体、有针对性,并非简单的功能介绍手册。


App Store Connect API Webhook 串接|提升 iOS CI/CD 自动化效率与通知流程

苹果在 WWDC 2025 中发布了 App Store Connect API Webhook,支持构建状态、App 版本状态、TestFlight 反馈等事件的实时推送。Zhong Cheng 针对打包上传后传统 Polling 方式需等待约 20 分钟(GitHub Runner 浪费 $1.24/次)的痛点,详细介绍了如何在 CI/CD 中应用该能力,实现零等待成本;GitFlow 回 master 时机可精确对齐 App 实际发布时间;开发者权限受限时也能及时收到拒审通知。


使用 WendyOS 开发嵌入式 Linux 应用 (Setting up Embedded Linux with WendyOS)

WendyOS 是一个专为嵌入式设备设计的 Linux 发行版,用 Swift 编写,旨在将 iOS 开发的便捷性带到嵌入式领域。Joannis Orlandos 在本文中提供了完整上手教程:从安装 Homebrew 和 Wendy 工具、刷写 WendyOS 到树莓派/Jetson Orin Nano 等设备、通过 USB 连接设备、配置 WiFi、创建 Swift 项目(含 wendy.json 权限配置)到使用 VSCode 扩展进行远程调试(支持断点和状态检查)。适合想将 Swift 应用到嵌入式设备或 IoT 场景的开发者作为入门教程。

工具

Swift 并发:通俗易懂的学习指南 (Fucking Approachable Swift Concurrency)

这是一个由 Pedro Piñera 创建、基于 Matt Massicotte 的 Swift 并发理念整理的学习资源,用通俗易懂的方式解释 async/await、Task、Actor、Sendable 等核心概念。Pedro 通过 "Office Building(办公楼)" 这一场景,将 MainActor 比作前台、actor 比作部门办公室、await 比作敲门等待,帮助开发者建立直观的心智模型。 另外,还提供了一个适用于 AI 工具的 Skill.md 文件,方便开发者将上述并发实践直接嵌入到开发工作流的规则引擎中。


Dimillian's Skills - iOS/Swift 开发 AI Agent Skills 集合

Thomas Ricouard 创建的用于 iOS/Swift 开发的 Skills 仓库,包含六个专注于实际工作流的 AI Agent Skills。涵盖 App Store Changelog 生成(从 git history 自动生成发布说明)、iOS Debugger Agent(使用 XcodeBuildMCP 构建/调试 iOS 项目)、Swift Concurrency Expert(修复 Swift 6.2 并发问题)、SwiftUI Liquid Glass(实现 iOS 26+ Liquid Glass API)、SwiftUI View Refactor(重构视图结构和依赖模式)、SwiftUI Performance Audit(审查性能瓶颈并提供优化建议)等。


StoreKit Helper - SwiftUI 应用内购买封装库

jaywcjlove开发的轻量级 StoreKit 2 封装库,专为 SwiftUI 设计,大幅简化应用内购买实现。相比直接使用 StoreKit 2 API,StoreKitHelper 减少了约 70% 的样板代码,特别适合需要快速集成应用内购买且不想处理底层复杂性的 SwiftUI 开发者。

核心特性包括:基于 @ObservableObject 的状态管理、协议驱动的类型安全产品定义、实时交易监听和自动状态更新、内置的 StoreKitHelperViewStoreKitHelperSelectionView UI 组件。通过 hasNotPurchased/hasPurchased 属性可以轻松控制界面显示,支持链式 API 配置购买弹窗的各种回调。

求贤

Mac OS 工程师

Photon 正在构建开源基础设施,帮助开发者将 AI Agent 带到人类已经熟悉的交互界面中,例如 iMessage、WhatsApp、电话通话、Discord、Signal 等。在此之上,我们还在打造以交互为核心的开源 Agent SDK,覆盖多段消息处理、消息线程处理、表情/回应(Tapbacks)等能力,让开发者和企业能够开发真正"像人一样"交流的 Agent。

职位要求

我们正在招聘 macOS 工程师,理想的候选人应具备以下条件:

  • 对 macOS 内部机制以及系统组件之间的交互有深入理解
  • 具备 macOS 系统分析与调试经验
  • 熟悉 macOS 系统级 API 及底层机制
  • 对探索 Apple 服务中的未知部分有好奇心
  • 加分项:有 iMessage、IMAgent 或相关消息基础设施的经验

薪资待遇

我们将提供具有竞争力的薪资(工作地点:美国,支持远程办公)。此外,Photon 获得多家知名投资机构的支持。

联系方式

ryan@photon.codes

这是朋友创业团队 Photon 的招聘。他们在做 AI Agent 在 iMessage/WhatsApp 等平台的基础设施,是个早期项目。如果你对 macOS 底层技术和早期创业机会感兴趣,可以了解一下。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

【iOS老生常谈】-Blocks知多少

2026年1月5日 16:47

一、Block的概念

1、是什么Block

Block是带有局部变量的匿名函数。

iOS4引入,是对c语言的扩充功能,先来理解一下局部变量匿名函数的含义

1.1.局部变量

那带有局部变量又是什么意思? 先理解一下c语言的都有哪些变量

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

每个变量的作用域不同。

int aa = 2; //全局变量
static int bb = 3; //静态全局变量
int main(int argc, const char * argv[]) { //argc 函数的参数
   int a = 1;  //auto修饰局部变量
   static int b = 1; //静态局部变量
   return 0;
}
变量 作用域 存储位置 生命周期 关键特性
全局 aa 整个程序(文件间) 全局数据区.data 程序全程 外部链接,多文件可访问
静态全局 bb 仅当前源文件 全局数据区.data 程序全程 内部链接,仅本文件可见
局部 a main 函数内 栈区 main 执行期间 自动销毁,每次调用重新初始化
静态局部 b main 函数内 全局数据区.data 程序全程 仅初始化一次,保留值
argc/argv main 函数内 栈区 main 执行期间 函数参数,栈上分配

1.2 匿名函数

所谓匿名函数,就是不带有函数名称的函数。

而在c语言中是不允许函数不带有名称的。

先理解一下c语言的函数定义:

int func(int count); //声明了名为func的函数,参数为int类型的count,返回值为int类型

调用func

int result = func(3);

使用函数指针funcPtr直接调用函数func,这种也是需要函数名才能通过函数指针调用。

int (*funPtr)(int) = &fun;
int result = (*funcPtr)(3);

二、Block的语法

2.1.Block的定义

官方文档:Blocks Programming Topics

//官方实例
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

image-20251219下午20836996

其中myBlock是声明的快对象,返回类型为int,myBlock 快对象有一个int类型的参数,myBlock的主体部分为 return num * multiplier;

上面表达式的特点:

  • 没有函数名 (匿名函数)
  • 带有^ ,插入记号,便于查找。

Block的表达式: ^返回值类型(参数列表){表达式}

^int (int count){ return count +1 };

Block可以省略如返回值,参数列表,如果用不到的话

  • 省略返回值类型

    //省略返回值类型:^(参数列表){表达式};
    ^(int count){return count+1};
    
  • 省参数列表

    ^int (void){ return  1};
    ^int {return  1};
    
  • 省略返回值类型、参数列表:

    ^{  }; //最简洁的block
    

2.2.Block类型的变量

通过Block语法将Block赋值为Block类型的变量

int (^blk) (int) = ^(int count) { return  count+1}; 

此时的表达式和c语言的指针函数表达式对比

int (*funPtr)(int) = &fun; //指针函数

block的变量声明就是把声明指针函数类型的* 变为^

  • 在函数参数中使用 Block类型的变量

    //作为函数参数的block变量
    void func(int (^blk)(int)){
        
    }
    
  • 作为函数的返回值

    int (^blk1(void))(int){
        return ^(int count){
          return count+1 ;
        };
    }
    //作为函数返回值时,需要注意:
    // 1. 调用 blk1 函数,并且是无参函数,它返回一个Block
    int (^myBlock)(int) = blk1();
    
    // 2. 调用(执行)这个返回的Block,并传入整数参数 5
    int result = myBlock(5);
    NSLog(@"结果是: %d", result); // 输出:结果是: 6
    

    上述当block作为参数或返回值时,可以通过typedef声明类型,来简化,如下

    typedef int ^(Blk_t)(int); //声明一个Blk_t类型的block
    Blk_t blck = ^{
    }
    
    //作为函数参数和返回值就可以简化为
    void func(Blk_t blck){
    
    }
    
    //作为函数返回值时可以简化为
    Blk_t func1(){
    
    }
    

2.3. 截获自动变量值

void test1(void){
   //默认为auto修饰局部变量
   int a = 1;
   void (^bck1)(void)=^(){
       NSLog(@"访问量局部变量a:%d",a);
   };
    a = 2;
    NSLog(@"访问量局部变量a1:%d",a);
   bck1();
}
//访问量局部变量a1:2
//访问量局部变量a:1

block访问局部变量 auto修饰,此时block截获了变量a的当前的瞬间值,底层为值传递,所以block内部不能直接赋值修改,block外侧修改了局部变量,block内部变量值不会修改。

如果在block内部尝试修改局部变量会报错

image-20251223下午31225845

报错信息:变量不可赋值(缺少__block类型说明符)

Variable is not assignable (missing __block type specifier)

2.4. __block 说明符

__block说明符更准确的描述方式为“__block存储域类说明符” __block storage-class-specifier,c语言的存储域类说明符右以下几种:

  • typedef
  • extern
  • static
  • auto
  • register

__block类似于static、auto、register等说明符。用于指定将变量值设置到哪个存储域中,例如auto作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。

如果非要在block内部修改局部变量,就需要再局部变量前通过__block修饰

void test1(void){
   //默认为auto修饰局部变量
   __block int a = 1;
   void (^bck1)(void)=^(){
       NSLog(@"访问量局部变量修改前a:%d",a);
       a = 3;
       NSLog(@"访问量局部变量修改后a:%d",a);
   };
    a = 2;
    NSLog(@"访问量局部变量a1:%d",a);
   bck1();
}
//访问量局部变量a1:2
//访问量局部变量修改前a:2
//访问量局部变量修改后a:3

2.5.截获的自动变量

block截获自动变量会报错,那截获OC对象呢,比如NSMutableArray还会报错吗?

//截获可变数组,
void test11(void){
   //默认为auto修饰局部变量
    NSMutableArray *array = [NSMutableArray array];
   void (^bck1)(void)=^(){
       [array addObject:[[NSObject alloc]init]];
       NSLog(@"访问量局部变量array.count:%lu",(unsigned long)array.count);
   };
    NSLog(@"访问量局部变量array1.count:%lu",(unsigned long)array.count);
   bck1();
}
//访问量局部变量array1.count:0
//访问量局部变量array.count:1

此时block截获的变量值是NSMutableArray类对象,及NSMutableArray类对象的结构体实例指针,因此,对变量值进行addObject操作,是没有影响的,如果在block内部,要对array进行赋值时是不行的。依然需要用__block修饰

image-20251223下午33532635

使用c语言的数组时必须小心使用其指针,下面这个例子,看似并没有像截获的自动变量text赋值,但还是编译不通过,报错信息:Cannot refer to declaration with an array type inside block(不能引用块内数组类型的声明)

image-20251223下午34352216

需要把text声明为指针来解决

void test111(void){
   //使用c语言的数组时必须小心使用其指针,
   const char *text = "hello";
   void (^bck1)(void)=^(){
       //截获自动变量的方法并没有实现对c语言数组的截获,此时需要用指针来解决该问题
       NSLog(@"截获的局部变量:%c",text[2]);
   };
   bck1();
}

三、Block的底层实现

3.1、Block的实质

一开始讲了Blocks是带有局部变量匿名函数,但是Block实质究竟是什么,类型、 变量、还是其他什么?

先说结论:Block其实是一个对象。因为它的结构体里有isa指针

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

image-20251223下午43004746

执行后,会生成一个main.cpp文件

image-20251223下午44246125

  • 转换前的oc代码

    //为了简化生成的c++代码,把原来oc代码的main方法传参(int argc, const char * argv[])省略。以及
    int main(void) {
            void (^donyBck)(void)=^(){
                //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
                NSLog(@"The block prints logs internally");
            };
            //donyBck的调用
            donyBck();
        return 0;
    }
    
  • 转换后的C++源码

    //包含block实际函数指针的结构体
    struct __block_impl {
      void *isa; //有isa
      int Flags;
      int Reserved;  //今后升级所需区域大小
      void *FuncPtr; //函数指针
    };
    
    //
    static __NSConstantStringImpl __NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"The block prints logs internally",32};
    
    //block结构体
    struct __main_block_impl_0 {
      struct __block_impl impl; //Block的实际指针,
      struct __main_block_desc_0* Desc;
      //block构造函数
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
        impl.Flags = flags; //block标志位
        impl.FuncPtr = fp; //Block执行的函数指针
        Desc = desc; //Block描述信息,Block大小等元信息
      }
    };
    
    //Block内部函数调用
    /* 
    void (^donyBck)(void)=^(){
                //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
                NSLog(@"The block prints logs internally");
            };
    */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
            }
    
    //Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
    static struct __main_block_desc_0 {
      size_t reserved; //成员1 :保留字段
      size_t Block_size; //成员2:Block实例的内存大小
    } __main_block_desc_0_DATA = { //初始化变量
      0, //给Reserved赋值
      sizeof(struct __main_block_impl_0) //给Block——size赋值
    };
    
    
    //main函数
    int main(void) {
      //1.构造block对象,并将其强制转换为无参无返回值的函数指针。
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
          (void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
          &__main_block_desc_0_DATA //block的描述信息(版本,大小等)
        ));
      
      //2.调用block的核心逻辑(donyBck的调用)
      // ((void (*))(donyBck):将函数指针转回Block结构体指针
      //->FuncPtr :取出Block的执行函数指针
      //最后调用该函数,并传入Block自身作为参数,(block的隐式self)
       ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
        return 0;
    }
    

3.2 、源码分析

3.2.1 Block结构体-__main_block_impl_0

先看看 __main_block_impl_0 结构体

//block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; //Block的实际指针,
  struct __main_block_desc_0* Desc;
  //block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
    impl.Flags = flags; //block标志位
    impl.FuncPtr = fp; //Block执行的函数指针
    Desc = desc; //Block描述信息,Block大小等元信息
  }
};

从源码里可以看出,__main_block_impl_0结构体,包含三部分

  • 成员变量 impl,是结构体__block_impl

  • 成员变量 Desc指针,是结构体 __main_block_desc_0

  • __main_block_impl_0 构造函数

分别分析一下这三部分

3.2.1.1 struct __block_impl impl
//包含block实际函数指针的结构体
struct __block_impl {
  void *isa; //有isa
  int Flags;  //标志位
  int Reserved;  //今后升级所需区域大小
  void *FuncPtr; //函数指针
};
  • isa指针,保存Block结构体的实例指针
  • Flags 标志位
  • Reserved 后续版本升级所需区域大小
  • FuncPtr 函数指针,指向了Block的主体部分,也及时对应oc代码里的 { NSLog(@"The block prints logs internally");};

3.1.1里的__main_block_impl_0 里的impl 是__block_impl结构体,而__block_impl 包含了Block实际函数指针 FuncPtr

总结:impl 主要就是包含了Block的函数指针 FuncPtr

3.2.1.2 struct __main_block_desc_0* Desc
//Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
static struct __main_block_desc_0 {
  size_t reserved; //成员1 :保留字段
  size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
  0, //给Reserved赋值
  sizeof(struct __main_block_impl_0) //给Block——size赋值
};
  • reserved 版本升级后所需的区域大小
  • Block_size Block的大小。

总结:__main_block_desc_0是block的描述信息,也就是附加信息

3.2.1.3 __main_block_impl_0构造函数
 //block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
    impl.Flags = flags; //block标志位
    impl.FuncPtr = fp; //Block执行的函数指针
    Desc = desc; //Block描述信息,Block大小等元信息
  }

传三个参数:

  • void *fp :主要赋值给impl的FuncPtr ,Block执行的函数指针
  • struct __main_block_desc_0 *desc, Block的描述信息
  • int flags=0 ,默认值为0,标志位。

总结:构造函数主要用来初始化__block_impl的成员变量,以及把描述信息赋值给Desc

3.2.1.4 在main方法里的过程

关于:__main_block_impl_0 基本概念就了解完了,那在main方法里,__main_block_impl_0 又是怎么赋值的呢

//原函数
 void (^donyBck)(void)=^(){
            //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
            NSLog(@"The block prints logs internally");
 };

//转换c++后
//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
    void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
      (void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
      &__main_block_desc_0_DATA //block的描述信息(版本,大小等)
    ));

可以看出通过 __main_block_impl_0构造函数,生成 __main_block_impl_0结构体(Block结构体)的实例指针,并赋值给donyBck

然后对 __main_block_impl_0构造函数传了两个参数

  • __main_block_func_0

    //Block内部函数调用
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //对应的就是原oc  NSLog(@"The block prints logs internally");
       NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
    }
    

    可以看出 对应的是oc block的主体部分也就是 {NSLog(@"The block prints logs internally"); };

    这里的参数 __cself就是指向Block的值的指针变量,相当于oc的self;

    🎯这里画个重点:从这里可以看出,block内部,把block的^{}执行函数{}在Block定义完后,当成一个参数类型为__main_block_func_0传入到 __main_block_impl_0->__block_impl-> FuncPtr 里了。

  • __main_block_desc_0_DATA

    __main_block_desc_0_DATA 是包含了Block的描述信息,

    static struct __main_block_desc_0{
      size_t reserved; //成员1 :保留字段
      size_t Block_size; //成员2:Block实例的内存大小
    } __main_block_desc_0_DATA = { //初始化变量
      0, //给Reserved赋值
      sizeof(struct __main_block_impl_0) //给Block——size赋值
    };
    

3.2.2 总结

至此Block的内部原理就浮出水面了。

Block内部是由 __main_block_impl_0结构体组成的,内部isa指针,指向所属类的结构体的实例指针,_NSConcreteStackBlock相当于Block的结构体实例,对象impl.isa = &_NSConcreteStackBlock ,将Block结构体的指针赋值给impl的成员变量isa ,相当于Block结构体成员变量保存了Block结构体的指针,和OC的对象处理方式是一致的。

所以Block的实质就是对象,和NSObject一样,都是对象。

image-20251225上午114937229

3.3 Block截获局部变量和特殊区域变量

3.3.1 Block截获局部变量的实质

在2.3里我们知道了Block可以截获局部变量,那背后Block是怎么截获的,为什么不能在block内部直接修改截获的局部变量呢?

先说结论:Block截获的局部变量是值传递的方式传入Block结构体中,并保存为Block的成员变量。因此当外部局部变量值发生修改后,Block内部对应的成员变量的值并没有发生改变。

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    int main(void) {
        int a = 2;
        void (^donyBck)(void)=^(){
            NSLog(@"The block Capture local variables:%d",a);
        };
        
        a = 4;
        //donyBck的调用
        donyBck();
        NSLog(@"The block prints local variables:%d",a);
        return 0;
    }
    //The block Capture local variables:2
    //The block prints local variables:4
    
  • 转c++源代码

    //block结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a; //截获的局部变量,在block内部变成了成员变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //block执行函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_1,a);
        }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 
    0, 
    sizeof(struct __main_block_impl_0)};
    
    
    int main(void) {
        int a = 2;
    
        //block定义
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
        (void *)__main_block_func_0,
         &__main_block_desc_0_DATA, 
         a));
    
        a = 4;
    
        //block 执行方法
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)(
        (__block_impl *)donyBck
        );
      //打印
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_2,a);
        return 0;
    }
    
    • 从源码可以看出,结构体__main_block_impl_0 多了一个成员变量a,这个变量就是Block捕获的局部变量
    • 方法__main_block_func_0 看出 int a = __cself->a在block内部访问这个变量a时,通过值传递的方式,而不是指针传递,这也就说明了a是block的内部变量,外部修改a,Block内部捕获的a是不会发生变化的。

3.3.2 使用__block说明符更改局部变量

那使用__block来修饰局部变量后,就能让Block内部来修改这个变量,那背后__block 又做了什么呢?

先说结论:通过 __block修饰后,使这个局部变量在block内部通过指针传递,所以修饰后的局部变量,在block内部可以修改了

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    int main(void) {
        __block int a = 2;
        void (^donyBck)(void)=^(){
            a = 3;
            NSLog(@"The block Capture local variables:%d",a);
        };
        
        a = 4;
        //donyBck的调用
        donyBck();
        NSLog(@"The block prints local variables:%d",a);
        return 0;
    }
    
  • c++代码

    //__block修饰的
    struct __Block_byref_a_0 {
      void *__isa; //isa指针
    __Block_byref_a_0 *__forwarding; //传入变量的地址
     int __flags; //标志位
     int __size; //结构体大小
     int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
    };
    
    //block内部
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref //加入__Block修饰后,这里的a是__Block_byref_a_0类型了
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock; //栈block
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //block执行函数
    static void __main_block_func_0(struct  __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref 
            (a->__forwarding->a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
        }
    
    //新增了 
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign(
        (void*)&dst->a, 
        (void*)src->a,
         8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    //新增了
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
      _Block_object_dispose((void*)src->a, 
                            8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 
        0, 
        sizeof(struct __main_block_impl_0),
         __main_block_copy_0,
          __main_block_dispose_0
    };
    
    //main函数
    int main(void) {
      
      //__block修饰的局部变量a
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
          (void*)0,
          (__Block_byref_a_0 *)&a, 
          0, 
          sizeof(__Block_byref_a_0), 
          2};
        
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
          (void *)__main_block_func_0, 
          &__main_block_desc_0_DATA, 
          (__Block_byref_a_0 *)&a, 
          570425344));
    
      //修改局部变量a的值
      (a.__forwarding->a) = 4;
      
      //block调用
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
      
      //block打印
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_4,(a.__forwarding->a));
        return 0;
    }
    
    

    可以看出局部变量a加上 __block后,c++代码里新增了 __Block_byref_a_0__main_block_copy_0__main_block_dispose_0

    • 从结构体 __main_block_impl_0 可以看出原来oc的 被__block修饰后的局部变量a,在结构体__main_block_impl_0内部变成了 __Block_byref_a_0 *a,也就是说Block内部的结构体__main_block_impl_0实例持有指向__block变量的__Block_byref_a_0结构体实例指针。
    • __main_block_copy_0__main_block_dispose_0留在后面在说。

这里有点绕,我们用白话文理解一下

可以把整个过程想象成寄送一个易碎品(变量a)

角色 对应代码 比喻说明
易碎品本身 局部变量 int a = 2 比如一个玻璃杯。
加固包装盒 __Block_byref_a_0结构体 一个专门用来固定玻璃杯的防震包装盒。
快递单/指针 __Block_byref_a_0 *a(Block结构体里的成员) 一张写着包装盒地址的快递单。
整个Block __main_block_impl_0结构体实例 快递仓库。
🎯 整个过程是这样的:
  1. 打包: 当你用 __block修饰变量 a时,编译器会自动创建一个“包装盒”(__Block_byref_a_0结构体),然后把你的变量 a(玻璃杯)放进这个盒子里。
  2. 填写快递单: Block(快递仓库)想要操作这个玻璃杯,但它并不直接把玻璃杯拿进来(那样就成了它的私有物品,无法和外界共享了)。Instead,它只拿了一张写着“包装盒地址”的快递单(即指针 __Block_byref_a_0 *a)。
  3. 共享修改: 当Block内部或外部代码需要修改 a的值时,它们会凭着这张“快递单”找到同一个“包装盒”,然后打开盒子修改里面的玻璃杯。因为大家访问的是同一个盒子里的东西,所以任何一方的修改,另一方都能立刻看到

白话文总结:被__block修饰后的变量a,在Block内并没有直接把变量装在自己口袋里,而是记下了变量所在包装盒的地址。通过这个共享的地址,Block和外部代码就能共同修改同一个变量了。

在看看__Block_byref_a_0 结构体

//__block修饰的
struct __Block_byref_a_0 {
  void *__isa; //isa指针
__Block_byref_a_0 *__forwarding; //传入变量的地址
 int __flags; //标志位
 int __size; //结构体大小
 int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
};

在看一下在main()中原oc代码为

     __block int a = 2;

转为c++为

    //__block修饰的局部变量a
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
      (void*)0,
      (__Block_byref_a_0 *)&a, 
      0, 
      sizeof(__Block_byref_a_0), 
      2};

从赋值里可以看出在main()__block修饰后的变量a,底层赋值给__Block_byref_a_0结构体时,传入的值如下

  • isa传入空

  • __forwarding 传入了局部变量a的本身地址

  • __flags :分配了0

  • sizeof:结构体的大小

  • a值赋值为2.

    image-20251225下午44345718

总结一下:到此知道了__forwarding就是局部变量a的本身地址,可以通过 __forwarding指针来访问局部变量。同时也能对其修改了。

    //block执行函数
    static void __main_block_func_0(struct  __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref 
            (a->__forwarding->a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
        }

可以看出 (a->__forwarding->a) = 3;,是通过指针取值的方式来改变局部变量的值,

🎯总结:

从而解释了通过__block修饰后的变量,在Block内部通过指针传递的方式修改局部变量

另外__block__Block_byref_a_0 结构体并不在Block的 __main_block_impl_0结构体中,这样做是为了多个Block同时使用__block变量。

  • 继续看一下OC代码

            int main(void) {
                    //__block被多个Block使用
                __block int aa = 2;
    
                void (^donyBck)(void)=^(){
                    aa = 3;
                    NSLog(@"The block Capture local variables:%d",aa);
                };
    
                void (^donyBck1)(void)=^(){
                    aa = 4;
                    NSLog(@"The block Capture local variables1:%d",aa);
                };
            }
    
  • 转换为c++

      //__block修饰后
      __Block_byref_aa_0 aa = {
         0,
         &aa, 
         0, 
         sizeof(__Block_byref_aa_0), 2};
         //donyBck
         donyBck)=&__main_block_impl_0(
            __main_block_func_0, 
            &__main_block_desc_0_DATA, 
            (__Block_byref_aa_0 *)&aa, 
            570425344));
            //donyBck1
          donyBck1)=&__main_block_impl_1(
            __main_block_func_1, 
            &__main_block_desc_1_DATA, 
            (__Block_byref_aa_0 *)&aa, 
            570425344));
    

可以看出donyBckdonyBck1 都是用了__Block_byref_aa_0结构体的实例aa的指针。反过来一个block中使用多个 __block也是可以的。

3.3.3 更改特殊区域变量值

除了通过__block修饰局部变量外,其他变量如静态局部变量、静态全局变量、全局变量能否在block内部进行修改?

为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    int global_a = 2;
    static int static_global_a = 3;
    int main(void) {
        static int static_a = 4;
        void (^donyBck)(void)=^(){
            global_a = 1;
            static_global_a = 2;
            static_a = 3;
            NSLog(@"The block Capture global_a:%d ,static_global_a:%d,static_a:%d",global_a,static_global_a,static_a);
        };
        
        //donyBck的调用
        donyBck();
        return 0;
    }
    
  • C++代码

    //全局变量
    int global_a = 2;
    //静态全局变量
    static int static_global_a = 3;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_a; //静态局部变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_a = __cself->static_a; // bound by copy
    
            global_a = 1;
            static_global_a = 2;
            (*static_a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
        }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    int main(void) {
        static int static_a = 4;
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_a));
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
        return 0;
    }
    

    __main_block_impl_0可以看出,静态局部变量static_a以指针形式添加添加为成员变量,而静态全局变量 static_global_a 和全局变量global_a并没有添加到__main_block_impl_0内部。

    //全局变量
    int global_a = 2;
    //静态全局变量
    static int static_global_a = 3;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_a; //静态局部变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    再从__main_block_func_0,可以看出全局变量global_a和全局静态变量static_global_a是在block内部访问直接访问的,而静态局部变量static_a是通过指针传递的方式进行访问和赋值的。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_a = __cself->static_a; // bound by copy
    
            global_a = 1; //全局变量
            static_global_a = 2; //全局静态变量
            (*static_a) = 3; //局部静态变量
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
        }
    

3.4 Block的存储域

在3.2 中的Block实质源码分析里,看出impl.isa = &_NSConcreteStackBlock; //栈block,可以看出该Block存储在栈区,那Block还可以存在哪个区呢?

先说结论:Block分别可以存储在_NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

设置对象的存储域
_NSConcreteGlobalBlock 数据区域(.data区)
_NSConcreteStackBlock
_NSConcreteMallocBlock

image-20251225下午74628893

3.4.1 _NSConcreteGlobalBlock

为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    //全局block
    void (^donyBck)(void) =^(){
        NSLog(@"global block");
    };
    
    int main(void) {
        donyBck();
        return 0;
    }
    
  • c++源码

    struct __donyBck_block_impl_0 {
      struct __block_impl impl;
      struct __donyBck_block_desc_0* Desc;
      __donyBck_block_impl_0(void *fp, struct __donyBck_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock; //全局block
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

通过源码可以看出 impl.isa = &_NSConcreteGlobalBlock,说明该Block为 _NSConcreteGlobalBlock类型。

这里需要注意使用全局block时,因为本身已经在全局区域,所以不会捕获自动变量(局部变量),存储在数据区域

3.4.2 _NSConcreteStackBlock

除了全局block外,其他基本都存储在栈上,也就是StackBlock。

NSConcreteStackBlock类的block,存储在栈区,如果所属的变量作用域结束,该Block就会被废弃。由于 __block变量也配置在栈上,同样的所属变量作用域结束后,该 __block变量同样也被废弃。

image-20251226下午125508437

3.4.3 _NSConcreteMallocBlock

既然在栈上的Block在变量作用域结束后就立即被废弃,那如果不想废弃怎么办?

Block提供了 【复制copy】操作,可以将Block对象和 __block变量从栈区复制到堆区上,当Block从栈区复制到堆区后,即时变量作用域结束时,堆区上的Block和 __block还可以继续使用。

image-20251226下午10020548

此时的在堆区的Block就是_NSConcreteMallocBlock 对象,Block结构体成员变量isa赋值为 imp.isa = &_NSConcreteMallocBlock;

而此时被__block修饰的变量用结构体成员变量 __forwarding可以实现无论__block变量配置在堆上还是在栈上,都能够正确访问__block变量

3.5 Block的自动拷贝和手动拷贝

3.5.1 Block的自动拷贝

在ARC下,大多数情况下,编译器会自动进行判断,自动生成将Block从栈上复制到堆上的代码

  • 将Block作为函数返回值返回时,会自动拷贝

  • 向方法或函数的参数中传递Block时:(以下两种情况内部底层实现了copy操作,其他都需要手动拷贝)

    • Cocoa框架的方法且方法名中含有usingBlock等 如NSArray类的enumerateObjectsUsingBlock方法
    • GCD的API时如dispatch_async函数
    //在MRC下,initWithObjects后面的Block,编译器不会主动给Block添加copy操作,所以Block还存在栈上,所以会报错。
    //在ARC下,编译器主动添加了copy操作,此时的block被复制到堆上了。
    @implementation SDPerson
    
    - (id) getBlockArray{
        int val = 10;
      void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
        void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
        NSLog(@"blk01:%@",blk0);
      NSLog(@"blk02:%@",blk1);
        /*
      blk01:<__NSStackBlock__: 0x7ff7bfeff0d8>
    blk02:<__NSStackBlock__: 0x7ff7bfeff0a8>
      */
      //array 的initWithObjects 纯容器存储AIP,框架内部不会自动copy
      return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
    }
    
    @end
    

    image-20251226下午45612397

    报错原因是:在执行完getBlockArray后栈上的Block被废弃,MRC 无任何自动优化,initWithObjects: 仅存栈 Block 指针 → 方法返回栈帧销毁 → 执行 Block 访问野内存 → 崩溃。此时我们需要手动复制下即可。

    修改一下getBlockArray,即可

    - (id) getBlockArray{
        int val = 10;
        
        void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
        void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
        blk0 = [blk0 copy];
        blk1 = [blk1 copy];
        NSLog(@"blk01:%@",blk0);
        NSLog(@"blk02:%@",blk1);
        //blk01:<__NSMallocBlock__: 0x600000c00540>
        //blk02:<__NSMallocBlock__: 0x600000c00570>
        return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
    }
    
3.5.2 Block的手动拷贝

所有需要让 Block「脱离原栈帧存活」的场景,都必须手动调用[block copy]

关于Block不同类的拷贝效果总结如下

block类 副本源的存储域 复制效果
_NSConcreteStackBlock 栈区 从栈拷贝到堆区
_NSConcreteGlobalBlock 程序的数据区域 不做改变
_NSConcreteMallocBlock 堆区 引用计数增加

不管Block配置在何处,用copy复制不会引起任何问题,在不确定时,调用copy方法即可

3.6 __block变量存储域

在使用 __block变量的Block从栈复制到堆上,__block变量也受到了如下影响

__block变量的配置存储区域 Block从栈上复制到堆上时的影响
堆区 从栈复制到堆区,并被Block所持有
栈区 被Block所持有

和OC引用计数方式内存管理完全相同。

  • __block修饰的变量被Block所持有,如果Block废弃,持有的__block变量也跟着废弃

3.7 截获对象

在Block语法中使用局部变量array来添加元素。

理论上在变量作用域的同时,变量array被废弃,因此赋值给变量array的NSMutableArray类对象必定释放并废弃,但上述代码在main方法里的内{}外仍然可以执行,并打印日志。 这意味着array在Block的执行部分超出其变量作用域而存在。

先说结论:Block从栈复制到堆,归结为__Block_copy函数被调用,使__strong修饰的自动变量对象和__block修饰的变量,被堆上的Block所持有,所以可以超出其变量作用域而存在。

那我们转为c++代码再探个究竟

  • OC

    typedef int (^Blk_t)(id obj);
    Blk_t donyBck;
    
    int main(void) {
        
        {
            id array = [NSMutableArray array];
            donyBck = [^(id obj){
                [array addObject:obj];
                NSLog(@"access local variables array.count:%lu",[array count]);
            } copy];
        }
        
        donyBck([[NSObject alloc]init]);
        donyBck([[NSObject alloc]init]);
        donyBck([[NSObject alloc]init]);
        return 0;
    }
    /*
    access local variables array.count:1
    access local variables array.count:2
    access local variables array.count:3
    */
    
  • C++

    typedef int (*Blk_t)(id obj);
    Blk_t donyBck;
    
    //Block用的结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      id array; //这里array被Block强引用 __strong
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
    };
    
    //block执行方法
    static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
      id array = __cself->array; // bound by copy
    
      //添加方法
                ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
      //打印日志
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_b8c0c1_mi_8,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
     }
    
    //copy
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    //废弃
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    //block描述
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    //main方法
    int main(void) {
        {
            id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
            donyBck = (Blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
        }
      
        //Block执行
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        return 0;
    }
    
    
    

    通过源码可以看出 array被Block截获,并成为__strong修饰成员变量,这里虽然没有显示__strong,默认就是强引用

        //Block用的结构体
        struct __main_block_impl_0 {
          struct __block_impl impl;
          struct __main_block_desc_0* Desc;
          id array; //这里array被Block强引用 __strong
        };

那Block捕获的array是在什么时候进行初始化和废弃的呢?

我们可以从__main_block_desc_0可以看出结构体新增了copydispose,以及对应__main_block_copy_0__main_block_dispose_0,这两个结构体在3.3.2里__block时也遇到了。 不过__block修饰的变量和捕获的对象有一点点区别:

对象 BLOCK_FIELD_IS_OBJECT
__block变量 BLOCK_FIELD_IS_BYREF

仅仅主要用来区分是对象还是__block变量

   //block描述
   static struct __main_block_desc_0 {
     size_t reserved;
     size_t Block_size;
     void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
     void (*dispose)(struct __main_block_impl_0*);
   } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

   //copy
   static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
     //_Block_object_assign对使Block内部对array持有
     _Block_object_assign(
       (void*)&dst->array, 
       (void*)src->array, 
       3/*BLOCK_FIELD_IS_OBJECT*/
     );
   }

   //废弃
   static void __main_block_dispose_0(struct __main_block_impl_0*src) {
     _Block_object_dispose(
       (void*)src->array, 
       3/*BLOCK_FIELD_IS_OBJECT*/
     );
   }
  • __main_block_copy_0可以看出,内部通过_Block_object_assign方法对array持有,_Block_object_assign相当于retain,将对象赋值在对象类型结构体成员变量中。

  • __main_block_dispose_0使用_Block_object_dispose函数,相当于release,释放赋值在Block用结构体成员变量的array中的对象。

  • 因此_Block_object_assign__main_block_dispose_0指针赋值在__main_block_desc_0的copy和dispose中。

    但是在源代码里没有看到这些函数以及指针被调用,那他们的调用时机在什么时候?

    image-20251229下午84719431

那什么时候栈上的Block会复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时。

array默认为__strong修饰,那如果改为__weak呢?

typedef void (^Blk_t)(id obj);
Blk_t donyBck;

int main(void) {
    {
        id array = [NSMutableArray array];
        id __weak array2 = array;
        donyBck = [^(id obj){
            [array2 addObject:obj];
            NSLog(@"access local variables array.count:%lu",[array2 count]);
        } copy];
    }
    
    donyBck([[NSObject alloc]init]);
    donyBck([[NSObject alloc]init]);
    donyBck([[NSObject alloc]init]);
    return 0;
}
/**
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
*/

这是因为array在变量作用域结束后同时被释放、废弃。nil被赋值给__weak修饰的array2。

那如果__block__weak同时修饰呢?

__block id __weak array2 = array;

结果一样:

    access local variables array.count:0
    access local variables array.count:0
    access local variables array.count:0

即使被附加了__block说明符,__strong修饰符的变量array也会在变量作用域结束的同时被释放掉,nil被赋值给附有__weak的变量array2中。

另外被__unsafe_unretained修饰符的变量只不过与指针相同,所以不管在Block中使用还是附加到__block变量中,也不会想 __strong__weak那样进行处理,使用__unsafe_unretained修饰符需要注意不能通过悬垂指针访问已被废弃的对象。

__autoreleasing修饰符也不能和__block同时使用

3.8 Block循环引用

我们知道Block内部使用__strong修饰符的对象类型的自动变量,那当Block从栈复制到堆的时候,该对象就会被Block所持有

那么如果这个对象还同时持有Block的话,就容易发生循环引用。正所谓你中有我,我中有你

image-20251231上午100014948

  • 示例1:

    // 文件SDPerson.m
    typedef void (^blk_t)(void);
    @interface SDPerson()
    {
        blk_t blk_;
    }
    @end
    
    
    @implementation SDPerson
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"self = %@",self);
            };
        }
        return self;
    }
    
    - (void)dealloc{
        NSLog(@"dealloc");
    }
    
    //文件main.m
    #import "SDPerson.h"
    int main(void) {
        SDPerson *person = [[SDPerson alloc]init];
        NSLog(@"%@",person);
        return 0;
    }
    

    最终执行结果 <SDPerson: 0x60000020d340> SDPerson的dealloc没有执行,发生了Block的循环引用

    具体分析:

    SDPerson内部blk_t持有了self,而self也同时持有作为成员变量的blk_t

    另外编译器也会有提示

    image-20251231上午100056778

    如果Block内部不使用self,还会造成循环引用吗?

  • 示例2:

    typedef void (^blk_t)(void);
    @interface SDPerson()
    {
        blk_t blk_;
        id obj_;
    }
    @end
    
    
    @implementation SDPerson
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"obj_ = %@",obj_);
            };
        }
        return self;
    }
    

    答案:会

    分析一下:表面上看obj_没有使用self,但是它是self的成员变量,因此Block想持有obj_,就必须引用self,所以同样造成循环引用。

    那如果这个属性使用weak修饰符呢

  • 实例3 :

    typedef void (^blk_t)(void);
    @interface SDPerson()
    @property (nonatomic, weak) NSArray *array;
    @end
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"obj_ = %@",_array);
            };
        }
        return self;
    }
    

    答案:还是会循环引用,因为循环引用是self和block之间的事情,这个被block持有的成员变量是strong或weak没有关系,即使是基本类型assign也是一样的。

那如何解决这样的循环引用呢?

3.8.1 __weak修饰符

为了避免循环引用,可以通过__weak修饰符,来打破互相持有

- (instancetype)init
{
    self = [super init];
    if (self) {
      //使用__weak修饰符,使block内部为弱引用关系
        id __weak tmp = self;
        // id __unsafe_unretained tmp = self;
        blk_ = ^{
            NSLog(@"self = %@",tmp);
        };
    }
    return self;
}

常见用 __weak typeof(self) weakSelf = self; 来进行弱引用self

__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    [weakSelf doSomething];
};

上述存存在一个问题,当block执行过程中,weakSelf可能被释放,导致后续操作无效

这里需要再block内部进行强化弱引用,使用__strong在局部作用域内临时强引用弱引用对象,确保在执行期间对象存活。__strong在Block内部栈上创建局部强指针,不会造成循环引用。

__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomething];
        [strongSelf doAnotherThing];
    }
};

除了__weak typeof(self) weakSelf = self; 这里还可以参考第三方开源库ReactiveObjC这样的简洁写法 @weakify(self); 作用是一样的,都是对self进行弱引用。

@weakify(self);
self.myBlock = ^{
@strongify(self);
    if (self) {
        [self doSomething];
        [self doAnotherThing];
    }
};

除了__weak修饰符外,还可以使用__unsafe_unretained,如id __unsafe_unretained tmp = self; 效果是一样的,都是使对象为弱引用,那两者有什么区别?更推荐使用__weak

两者的区别在于当所指向的对象被释放时,如何处理指针

  • __weak(安全)当对象释放后,所有指向它的__weak变量会被运行时自动设置为nil,意味着后续在访问这个指针,就像nil发送消息一样,在OC中是安全的,不会导致程序崩溃 。
  • __unsafe_unretained(不安全)当对象释放后,__unsafe_unretained指针不会自动置空,仍然保存着对象被释放前的那个内存地址,也就是变成了“悬垂指针”或“野指针”,如果此时访问了这个指针,就会发生BAD_ACCESS

image-20251231上午101006054

3.8.2 __block修饰符

除了__weak外,还可以使用 __block解决block循环引用问题

typedef void (^blk_t)(void);
@interface SDPerson()
{
    blk_t blk_;
}
@end
@implementation SDPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        __block id tmp = self;
        blk_ = ^{
            NSLog(@"obj_ = %@",tmp);
            tmp = nil;
        };
    }
    return self;
}

//执行block
- (void)execBlock{
    blk_();
}
int main(void) {
    SDPerson *person = [[SDPerson alloc]init];
    [person execBlock];
    NSLog(@"%@",person);
    return 0;
}
//执行结果
//obj_ = <SDPerson: 0x600000c08690>
//<SDPerson: 0x600000c08690>
//dealloc 

如果Block不执行execBlock,依然会存在循环引用

此时-SDPerson持有Block,Block持有__block变量,__block持有SDPerson类对象。三者互相持有,导致引用循环

image-20251231下午10424295

如何解决?

就是执行Block execBlock方法,Block内部,会把tmp置为nil。并执行block execBlock方法,因此__block持有类对象的强引用就失效了,

blk_ = ^{
     NSLog(@"obj_ = %@",tmp);
     tmp = nil;
};
//并执行block execBlock方法

image-20251231下午10913331

所以__block需要执行Block来解决循环引用,基于此特点,可以通过__block控制对象的持有时间。

这里需要区分,这里利用__block解决循环引用,不是因为__block本身直接解决的,而是利用了__block的**「可写特性」+ 手动执行 tmp = nil**,并执行block的execBlock方法,使block内部tmp=nil生效,从而主动打破了循环链。

在实际开发过程中,需要具体根据实际情况,来使用__weak还是__block.

四、常见面试题

4.1 说一下什么是Block?

Block是带有局部变量的匿名函数,本质是一个对象,内部有isa指针,内部是由结构体**__main_block_impl_0->__block_impl**组成,Block的执行函数通过在__block_impl->FuncPtr函数指针,找到封装的函数,并将block地址作为参数传给这个函数进行执行。Block捕获的变量,存入__main_block_impl_0结构体内,并通过block地址拿到捕获变量的值。

//包含block实际函数指针的结构体
struct __block_impl {
  void *isa; //有isa
  int Flags;
  int Reserved;  //今后升级所需区域大小
  void *FuncPtr; //函数指针
};

//block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; //Block的实际指针,
  struct __main_block_desc_0* Desc;
};

4.2 Block 有几种类型?分别是什么?

有三种类型,分别为 _NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

定义在全局的block,为全局block,存储在数据区的全局区里,因为本身就是全局,所以不会访问局部变量,因此不需要捕获局部变量。

一般用到的是栈block,但是栈上的block是临时的,在它的作用域结束后就被销毁,为了延长生命周期,在arc下系统会默认会copy到堆上,来延迟生命周期,这样可以在定义它的作用域外部使用。mrc下,需要手动进行copy

4.3 Block 自动截取变量

Block外部的变量,可以被block捕获到内部进行使用,这里需要注意的是变量类型

  • 全局变量/静态全局变量 ,block不需要捕获,因为全局变量和静态全局变量数据存储在全局数据区,Block内部直接使用

  • 局部静态变量 捕获变量地址,所以外部变量修改后,通过地址访问到变量的值,也会跟着修改。

  • 静态变量 捕获变量的值,是通过值传递的方式捕获到block内部,并且捕获的是变量的当前瞬时值,所以外部修改了变量,block内部的变量值不会发生改变,如果需要修改,需要通过__block来修饰,然后通过指针引用传递的方式在内部使用。

    4.3.1 为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗

    不行,和局部变量的生命周期有关系,因为局部变量在出大括号后就会被释放掉,这事我们在大括号外部调用这个Block,此时局部变量已经被释放了,block内部通过变量的指针访问变量,就会抛出异常。而静态局部变量的生命周期是和整个程序的生命周期一样,也就是在整个程序运行过程中不会释放,所以可以通过指针地址访问。

    4.3.2既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?

    这是因为静态局部变量作用域只在大括号内,出了括号,它虽然存在,但外面已经访问不了,这时通过block执行函数只能通过捕获的方式。

    4.3.3 静态局部变量一直都不会被释放,会导致内存泄漏吗?

    不会,静态局部变量是语言设计的特性,行为可预测,是一种特殊的局部变量,具有局部作用域,存储在数据区.data(初始化).bss(未初始化),程序在运行期间只被分配一次内存(且占内存有限),生命周期有编译器自动管理,启动时初始化,结束时销毁,不会导致运行时的内存泄漏

    首先明确内存泄漏的概念:程序在运行过程中,不断分配内存而没有适当的释放,导致内存逐渐减少的情况

4.4Block 处理循环引用

如果Block内部捕获了外部的strong(强引用)类型的引用对象,那么这个对象有强引用block,就会形成循环引用,会导致内存泄漏,因为参与循环引用的对象和block无法正常释放,长期下去会导致性能问题。

这时,就需要通过__weak关键字,进行对强引用对象进行弱引用,来打破你中有我,我中有你。

4.4.1 __weak typeof(self) weakSelf = self; 和@weakify(self);有什么区别

都是用来弱引用self,避免循环引用,__weak typeof(self) weakSelf = self;

@weakify(self) 是宏定义,预编译阶段展开就是 __weak typeof(self) weakSelf = self;,设计初衷就是为了更简洁更优雅。

4.5 Block 的内存管理

首先围绕Block的三个核心点

4.6__block 的解释以及在 ARCMRC 下有什么不同

默认情况下,Block捕获的外部自动变量(局部变量)是值捕获,在Block内部是无法修改的。

__block是修饰符,主要用于解决上述问题,block捕获的局部变量,在block内部,可以进行修改。

原理:被__block修饰后的局部变量,编译器会把这个变量包装成一个结构体对象,底层其实一个结构体__Block_byref_a_0,内部有一个__forwarding指针,和当前变量的值等成员,无论Block和__block变量本身被复制到栈上还是堆上,都可以通过这个指针访问和修改值,这样就使从值传递变成了引用传递

在ARC和MRC下的不同

  • 在ARC下

__block修饰的对象是强引用,需要注意循环引用,常用的解决方案为使用__weak弱引用。

  • 在MRC下

对__block修饰对象,不会对对象进行retain,避免循环引用

4.7为什么需要使用copy关键字

Block默认创建在栈上,为了延迟生命周期,需要copy到堆上。

在arc之前,手动管理内存,为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上,通过copy关键字操作,如下

@property (copy, nonatomic) void (^block)(void);

在ARC之后,虽然编译器会自动进行copy操作,把block复制到堆上,为了和MRC下保持一致,避免在不同内存管理环境下切换的混淆,在ARC之后,还是推荐用copy关键字

4.7.1 那用strong可以修饰block吗?

在ARC下可以,在MRC下不可以。

4.7Dispatch_block_t这个有没有用过?解释一下?

dispatch_block_t是GCD中的一个类型定义,代表无参数,也没有返回值的代码里。

基本定义 typedef void (^dispatch_block_t)(void);

常见的使用场景:将任务放入队列后立即返回,不阻塞当前线程

    dispatch_async(queue,^{
    NSLog(@"在后台执行任务");
    })

五、参考文献

注册苹果个人开发者账号,千万别换设备!!!

作者 CocoaKier
2026年1月5日 09:43

记录一下我最近注册个人开发者账号的经历,前后历时2周,换了3个个人身份,废了两台新买的测试机。西天取经,九九八十一难,各种问题,全靠猜,联系苹果也是模棱两可,等几天最后告诉你,你的账号废了,你的设备废了,请换新的!!难度堪比提审遇到账号调查。

先说结论:
1、(非常重要)注册过程中千万不要换设备,不要换账号。遇到任何问题联系苹果解决。如果你把账号换到别的设备上尝试注册,那你这个账号和这两台设备大概率会被风控。你的这个账号和这“两台”设备就废了,无法继续用于注册了。
2、填个人信息地址时要填对,最好填身份证上的地址(这是联系苹果时,苹果告诉我的)。我猜苹果会校验的地址,比如地址是否有效,是否完整,是否精确到门牌号等。瞎填或者填的不完整是过不了的,会提示“如果要继续注册,请联系苹果”。

3、一个身份证只能注册一个个人开发者账号。即使这个身份证之前注册流程没走完,也算使用过了。无法使用新的AppleID绑定这个身份证重新注册。只能用原来的AppleID继续绑定注册

在和苹果技术支持沟通过程中,苹果工作人员提到官方文档中明确写明了,注册过程中不能更换设备。为此,我专门去查了一下官方文档。苹果官方文档,确实有提到“你必须在整个注册流程中使用相同的设备”,无论是注册个人账号还是公司账号。 图片.png


下面是我这次注册的经历,当时没想到注册个人账号这么复杂,这么多坑。下面是没有任何心眼下的小白操作。

我这边有个新项目,需要注册一个新的个人开发者账号。我顺便买了两部新测试手机(手机1、手机2)。

1、小H注册
最开始找到同事小H注册。小H重新注册了一个新的AppleID,使用手机1,下载Developer App去注册,到填街道地址那一步卡住了,提交报错,具体什么错不记得了。联系苹果,苹果说小H的身份信息以前注册过开发者账号,请使用以前的账号继续注册或登录图片.png

由于时间太久了小H也不记得以前是否注册过了,也不记得是哪个账号了。我们又联系苹果说我们不记得账号了,能否申请用新的账号注册苹果回信说,“不可以,请回忆之前的账号或者使用公司其他人的身份注册”。我们把小H所有有可能的AppleID通过找回密码都试了一遍,都是AppleID不存在。放弃。 图片.png

2、小L注册
我又找到同事小L。小L新注册了一个AppleID,使用手机1,用Developer App去注册。到了填街道页面,小L把街道和详细地址填的很简略,提交后报错“Action not allowed”。网上查资料说设备可能被风控了,可以换个设备尝试。于是,我换了另一个新买的测试手机2,结果账号登录后,“现在注册”按钮是置灰的,无法点击。(我们没有联系苹果,自己瞎摸索后)在苹果后台找到了网页注册入口,点进去上传了身份证。第二天,苹果邮件通知我们身份信息校验通过可以继续注册了。Developer App的注册按钮恢复正常了,还是在街道页面,街道和详细地址填的很简略,提交报“Action not allowed”。

联系苹果,苹果问我们注册过程中是否换过设备,我说换过。苹果告诉我,注册条款里有明确说明,注册的时候不能换设备我说我不知道有这个条款,能不能帮我把设备重置一下。苹果说她没有权限,帮我连线资深顾问。资深顾问说她帮我联系中国的运营团队。等了2天,收到苹果邮件:“由于一个或多个原因,你无法完成Apple Developer Program的注册。我们目前无法继续处理你的注册申请。”再次放弃。 图片.png

至此,我们的两个账号、两个身份、两台设备都废了!

3、小X注册
没办法只能再换人换设备了,这次长教训了,直接用小X的私人手机注册。用小X身份新注册一个AppleID(因为用私人AppleID后续不方便),在他私人手机上下载了Developer App进行注册。前面还算顺利,直到填写街道那一步,提交后弹窗提示“请联系苹果支持”。登录苹果开发者网站 - 联系我们 - 账号注册 - 电话沟通。苹果告诉我们街道地址填的有问题,最好填身份证上的地址,这样大概率是没问题的图片.png Developer App上改为填小X身份证地址后果然可以到下一步了,后续就交钱了。交完钱并不代表你注册成功了第二天收到苹果的邮件,让我们上传身份证。通过邮件链接打开苹果网站,上传身份证正面照片。 图片.png 上传后收到苹果回复邮件,说两个工作日审核完毕。实际上还挺快的半天就审核完了,下午我们就收到了开发者账号注册成功的邮件。 图片.png

总算是注册成功了。

划重点:

  • 确保使用未曾注册过(旧测试机就不要用了,否则还浪费身份证名额);
  • 注册过程中千万不要换设备,遇到任何问题联系苹果解决;
  • 街道地址填身份证地址。

最后,祝大家注册顺利,少踩坑。

Flutter 3.38.1 之后,因为某些框架低级错误导致提交 Store 被拒

2026年1月5日 09:24

如果你近期已经升级到 3.38.1 之后的版本,包括 3.38.5 ,你就有概率发现,打包提交 iOS 的包会出现 The binary is invalid 的相关错误,简单来说,就是App Store 拒绝了某个二进制文件,因为它包含了无效的内容

那么这个内容是怎么来的?大概率是模拟器架构的 Framework 被错误地打包进了正式发布的 App ,具体原因还要提到最新版本增加的 Native Assets 功能。

Native Assets 的目标是让在 Flutter/Dart 包中集成 C、C++、Rust 或 Go 代码,可以像集成普通 Dart 包一样简单,也就是它允许 Dart 包定义如何构建和打包原生代码,开发者不需要深入了解每个平台的底层构建系统,也是 Dart FFI 未来的重要基建。

详细可见:《Flutter 里的 Asset Transformer 和 Hooks ,这个实验性功能有什么用》

那它怎么导致了这次这个低级问题的出现?实际上这是一个构建脚本逻辑缺陷导致的“脏构建”问题,当 Flutter 构建依赖于 Native Assets(比如 sqlite3 等库)的 Plugin 时,这些原生资源会被编译并输出到 build/native_assets/$platform 目录(例如 build/native_assets/ios)。

因为在现有的构建脚本(xcode_backend.dart)在打包时,会简单粗暴地将 build/native_assets/ios 目录下的所有框架复制到最终的 App Bundle (Runner.app/Frameworks) ,例如:

  • 先运行了模拟器跑应用,这时模拟器专用的框架(如 sqlite3arm64ios_sim.framework)就会被生成并留在了 build/native_assets/ios 目录
  • 接着,开发者在没有运行 flutter clean 的情况下,直接运行了 Release 构建
  • 构建脚本会把之前遗留的“模拟器框架”也一并复制进了 Release 包
  • App Store 检测到 Release 包中含有模拟器架构的代码,因此拒绝接收

所以说,大厂也有大厂的草台。

当然,这个问题解决起来也很简单,就是发布前 flutter clean 清理一下,当然,如果你之前打过包了,那么 Xcode 的构建缓存也需要清理下,因为可能存在即使你通过 flutter clean 删除了 Flutter 的构建产物,但是 Xcode 可能仍然认为某些中间文件(Intermediate Build Files)存在可用。

比如 DerivedData 缓存

那么这么低级的问题,修复下也很简单,所以 sqlite3 的作者也提交了一个 #179251 ,简单来说就是,针对 Native Assets :

  • 读取构建过程中生成的 native_assets.json 文件
  • 解析文件,获取当前构建真正引用的依赖列表
  • 仅复制 native_assets.json 中列出的框架,忽略目录中残留的其他无关文件(如模拟器文件)

这个修复其实很简单,但是在流程上,因为目前 PR 还缺少 integration test ,所以一直卡在了等待 Review 阶段,除非有人申请豁免,不然这个 PR 的合并还会继续卡着。

只能说,一代人有一代人的草台。

参考链接

GetX 状态管理详解

作者 妖枪银弹
2026年1月4日 16:31

一、 GetX 状态管理核心机制

GetX 的状态管理并非单一方案,而是提供了三种核心状态管理模式,兼顾简洁性和灵活性,适配不同业务场景,其核心设计围绕「轻量、无侵入、响应式」展开。

1. 简单状态管理(GetBuilder:非响应式)

适用于简单的状态更新场景(如按钮点击刷新文本、列表局部更新),基于手动触发重建实现,无响应式依赖,性能开销极低。

  • 核心原理:
    1. 定义 Controller 继承 GetxController,在控制器中维护状态变量,并提供状态更新方法。
    2. 通过 update() 方法手动标记状态变更,通知对应的 GetBuilder 进行组件重建。
    3. GetBuilder 关联指定控制器,仅在收到 update() 通知时刷新自身布局,不影响其他组件。
  • 代码示例:
// 1. 定义控制器
class CountController extends GetxController {
  int count = 0;

  void increment() {
    count++;
    update(); // 手动触发状态更新
  }
}

// 2. 在UI中使用
Widget build(BuildContext context) {
  // 无需手动初始化控制器,GetX自动管理生命周期
  return GetBuilder<CountController>(
    builder: (controller) {
      return Column(
        children: [
          Text("计数:${controller.count}"),
          ElevatedButton(
            onPressed: controller.increment,
            child: const Text("点击增加"),
          ),
        ],
      );
    },
  );
}

2. 响应式状态管理(GetX / Obx:自动响应式)

适用于复杂状态依赖场景(如网络请求结果刷新、多组件共享状态、实时数据同步),基于Dart 扩展方法+观察者模式实现,无需手动调用 update(),状态变更自动触发 UI 重建。

  • 核心原理:
    1. 状态变量通过 .obs 扩展方法转化为「可观察对象(Observable)」,GetX 会监听该对象的所有变更。
    2. 使用 Obx(或 GetX)包裹需要响应状态变更的 UI 组件,Obx 作为观察者,订阅可观察对象的状态变化。
    3. 当可观察对象的值发生改变时,会自动通知所有订阅它的 Obx 组件,触发局部重建,无需全局刷新。
  • 核心特性:
    • 无需继承 GetxController(可直接使用全局变量,也可结合控制器管理)。
    • 支持多种数据类型:基本类型(int/string/bool)、集合类型(List/Map/Set)、自定义对象。
    • 局部重建:仅 Obx 包裹的组件会重建,性能优于全局状态刷新。
  • 代码示例:
// 1. 定义响应式状态(两种方式:直接使用 / 结合控制器)
// 方式1:直接使用全局响应式变量
var userName = "张三".obs;

// 方式2:结合控制器管理(推荐,便于状态统一维护)
class UserController extends GetxController {
  var userAge = 20.obs;
  var userInfo = UserModel(name: "李四", age: 25).obs; // 自定义对象

  void updateAge() {
    userAge.value++; // 注意:基本类型响应式变量需通过 .value 访问/修改
    userInfo.update((info) { // 自定义对象批量更新
      info?.age = userAge.value;
    });
  }
}

// 2. 在UI中使用 Obx 监听
Widget build(BuildContext context) {
  final userController = Get.put(UserController()); // 初始化控制器(单例)
  return Column(
    children: [
      Obx(() => Text("用户名:${userName.value}")),
      Obx(() => Text("用户年龄:${userController.userAge.value}")),
      Obx(() => Text("自定义对象年龄:${userController.userInfo.value.age}")),
      ElevatedButton(
        onPressed: () {
          userName.value = "王五"; // 自动触发UI刷新
          userController.updateAge(); // 控制器内状态变更,自动刷新
        },
        child: const Text("更新状态"),
      ),
    ],
  );
}

3. 依赖注入式状态管理(Get.put / Get.find:生命周期管理)

GetX 状态管理的核心支撑能力,通过内置依赖注入(DI)容器管理控制器生命周期,无需手动创建和销毁控制器,实现状态的全局共享或局部共享。

  • 核心原理:
    1. Get.put(Controller()):将控制器实例存入 GetX 的 DI 容器,默认全局单例,可指定 tag 实现多实例,或指定 permanent: false 实现自动销毁。
    2. Get.find<Controller>():从 DI 容器中获取已存入的控制器实例,无需跨组件传递。
    3. 生命周期绑定:控制器继承 GetxController 后,可重写 onInit()onReady()onClose() 方法,对应组件的初始化、就绪、销毁生命周期,自动执行。
  • 核心特性:
    • 无需 InheritedWidget 包裹(对比 Provider),无组件嵌套冗余。
    • 支持局部状态:通过 Get.create() 或在路由中传入 binding,实现路由级别的局部状态,路由销毁时自动销毁控制器。

二、 GetX 与 Provider、Bloc 的核心对比

1. 核心设计差异

特性 GetX Provider Bloc
状态管理模式 支持3种模式(GetBuilder/Obx/DI),兼顾简单与复杂场景 单一响应式模式(基于InheritedWidget + ChangeNotifier) 单一事件驱动模式(基于Stream + Event/State分离)
核心依赖 无额外依赖(GetX自身集成) 依赖 provider 包(基于Flutter原生组件) 依赖 bloc/flutter_bloc 包(基于Dart Stream)
组件侵入性 极低(无需顶层包裹,可按需使用Obx/GetBuilder) 较高(需顶层包裹 MultiProvider,子组件需 Consumer/Provider.of 较高(需 BlocProvider 包裹,子组件需 BlocBuilder/BlocConsumer
状态传递方式 依赖注入(Get.find),无需跨组件层层传递 基于InheritedWidget,自上而下跨组件传递 依赖注入(BlocProvider)+ Stream监听,自上而下传递
事件处理方式 灵活(可直接调用方法,也可自定义事件) 简单(调用ChangeNotifier的更新方法) 严格(Event入参 → Bloc处理 → State输出,单向数据流)

2. GetX 的优势

(1) 极致简洁,开发效率极高

  • 代码量极少:无需编写大量模板代码(对比 Bloc 的 Event/State/Bloc 三层模板,Provider 的 ChangeNotifier 子类+Consumer 包裹),大幅减少冗余代码。
    • 示例:实现一个计数功能,GetX 只需 10 行左右代码,Bloc 需编写 Event(CountIncrementEvent)、State(CountState)、Bloc(CountBloc)三层代码,代码量增加 3-5 倍。
  • 无嵌套地狱:无需顶层包裹 MultiProvider/MultiBlocProvider,解决了 Provider/Bloc 中多层嵌套导致的代码可读性差的问题。
  • 无需上下文(Context):通过 Get.find() 可在任意位置获取控制器,无需传递 BuildContext,尤其在工具类、网络请求类中使用便捷。

(2) 功能全面,一站式解决方案

  • 状态管理 + 路由管理 + 依赖注入 + 国际化 + 主题管理:GetX 并非单一状态管理库,而是集成了 Flutter 开发所需的核心功能,无需引入多个第三方库(如 Provider 需配合 flutter_routeget_it 实现路由和DI),减少库之间的兼容性问题。
  • 灵活适配场景:简单场景用 GetBuilder(性能最优),复杂场景用 Obx(自动响应式),全局共享用 Get.put(单例),局部共享用路由 Binding(自动销毁),适配所有业务场景。

(3) 性能更优,资源开销更低

  • 局部重建更精准:Obx/GetBuilder 仅刷新自身包裹的组件,且无需遍历 InheritedWidget 树(对比 Provider),减少了布局遍历的性能开销。
  • 控制器自动销毁:GetX 可根据路由生命周期自动销毁控制器(permanent: false),避免 Provider/Bloc 中手动管理控制器生命周期导致的内存泄漏问题。
  • 无 Stream 额外开销:GetX 的响应式状态管理基于自定义 Observable,无需像 Bloc 那样依赖 Stream 流处理,减少了 Stream 订阅/取消订阅的性能开销。

(4) 学习成本更低

  • 无需掌握 Stream 原理:Bloc 强依赖 Dart Stream 知识,对于新手而言,学习成本较高;GetX 无需了解 Stream,只需掌握 .obsObx 的使用,即可快速上手。
  • API 设计简洁直观:GetX 的 API 命名清晰(如 Get.put、Get.find、update、Obx),符合开发者的使用习惯,无需记忆复杂的 API 结构。

3. GetX 的劣势

(1) 状态管理过于灵活,团队协作易出规范问题

  • 由于 GetX 支持全局响应式变量(无需控制器)、控制器管理状态、局部状态等多种方式,若团队无统一开发规范,容易出现状态分散、难以维护的问题(如部分状态在全局变量中,部分在控制器中,排查问题时难以定位)。
  • 对比 Bloc:Bloc 的 Event/State 严格分离,强制遵循单向数据流,团队协作时规范统一,即使是大型项目,状态流转也清晰可追溯;Provider 虽灵活,但依赖 InheritedWidget,状态范围相对明确。

(2) 生态成熟度略低于 Provider、Bloc

  • Provider:作为 Flutter 生态中最早的状态管理库之一,几乎被所有 Flutter 开发者熟知,相关教程、插件、问题解决方案极为丰富,且与 Flutter 原生组件深度兼容。
  • Bloc:由 Google 团队成员参与维护,生态成熟,支持 bloc_test(单元测试)、flutter_bloc(Flutter 适配)、bloc_concurrency(并发处理)等周边库,在大型企业级项目中应用广泛。
  • GetX:生态虽在快速发展,但相比 Provider、Bloc,部分小众场景的解决方案较少,且第三方插件对 GetX 的适配度略低。

(3) 不适用于对状态流转有严格要求的大型项目

  • 对于金融、电商等大型企业级项目,状态流转的可追溯性、可测试性要求极高:
    • Bloc 的 Event/State 分离模式,每一次状态变更都对应一个明确的 Event,可通过 bloc_test 轻松编写单元测试,且能通过 DevTools 追踪状态流转过程,便于问题排查。
    • GetX 的状态更新方式(直接调用方法修改状态),虽然简洁,但状态变更的触发来源难以追溯,单元测试需要额外编写更多代码来模拟状态变更,对于大型项目的维护性略有不足。

(4) 存在“过度封装”的争议,底层可定制性弱

  • GetX 为了追求简洁,对底层实现进行了大量封装,开发者难以自定义其响应式机制、依赖注入容器的行为。
  • 对比 Provider:基于 Flutter 原生 InheritedWidgetChangeNotifier 实现,开发者可轻松扩展 ChangeNotifier 或自定义 InheritedWidget,实现个性化需求。
  • 对比 Bloc:基于 Stream 实现,开发者可灵活自定义 Stream 控制器、并发策略、状态转换逻辑,底层可定制性极强。

(5) 调试体验略逊于 Bloc

  • Bloc 提供了专门的 BlocDevTools,可实时监控 Event 发送、State 变更、Bloc 生命周期,便于调试复杂的状态流转问题。
  • GetX 无官方专属调试工具,状态变更的监控需要手动打印日志或借助 Flutter DevTools,对于复杂的响应式状态依赖,调试效率略低。

4. 补充:Provider、Bloc 的各自优势(对应 GetX 的劣势)

  • Provider 优势
    1. 与 Flutter 原生深度融合,学习成本低(只需掌握 InheritedWidget 基础概念)。
    2. 轻量、稳定,无多余功能,专注于状态管理,适合小型项目或对第三方库依赖敏感的项目。
    3. 生态成熟,问题解决方案丰富,新手友好。
  • Bloc 优势
    1. 严格的单向数据流,状态流转清晰可追溯,适合大型团队协作和企业级项目。
    2. 强大的测试能力,便于编写单元测试和集成测试,保证代码质量。
    3. 高度可定制化,可灵活扩展底层逻辑,适配复杂业务场景。
    4. 官方支持完善,调试工具成熟,生产环境稳定性更高。

三、 选型建议

  1. 优先选 GetX:小型项目、快速迭代项目、个人项目,或团队追求开发效率、希望减少模板代码的场景;适合需要一站式解决状态管理、路由、DI 的场景。
  2. 优先选 Provider:新手入门、对第三方库功能冗余敏感的项目,或需要与 Flutter 原生组件深度兼容的场景;适合小型到中型项目。
  3. 优先选 Bloc:大型企业级项目、金融/电商等对状态可追溯性和可测试性要求极高的场景,或团队需要严格编码规范的场景;适合中型到大型项目。

总结

  1. GetX 状态管理的核心是「3种模式+依赖注入」,兼顾简洁性和灵活性,Obx(响应式)和 GetBuilder(非响应式)适配不同场景,依赖注入实现状态全局/局部共享。
  2. GetX 的核心优势是「开发效率高、代码简洁、功能全面、性能优」,劣势是「规范性弱、生态成熟度略低、大型项目维护性不足」。
  3. Provider 胜在「原生兼容、轻量稳定」,Bloc 胜在「规范严格、可测试性强、大型项目友好」,GetX 胜在「高效简洁、一站式解决方案」,需根据项目规模和团队需求选型。

Flutter 项目启动全流程详解

作者 妖枪银弹
2026年1月4日 13:18

Flutter 项目启动全流程详解

作为资深 Flutter 架构师,我会从分层视角(原生层 → Flutter 引擎层 → Dart 运行时层 → App 业务层)为你拆解 Flutter 项目启动的完整流程,涵盖核心步骤、关键机制和底层细节,帮你全面掌握启动原理。

一、 第一阶段:原生平台初始化(Native Bootstrapping)

Flutter 是跨平台框架,最终会打包为 iOS/Android 原生应用,启动流程首先从原生平台侧开始,这是 Flutter 启动的入口。

1. Android 平台启动流程

  • 核心入口:FlutterActivity(或 FlutterFragment,对应 Fragment 嵌入场景)
  • 关键步骤:
    1. FlutterActivity 初始化:继承自 AppCompatActivity,启动时先执行原生 Android 的 onCreate() 生命周期方法。
    2. 加载 Flutter 引擎依赖:初始化 FlutterEngine 相关配置(如 Dart 入口路径、初始化参数),若使用预加载引擎(提前初始化优化启动速度),会直接获取预创建的 FlutterEngine 实例;若未预加载,则现场创建 FlutterEngine
    3. 配置 FlutterView:创建用于承载 Flutter UI 的原生 View(FlutterView),并将其挂载到 Android 布局层级中,作为 Flutter 渲染内容的显示载体。
    4. 启动引擎桥接:通过 FlutterNativeView 建立原生 Android 与 Flutter 引擎的通信通道,传递初始化参数(如屏幕尺寸、系统主题、原生平台信息等)。

2. iOS 平台启动流程

  • 核心入口:FlutterViewController(对应 iOS 的视图控制器)
  • 关键步骤:
    1. FlutterViewController 初始化:执行 iOS 原生的 initWithNibName:bundle:init 方法,完成控制器自身初始化。
    2. Flutter 引擎初始化:创建 FlutterEngine 实例(同样支持预加载优化),配置 Dart 执行环境参数。
    3. 绑定视图载体:FlutterViewController 的视图(view 属性)本质是 FlutterView,用于渲染 Flutter UI,完成视图层级挂载。
    4. 建立通信通道:通过 FlutterMethodChannel/FlutterEventChannel 的底层初始化,完成 iOS 原生与 Flutter 引擎的双向通信准备。

核心作用

  • 提供 Flutter 运行的原生容器环境(Activity/ViewController + View);
  • 初始化 Flutter 引擎的原生依赖,建立跨平台通信的基础通道;
  • 传递系统级参数(如设备信息、屏幕参数),为 Flutter 后续初始化提供上下文。

二、 第二阶段:Flutter 引擎初始化(Engine Initialization)

Flutter 引擎(C/C++ 实现,核心是 Skia 渲染引擎、Dart 虚拟机、排版引擎等)是 Flutter 的核心运行时,原生平台初始化完成后,会触发 Flutter 引擎的启动与初始化。

1. 引擎核心组件初始化

Flutter 引擎初始化是多组件协同启动的过程,核心组件包括:

  • Skia 渲染引擎:初始化 2D 图形渲染上下文,绑定原生平台的渲染表面(Android 的 Surface、iOS 的 CALayer),配置抗锯齿、渲染精度等参数,为后续 UI 渲染提供底层支持。
  • Dart 虚拟机(VM):初始化 Dart 运行时环境,包括内存管理(堆内存分配、垃圾回收机制初始化)、指令解析器(JIT/AOT 模式切换,移动端默认 AOT 编译模式,调试模式 JIT)。
  • 排版引擎(Layout Engine):初始化 Flutter 专属的排版规则(如 Flex 布局、Text 排版),加载系统默认字体、自定义字体配置,建立排版计算的上下文环境。
  • 事件分发系统:初始化触摸事件、键盘事件、生命周期事件的分发通道,确保原生事件能传递到 Flutter 层。

2. 引擎与原生平台的绑定

  • 渲染绑定:将 Skia 渲染引擎与原生 FlutterView 的渲染缓冲区绑定,确保 Flutter 绘制的内容能显示在原生视图上。
  • 线程绑定:Flutter 引擎启动后会创建4个核心线程,并与原生平台线程建立映射:
    1. UI 线程(Platform Thread):对应原生主线程,处理 Flutter 组件构建、布局计算、状态更新。
    2. 渲染线程(Render Thread):独立线程,处理绘制命令生成、图层合成,最终将合成结果提交给 Skia 渲染。
    3. I/O 线程(I/O Thread):处理异步任务(如网络请求、文件读写、图片解码),避免阻塞 UI 线程和渲染线程。
    4. Dart 虚拟机线程:执行 Dart 代码逻辑,与 UI 线程协同工作(调试模式下独立,Release 模式下与 UI 线程合并优化)。

三、 第三阶段:Dart 运行时初始化(Dart Runtime Setup)

Flutter 引擎初始化完成后,会启动 Dart 虚拟机,并执行 Dart 代码的初始化流程,这是 Flutter 业务逻辑的入口。

1. 加载并执行 Dart 根隔离区(Root Isolate)

  • Isolate 是 Dart 的轻量级线程(无共享内存,通过消息传递通信),Flutter 启动时首先创建根 Isolate(主 Isolate),作为 Dart 代码的执行入口。
  • 核心操作:
    1. 加载 AOT 编译产物(Release 模式):移动端 Flutter 项目打包后会生成 .so(Android)/ App.framework(iOS)格式的 AOT 编译产物,Dart 虚拟机直接加载并执行该产物,无需即时编译,启动速度更快。
    2. 加载 JIT 快照(Debug 模式):调试模式下,Flutter 会生成 Dart 代码的 JIT 快照,Dart 虚拟机加载快照后启动,支持热重载(Hot Reload)功能。

2. 执行 main() 函数(Dart 入口)

  • 根 Isolate 初始化完成后,会自动执行 Dart 项目的 main() 函数,这是 Flutter 业务代码的第一个入口方法,典型代码如下:
void main() {
  // 可选:初始化全局配置(如网络拦截器、日志工具、依赖注入)
  WidgetsFlutterBinding.ensureInitialized(); // 关键:初始化 Flutter 核心绑定
  runApp(const MyApp()); // 启动 Flutter 应用
}
  • 关键说明:WidgetsFlutterBinding.ensureInitialized() 是 Flutter 核心绑定初始化方法,若省略,runApp() 内部会自动调用,其作用是初始化 Flutter 框架的核心服务(如渲染绑定、手势绑定、生命周期绑定等)。

四、 第四阶段:Flutter 框架初始化与 UI 渲染(Framework & UI Rendering)

main() 函数中调用 runApp() 后,进入 Flutter 框架初始化和 UI 首次渲染流程,这是 Flutter UI 显示的核心步骤。

1. Flutter 框架核心绑定初始化

WidgetsFlutterBinding 是 Flutter 框架的核心绑定类,它整合了 7 大核心绑定,确保 Flutter 框架正常工作:

  • GestureBinding:手势识别与事件分发绑定。
  • ServicesBinding:平台消息通信绑定(如 MethodChannel 通信)。
  • SchedulerBinding:任务调度与帧回调绑定(控制 UI 刷新帧率,默认 60fps)。
  • PaintingBinding:绘制相关绑定(如图片缓存、字体加载)。
  • SemanticsBinding:语义化绑定(支持无障碍访问)。
  • RenderBinding:渲染管线绑定(布局、绘制、合成)。
  • WidgetsBinding:组件框架绑定(组件构建、状态管理、路由管理)。

2. 执行 runApp(Widget app) 核心逻辑

runApp() 是 Flutter UI 启动的关键方法,核心操作如下:

  1. 挂载根组件:将传入的根 Widget(如 MyApp)设置为 Flutter 框架的根组件(rootWidget),建立组件树的顶层节点。
  2. 触发首次帧调度:通过 SchedulerBinding 向 Flutter 引擎发送「首次绘制帧」的调度请求,引擎接收到请求后,启动 UI 线程的布局与绘制流程。

3. 首次 UI 渲染管线(Layout & Paint)

Flutter 首次渲染遵循「构建 → 布局 → 绘制 → 合成 → 渲染」的流水线:

  1. 构建(Build):UI 线程遍历组件树(从根 Widget MyApp 开始),执行每个 Widget 的 build() 方法,生成「元素树(Element Tree)」(Widget 是配置模板,Element 是实际渲染实例)。
  2. 布局(Layout):基于元素树,Render 层(RenderObject)执行布局计算,确定每个组件的大小、位置(如 RenderFlex 处理 Flex 布局,RenderText 处理文本排版),生成「布局树(Layout Tree)」。
  3. 绘制(Paint):Render 层根据布局结果,生成每个组件的绘制命令(如绘制矩形、文本、图片),生成「绘制树(Paint Tree)」。
  4. 合成(Compositing):渲染线程将绘制命令按「图层(Layer)」进行分层合成(如透明组件、滚动组件会单独分层),生成「图层树(Layer Tree)」,优化渲染性能。
  5. 渲染(Render):渲染线程将合成后的图层树提交给 Skia 渲染引擎,Skia 将图层绘制到原生 FlutterView 的渲染缓冲区,最终在屏幕上显示 Flutter UI。

4. 启动完成:触发 onFirstFrame 回调

当 Flutter 首次帧渲染完成后,会触发 WidgetsBindingonFirstFrame 回调(可监听该回调统计启动耗时),此时用户可以看到 Flutter 应用的首屏 UI,标志着 Flutter 项目启动流程全部完成。

五、 补充:启动模式差异(Debug vs Release)

Flutter 调试模式(Debug)和发布模式(Release)的启动流程存在核心差异,直接影响启动速度:

对比维度 Debug 模式 Release 模式
Dart 执行模式 JIT(即时编译) AOT(提前编译)
产物加载 加载 Dart 快照,支持热重载 加载 AOT 编译产物(.so/Framework),直接执行
引擎优化 关闭部分渲染优化、线程优化 开启全量优化(线程合并、绘制优化、内存优化)
启动速度 较慢(JIT 初始化 + 快照加载) 较快(AOT 产物直接执行,无编译开销)
额外功能 支持 Hot Reload、DevTools 调试 无调试功能,体积更小、性能更优

总结

Flutter 项目启动是一个跨平台、分层级、多线程协同的复杂流程,核心步骤可概括为 4 个关键阶段:

  1. 原生平台初始化:提供容器(Activity/ViewController)和视图载体(FlutterView),初始化引擎依赖;
  2. Flutter 引擎初始化:启动 Skia 渲染、Dart 虚拟机等核心组件,创建 4 个核心线程,完成与原生的绑定;
  3. Dart 运行时初始化:创建根 Isolate,加载 Dart 编译产物,执行 main() 函数;
  4. Flutter 框架与 UI 渲染:初始化框架绑定,执行 runApp(),通过「构建-布局-绘制-合成-渲染」管线完成首屏显示。

理解该流程有助于你优化 Flutter 项目启动速度(如预加载 Flutter 引擎、延迟初始化非核心业务、优化首屏 Widget 构建),以及排查启动阶段的跨平台兼容问题。

2026 码农漫游:AI 辅助 Swift 代码修复指南

2026年1月4日 11:28

在这里插入图片描述

☔️ 引子

这是一个雨夜,霓虹灯的光晕在脏兮兮的窗玻璃上晕开,像极了那个该死的 View Hierarchy 渲染不出高斯模糊的样子。

在新上海的地下避难所里,老王(Old Wang)吐出一口合成烟雾,盯着全息屏幕上不断报错的终端。作为人类反抗军里仅存的几位「精通 Apple 软件开发」的工程师之一,他负责给 AI 霸主「智核(The Core)」生成的垃圾代码擦屁股。

在这里插入图片描述

门被撞开了,年轻的女黑客莉亚(Liya)气喘吁吁地冲进来,手里攥着一块存满代码的神经晶片。“老王!救命!‘智核’生成的 SwiftUI 代码在 iOS 26 上又崩了!反抗军的通讯 App 根本跑不起来!”

老王冷笑一声,掐灭了烟头。“我就知道。那些被捧上神坛的 LLM(大型语言模型),不管是 Claude、Codex 还是 Gemini,写起 Python 来是把好手,但一碰到 Swift,就像是穿着溜冰鞋走钢丝——步步惊心。”

在本篇博文中,您将学到如下内容:

  • ☔️ 引子
    • 🤖 为什么 AI 总是在 Swift 上「鬼打墙」?
    • 🎨 1. 别再用过时的调色盘了
    • 📐 2. 只有切掉棱角,才能圆滑处世
    • 🔄 3. 监控变化,不要缺斤少两
    • 📑 4. 标签页的「指鹿为马」
    • 👆 5. 别什么都用「戳一戳」
    • 🧠 6. 扔掉旧时代的观察者
    • ☁️ 7. 数据的陷阱
    • 📉 8. 性能的隐形杀手
    • 🔠 9. 字体排印的法西斯
    • 🔗 10. 导航的死胡同
    • 🏷️ 11. 按钮的自我修养
    • 🔢 12. 数组的画蛇添足
    • 📂 13. 寻找文件的捷径
    • 🧭 14. 导航栈的改朝换代
    • 💤 15. 睡个好觉
    • 🧮 16. 格式化的艺术
    • 🏗️ 17. 不要把鸡蛋放在一个篮子里
    • 🖼️ 18. 渲染的新欢
    • 🏋️ 19. 字重的迷惑行为
    • 🚦 20. 并发的万金油(也是毒药)
    • 🎭 21. 主角光环是默认的
    • 📐 22. 几何的诅咒
  • 尾声:数字幽灵的低语

他把晶片插入接口,全息投影在空中展开。“坐下,莉亚。今天我就给你上一课,让你看看所谓的‘人工智能’是如何在 Swift 的并发地狱快速迭代中翻车的。”

在这里插入图片描述


🤖 为什么 AI 总是在 Swift 上「鬼打墙」?

老王指着屏幕上乱成一锅粥的代码说道:“这不怪它们。Swift 和 SwiftUI 的进化速度比变异病毒还快。再加上 Python 和 JavaScript 的训练数据浩如烟海,而 Swift 的高质量语料相对较少,AI 常常会产生幻觉。更别提 Swift 的 Concurrency(并发) 模型,连人类专家都头秃,更别说这些只会概率预测的傻大个了。”

在这里插入图片描述

“听着,莉亚,”老王严肃地说,“要想在 iOS 18 甚至更高版本的废土上生存,你必须学会识别这些‘智障操作’。我们不谈哲学,只谈生存。以下就是我从死人堆里总结出来的代码排雷指南。”


🎨 1. 别再用过时的调色盘了

💀 AI 的烂代码: foregroundColor() ✨ 老王的修正: foregroundStyle()

“看这里,”老王指着一行代码,“AI 还在用 foregroundColor()。这就像是还在用黑火药做炸弹。虽然字数一样,但前者已经是个行将就木的Deprecated API。把它换成 foregroundStyle()!后者才是未来,它支持渐变(Gradients)等高级特性。别让你的 UI 看起来像上个世纪的产物。”

在这里插入图片描述

📐 2. 只有切掉棱角,才能圆滑处世

💀 AI 的烂代码: cornerRadius() ✨ 老王的修正: clipShape(.rect(cornerRadius:))

“又是一个老古董。cornerRadius() 早就该进博物馆了。现在的标准是使用 clipShape(.rect(cornerRadius:))。为什么?因为前者是傻瓜式圆角,后者能让你通过 uneven rounded rectangles(不规则圆角矩形)玩出花来。在这个看脸的世界,细节决定成败。”

🔄 3. 监控变化,不要缺斤少两

💀 AI 的烂代码: onChange(of: value) { ... } (单参数版本) ✨ 老王的修正: onChange(of: value) { oldValue, newValue in ... }

老王皱起眉头:“这个 onChange 修改器,AI 经常只给一个参数闭包。这在旧版本是‘不安全’的,现在已经被标记为弃用。要么不传参,要么接受两个参数(新旧值)。别搞得不清不楚的,容易出人命。”

在这里插入图片描述

📑 4. 标签页的「指鹿为马」

💀 AI 的烂代码: tabItem() ✨ 老王的修正: 新的 Tab API

“如果看到老旧的 tabItem(),立刻把它换成新的 Tab API。这不仅仅是为了所谓的‘类型安全(Type-safe)’,更是为了适配未来——比如那个传闻中的 iOS 26 搜索标签页设计。我们要领先‘智核’一步,懂吗?”

👆 5. 别什么都用「戳一戳」

💀 AI 的烂代码: 滥用 onTapGesture() ✨ 老王的修正: 使用真正的 Button

“AI 似乎觉得万物皆可 onTapGesture()。大错特错!除非你需要知道点击的具体坐标或者点击次数,否则统统给我换成标准的 Button。这不仅是为了让 VoiceOver(旁白)用户能活下去,也是为了让 visionOS 上的眼球追踪能正常工作。别做一个对残障人士不友好的混蛋。”

🧠 6. 扔掉旧时代的观察者

💀 AI 的烂代码: ObservableObject ✨ 老王的修正: @Observable

“莉亚,看着我的眼睛。除非你对 Combine 框架有什么特殊的各种癖好,否则把所有的 ObservableObject 都扔进焚化炉,换成 @Observable 宏。代码更少,速度更快,这就好比从燃油车换成了核动力战车。”

在这里插入图片描述

☁️ 7. 数据的陷阱

💀 AI 的烂代码: SwiftData 模型中的 @Attribute(.unique) ✨ 老王的修正: 小心使用!

“这是一个隐蔽的雷区。如果在 SwiftData 模型定义里看到 @Attribute(.unique),你要警惕——这玩意儿跟 CloudKit 八字不合。别到时候数据同步失败,你还在那儿傻乎乎地查网络连接。”

📉 8. 性能的隐形杀手

💀 AI 的烂代码: 将视图拆分为「计算属性(Computed Properties)」 ✨ 老王的修正: 拆分为独立的 SwiftUI Views

“为了图省事,AI 喜欢把大段的 UI 代码塞进计算属性里。这是尸位素餐!尤其是在使用 @Observable 时,计算属性无法享受智能视图失效(View Invalidation)的优化。把它们拆分成独立的 SwiftUI 结构体!虽然麻烦点,但为了那 60fps 的流畅度,值得。”

🔠 9. 字体排印的法西斯

💀 AI 的烂代码: .font(.system(size: 14)) ✨ 老王的修正: Dynamic Type (动态字体)

“有些 LLM(尤其是那个叫 Claude 的家伙)简直就是字体界的独裁者,总喜欢强行指定 .font(.system(size: ...))。给我搜出这些毒瘤,全部换成 Dynamic Type。如果是 iOS 26+,你可以用 .font(.body.scaled(by: 1.5))。记住,用户可能眼花,别让他们看瞎了。”

在这里插入图片描述

🔗 10. 导航的死胡同

💀 AI 的烂代码: 列表里的内联 NavigationLink ✨ 老王的修正: navigationDestination(for:)

“在 List 里直接写 NavigationLink 的目标地址?那是原始人的做法。现在的文明人使用 navigationDestination(for:)。解耦!解耦懂不懂?别把地图画在脚底板上。”


老王喝了一口已经凉透的咖啡,继续在这堆赛博垃圾中挖掘。

🏷️ 11. 按钮的自我修养

💀 AI 的烂代码:Label 做按钮内容 ✨ 老王的修正: 内联 API Button("Title", systemImage: "plus", action: ...)

“期待看到 AI 用 Label 甚至纯 Image 来做按钮内容吧——这对 VoiceOver 用户来说简直是灾难。用新的内联 API:Button("Tap me", systemImage: "plus", action: whatever)。简单,粗暴,有效。”

🔢 12. 数组的画蛇添足

💀 AI 的烂代码: ForEach(Array(x.enumerated()), ...) ✨ 老王的修正: ForEach(x.enumerated(), ...)

“看到这个 Array(x.enumerated()) 了吗?这就是脱裤子放屁。直接用 ForEach(x.enumerated(), ...) 就行了。省点内存吧,虽然现在的内存不值钱,但程序员的尊严值钱。”

在这里插入图片描述

📂 13. 寻找文件的捷径

💀 AI 的烂代码: 冗长的文件路径查找代码 ✨ 老王的修正: URL.documentsDirectory

“那些又臭又长的查找 Document 目录的代码,统统删掉。换成 URL.documentsDirectory。一行代码能解决的事,绝不写十行。”

🧭 14. 导航栈的改朝换代

💀 AI 的烂代码: NavigationView ✨ 老王的修正: NavigationStack

NavigationView 已经死了,有事烧纸。除非你要支持 iOS 15 那个上古版本,否则全部换成 NavigationStack。”

💤 15. 睡个好觉

💀 AI 的烂代码: Task.sleep(nanoseconds:) ✨ 老王的修正: Task.sleep(for: .seconds(1))

“‘智核’ 似乎很喜欢纳秒,可能它觉得自己算得快。但你要用 Task.sleep(for:),配合 .seconds(1) 这种人类能读懂的单位。别再像个僵尸一样数纳秒了。”

在这里插入图片描述

🧮 16. 格式化的艺术

💀 AI 的烂代码: C 风格格式化 String(format: "%.2f", ...) ✨ 老王的修正: Swift 原生格式化 .formatted()

“我知道 C 风格的字符串格式化很经典,但它不安全。把它换成 Swift 原生的 Text(abs(change), format: .number.precision(.fractionLength(2)))。虽然写起来长一点,但它像穿了防弹衣一样安全。”

🏗️ 17. 不要把鸡蛋放在一个篮子里

💀 AI 的烂代码: 单个文件塞入大量类型 ✨ 老王的修正: 拆分文件

“AI 喜欢把几十个 struct 和 class 塞进一个文件里,这简直是编译时间毁灭者。拆开它们!除非你想在编译的时候有时间去煮个满汉全席。”

🖼️ 18. 渲染的新欢

💀 AI 的烂代码: UIGraphicsImageRenderer ✨ 老王的修正: ImageRenderer

“如果你在渲染 SwiftUI 视图,别再用 UIKit 时代的 UIGraphicsImageRenderer 了。拥抱 ImageRenderer 吧,这是它的主场。”

在这里插入图片描述

🏋️ 19. 字重的迷惑行为

💀 AI 的烂代码: 滥用 fontWeight() ✨ 老王的修正: 区分 bold()fontWeight(.bold)

“三大 AI 巨头都喜欢滥用 fontWeight()。记住,fontWeight(.bold)bold() 渲染出来的结果未必一样。这就像‘微胖’和‘壮实’的区别,微妙但重要。”

🚦 20. 并发的万金油(也是毒药)

💀 AI 的烂代码: DispatchQueue.main.async ✨ 老王的修正: 现代并发模型

“一旦 AI 遇到并发问题,它就会像受惊的鸵鸟一样把头埋进 DispatchQueue.main.async 里。这是不可原谅的懒惰!那是旧时代的创可贴,现在的我们有更优雅的 Actor 模型。”

🎭 21. 主角光环是默认的

💀 AI 的烂代码: 到处加 @MainActor ✨ 老王的修正: 默认开启

“如果你在写新 App,Main Actor 隔离通常是默认开启的。不用像贴符咒一样到处贴 @MainActor。”

在这里插入图片描述

📐 22. 几何的诅咒

💀 AI 的烂代码: GeometryReader + 固定 Frame ✨ 老王的修正: visualEffect()containerRelativeFrame()

“最后,也是最可怕的——GeometryReader。天哪,AI 对这玩意儿简直是真爱,还喜欢配合固定尺寸的 Frame 使用。这是布局界的核武器,一炸毁所有。试着用 visualEffect() 或者 containerRelativeFrame() 来代替。别做那个破坏布局流的罪人。”


尾声:数字幽灵的低语

老王敲下最后一个回车键,全息屏幕上的红色报错瞬间变成了令人愉悦的绿色构建成功提示。

// Human-verified Code
// Status: Compiling... Success.
// Fixed by: The Refiners (Old Wang & Liya)

“搞定。” 老王瘫坐在椅子上,听着窗外雨声渐大。

在这里插入图片描述

莉亚看着完美运行的 App,眼中闪烁着崇拜的光芒:“老王,你简直是神!既然我们能修复这些代码,为什么 AI 还是会不断地生成这种垃圾?”

老王点燃了最后一支烟,看着烟雾在霓虹灯下缭绕。“因为 AI 会产生幻觉(Hallucinations)。它们会编造出看起来很美、名字很像样,但实际上根本不存在的 API。这就像是在数字世界里见鬼了一样。”

在这里插入图片描述

他转过头,意味深长地看着莉亚:“对此,我也无能为力。我只能修补已知的错误,却无法预测未知的疯狂。”

“那么,”老王把目光投向了屏幕前的你——第四面墙之外的观察者,“轮到你了。在你的赛博探险中,通常会在 AI 生成的代码里发现什么‘惊喜’?

在这里插入图片描述

如果你还活着,请在评论区告诉我们。毕竟,在这场人机大战中,知识是我们唯一的武器。

那么,感谢观赏,再会啦!8-)

Swift 6.2 列传(第十五篇):王语嫣的《万剑归宗》与 InlineArray

2026年1月4日 11:24

在这里插入图片描述

摘要:当动态数组的随性(Array)与元组的刻板(Tuple)陷入两难,高性能的内联数组(InlineArray)横空出世。Swift 6.2 引入的 SE-0453 就像是武学中的“万剑归宗”,旨在以固定大小的内存布局,解决性能与灵活性的鱼和熊掌不可兼得之困。

0️⃣ 🐼 序章:琅嬛福地的“内存”迷局

琅嬛福地,天山童姥遗留的虚拟数据中心。

这里是存储着天下所有数据结构秘籍的宝库。大熊猫侯佩穿梭在巨大的全息卷轴之间,背景音乐是《天龙八部》的BGM,他一边走一边摸了摸头顶——黑毛油光锃亮,头绝对不秃,安全感十足。

他之所以来这,是因为上一回任务结束后,他仍对竹笋的存储问题耿耿于怀。

“我那四根‘镇山之宝’级竹笋,必须以最快的速度取用!”侯佩对着一卷写着 Array 的秘籍大声嚷道,“用普通的 Array,虽然方便,但那动态分配内存的方式,让我每次取笋都感觉像是在丐帮的袋子里掏东西,随性得很,但太慢了!

“若追求极致速度,何不用 Tuple(元组)?”

在这里插入图片描述

一个清冷的声音传来。侯佩循声望去,只见一位容貌比数据流还精致的少女站在光影之中,她皓齿明眸,手中正拿着一本《九阴真经》的代码版本,正是王语嫣

王语嫣,熟读天下武学秘籍,对各种数据结构了如指掌。她的爱好就是整理和分类这些代码秘籍,特点是理论知识丰富到可以开宗立派,但从未亲手实践(编写)过一行代码

“王姑娘!”侯佩的眼睛瞬间变成了心形(花痴属性发作),“元组是快,它内存连续且固定,但您看,我如果要用下标循环遍历我的四根竹笋,myTuple.0myTuple.1……这写法简直是望洋兴叹,既不优雅,又不支持循环!”

在本次冒险中,您将学到如下内容:

  • 0️⃣ 🐼 序章:琅嬛福地的“内存”迷局
  • 1️⃣ 💥 混血秘籍:InlineArray 的诞生 (SE-0453)
  • 2️⃣ 📜 固若金汤:InlineArray 的使用法门
    • 招式一:明确指定大小与类型
    • 招式二:让编译器“心领神会”
    • 下标读写:行云流水
  • 3️⃣ 🚧 乾坤已定:固定大小的代价
    • 迭代之困:不入流的限制
  • 4️⃣ 🐼 熊猫的黑色幽默与抉择
  • 5️⃣ 🛑 尾声:慕容复的伪装与“向后看”的难题

王语嫣轻轻叹了一口气:“是啊,鱼和熊掌不可兼得。内存布局(Performance)和下标访问(Usability),历来是程序员江湖的千年难题。”

在这里插入图片描述


1️⃣ 💥 混血秘籍:InlineArray 的诞生 (SE-0453)

就在侯佩和王语嫣陷入技术哲学的死循环时,一道新的全息卷轴从天而降,正是 SE-0453 秘籍。

“快看,这是最新的‘混血’数据结构,”侯佩激动地喊道,“它叫 InlineArray(内联数组),它把元组的‘固定大小’与数组的‘自然下标’完美地融合了!”

InlineArray 最核心的奥义在于:它将固定数量的元素直接存储在结构体内部,没有动态分配的开销,从而实现了结构体级别的内存连续性媲美 C 语言数组的存取速度

💡 前置条件(SE-0452): 要实现这种固定大小的泛型(Generic),Swift 6.2 还必须引入另一个重要的前提:SE-0452:Integer Generic Parameters(整数泛型参数)。这使得我们可以用一个整数值来约束泛型类型的大小,比如 InlineArray<4, String> 中的 4,这在以前的 Swift 版本中是无法想象的。

在这里插入图片描述

2️⃣ 📜 固若金汤:InlineArray 的使用法门

王语嫣作为理论大师,立刻解析了这段秘籍。

创建 InlineArray 有两种法门:

招式一:明确指定大小与类型

我们可以像使用泛型一样,明确告知编译器:“我要一个固定大小为 4String 类型数组。”

// 明确告诉编译器:我要一个固定大小为 4 的 String 数组
var names1: InlineArray<4, String> = ["Moon", "Mercury", "Mars", "Tuxedo Mask"]

在这里插入图片描述

招式二:让编译器“心领神会”

侯佩这种懒人当然更喜欢让编译器自己推断(Type Inference)大小。只要传入的元素数量和类型固定,编译器就能自动搞定。

// 编译器会根据传入的 4 个 String,自动推断出它是 InlineArray<4, String>
var names2: InlineArray = ["Moon", "Mercury", "Mars", "Tuxedo Mask"]

“太完美了!”侯佩赞叹道,“这就像是把我的四根竹笋严丝合缝地放进了四个精确尺寸的格子,一劳永逸,再也不用担心内存跳来跳去了。”

下标读写:行云流水

虽然它内存布局像元组,但使用起来却和数组一样,支持直观的下标读写:

// 读取:就像普通的数组一样
print(names1[0]) // 输出: Moon
// 写入:轻松修改特定位置的元素
names1[2] = "Jupiter" // 火星变木星,改写数据,毫不费力

在这里插入图片描述

3️⃣ 🚧 乾坤已定:固定大小的代价

王语嫣很快指出了这种“神功”的限制:“侯大哥,此功法虽然内力雄厚(性能卓越),但限制也多。既然是固定大小,那么它就失去了数组的动态伸缩性。”

在这里插入图片描述

侯佩一听,赶紧问道:“那是不是不能再多塞一根竹笋进去了?”

王语嫣点头:“正是。InlineArray 没有 append()remove(at:) 方法。 它的容量在诞生之初就已是天数,无法更改。”

迭代之困:不入流的限制

更让人头疼的是它的“不入流”限制:

🚨 技术哲学InlineArray 不兼容传统的 Sequence(序列)和 Collection(集合)协议。

“为什么?”侯佩不解,“它不是数组吗?”

在这里插入图片描述

“因为它的设计目标是极致性能和编译时确定性。”王语嫣解释道,“为了避免遵循这些协议可能带来的抽象层开销,它选择‘自绝经脉’。如果你想遍历它,你必须通过它的 indices 属性,配合下标访问来实现。”

// 侯佩:虽然不方便,但为了性能,忍了!
for i in names1.indices {
    // 必须通过索引 i 来访问,不能直接用 for element in names1
    print("Hello, \(names1[i])!") 
}

侯佩总结道:“这就像是说,虽然它是武林高手,但它拒绝参加武林大会(不遵循 Collection 协议),如果你想请教它,必须先拿到它的拜帖(indices)才行。”

在这里插入图片描述

4️⃣ 🐼 熊猫的黑色幽默与抉择

“哎呀,这世道,连数据结构都得看颜值和出身。”侯佩叹了口气,把竹笋收进了虚拟的 InlineArray 容器里,感觉身轻如燕,连走路都带风了。

“不过话说回来,”侯佩看向王语嫣,“我还是觉得这种硬编码的语法有点不够‘熊猫化’(不够懒)。听说社区里有人想搞个更直观的语法?”

在这里插入图片描述

王语嫣提起了一件江湖轶事(SE-0483):

插曲:夭折的提案: “有一个叫做 SE-0483 的提议,想要引入类似 var names: [5 x String] = .init(repeating: "Anonymous") 的简洁语法,来表示一个固定包含 5 个 String 的数组。但由于反馈意见认为它过于突兀且不够 Swift 风格,目前已被‘打回重修’。”

侯佩嘿嘿一笑:“果然,任何新秘籍的推广,都会遇到‘保守派’的阻力。不过,能用,速度快,头不秃,对我来说就够了。”

在这里插入图片描述

5️⃣ 🛑 尾声:慕容复的伪装与“向后看”的难题

就在侯佩沉浸在高性能竹笋容器的喜悦中时,王语嫣突然脸色大变。

“侯大哥!我刚才在整理慕容复留下的数据卷轴时,发现了一个惊天的秘密!”

她指着屏幕上的一段文本,那是一篇关于“兴复大燕”的宏大计划书。

在这里插入图片描述

“我想用 Regex(正则表达式) 查找卷轴中所有提到他名字的地方。但是,我不想要匹配到那些他用来伪装自己身份的称呼,比如‘公冶乾’、‘包不同’这些名字后面的‘慕容复’。”王语嫣急道,“我只想匹配到那些,前面紧跟着‘我的挚爱’这四个字的‘慕容复’!”

侯佩挠了挠头:“你的意思是,你想找到一个模式,但这个模式必须满足它前面有一个特定的前置条件,而这个前置条件本身,又不被纳入匹配结果?”

在这里插入图片描述

“对!”王语嫣焦急万分,“我的 Regex 功夫只能‘向前看’(Lookahead),却无法完美地**‘向后看’**,我不能确定文本中这三个字前面是不是真的有‘我的挚爱’。”

侯佩望着卷轴深处那段充满秘密的代码,神秘地一笑:“王姑娘,你不用再对着旧秘籍望洋兴叹了。下一章,Swift 6.2 就要教我们一招绝顶的侦查武功:Regex lookbehind assertions(正则表达式向后查找断言)!”

在这里插入图片描述

(欲知后事如何,且看下回分解:Regex lookbehind assertions —— 如何在不匹配前文的情况下,精确判断前文的存在性,找到王语嫣真正的“挚爱”。)

❌
❌