WWDC 2021 笔记

WWDC21 的阅读笔记, 大量参考 《WWDC21 内参》.

StoreKit 2.0

WWDC21 10114 Meet StoreKit 2 - 初见 StoreKit 2

appAccountToken

  1. 后台收到发货号码与票据
  2. 后台对调用苹果接口对票据进行校验,并获取其苹果物品 productId 信息
  3. 后台检索该发货号码下,有无该 productId 的订单,若有,则认为该笔订单与该笔票据(支付)相关

IAP 长期以来不存在稳定的透传字段(目前业界一般使用苹果的风控字段 applicationUsername 作为透传字段,但在 iOS 14 后,该字段基本不可用),
新增透传字段 appAccountToken,该字段可传递至票据内。即未来,我们可以从 appAccountToken 直接获取订单 / 用户发货号码,这个时候我们就无需再做上述的搓单逻辑

Transaction.currentEntitlements & sync—— 恢复购买优化
StoreKit 2,对恢复购买内容进行了重新设计,我们可以直接通过 Transaction.currentEntitlements 获取当前有效的自动续期订阅及非消耗型,无需再做无意义的补发。而且该接口根据苹果说明,无需密码授权,自动做同步。而老接口需要用户输入密码,某些敏感用户可能就会因此取消。不过此处苹果目前还是建议我们用 AppStore.sync() 方法拉起密码授权来回应用户的触发恢复购买按钮的事件,以保持历史习惯

Transaction.all —— 准确的历史购买记录

在 StoreKit 2 之前,我们如果想要获取用户的购买记录,那么我们只能上传票据至苹果后台进行校验,然后遍历 in_app / 自动续期订阅特有的 latest_receipt_info 数组,而该部分数组只包含 100 笔最近交易,且不包含消耗型。如果我们想要获取历史购买记录,并尝试对未发货的物品进行补发,这个时候不但时耗极高(App Receipt 获取及校验时耗往往在 5s 以上),且无法处理消耗型物品

估计苹果也是意识到这块的痛点,于 StoreKit 2 中为客户端提供了一个简单获取历史购买记录的接口。根据苹果目前的说法,该接口存在缓存数据,能够快速获取;且所有苹果物品类型(包括消耗型物品)的历史购买记录均可获取

isEligibleForIntroOffer —— 可靠的订阅促销资格判断
StoreKit 2 提供了直接的资格判断接口,该接口只需传入订阅组信息,即可获得是否首开的资格判断结果。且该接口自身会建立缓存,并保证缓存有效,这样子,我们就可以快速、无误地获得用户订阅优惠的资格

displayName & displayPrice —— 脱敏简化的本地化字段
在 Product 对象内提供了 displayName & displayPrice 字段,直接返回本地化后的信息,无需我们再利用敏感 API 进行处理

WWDC21 10175 - Support customers and handle refunds - IAP 用户退款与客诉处理优化

Invoice lookup API
以前:

  1. 无法判断用户提供的苹果订单是否已经发货, 苹果订单无法反查出业务订单, 所以灰产很有可能多次利用同一截图,进行骗补的操作
  2. 无法判断用户提供的苹果订单截图的真实性.
    现在可以直接通过用户反馈的苹果订单,关联到具体的业务订单.
  3. 、引导用户提供苹果订单截图 / 直接引导用户提供苹果订单中的 Invoice Order ID (即 api 中的 customer_order_id)
  4. 通过 Invoice lookup API (/inApps/v1/lookup/{customer_order_id})进行查询,其返回的 JWS 形式的票据内容
  5. 解码 JWS 形式的票据,获得苹果唯一订单标示(originalTransactionId),继而关联至业务订单;若是通过 StoreKit 2 支付的订单,还可获取稳定透传字段(appAccountToken),用于映射关联用户
  6. 对用户进行查单 / 补发等操作(苹果建议我们把用户已经反馈的 Invoice Order ID 存储至业务订单中)

Refunded purchases API 对历史退款订单进行索回
以前用户退款只在开发者账号显示有多少比退款, 以及 WWDC20 Session 10661 引入了退款通知, 但是仍然有两个问题. 一是不是所有的业务线都能接入退款通知以及后续处理, 二是通知收到之前有较长时间间隔, 期间若是发生大量恶意退款无法对这部分订单进行索回.
Refunded purchases API 用于主动判断某笔订单是否退款。我们就可以起一个任务,遍历所有的历史订单,对未接入退款通知 / 接入退款通知前的退款订单进行索回.
Refunded purchases API 需要提供订单唯一标示 {original_transaction_id} 与 {appAppleId}。 在结果中, 我们一样可以获得苹果唯一订单标示(originalTransactionId)以及稳定透传字段(appAccountToken). 此外苹果建议将我们把 PurchaseDate、RefundDate 进行存储.

Renewal extension API —— 赠送一定的自动续期订阅时长,用于挽留用户

可以在用户不取消订阅的前提下 赠送订阅时长. 限制:每次最多延长 90 天/一年针对具体某个订阅,最多延长两次/延长的时间不算在 85% 分成的一年时长

Consumption API —— 退款前置通知(已上线)
Consumption API 有两类定义:
狭义的 Consumption API:我们通过 API 把业务侧信息告知苹果时调用的接口
广义的 Consumption API:既包含狭义的 Consumption API,还包括 CONSUMPTION_REQUEST 通知
苹果目前在文档中使用的定义是广义的 Consumption API,即包含了 CONSUMPTION_REQUEST 通知,所以本文之后的 Consumption API 含义与苹果保持一致,即指广义的 Consumption API

Consumption API 实际上就是为我们提供了一个退款前置查询的能力,其整个交互过程如下:
1、用户退款
2、苹果收到退款请求后,会在 48 小时内进行审核,同时发送 CONSUMPTION_REQUEST 通知至我们
3、我们收到 CONSUMPTION_REQUEST 通知后,需要在 12 小时内,调用狭义的 Consumption API 进行请求,告知苹果用户信息
4、苹果根据我们反馈的信息,结合 AppleID 信息,对退款用户进行审核(需要注意的是,苹果不会完全采信我们信息,仅用做参考)
5、苹果同意退款
6、我们收到退款通知后,给予苹果正常回包,并进行索回等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
狭义的 Consumption API 需要提供下列一些用户状态相关的信息:

accountTenure:用户账号年龄(推测用于未成年退款判断)
appAccountToken:用户账号的唯一 UUID 映射值,建议在后台建立并存储 UUID 与用户账号的映射
consumptionStatus:表示用户对购买商品的消耗程度
0:暂未定义
1:商品未消耗
2:商品部分消耗
3:商品完全消耗(当出现商品从 A 账号转移至 B 账号时,也认为是该状态)
customerConsented:表示用户是否同意分享商品消耗数据,建议根据用户是否同意隐私协议传参。若用户不同意,该接口其他字段可不传入
deliveryStatus:商品是否正常发货
lifetimeDollarsPurchased:用户全平台累计付款金额(货币单位:美元)
lifetimeDollarsRefunded:用户全平台累计退款金额(货币单位:美元)
platform:用户对商品进行消耗的平台
0:暂未定义
1:苹果平台
2:非苹果平台
playTime:用户累计使用时长
0:暂未定义
1:五分钟以内
2:一小时以内
3:六小时以内
4:一天以内
5:四天以内
6:十六天以内
7:超过十六天
sampleContentProvided:购买前是否提供免费试用 / 购买后获得的商品样例,以及是否介绍商品具体作用
userStatus:用户账号状态
0:暂未定义
1:可用
2:冻结
3:封禁
4:部分限制

需要注意的是

  1. 由于交互过程中涉及到将在某应用的用户信息传输至苹果侧,所以需要在用户隐私协议中予以声明
  2. 在收到 CONSUMPTION_REQUEST 后, 到退款出结果有一段时间, 期间应该冻结相关资产.

Manage subscriptions API —— 应用内拉起自动续期订阅管理界面(StoreKit 2, iOS 15)
原有的取消订阅/修改订阅入口太深, 可以调用API:showManageSubscriptions(in:) 直接应用内拉起自动续期订阅管理界面.

Request refund API —— 应用内退款(StoreKit 2, iOS 15)

同样现在可以直接拉起用户退款界面, API:beginRefundRequest(for:in:).

Group Activity

Meet Group Activities/Design for Group Activities - 初探 Group Activities

Group Activity
Group Activities 是一个基于 Facetime 实时通讯技术的实时通讯框架。使用 Swift 实现,需要iOS 15以上的操作系统支持。
要了解使用这个框架首先需要理解两个核心概念, GroupActivity 和 GroupSession。

GroupActivity
GroupActivity 是应用用来定义共享内容的实体。主要是用来提供被共享的元信息,例如,标题、副标题、播放媒体的url等。 也可以存放一些自定义数据等。发起 SharePlay 是通过操作 GroupActivity 来完成的。你可以把任何一个你想要共享的实体,定义成一个 GroupActivity。
代码层面上, GroupActivity 是一个继承自 Codable协议的协议。 当一个 GroupActivity 实体被共享时, 它会被序列化并通过实时数据通道传递给所有参与者。 这样参与者就可以获得被共享的内容信息。

GroupSession
GroupSession 代表一个 SharePlay 会话。它维护着参与者列表,以及实时数据传输通道,可以通过它来收发一些数据,来保持各个设备同步。这个通道不是传输大量数据的, 只是用来传输轻量的状态同步数据。 参考 APNS 推送的实际到达率,Facetime 传输通道的实际质量也待验证。
GroupSession 只能通过系统(FaceTime)创建,通过监听 GroupSession 的异步类方法获得实例,目前还无法自行创建 GroupSession。

附加能力

  1. 后台启动播放, Group Activity API 支持在后台直接启动应用并播放多媒体。
  2. 启动应用到前台, 有时候应用可能需要一些互动只需调用 Group Activity API, 用户将点击横幅,应用程序将被带到前台。允许用户在加入体验之前进行交互。
  3. 安装应用, 如果有人在 SharePlay 开始时没有安装分享应用程序,Facetime 会提示用户跳转到 Appstore 安装应用。

需要开发者额外考虑的

  1. GroupActivities 框架本身没有提供权限控制的能力,所有人相同的权限,都可以操作。 这个在家庭场景下没有什么问题。不过对于某些类型应用,需要自己考虑操作权限控制策略。
  2. 应用需要自己设计启动流程,尤其是新用户流程。 冷启动结束之后才会进入共享场景.

Coordinate media experiences with Group Activities - 使用 Group Activity 共享媒体

当两台设备在同一个 FaceTime 通话中,其中一台打开一个可以进行 SharePlay 的程序,并发起 SharePlay 时,GroupActivity 框架会初始化一个 GroupSession,同时生成对应的 GroupActivity. 同时,这个 session 也会分享给第二台设备,第二台设备首先会唤起对应的 App,session 也会创建一个 GroupActivity 的实例。到此为止,两台设备在同一个 session 中,可以进行接下来的活动。

我们需要实现的协议是 GroupActivity, 拥有两个属性activityIdentifier 以及 GroupActivityMetadata.
activityIdentifier 是活动的唯一标志, app 可能同时存在多个活动. GroupActivityMetadata 会有 title、previewImage 等参数, 以及活动类型 listenTogether /watchTogether/generic, generic是自定义分享类型, 可以用于分享 PPT 等

然后是发起流程 prepareForActivation, 当准备就绪后 调用 activity.activate().

通过 GroupSession 管理共享
两台设备会接收到 GroupSession,系统会打开对应的 App 准备好状态,就绪后在加入到 GroupSession 中

通过 AVPlaybackCoordinater 共享状态
只需要将 session 挂载到 AVPlayer的Coordinater 上即可,框架会同步好播控状态。
player.playbackCoordinator.coordinateWithSession(session)

加入和结束 Session
加入 session groupSession.join()
结束 session 有两个方法: end() 和 leave(). 前者会终止所有用户的 SharePlay,后者仅会终止自己的。

到此为止,一个可以用来 SharePlay 的 App 就完成了!

使用画中画来增强体验

共享状态如何完成:AVPlaybackCoordinator
AVPlaybackCoordinator 有两个子类:AVPlayerPlaybackCoordinator 总是会挂载到一个 AVPlayer 上,并且会处理所有的播放变化,这个是较为建议使用的一个类; AVDelegatingPlaybackCoordinator 提供更灵活的方法,可以用于非 AVPlayer 的情况。本文主要针对前一种的工作原理进行描述。
AVPlayerPlaybackCoordinator 会拦截所有AVPlayer内部的状态变化,将这个变化传递给另一台设备的 AVPlayerPlaybackCoordinator 同时暂缓自己设备的状态更新,在双方都获取到新的状态后,双方的 AVPlayerPlaybackCoordinator 会选取一个合适的时机,更新自己设备的 AVPlayer.
AVCoordinatedPlaybackSuspension 可以使某个用户的播放状态隔离于整个 session. 在这个状态下的所有操作不会影响到别人. 比如来电/闹钟/网速太慢等, 设置到AVCoordinatedPlaybackSuspension状态下可以不影响其他人. AVPlayerPlaybackCoordinator 会在需要的时机自己添加,比如 AVAudioSession 被迫中断,或者一个用户在 seek 过程中拖动进度条时的操作。当这些行为结束后,这个用户会结束隔离,重新和 session 同步。
在第二种情况下,开发者可以手动加入 suspension 来实现一些特性。我们假设一个场景:Alice 和 Bob 在看电影,Bob 的闹钟突然响了,在结束闹钟的时间里,Bob 错过了一场精彩的战斗,我们希望当 Bob 回来的时候,可以以 2 倍速观看错过的内容,直到追上 Alice 的进度后,双方再保持原始的速度进行观看。

AVDelegatingPlaybackCoordinator
可惜的是,许多程序采用了自研的播放器,而非直接使用 AVPlaye. 在这种情况下,就需要使用 AVDelegatingPlaybackCoordinator 来进行状态管理。
AVDelegatingPlaybackCoordinator 的使用和上文所提到的基本一致,主要的区别是,所有的播控操作需要额外的包一层。一些细节需要注意的是:

需要在手动实现一个 delegate 协议,完成 seek、play、pause、buffer 等操作
UI 首先通知 AVDelegatingPlaybackCoordinator,而不是播放器。AVDelegatingPlaybackCoordinator 来决定先和别的 Coordinator 沟通,还是通知播放器改变状态
播放器的回调也要给到 AVDelegatingPlaybackCoordinator,所有的 UI 刷新也要通知给另外一端的 AVDelegatingPlaybackCoordinator
AVDelegatingPlaybackCoordinator 不提供自动的suspension,需要开发者手动实现
AVDelegatingPlaybackCoordinator 可以和 AVPlayerPlaybackCoordinator 通讯,但是必须使用自定的 item identifier

Coordinate media playback in Safari with Group Activities - 使用 Group Activity 在 Safari 中共享媒体

Build custom experiences with Group Activities - 使用 Group Activity 共享定制化内容

先简单回顾一下 SharePlay 共享媒体的基本流程,详细描述参见 WWDC21 10225 - 使用 Group Activity 共享媒体

  1. 应用声明支持 GroupActivity, 并创建实现 GroupActivity 协议的对象
  2. 发送方通过 GroupActivity 对象的 prepareForActivation、activate 等方法发起共享
  3. 会话中的设备通过 GroupActivity 对象的异步方法 sessions 来获得 GroupSession
  4. 会话中各方将 GroupSession 配置到 AVPlaybackCoordinater 或者 AVDelegatingPlaybackCoordinator 来同步状态
  5. 会话中各方调用 GroupSession 的 join 方法,开始同步数据
  6. 会话中各方调用 GroupSession 的 end 或者 leave 方法来结束会话,停止同步数据

相比于共享媒体,共享定制化内容主要有两个地方不同。 一个是 GroupActivity 定义 metadata 需要更改类型。另一个是获得 GroupSession 后不再配置到 AVPlaybackCoordinater, 而是用GroupSession构造一个 GroupSessionMessenger 对象来管理数据传输。
GroupActivity 定义
设置 metadata 的类型为 generic
GroupSessionMessenger
GroupSessionMessenger 支持传递 Data 类型的数据,或者 Codable 类型的数据。GroupSessionMessenger 会自动进行序列化和反序列化 Codable 类型的数据,并且进行端到端的加密。
发送数据的 API 支持 async 和回调函数两种方式。默认会发送给所有的参与者,并保证数据可靠传输。 在某些情况下, 只想发送特定消息给特定的参与者, 此时需要提供 participants 参数, 在本文同步全量数据章节中有具体的示例。
默认情况下 GroupSessionMessenger 会在发送失败的情况下尝试重试。 对于一些实时性很强,很快会被更新,可以丢弃的数据,可以将reliablility 参数设置为 unreliable,来表示失败时不重试。

需要注意的细节
通过监听与会者变化,给新加入的参与者发送全量的数据。
GroupSessionMessenger 会根据类型区分,两种消息不会混淆。

更换共享内容
有两种办法可以更换共享的内容。创建新的 GroupSession 或者更新 GroupSession 所持有的 GroupActivity.

符号化

Symbolication: Beyond the basics - iOS 符号化:基础与进阶

符号化的两个步骤:
第一步:从内存地址回溯到文件
1. otool -l 可以帮助我们输出 Mach-O 中指定二进制段的地址和属性信息,其中包括 Linker address otool -l MagicNumber | grep LC_SEGMENT -A8 输出结果中的 segname __TEXT 即表示 text 段的起始位置以及长度
2. 崩溃日志中的 Binary Images 列表中可以获取崩溃发生时的 load address, 即 发生崩溃的程序的地址 (vmmap 也可以获得正在运行 App 的 load address)
3. ASLR Slide = Load address - Linker address
第二步:还原运行时调试信息
1. function starts symbols -onlyFuncStartsData -arch arm64 $AppName 信息很少, 不提供函数名, 能查到知道方法内偏移, 只能在低级的机器码层面来分析
2. Nlist symbols List - Nlist 符号表 nm -defined-only —numberic-sort -arch arm64 $AppName 基于编码 nlist_64 结构体将调试信息升级到两个维度,即地址信息和函数名称
3. DWARF

Swift

Swift 中的 ARC 机制

ARC in Swift: Basics and beyond - Swift 中的 ARC 机制: 从基础到进阶

编译器会自动插入 retain 以及 release 来增加计数或释放, 但是两个对象互相引用就会都不释放
weak 和 unowned 引用类型并不会参与引用计数管理,因此,weak 和 unowned 引用常常会被用来打破对象间的循环引用, 但是也会导致已经释放了再次引用导致崩溃.
可以使用 withExtendedLifetime($WeakProperty) 在使用 weak 属性的时候 主动保证 $WeakProperty 的生命周期. 但是这样做 会有很高的维护成本, 没哟个 weak 引用都要思考需不需要保护, 应当重新梳理引用结构来解决.

deinitializer 带来的问题及解决方法
Swift 中一个类的 deinitializer 会在对象被释放前被调用, 如果对象生命周期被优化缩短, deinit 就会被提前调用. 上述优化依旧有用..

Swift 编译器的相关新特性
Xcode 13 引入了一个新的优化选项: Optimize Object Lifetime, 它对应的 Swift 编译器参数是: -Xfrontend -enable-copy-propagation,开启这项优化后会导致已有代码中一些对象实际生命周期被缩短,从而暴露一些隐藏已久的 bug. 目前默认没有开启

Swift 中的异步与并发

https://xiaozhuanlan.com/topic/8627905413
https://xiaozhuanlan.com/topic/3625784190
https://xiaozhuanlan.com/topic/2957164803

算法与集合

https://xiaozhuanlan.com/topic/0958147326

Swift 并发编程:原理探究

https://xiaozhuanlan.com/topic/7604819352

内存

https://xiaozhuanlan.com/topic/2079653148

Shortcuts 小组件

https://xiaozhuanlan.com/topic/4837521960
https://xiaozhuanlan.com/topic/8450167329

网络

https://xiaozhuanlan.com/topic/5297843106

AVFoundation & AVKit

https://xiaozhuanlan.com/topic/1574609238
https://xiaozhuanlan.com/topic/2879104653
https://xiaozhuanlan.com/topic/0692873541

音频

https://xiaozhuanlan.com/topic/5479286310
https://xiaozhuanlan.com/topic/6102839475
https://xiaozhuanlan.com/topic/4016857239
https://xiaozhuanlan.com/topic/8750491623

列表流畅度

https://xiaozhuanlan.com/topic/6813072594

OCR

https://xiaozhuanlan.com/topic/6204139578

图像处理

https://xiaozhuanlan.com/topic/9352746081

性能优化

https://xiaozhuanlan.com/topic/5073916284
https://xiaozhuanlan.com/topic/2936041578

WebView

https://xiaozhuanlan.com/topic/1352486079

XCode

https://xiaozhuanlan.com/topic/1084569237

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道