跳到主要内容

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 及以上
关于 iOS 15 下限

应用内升级能力自 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 需要宿主传入的字段​

字段类型必填说明
appVersionNSString否应用版本名(versionName)
buildNoNSString否应用构建号(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 / appKeyNSString是应用凭证
guidNSString是用户 ID(协议中的 userId)
qimeiNSString是设备标识(协议中的 deviceId)
bundleIdNSString是包名,与策略登记值比对;业务改过包名时必须显式传
appVersionNSString否versionName;不传回退 CFBundleShortVersionString
buildNoNSString否buildNo;不传回退 CFBundleVersion
envIdNSString否RDeliveryReleaseEnvId(正式)/ "1"(测试)
systemVersionNSString否系统版本,用于匹配下发条件
deviceTypeNSString否设备型号,用于匹配下发条件
profilesNSDictionary否自定义属性,用于匹配下发条件
customServerUrlNSString否独立部署域名,传裸域名(带尾斜杠会自动去除;/business 由编译宏统一补)
businessSetNameNSString否独立部署子业务集名
ignoreFrequencyLimitBOOL否忽略限频,默认 NO
configUpdateIntervalNSTimeInterval否配置更新间隔,默认 4 小时
dependsid\<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策略下发出错 / 弹窗条件不满足
NoTactic0没有拉到策略
NewTactic1有可生效的新策略
SUGrayTacticsErrorCode值说明
None0无错误
AlreadyUpdated1已经是策略的版本
InvalidType2策略类型不正确(pkgType==APK)
BundleId3bundleId 与策略不匹配
InvalidURL4downloadUrl 为空
PopTimes5弹窗次数超过上限
TimeInterval6弹窗时间小于最小间隔
NoTactic7没有拉到策略
FetchFailed8拉取失败且本地无可用缓存

出错时 errorCode 恒为本域枚举,不会出现 RDelivery/SMUError 等外部错误域的码值。

弹窗判定链​

判定顺序(任一不通过即拦截):

  1. downloadUrl 为空 → InvalidURL
  2. bundleId 与宿主不匹配 → BundleId
  3. pkgType == APK → InvalidType
  4. 策略版本 == 本机版本 → AlreadyUpdated
  5. remindType == 红点 → 放行(不受限频)
  6. 限频: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 形态下不使用,接入方无需关心。)

激活事件由 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 / theNewFeatureNSString更新标题 / 版本描述
publishTime / updateTime毫秒策略发布 / 更新时间
updateStrategyNSNumber建议(1) / 强制(2) / 静默(3)
grayTypeNSInteger灰度类型:灰度升级(1) / 全量升级(2) / 热更新(3)。描述灰度投放方式,非包形态(全量包/差量包是另一维度)
popTimes / popInterval整数 / 毫秒最大弹窗次数 / 最小弹窗间隔
remindTypeNSInteger弹窗(1) / 红点(2) / 全部(3)
tacticsIdNSString策略 ID
statusNSInteger策略状态
apkBasicInfoSUPackageBasicInfo安装包信息
clientInfoSUClientInfo弹窗文案(title / theDescription)
extraNSDictionary自定义下发字段

安装包信息(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];

九、注意事项​

  1. 版本参数是接入第一坑:appVersion / buildNo 需与策略登记值一致,任一不匹配都会导致匹配不到策略或装完新包仍弹窗。buildNo 未注入时会回退 Info.plist。详见第三节。
  2. 依赖必须注入:SDK 无 Default*Impl 回退,缺注入启动即失败(返回 NO)。
  3. SDK 不下载不安装:只下发 downloadUrl(商店/更新页链接),业务打开该链接跳转应用商店完成更新;激活状态在下次冷启动时判定。
  4. customServerUrl 传裸域名:不要自行拼 /business,该前缀由编译宏 SHIPLY_COMMERCIAL_VERSION 统一补,两侧各拼一次会产出 /business/business 而 404。尾部斜杠会被自动去除(对齐鸿蒙),https://host.com/ 与 https://host.com 等价,但契约仍要求裸域名。
  5. bundleId 建议显式传:业务改过包名、或用同一份策略跑不同壳工程时,只比 Info.plist 会永远判不中。
  6. 限频状态持久化在 NSUserDefaults:按 tacticsId 维度,30 天过期;同一 tacticsId 且 publishTime 变化视为服务端重发策略,会清零计数。
  7. ignoreFrequencyLimit 仅影响弹窗:不影响拉取,也不绕过版本/bundleId/pkgType 校验。
  8. 单例限制:sharedInstance 仅适用于单 appId;多 appId 场景用 alloc init 各自持有。
  9. 激活事件的跨账号语义:激活归因快照按 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)——复用已有实例即可。

更多问题见 常见问题。

这篇文档对您有帮助吗?