在 Bluesky 里浏览帖子时,右上角看到的是「Follow」按钮;可一旦截屏,这个位置却可能变成 Bluesky Logo。它并不是截图完成后再修改图片,也不是服务器给图片加了水印,而是利用 iOS 对受保护界面内容的特殊捕获规则,让用户看到的画面与系统截图得到的画面不同。
更直白的是,Bluesky 开源代码里负责这段逻辑的文件就叫 GrowthHack.tsx。这个命名没有掩饰目标:让用户随手传播的截图自动携带品牌标识。
同一块屏幕,为什么能产生两种结果
这类实现可以理解为两层叠放的界面:
- 底层放置 Bluesky Logo。
- 上层放置正常交互用的「Follow」按钮。
- 上层被放入 iOS 视为敏感或受保护内容的容器。
- 用户正常看屏幕时,上层按钮遮住底层 Logo。
- 系统生成截图时,不捕获受保护的上层,于是底层 Logo 露出来。
用简化后的 React 组件表达,大致是下面这种结构。这里是机制示意,不是 Bluesky 源码的逐行复刻:
function ScreenshotAwareFollowButton() {
return (
<View style={{width: 96, height: 40}}>
<View style={StyleSheet.absoluteFill}>
<BlueskyLogo />
</View>
<ScreenshotProtectedView style={StyleSheet.absoluteFill}>
<FollowButton />
</ScreenshotProtectedView>
</View>
)
}
关键不在于收到“用户截屏了”的事件后快速替换按钮。那样通常已经来不及,因为截图内容已经生成。真正的关键发生在 iOS 合成和捕获界面的阶段:某一层可以显示在设备上,却不进入系统捕获的最终图像。
这原本属于隐私保护思路。例如,密码、支付信息或一次性验证码不应出现在屏幕录制和系统快照中。Bluesky 则反向利用了这一行为:不是单纯隐藏敏感信息,而是隐藏一个覆盖层,让预先埋在下面的品牌 Logo 出现在截图里。
为什么监听截图事件做不到同样的事
iOS 提供了 UIApplication.userDidTakeScreenshotNotification,但这个通知主要适合记录、提示或清理后续状态。它不能可靠地修改刚刚生成的截图。
可以用一个最小 UIKit 项目验证时序。新建 iOS App,将下面内容替换到 ViewController.swift,运行后截一次屏:
import UIKit
final class ViewController: UIViewController {
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "尚未收到截屏通知"
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
statusLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 24),
statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -24)
])
NotificationCenter.default.addObserver(
self,
selector: #selector(didTakeScreenshot),
name: UIApplication.userDidTakeScreenshotNotification,
object: nil
)
}
@objc private func didTakeScreenshot() {
statusLabel.text = "已收到截屏通知,但刚才的截图已经生成"
view.backgroundColor = .systemYellow
print("Screenshot notification received at \(Date())")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
你会发现,截图里的标签仍是截屏前的内容;黄色背景和新文字只会在截屏之后出现在 App 中。因此,下面这种 React Native 写法无法实现 Bluesky 的效果:
useEffect(() => {
const subscription = ScreenCapture.addScreenshotListener(() => {
setShowLogo(true)
})
return () => subscription.remove()
}, [])
它可以在截屏后显示提示,但无法回到过去改变已经保存的像素。
GrowthHack.tsx 这个名字说明了什么
这个功能并没有增加发帖能力,也没有改善信息流加载速度。它优化的是传播链路:
用户看到帖子
→ 用户截屏
→ 截图自动出现品牌标识
→ 图片被发到聊天群或其他社交平台
→ 看到图片的人知道内容来自 Bluesky
与传统水印相比,它有两个明显特点。
一方面,正常浏览界面不必长期占用空间显示 Logo,产品仍可保留「Follow」这样的高价值操作入口。另一方面,用户没有明确点击“生成分享卡片”,品牌信息却进入了他创建的截图。这正是它既聪明又有争议的地方。
这种设计还存在技术边界:
- iOS 与 Android 的捕获机制不同,效果不能假定跨平台一致。
- 系统版本升级可能改变受保护视图的合成行为。
- 某些实现会依赖安全文本输入视图的内部层级,属于脆弱的工程技巧。
- 截屏、录屏、应用切换器快照和第三方投屏不一定走同一条路径。
- 无障碍树、点击区域和视觉层不一致时,可能影响 VoiceOver 与自动化测试。
因此,不应把这种方案当成稳定的公开水印 API。尤其当实现需要访问 UIKit 私有层级时,还要评估 App Store 审核和长期维护风险。
更稳妥的实践:把品牌化放进明确的分享动作
如果目标是让分享图片携带来源,推荐提供显式的“分享为图片”功能:使用 UIGraphicsImageRenderer、SwiftUI ImageRenderer 或服务端模板生成分享卡片,再交给系统分享面板。这样用户知道自己分享的是什么,测试结果也更可控。
采用截图分层技巧之前,可以检查下面几项:
- 是否真的需要修改系统截图中的呈现?
- 用户是否会误以为截图与自己看到的界面完全相同?
- Logo 会不会遮挡内容或被误认为原帖的一部分?
- Android、录屏和旧版 iOS 的降级行为是什么?
- 系统升级后是否有截图回归测试?
- 能否用明确的分享卡片实现同样的增长目标?
Bluesky 的实现值得工程师研究,因为它展示了 UI 合成层、隐私机制和产品增长之间少见的交叉点。但“技术上做得到”不等于“任何产品都应该照做”。如果品牌传播必须依赖用户没有预期到的截图差异,那么透明度、兼容性与信任成本也应该进入同一张技术方案评审表。