news 2026/10/7 3:52:55

iOS上架防相似审核:IPA结构调整与资源指纹变化实操指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iOS上架防相似审核:IPA结构调整与资源指纹变化实操指南

做 iOS 应用上架和长期迭代的都知道,App Store 对重复应用、相似应用的审核力度这几年一直没松过。尤其是同一个团队做出多个功能相近的产品、同一套源码出不同区域版本的时候,很容易被审核侧判定为“应用相似度过高”而拒绝上架。这个场景下,“IPA 结构调整”和“资源指纹变化”就成了绕不开的关键词。

简单说,IPA 结构调整是对安装包内部的文件布局、配置信息、目录组织做合理范围内的改动;资源指纹变化则是让包内的图片、音频、配置等资源在保留原有视觉效果和功能的前提下,产生有区分度的特征码变化。两者配合,能有效降低应用被误判为“重复提交”或“相似马甲包”的概率。这篇文章我会从相似度检测的原理讲起,拆解 IPA 结构和资源指纹的实操方案,再给出一整套从解包、改包到重签验证的完整流程。适合正在做 iOS 多形态产品、长期多次上架,或者需要维护多地区版本的开发者和负责应用分发运营的同学参考。

1. 先搞清楚状况:相似度检测到底在看什么

很多人拿到拒审邮件,第一反应是“那我换个图标、改个名字再传一次”。结果往往是被打回得更快,甚至直接触发更严格的审核标记。要解决相似度问题,第一步不是动手改包,而是先理解审核侧和自动查重系统到底在比对哪些维度。

1.1 检测侧常见的四层比对维度

我把这些年观察到的相似度检测逻辑总结成四层,每一层对应包内的不同部分:

检测层级比对对象典型特征检测难度
元数据层Bundle ID、App 名称、版本号、开发者账号、关键词字符串完全一致或高度相似低
资源层图片、音频、视频、本地化文本、配置文件文件哈希一致、图像感知哈希接近中
结构层Zip 目录结构、文件命名、文件大小、文件数量目录树高度相似、可执行文件名一致中
二进制层Mach-O 可执行文件的代码段、字符串、符号表、类名代码逻辑相似、类名方法名一致高

元数据层最容易理解,也是很多人已经会改的部分。但资源层和结构层才是“换皮”方案真正失效的地方。说到底,自动查重系统不是在“看”你的应用长什么样,而是在“算”你的应用指纹。

1.2 为什么改个名字换套图根本没用

这里要引入一个概念:文件指纹。任何一个文件,只要内容确定,通过 MD5、SHA-1 或 SHA-256 计算,就能得到一个固定长度的特征值。这个特征值就像指纹一样,理论上内容不同则指纹不同,内容相同则指纹必然相同。而且哈希算法有一个特性:哪怕文件里只有一个字节发生变化,整个哈希值也会彻底变成另一个完全不同的值。

听起来似乎很容易“欺骗”检测系统?只要随便改一个字节,哈希不就变了吗?问题恰恰出在“资源指纹”不只看精确哈希上。

真实的查重系统对图片资源通常会抽取感知哈希(Perceptual Hash)。感知哈希的核心思路是把图片缩小到固定尺寸、转换成灰度图、计算离散余弦变换的低频分量,最后编码成一串指纹。肉眼看起来相同的图片、经过不同压缩级别处理的同一张原图、只是改了文件名的同一张图,它们的感知哈希会非常接近甚至完全一致。

这就是为什么“把 icon.png 改成 icon2.png”毫无意义,因为文件名变了,文件内容没变,精确哈希和感知哈希一个都没变。同理,把图片重新压缩保存一次,精确哈希确实变了,但如果只是单纯改了下压缩率,感知哈希依然可能保持不变,检测系统一样能把你捞出来。

所以真正的资源指纹变化,必须同时满足两个条件:文件级哈希要变,感知级特征也要有足够差异。这就是后面第三节要讲的重点,这里先记下这个判断标准。

2. 从解剖 IPA 开始:结构调整能动的区域

IPA 结构这个词听起来玄乎,其实它就是一个特殊格式的 Zip 压缩包。理解它的组成方式,才知道哪些地方能动、哪些地方不能碰。

2.1 标准 IPA 长什么样

把 IPA 下载下来,用 unzip 解包,你会看到这样一个目录:

MyApp.ipa └── Payload └── MyApp.app ├── MyApp # Mach-O 可执行文件 ├── Info.plist # 应用配置信息 ├── Assets.car # 编译后的资源目录 ├── embedded.mobileprovision # 描述文件 ├── _CodeSignature │ └── CodeResources # 代码签名资源 ├── Frameworks # 动态库目录 ├── Base.lproj # 本地化资源 └── 各种资源文件...

这里最容易忽略的是签名体系。iOS 要求所有文件都被代码签名覆盖,签名信息记录在 _CodeSignature/CodeResources 里。也就是说,你往包里添加任何一个文件、修改任何一个资源,都会破坏签名完整性。不改签名直接安装,设备会直接拒绝启动。

结构层能被“调整”的区域,我从实际操作角度分成了三类,下面逐个说。

2.2 能动的结构点:配置、目录和占位文件

第一类:Info.plist 配置字段。这里不是让你瞎改 Bundle ID,那个改了包就变新应用了,反而容易引发其他问题。可动的是展示层面的配置,比如 CFBundleDisplayName(桌面显示名)、CFBundleShortVersionString 的展示文案、权限描述文案(NSCameraUsageDescription 这类),以及 URL Scheme 相关描述。修改这些字段会改变整个 plist 文件的哈希,但不会影响程序功能。

第二类:目录层级调整。所谓“调整”不是把系统要求的目录改掉,而是在不破坏 Framework 加载路径、资源搜索路径的前提下,通过新增子目录、合理移动非关键资源的位置,让目录树整体形态发生变化。比如把一部分音频资源移到子目录再在代码里调整加载路径,或者把原本散落在根目录的配置文件收拢进子目录。

第三类:插入合法占位文件。很多结构查重会统计文件数量、文件名列表、目录树结构。往包里加几个明确不参与运行但内容无害的文件(比如版本说明、合规声明、渠道标识文本),也能让结构特征产生差异。这里的核心原则是:新增的文件内容必须无害、不影响启动流程,且能被代码签名机制正确覆盖。

2.3 代码签名:所有改动的前提

一个新手最容易踩的坑,是改完文件后直接尝试安装,然后发现设备提示“无法安装应用”。原因很简单:iOS 对签名的校验粒度是“包内每一个文件”。任何文件变动,哪怕只是往资源目录里多放了一个 1KB 的 txt,都会让签名校验失败。

所以整个处理流程的正确顺序是:解包——调整结构——修改资源——重新计算签名——安装验证。

重签的基本命令如下,我会在第四节完整流程里再细化:

codesign --force --deep --sign "iPhone Developer: Your Name (XXXXXX)" Payload/MyApp.app

到这里,结构调整部分的基本盘就有了。接下来是重头戏:资源指纹变化。

3. 资源指纹变化:理论与实操

这一节是整个方案的核心。资源指纹变化的技术含量,不在于你会用几个命令,而在于你理解“指纹”到底有几层含义,以及每层都应该怎么去动。

3.1 精确指纹和感知指纹的区别

先做一个对照表,把两个概念彻底区分开:

指纹类型计算对象特点典型算法
精确指纹文件原始字节一字节变化,整个指纹完全变化MD5、SHA-1、SHA-256
感知指纹图片/音频的内容特征相似内容会得到相近指纹pHash、dHash、pHash for audio

精确指纹的作用是确认“文件完全相同”,感知指纹的作用是判断“文件是否视觉上相似”。

苹果生态内部分析工具以及对包做比对的第三方检测服务,通常会先用感知哈希做一轮粗筛,排除掉大量完全一样的资源;再对疑似相似资源计算精确哈希做二次确认。因此在改资源时不能被动地只改一边,必须两边同时拉开距离。

3.2 图片资源改造:从重压缩到像素微调

先说图片。iOS 应用里的图片资源,绝大多数都是 PNG。这里有一个 iOS 平台特有的坑:Xcode 在编译安装包时,会把 PNG 图片转成 Apple 私有的 PNG 格式,也就是带 CgBI chunk(Apple 私有块)的变种格式。这就是为什么从 IPA 里解出来的 PNG 用普通 Windows 看图软件打不开,或者显示成奇怪的绿色。

针对这种情况,第一道工序是“还原”成标准 PNG 再处理,或者在还原后的标准 PNG 上进行改动。

我常用的处理思路是这样:

第一步,用 sips 命令把 CgBI PNG 先转成标准 PNG 或 JPEG 后再操作:

sips -s format png input.png --out output.png

第二步,用 Python PIL 做像素级微调。微调不是让你肉眼能看出来,而是让图像内容的数字化特征产生足够差异。常见做法包括整体亮度的微小偏移、RGB 通道的微弱增益变化、或者给图片内容加一个几乎不可见的渐变层。

from PIL import Image, ImageEnhance def tweak_image(src, dst, brightness_delta=0.01): img = Image.open(src).convert("RGB") enhancer = ImageEnhance.Brightness(img) img = enhancer.enhance(1.0 + brightness_delta) # 对红绿蓝通道分别做微小增益调整 r, g, b = img.split() r = r.point(lambda v: max(0, min(255, int(v * 1.01)))) img = Image.merge("RGB", (r, g, b)) img.save(dst, "PNG")

这个脚本的核心思想是:让图像文件的每一个字节都产生变化,同时不影响肉眼观感。亮度偏移控制在 1% 左右,通道增益控制在 1% 左右,这个量级下用户完全察觉不到差异,但感知哈希会明显改变。

第三步,重新编码。即使像素值完全不变,仅仅把 PNG 的压缩级别从默认值改成不同的级别,也能让文件级精确哈希发生变化。最稳的做法是连像素也一起微调,这样对精确哈希和感知哈希都有改动。

3.3 音频、视频和文本类资源的处理

图片之外,包内资源主要还有音频、视频、plist/json 配置文件、本地化字符串。

音频的处理逻辑跟图片类似,不能只是把一个音频文件复制一份改个后缀,而要真正改变音频编码数据。我个人常用 ffmpeg 做无感重编码,思路是调整码率或采样率后强制重新编码,必要时在音频尾部追加一段静音节。追加静音节很重要,因为它的存在会彻底改变音频文件的数据长度和哈希,而用户播放体验几乎不受影响。

ffmpeg -i input.wav -codec:a libmp3lame -b:a 64k -af "apad=pad_dur=0.5" output.mp3

视频资源的处理类似。如果包内带视频文件,比如开机动画、引导页视频,最简单的做法是用 ffmpeg 重新编码,微调码率和帧率,或者用无操作的滤镜链强制重新编码:

ffmpeg -i input.mp4 -vf "eq=contrast=1.01:saturation=1.01" -c:a aac -b:a 96k output.mp4

等于是给视频数据做了一次整体洗牌,画面观感不变,但指纹完全不同。

文本类资源的处理方式有些不同。Localizable.strings 和各类 plist 文件,要处理的是键名、键值排序和空白字符。比如 plist 里的键值顺序调整一下、字符串里的换行符和空格做细微变化,都会让文件哈希产生变化。这里有一个安全原则:只处理不影响代码逻辑的显示文本和配置注释,不要在核心变量名上乱动,否则容易出现文案丢失或配置失效。

3.4 Assets.car 的特殊处理

很多开发者会在这里卡壳:解包后发现图片不散落在目录里,全在一个 Assets.car 文件里。Assets.car 是 Xcode 编译 Asset Catalog 后生成的二进制资源容器,不能用普通的 zip 解压方式取得里面的图片。直接用二进制工具去改 Assets.car 风险极高,因为它的内部表结构有校验机制,改坏了直接导致应用启动时资源加载失败。

面对 Assets.car,我通常建议两条路线。一条是放弃 Assets.car 内的资源,在改动后用 Xcode 重新构建生成新的 Assets.car;另一条是用开源的 cartool 工具把 Assets.car 里的图提取出来,微调完图片后,再重新通过 Xcode 工程打包生成新的 Assets.car。这里没有捷径,凡是涉及 Assets.car 的修改,老老实实走 Xcode 重编译流程最稳妥。

换句话说,如果你手上的基础工程还能用 Xcode 正常编译,最优策略是把“资源指纹变化”下沉到工程层面,在源图片上做微调再重新出包;如果你只能拿到一个已经打好的 IPA 而拿不到工程,那就要优先处理散落在 Bundle 目录里的那些资源,Assets.car 能不动就不动。

4. 实操流程:从一份 IPA 到指纹可验证的新包

这部分我把整个流程完整走一遍,从环境准备到最终验证,每一步都给出可复制执行的命令和判断标准。我会以“手头只有已经打包好的 IPA,没有 Xcode 工程”的场景为例,因为这是难度更高、也更常见的情况。

4.1 准备环境

操作环境建议在 macOS 上完成,因为重签和真机验证都需要 macOS 生态自带的工具链。需要准备的东西包括:

  • Xcode 及 Command Line Tools(提供 codesign、plutil 等命令)
  • Homebrew(用于安装 ffmpeg、Python 等工具)
  • 对应的 iOS Development 证书和描述文件(重签必须要用,最好是用自己开发者账号下的证书)
  • 原始 IPA 文件的备份

建议正式开干前先跑一条验证命令,确认签名工具可用:

xcrun --show-build-version codesign --version

4.2 解包与摸底

先把 IPA 解包,并生成一份资源指纹基线清单:

mkdir ipa_working && cd ipa_working unzip ../MyApp.ipa cd Payload/MyApp.app find . -type f -exec md5 {} \; > ../../baseline_fingerprint.txt wc -l ../../baseline_fingerprint.txt

这份基线清单里记录了当前包内每一个文件的 MD5 值,是整个改包工作的“对照组”。后面所有改动是否有效,都要以这份清单为基准做对比。

同时要把目录树结构导出来,结构层改完以后,这份目录树也要对比:

find . -print | sort > ../../baseline_structure.txt

4.3 结构层调整实战

先改动 Info.plist 中安全的展示字段。用 plutil 直接操作:

plutil -replace CFBundleDisplayName -string "你的新展示名" Info.plist plutil -replace NSCameraUsageDescription -string "用于扫描功能以提升输入效率" Info.plist

注意,不要动 CFBundleIdentifier、CFBundleExecutable 这类关键字段,改了容易出大问题。

然后增加合法的占位文件。我推荐的做法是新建一个目录,放一两份内容完全无害的说明文本,比如合规声明、SDK 列表说明。这里有个细节:占位文件的文件名不能是中文或包含特殊字符,用纯英文小写加下划线最稳妥。

mkdir -p AdditionalInfo echo "App version compliance statement" > AdditionalInfo/ComplianceNote.txt printf "This archive contains app resources for distribution.\n" > AdditionalInfo/DistributionInfo.txt

再到 Zip 归档层面做一点操作:给最终 IPA 包加一段 zip 注释。这样至少能在“包级哈希”维度制造差异,和内部文件哈希相比,这是比较容易被忽略但确实有效的补充手段:

zip -z ../MyApp_resigned.ipa <<< "Distribution channel: stable build channel"

完整流程里,这一步要放在最后封装 IPA 时执行。

4.4 资源指纹变化脚本执行

对散落在 Bundle 目录里的 PNG 图片,批量执行微调脚本。这里用 Python 脚本处理,具体见 3.2 节的示例。实际操作时,我会对脚本做一点扩展:让图片在微调后统一输出到 MEDIAResources 目录下,方便与原文件区分。

import os from PIL import Image src_root = "." dst_root = "../tweaked_media" os.makedirs(dst_root, exist_ok=True) for root, dirs, files in os.walk(src_root): for name in files: if name.lower().endswith(".png"): src_path = os.path.join(root, name) dst_path = os.path.join(dst_root, name) tweak_image(src_path, dst_path, brightness_delta=0.012) print(f"tweaked: {src_path} -> {dst_path}")

脚本跑完之后,把微调后的图片覆盖回原路径:

cp -f ../tweaked_media/*.png 对应路径

音频和视频文件用 ffmpeg 批量处理,命令和参数参考 3.3 节的写法。处理完后记得用 file 命令或 md5 检查一下,确认文件确实被重编码了。

4.5 重签名操作

资源改动完成之后,进入最容易出问题的环节:重签名。这里我列一套完整的命令流程,每一步都不要漏。

先处理动态库签名,再处理主 App 签名。动态库是独立签名单元,只签主 App 不签动态库会导致启动时崩溃:

find Payload/MyApp.app/Frameworks -name "*.dylib" -exec codesign --force --sign "证书名" {} \; codesign --force --deep --sign "iPhone Developer: Your Name (XXXXXX)" Payload/MyApp.app

描述文件要放到 Payload/MyApp.app/embedded.mobileprovision 路径下,替换掉原来的文件。如果签名证书和描述文件不匹配,后续验证环节会直接暴露问题。

签名完成后,可以先用 codesign 验证一下:

codesign --verify --deep --strict Payload/MyApp.app

看到“valid on disk”和“satisfies its Designated Requirement”两行输出,基本就稳了。

4.6 重新封装和验证

最后重新打包成 IPA:

zip -qr ../MyApp_resigned.ipa Payload/

然后对新的 IPA 做一次完整的指纹复检,和基线文件对比差异:

cd Payload/MyApp.app find . -type f -exec md5 {} \; > ../../final_fingerprint.txt diff ../../baseline_fingerprint.txt ../../final_fingerprint.txt

diff 输出里能看到大量文件的 MD5 都变化了。这里我给自己定的最低标准是:原包和现包的资源层指纹必须有超过 60% 的文件发生哈希变化,并且所有图片的感知哈希差异不能为零。如果差异比例太低,说明改动量不够,需要回头加强资源层的处理力度。

验证阶段不要只在模拟器上跑。模拟器对签名的校验远没有真机严格,很多签名问题在模拟器上根本不会暴露。找一台测试真机,把新包通过分发渠道装上去,跑一遍核心路径,确认启动正常、登录正常、主要页面图片加载正常。

5. 常见踩坑和排查方法

这个流程我实操过很多次,踩过的坑比教程里写的多得多。这里整理一份高频问题速查表,帮后来人少走弯路。

症状大概率原因解决办法
安装后提示“无法安装应用”描述文件和证书不匹配,或签名不完整检查 embedded.mobileprovision 是否对应当前证书;重新执行 4.5 全流程
启动闪退,报错 dyld: Library not loadedFrameworks 里的动态库没有单独重签对所有 dylib 和 framework 执行 codesign
部分图片显示异常微调图片时颜色模式或通道处理出错调整脚本,把图片统一转换成 RGB 模式再处理
中文文案变成英文或丢失本地化字符串修改时破坏了 key-value 对应关系检查 Localizable.strings 的编码,确保 UTF-8 且键名未被改动
包体积显著膨胀占位文件放太多、重编码音频码率不降反升占位文件总大小控制在几百 KB 内;音频重编码时明确指定较低码率
上传后仍被判定相似只改了元数据和少量文件名,资源层感知哈希变化不足加大图片像素微调力度;重新处理音频视频;检查 Assets.car 是否完全没动

5.1 重签后安装闪退的两种典型场景

第一种是“只签主 App,不签动态库”。如果你的包里有 Frameworks 目录,里面有 .dylib 或者 .framework,这些属于独立的签名单元。iOS 在启动加载动态库时会对它们单独做签名校验,漏了一个就直接崩溃。

第二种是盲签。也就是不校验描述文件里的 Application Identifier 就直接签。每个描述文件都绑定了固定的 App ID,如果签名用的证书和描述文件不是一对儿,重签出来的包即便装上了,也会在运行到某些需要访问受保护资源的界面时闪退,排查起来非常折磨人。

5.2 资源改动导致视觉异常

图片微调过度是常见问题。亮度偏移超过 5%,对深色背景的图片就会产生肉眼可感知的变化;通道增益如果对纯色区域做得太狠,会看到明显色偏。我的经验是亮度偏移控制在 0.8% 到 1.5% 之间,单通道增益控制在 1% 以内,这样既保证感知哈希有足够变化,又确保视觉上完全无感。

5.3 修改边界和合规提醒

最后必须说清楚边界。IPA 结构调整和资源指纹变化这套手段,适用于你自己开发的产品做合理差异化处理,比如同一套引擎做的多语言版本、面向不同市场做的适配版本、或者长期迭代过程中避免和历史包产生过度相似判定。它不适用于制作侵权内容、盗用他人资源、伪造他人应用,更不应该被用来做规避用户隐私机制的行为。App Store 审核对重复垃圾应用的打击力度一直在加强,试图通过技术手段批量制造功能完全相同、内容无差异的马甲包,包过是侥幸,被清退是迟早的事。合规使用这套方法,让它服务于你的产品质量和运营策略,这才是长久之计。

6. 把指纹变化能力沉淀成常态化流程

实操过几轮之后,我最大的体会是:临时抱佛脚式地改包,效率低且容易漏。真正有价值的做法,是把“指纹基线管理”和“资源差异化处理”沉淀成团队内部的常态化机制。

具体来说,有三个可以立刻落地的方向。第一,在每次正式打包的 CI 流程里自动生成一份资源指纹基线文件,归档保存。这样任何时候需要对比历史包,都有据可查。第二,把图片微调、音频重编码脚本化,纳入团队工具库,新项目接入时直接复用。第三,在发版前增加一个“相似度自检”环节,对将要上传的包和已上线的历史包做资源指纹对比,提前发现相似度过高的风险,而不是等审核结果出来再补救。

我实际维护多地区版本时,最有效的动作就是把 API 输出的指纹报告集成到发版 diff 里。每次提审前花五分钟看一眼报告,哪几类资源没改动到位、哪些文件哈希完全没变,一目了然。这个习惯帮我们躲过了好几次潜在的相似度误判。整套方案的价值不在一招鲜,而在于形成稳定的流程,让你每次出的包都有足够的“身份区分度”,同时又不偏离产品本身的质量底线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:52:32

t3code 整合 Claude Code、Codex 与 Cursor 的 AI 编程桌面工具

1. 从 t3code 这个标题说起&#xff1a;它到底想解决什么问题第一次看到 t3code 这个名字&#xff0c;我下意识把它拆成了两部分&#xff1a;t3 和 code。t3 在开发者圈子里通常指代某种技术栈的第三代版本&#xff0c;或者是一个轻量化的代号&#xff1b;code 则直接指向代码、…

作者头像 李华
网站建设 2026/10/7 3:51:55

IMX577-AACK-C驱动移植:从datasheet到上电时序与MIPI调参

简介&#xff1a;IMX577-AACK-C 是索尼推出的一款 1/2.3 型 1230 万像素背照堆叠式 CMOS 图像传感器官方数据手册&#xff0c;适合硬件工程师、嵌入式开发者及图像传感器选型人员参考。该 PDF 完整呈现传感器核心特性&#xff0c;包括数字重叠高动态范围&#xff08;DOL-HDR&am…

作者头像 李华
网站建设 2026/10/7 3:51:28

Kettle 9.3实战:从安装到自动化数据管道搭建

简介&#xff1a;这是一份围绕Kettle 9.3官方安装包下载的指引文档&#xff0c;同时也整理了ETL工具的核心使用要点&#xff0c;适合在Windows、Linux或Unix平台从事数据抽取、转换、加载的运维人员、数据工程师以及刚接触数据集成的新手&#xff0c;解决因网络或检索原因难以获…

作者头像 李华
网站建设 2026/10/7 3:51:26

Allegro X AI辅助布局:意图驱动的PCB模块化设计

1. 这不是“AI画图”&#xff0c;而是PCB工程师的第二双手最近在几个硬件设计群和论坛里&#xff0c;总有人发截图&#xff1a;Allegro X界面右下角弹出一个蓝色小图标&#xff0c;点开后输入“把DDR4控制器模块紧凑排布&#xff0c;避开下方散热片区域&#xff0c;预留2mm维修…

作者头像 李华
网站建设 2026/10/7 3:49:19

WebCodecs配置详解:codec字符串与description提取实战

做 WebCodecs 的同学&#xff0c;十有八九都在configure()这一步摔过跤。明明VideoDecoder、AudioDecoder的 API 一看就懂&#xff0c;真到了要给浏览器传codec和description的时候&#xff0c;要么编解码器初始化直接抛错&#xff0c;要么画面出来全是花屏马赛克。原因往往就一…

作者头像 李华
网站建设 2026/10/7 3:48:56

第099篇 属性委托实战:SharedPreferences 的现代写法

前面几节把属性委托的机制讲完了——getValue/setValue 运算符重载、标准库里的 lazy/observable/vetoable。这一节回到实战,回答一个更实际的问题:什么时候该自己写一个委托类,以及写了之后怎么落地到项目里。面试里这题被答好的比例不高,原因很直接——大多数人只知道 by…

作者头像 李华