Shiply iOS 应用内升级 SDK 集成
一、隐私安全说明
SDK 名称:Shiply iOS SDK(应用内升级)
版本:1.4.0
更新时间:2026-09-21
SDK 介绍:为移动开发者提供专业的应用内灰度升级能力,复用 Shiply 配置下发协议拉取升级策略(systemId=10016)。
服务提供方:深圳市腾讯计算机系统有限公司
接入指引:《Shiply iOS 应用内升级 SDK 接入指引》
隐私保护规则:《Shiply 灰度升级SDK隐私保护规则》
前置概念:接入前请先阅读 通用概念,了解各平台共用的
appId/appKey/guid/qimei/envId/依赖注入 等核心概念,避免跨平台混淆。
环境要求
| 项 | 要求 |
|---|---|
| 最低支持 | iOS 15.0 |
| 构建工具 | Xcode 27 及以上(SDK 以 Xcode 27 构建验证;Xcode 27 要求 deployment target ≥ 15.0) |
| macOS(SMU 门面,ShiplyMacUpdater 宿主) | macOS 12.0 及以上 |
应用内升级能力自 RDelivery SDK 1.4.0 起新增,同时最低支持 iOS 提升至 15.0(podspec s.ios.deployment_target = "15.0")。存量业务如仍需支持 iOS 15 以下,无法接入应用内升级能力,请继续使用 RDelivery 1.3.x。
二、接入 SDK
应用内升级能力包含在 ShiplyRDelivery SDK 中(xcframework 分发,含 iOS / macOS / Catalyst),无需额外依赖:
pod 'ShiplyRDelivery', '~> 1.4.0'
依赖注入(必填)
SDK 不自带默认的 Default*Impl 依赖实现——四个可替换组件(kv / log / http / jsonModel)必须由宿主注入,缺注入会在 startWithConfiguration: 同步返回 NO(不静默回退)。除 logImpl 外,其余三个缺失即启动失败:
#import <ShiplyRDelivery/ShiplyUpgrade.h>
#import <ShiplyRDelivery/SUCoreConfiguration.h>
#import <ShiplyRDelivery/RDeliveryDepends.h>
#import <ShiplyRDelivery/RDMMKVFactoryImpl.h>
#import <ShiplyRDelivery/RDLoggerImpl.h>
#import <ShiplyRDelivery/RDeliveryJsonModelImpl.h>
RDeliveryDepends *depends = [RDeliveryDepends new];
depends.httpImpl = [RDNetworkImpl sharedInstance];
depends.kvImpl = [RDMMKVFactoryImpl sharedInstance];
depends.logImpl = [RDLoggerImpl sharedInstance];
depends.jsonModelImpl = [RDeliveryJsonModelImpl sharedInstance];
三、版本相关参数(必读)
升级策略按「应用版本」匹配,因此版本参数填错是最常见的接入问题:轻则匹配不到策略,重则升级完成后仍反复弹窗、激活事件不上报。
3.1 需要宿主传入的字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
appVersion | NSString | 否 | 应用版本名(versionName) |
buildNo | NSString | 否 | 应用构建号(buildNo) |
两点必须清楚:
- iOS 没有 versionCode 概念:iOS 侧不存在 versionCode,版本匹配仅由
appVersion+buildNo决定。 - 两个字段都可不传:未注入时 SDK 各自回退读 Info.plist 的
CFBundleShortVersionString/CFBundleVersion。显式注入优先——业务改过包名、或用同一份策略跑不同壳工程时建议显式传。
3.2 参数取值示例
// versionName:对应 Info.plist 的 CFBundleShortVersionString,如 "6.4.50"
configuration.appVersion = @"6.4.50";
// buildNo:对应 Info.plist 的 CFBundleVersion,如 "634"(字符串,非数字)
configuration.buildNo = @"634";
3.3 两个版本字段的作用与影响
appVersion(versionName):决定能否拉到对应版本的策略,并参与「是否已更新」判定。若与策略登记的版本不一致,策略匹配不到,或升级装完新包后仍被判为「未更新」而继续弹窗。buildNo(构建号):同一版本名下的不同构建靠它区分。与策略侧构建号不一致时,升级完成后的「已更新」判定失效——装完新包仍会继续弹窗,且激活事件不上报。
两个字段都会与策略下发的 apkBasicInfo 对应字段逐一比对,任一不匹配即判定为「未更新」。
四、初始化 SDK
SUCoreConfiguration *configuration = [[SUCoreConfiguration alloc] initWithAppId:@"your_app_id"
bundleId:@"com.your.app"
appKey:@"your_app_key"
guid:@"your_user_id"];
// 版本参数(见第三节)
configuration.appVersion = @"6.4.50";
configuration.buildNo = @"634";
configuration.qimei = @"your_device_id"; // 设备标识
configuration.envId = RDeliveryReleaseEnvId; // 正式环境;测试环境传 "1"
configuration.systemVersion = [UIDevice currentDevice].systemVersion;
struct utsname systemInfo;
uname(&systemInfo);
configuration.deviceType = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
configuration.profiles = @{@"channel": @"appstore"}; // 自定义属性,用于匹配下发条件
configuration.ignoreFrequencyLimit = NO; // 是否忽略限频(弹窗次数/间隔),默认 NO
configuration.depends = depends; // 依赖注入(见第二节)
ShiplyUpgradeSDK *upgradeSDK = [ShiplyUpgradeSDK sharedInstance];
NSError *error = nil;
BOOL ok = [upgradeSDK startWithConfiguration:configuration
logCallback:self.logHandler // 可传 nil
error:&error];
启动流程:初始化配置服务 → 读本地策略 → 激活检测上报(每次冷启动一次)。
startWithConfiguration: 返回 NO 的情形:configuration 为 nil 或 appId/appKey 为空(SMUErrorCodeInvalidParameter);重复启动(SMUErrorCodeAlreadyStarted,SDK 不支持同一实例启动两次,宿主应复用实例而非重复 start);依赖未注入或配置服务启动失败(SMUErrorCodeConfigInvalid)。
SUCoreConfiguration 参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
appId / appKey | NSString | 是 | 应用凭证 |
guid | NSString | 是 | 用户 ID(协议中的 userId) |
qimei | NSString | 是 | 设备标识(协议中的 deviceId) |
bundleId | NSString | 是 | 包名,与策略登记值比对;业务改过包名时必须显式传 |
appVersion | NSString | 否 | versionName;不传回退 CFBundleShortVersionString |
buildNo | NSString | 否 | buildNo;不传回退 CFBundleVersion |
envId | NSString | 否 | RDeliveryReleaseEnvId(正式)/ "1"(测试) |
systemVersion | NSString | 否 | 系统版本,用于匹配下发条件 |
deviceType | NSString | 否 | 设备型号,用于匹配下发条件 |
profiles | NSDictionary | 否 | 自定义属性,用于匹配下发条件 |
customServerUrl | NSString | 否 | 独立部署域名,传裸域名(带尾斜杠会自动去除;/business 由编译宏统一补) |
businessSetName | NSString | 否 | 独立部署子业务集名 |
ignoreFrequencyLimit | BOOL | 否 | 忽略限频,默认 NO |
configUpdateInterval | NSTimeInterval | 否 | 配置更新间隔,默认 4 小时 |
depends | id\<RDeliveryDependProtocol> | 是 | 依赖注入(kv/log/http/jsonModel) |
systemVersion / deviceType 由业务注入,SDK 不主动采集。详见 合规使用指南。
五、检查更新
初始化后,在业务时机调用 checkGrayUpgradeWithHandler:(拉取 + 判定一体,主线程回调):
[upgradeSDK checkGrayUpgradeWithHandler:^(SUGrayTacticsEvent event,
SUGrayTacticsErrorCode errorCode,
SUUpgradeModel * _Nullable grayTactics) {
switch (event) {
case SUGrayTacticsEventNewTactic: {
// 有可生效的新策略,业务自行弹窗
NSString *title = grayTactics.clientInfo.title;
NSString *desc = grayTactics.clientInfo.theDescription;
NSString *url = grayTactics.apkBasicInfo.downloadUrl; // 商店/更新页链接,业务打开它跳转更新
// 弹窗前建议上报一次曝光
[upgradeSDK reportPopupExposureWithUpdateConfig:grayTactics];
// 业务打开 url 跳转应用商店更新(示例)
break;
}
case SUGrayTacticsEventNoTactic:
// 服务端未下发策略
break;
case SUGrayTacticsEventError:
// errorCode 见 SUGrayTacticsErrorCode
break;
}
}];
SDK 不下载、不安装,只把 apkBasicInfo.downloadUrl(商店/更新页链接)交给业务。业务打开该链接跳转应用商店更新;装完新包后,下次冷启动 SDK 会自动检测到「本机版本 == 上次交付的目标版本」并上报激活事件。
因此同一个策略在宿主实际完成更新前会继续按限频策略弹窗,更新成功后自动停止。
downloadUrl 链接格式与打开方式
downloadUrl 由平台在策略中配置,业务直接打开即可:
| 场景 | downloadUrl 格式 | 打开方式 |
|---|---|---|
| 正式版(App Store) | itms-apps://itunes.apple.com/app/id<appId> | openURL: 跳转 App Store 详情页 |
接入方通常只需用 [[UIApplication sharedApplication] openURL:url options:{} completionHandler:] 打开链接;不要自行拼接或改写 downloadUrl。
事件与错误码
| SUGrayTacticsEvent | 值 | 说明 |
|---|---|---|
| Error | -1 | 策略下发出错 / 弹窗条件不满足 |
| NoTactic | 0 | 没有拉到策略 |
| NewTactic | 1 | 有可生效的新策略 |
| SUGrayTacticsErrorCode | 值 | 说明 |
|---|---|---|
| None | 0 | 无错误 |
| AlreadyUpdated | 1 | 已经是策略的版本 |
| InvalidType | 2 | 策略类型不正确(pkgType==APK) |
| BundleId | 3 | bundleId 与策略不匹配 |
| InvalidURL | 4 | downloadUrl 为空 |
| PopTimes | 5 | 弹窗次数超过上限 |
| TimeInterval | 6 | 弹窗时间小于最小间隔 |
| NoTactic | 7 | 没有拉到策略 |
| FetchFailed | 8 | 拉取失败且本地无可用缓存 |
出错时 errorCode 恒为本域枚举,不会出现 RDelivery/SMUError 等外部错误域的码值。
弹窗判定链
判定顺序(任一不通过即拦截):
downloadUrl为空 →InvalidURLbundleId与宿主不匹配 →BundleIdpkgType == APK→InvalidType- 策略版本 == 本机版本 →
AlreadyUpdated remindType == 红点→ 放行(不受限频)- 限频:
popTimes/popInterval(ignoreFrequencyLimit = YES时豁免)
「已更新」判定用逐字段比较(versionName + buildNo),不做字符串拼接。策略侧 buildNoStr 优先,为空时回退整型 buildNo。
六、事件上报
// 弹窗曝光
[upgradeSDK reportPopupExposureWithUpdateConfig:grayTactics];
// 通用上报
[upgradeSDK reportEvent:SUEventTypeActive
result:SUEventResultSuccess
updateConfig:grayTactics];
| SUEventType | 说明 |
|---|---|
SUEventTypeActive | 激活事件(SDK 自动上报) |
SUEventTypePopupExposure | 弹窗曝光(宿主触发,经便捷方法 reportPopupExposureWithUpdateConfig:) |
(SDK 另定义了下载 / 安装 / 点击类事件常量,本 SDK 形态下不使用,接入方无需关心。)
业务无需手动上报 SUEventTypeActive。SDK 在冷启动时比对「本机版本」与「上次交付的目标版本」(安装时刻落盘的快照),命中即上报并清除记录(不会重复上报)。
激活事件的归因(tacticsId / updateType)取自安装时刻落盘的快照——账号切换或策略重发后,当前缓存策略可能已变,快照才保证归因不串策略。归因快照在上报成功后才清除:HTTP 失败时保留,下次冷启动自动重试。
七、获取策略信息
// 读取本地缓存策略(不触发网络)
NSError *error = nil;
SUUpgradeModel *tactic = [upgradeSDK getConfigModelWithError:&error];
// 拉取远程策略(不判定,不弹窗)
[upgradeSDK updateConfigWithCompletion:^(SUUpgradeModel * _Nullable updateConfig, NSError * _Nullable error) {
// updateConfig 即策略数据
}];
策略字段(SUUpgradeModel):
| 字段 | 类型 | 说明 |
|---|---|---|
title / theNewFeature | NSString | 更新标题 / 版本描述 |
publishTime / updateTime | 毫秒 | 策略发布 / 更新时间 |
updateStrategy | NSNumber | 建议(1) / 强制(2) / 静默(3) |
grayType | NSInteger | 灰度类型:灰度升级(1) / 全量升级(2) / 热更新(3)。描述灰度投放方式,非包形态(全量包/差量包是另一维度) |
popTimes / popInterval | 整数 / 毫秒 | 最大弹窗次数 / 最小弹窗间隔 |
remindType | NSInteger | 弹窗(1) / 红点(2) / 全部(3) |
tacticsId | NSString | 策略 ID |
status | NSInteger | 策略状态 |
apkBasicInfo | SUPackageBasicInfo | 安装包信息 |
clientInfo | SUClientInfo | 弹窗文案(title / theDescription) |
extra | NSDictionary | 自定义下发字段 |
安装包信息(SUPackageBasicInfo):pkgName、md5、version(versionName)、versionCode、buildNo、buildNoStr、downloadUrl、pkgSize、bundleId、plistUrl、pkgType。
pkgType 取值:1=APK、2=TestFlight、3=Plist、4=AppStore、5=补丁包、9=Mac Sparkle(其余值为其他平台包类型)。
八、其他接口
// 切换用户(切换后需自行调用 updateConfigWithCompletion: 拉取一次)
[upgradeSDK switchGuid:@"new_user_id"];
// 切换设备标识
[upgradeSDK switchQimei:@"new_device_id"];
// 设置自定义属性
[upgradeSDK setProfileValue:@"vip" forKey:@"user_tag"];
// 清空所有缓存
[upgradeSDK clearAllCache];
// SDK 版本号
NSString *version = [ShiplyUpgradeSDK sdkVersion];
九、注意事项
- 版本参数是接入第一坑:
appVersion/buildNo需与策略登记值一致,任一不匹配都会导致匹配不到策略或装完新包仍弹窗。buildNo未注入时会回退 Info.plist。详见第三节。 - 依赖必须注入:SDK 无
Default*Impl回退,缺注入启动即失败(返回NO)。 - SDK 不下载不安装:只下发
downloadUrl(商店/更新页链接),业务打开该链接跳转应用商店完成更新;激活状态在下次冷启动时判定。 customServerUrl传裸域名:不要自行拼/business,该前缀由编译宏SHIPLY_COMMERCIAL_VERSION统一补,两侧各拼一次会产出/business/business而 404。尾部斜杠会被自动去除(对齐鸿蒙),https://host.com/与https://host.com等价,但契约仍要求裸域名。bundleId建议显式传:业务改过包名、或用同一份策略跑不同壳工程时,只比 Info.plist 会永远判不中。- 限频状态持久化在 NSUserDefaults:按
tacticsId维度,30 天过期;同一tacticsId且publishTime变化视为服务端重发策略,会清零计数。 ignoreFrequencyLimit仅影响弹窗:不影响拉取,也不绕过版本/bundleId/pkgType 校验。- 单例限制:
sharedInstance仅适用于单 appId;多 appId 场景用alloc init各自持有。 - 激活事件的跨账号语义:激活归因快照按 appId 维度落盘(不分账号),与 Android
installed_apk_version口径一致——账号 B 登录后装包完成,激活仍归因到安装时刻的策略(快照里的 tacticsId),不会串到 B 的当前策略。
十、常见问题
Q:升级完成后还在反复弹窗?
检查 appVersion / buildNo 是否与策略匹配:策略侧 apkBasicInfo.version 应与宿主 appVersion 相等,apkBasicInfo.buildNoStr(或 buildNo)应与宿主 buildNo 相等。任一不等都会被判为「未更新」。若宿主未注入这两个值,SDK 会用 Info.plist 的值——Info.plist 没改过的包(如渠道包回写失败)就会持续弹窗。
Q:checkGrayUpgradeWithHandler: 一直回调 NoTactic?
依次检查:① 平台是否已创建并发布灰度任务;② 策略状态是否为「灰度中」;③ bundleId 是否与策略登记值一致;④ 下发条件(版本号、自定义属性)是否命中。
Q:返回 FetchFailed(8)而不是 NoTactic?
拉取失败(网络异常/服务端错误)且本地无可用缓存。检查网络与 appId/appKey。
Q:startWithConfiguration: 返回 NO?
四种可能:① configuration 为 nil 或 appId/appKey 为空(SMUErrorCodeInvalidParameter);② 依赖未完整注入——检查 configuration.depends 的 httpImpl / kvImpl / jsonModelImpl 是否都非空;③ 依赖完整但配置服务启动失败(SMUErrorCodeConfigInvalid,看日志「配置服务启动失败」);④ 重复启动(SMUErrorCodeAlreadyStarted)——复用已有实例即可。
更多问题见 常见问题。