App 打包发布 / 性能优化 / 持续集成
移动端应用的打包发布、性能优化与持续集成是 App 研发流程中的核心环节。本文围绕 Android 与 iOS 双平台,从构建、签名、上架、性能调优、包体积优化到 CI/CD 自动化,提供完整的实施指南。
一、Android 打包与发布
1.1 构建类型
Android 构建类型通过 buildTypes 配置,通常包括 debug 和 release 两种。
android {
buildTypes {
debug {
debuggable true
minifyEnabled false
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
release {
debuggable false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}signingConfig signingConfigs 配置
签名配置统一管理签名密钥信息,密钥文件通常不提交到版本控制。
android {
signingConfigs {
release {
storeFile file(project.property("KEYSTORE_FILE") ?: "keystore.jks")
storePassword project.property("KEYSTORE_PASSWORD") ?: ""
keyAlias project.property("KEY_ALIAS") ?: ""
keyPassword project.property("KEY_PASSWORD") ?: ""
v1SigningEnabled true
v2SigningEnabled true
}
}
}密钥库(keystore)是一个包含私钥和证书的二进制文件,格式为 JKS(Java KeyStore)或 PKCS12。推荐使用 PKCS12 格式(自 Java 9 起默认)。
创建密钥库的命令:
keytool -genkey -v -keystore release.keystore -alias my-alias \
-keyalg RSA -keysize 2048 -validity 10000 \
-storetype PKCS12 -storepass 密码 -keypass 密码1.2 构建变体
构建变体(Build Variant)由构建类型(buildTypes)和产品风味(productFlavors)组合而成。
android {
flavorDimensions "tier", "channel"
productFlavors {
demo {
dimension "tier"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
}
full {
dimension "tier"
applicationIdSuffix ".full"
versionNameSuffix "-full"
}
free {
dimension "channel"
}
paid {
dimension "channel"
}
}
}上述配置将产生以下构建变体:demoFreeDebug、demoFreeRelease、demoPaidDebug、demoPaidRelease、fullFreeDebug、fullFreeRelease、fullPaidDebug、fullPaidRelease。
flavor 专属依赖
通过变体名称前缀声明专属依赖:
dependencies {
// 通用依赖
implementation 'androidx.core:core-ktx:1.12.0'
// demo 变体专属
demoImplementation 'com.example:demo-feature:1.0.0'
// full 变体专属
fullImplementation 'com.example:full-feature:1.0.0'
}dimension 与 fallback
当分维度不匹配时,通过 matchingFallbacks 指定回退策略:
android {
productFlavors {
// 某些变体不存在时使用 fallback
}
buildTypes {
release {
matchingFallbacks = ['debug']
}
}
}1.3 应用签名
Android 应用签名是验证开发者身份和确保应用完整性的关键机制。
创建签名密钥
使用 keytool 工具创建密钥库:
# 创建 JKS 格式密钥库(传统)
keytool -genkey -v -keystore my-release-key.jks \
-alias my-alias -keyalg RSA -keysize 2048 -validity 10000
# 创建 PKCS12 格式密钥库(推荐)
keytool -genkey -v -keystore my-release-key.keystore \
-alias my-alias -keyalg RSA -keysize 2048 -validity 10000 \
-storetype PKCS12Google Play App Signing
Google Play 应用签名分为两个密钥:
- 应用签名密钥(App Signing Key):由 Google Play 保管,用于验证应用身份。如果丢失,可向 Google 申请重置。
- 上传密钥(Upload Key):由开发者保管,用于将 APK/AAB 上传到 Google Play Console。
工作流程:
- 开发者使用上传密钥签名 AAB/APK 并上传到 Google Play Console。
- Google Play 使用应用签名密钥重新签名,分发给用户。
启用 Google Play App Signing 后,即使上传密钥泄露,也可以由 Google 重置,无需用户重新安装应用。
PGP 区别
签名密钥与 PGP 密钥的区别:
| 特性 | Android 签名密钥 | PGP 密钥 |
|---|---|---|
| 用途 | 应用身份验证和完整性 | 文件签名、加密、邮件签名 |
| 算法 | RSA/ECDSA | RSA/DSA/ElGamal |
| 格式 | JKS/PKCS12 | OpenPGP 密钥环 |
| 存储 | keystore 文件 | 密钥环/智能卡 |
| 吊销 | Google Play Console | 密钥服务器 |
密钥保护与 CI 密钥安全
在持续集成环境中保护签名密钥的推荐做法:
- 不要将密钥文件提交到版本控制。将
*.jks、*.keystore、*.p12添加到.gitignore。 - 加密存储密钥文件,在 CI 构建时解密。
- 使用 CI 平台的安全存储(如 GitHub Secrets、GitLab CI Variables)。
# GitHub Actions 示例:解密签名文件
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > keystore.jks
echo "${{ secrets.KEYSTORE_PASSWORD }}" > keystore-password.txt1.4 AAB vs APK
Android App Bundle(AAB)是 Google 官方推荐的应用发布格式。
Android App Bundle
AAB 是一个包含所有编译代码和资源的发布格式,将 APK 生成和签名推迟到 Google Play 完成。
AAB 优点:
- 动态分发,按需下载资源,减小初始安装包体积。
- 支持 Play Feature Delivery 和 Play Asset Delivery。
- Google Play 会自动生成优化过的 APK,包括 ABI 拆分和语言拆分。
AAB 缺点:
- 只能通过 Google Play 分发(无法直接安装到设备)。
- 需要 Google Play App Signing 支持。
- 第三方应用商店可能不支持 AAB。
Play Feature Delivery
按需功能分发(Play Feature Delivery)允许将应用功能模块化,用户可按需下载:
// build.gradle
android {
dynamicFeatures = [':feature_camera', ':feature_ar']
}
// 安装模式:
// install-time — 安装时一并下载
// conditional — 条件触发时下载(如设备特性)
// on-demand — 运行时按需下载Play Asset Delivery
Play Asset Delivery(PAD)用于分发大型游戏资源包:
android {
assetPacks = [":asset_pack_hd"]
}支持三种安装模式:install-time、fast-follow、on-demand。
ABI 拆分
ABI(Application Binary Interface)拆分针对不同 CPU 架构生成不同的 APK:
android {
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk true // 生成包含所有 ABI 的通用 APK
}
}
}语言拆分
针对不同语言生成独立 APK,减少包体积:
android {
bundle {
language {
enableSplit = true // 按语言拆分
}
density {
enableSplit = true // 按屏幕密度拆分
}
abi {
enableSplit = true // 按 ABI 拆分
}
}
}适用场景:
- 语言拆分:多语言应用,用户只下载自己需要的语言资源。
- ABI 拆分:armeabi-v7a、arm64-v8a 分别打包,避免冗余的 so 库。
1.5 Google Play 上架
开发者账号
- 注册 Google Play 开发者账号,一次性费用 25 美元。
- 需要 Google 账号,支付后即可开通。
- 企业账号和个人账号功能相同。
应用创建流程
- 登录 Google Play Console。
- 创建应用,选择默认语言和应用名称。
- 填写商品详情(Store Listing):
- 应用名称(最多 50 个字符)
- 简短描述(最多 80 个字符)
- 完整描述(最多 4000 个字符)
- 上传屏幕截图:
- 至少 2 张截图
- 支持 16:9 和 9:16 比例
- 支持手机、平板、Android TV、Wear OS 等设备类型
- 填写分级问卷(Content Rating):
- 基于 IARC 分级标准
- 涵盖暴力、性、赌博、酒精等内容项
- 设置目标受众和隐私政策:
- 隐私政策链接必须有效,适用于收集用户数据的应用
- 涉及敏感权限(通讯录、位置等)需要说明用途
- 遵守内容政策:
- 不包含色情、仇恨言论、侵权内容
- 不诱导用户点击虚假广告
发布轨道
Google Play Console 提供四种发布轨道:
| 轨道 | 说明 | 审核要求 |
|---|---|---|
| 内部测试(Internal Test) | 最多 100 人,无需审核 | 无需审核 |
| 封闭测试(Closed Test) | 指定测试组,可自定义人数 | 需要审核 |
| 开放测试(Open Test) | 公开测试,任何人都可加入 | 需要审核 |
| 正式发布(Production) | 面向全体用户 | 需要审核 |
在正式发布前,建议先进行内部测试和封闭测试。Google Play 要求新应用在正式发布前完成至少 2 周的封闭测试,并有一定数量的测试人员。
国家/地区选择与定价
- 默认选择所有国家/地区,也可按需排除。
- 免费应用不可转为付费,付费应用可以转为免费。
- 定价以美元为基准,Google 自动进行汇率转换。
- 不同国家/地区可设置不同价格。
更新说明
应用更新时需要填写更新说明(What's New),支持多语言。建议每次更新都包含清晰的更新内容和版本号。
1.6 应用内更新
应用内更新(In-app Update)是 Google Play Core 库提供的功能,允许在应用内提示用户下载更新。
更新类型
- 立即更新(Immediate Update):强制用户更新到最新版本才能继续使用,适用于关键安全补丁。
- 灵活更新(Flexible Update):在后台下载更新,用户可继续使用应用,下载完成后提示安装。
版本检查与更新流程
// 版本检查
val appUpdateManager = AppUpdateManagerFactory.create(context)
val task = appUpdateManager.appUpdateInfo
task.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
// 启动更新
appUpdateManager.startUpdateFlowForResult(
info,
activity,
AppUpdateType.IMMEDIATE,
requestCode
)
}
}Google Play 审核规则
- 应用内更新 API 仅适用于通过 Google Play 分发的应用。
- 不能绕过 Google Play 进行更新分发。
- 更新必须与原有应用使用相同的签名密钥。
- 更新流程需要符合 Google Play 开发者政策,不得使用欺骗性手段诱导更新。
二、iOS 打包与发布
2.1 Xcode Archiving
Xcode 归档(Archiving)是 iOS 应用打包的核心操作。
操作流程
- 在 Xcode 中选择目标设备和 Scheme。
- 选择 Product → Archive。
- 选择 Generic iOS Device 或连接的物理设备(不能是模拟器)。
- 归档完成后,Xcode Organizer 会自动打开。
- 点击 Distribute App,选择分发方式:
| 分发方式 | 用途 |
|---|---|
| App Store Connect | 提交到 App Store |
| Ad Hoc | 在已注册的设备上测试,最多 100 台 |
| Development | 开发设备上运行 |
| Enterprise | 企业内部分发(需企业开发者账号) |
Distribution Method
- App Store:生成提交到 App Store 的 IPA 包,自动处理签名和授权。
- Ad Hoc:生成含设备 UDID 列表的 IPA,仅限已注册设备安装。
- Development:开发包,仅限开发团队设备。
- Enterprise:企业应用,不限设备数但限组织内部使用。
2.2 证书与描述文件
Apple Developer 账号类型
| 类型 | 费用 | 用途 |
|---|---|---|
| 个人(Individual) | $99/年 | 个人开发者,上架 App Store |
| 公司(Organization) | $99/年 | 公司开发者,上架 App Store,支持多开发者管理 |
| 企业(Enterprise) | $299/年 | 企业内部应用分发,不上架 App Store |
证书类型
- Development 证书:开发阶段使用,可在开发设备上运行和调试。
- Distribution 证书:
- App Store Distribution:用于提交到 App Store。
- Ad Hoc Distribution:用于 Ad Hoc 分发。
- Enterprise Distribution:用于企业内部分发。
创建 CSR(CertificateSigningRequest)
- 打开 Mac 上的 钥匙串访问(Keychain Access)。
- 选择 钥匙串访问 → 证书助理 → 从证书颁发机构请求证书。
- 填写电子邮件地址和常用名称。
- 选择 存储到磁盘,生成
.certSigningRequest文件。
Code Signing Identity
Xcode 中的签名身份(Code Signing Identity)设置:
# 手动设置命令行签名
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath build/App.xcarchive \
CODE_SIGN_IDENTITY="Apple Distribution: Company Name (TEAMID)" \
PROVISIONING_PROFILE_SPECIFIER="match AppStore com.example.app" \
archive手动 vs 自动签名
自动签名(Automatic Signing):
- Xcode 自动管理证书和描述文件。
- 适合个人和小团队项目。
- 操作简单,勾选 "Automatically manage signing" 即可。
- 依赖 Xcode 的证书管理功能,在 CI 环境中不太可靠。
手动签名(Manual Signing):
- 开发者手动管理所有证书和描述文件。
- 适合 CI/CD 流程和大团队协作。
- 配合 Fastlane match 使用,可实现可重复的签名管理。
Provisioning Profile
描述文件(Provisioning Profile)关联了开发者证书、App ID 和已注册设备。
- Development Provisioning Profile:开发环境,包含开发证书和开发设备 UDID。
- Distribution Provisioning Profile:发布环境,包含分发证书(Ad Hoc 还需要设备列表)。
Bundle ID 命名规范
- 反向域名格式:
com.company.appname。 - App ID 支持通配符:
com.company.*。 - 通配符 App ID 无法使用某些功能(如推送通知、Apple Pay、iCloud)。
设备注册(Device Registration)
每台测试设备需要通过 UDID 注册到 Apple Developer Portal:
- 每个开发者账号每年可注册 100 台设备。
- 开发描述文件包含的设备数量没有限制。
- Ad Hoc 描述文件最多包含 100 台设备。
2.3 App Store Connect 上架
App 信息
在 App Store Connect 中创建新应用需要填写:
- 名称:App 显示名称,不超过 30 个字符。
- 副标题:补充说明应用功能,不超过 30 个字符。
- 隐私政策 URL:必须提供有效的隐私政策链接。
- 发布方式:手动发布或自动发布(审核通过后立即上架)。
截图要求
| 设备 | 屏幕尺寸 | 截图尺寸 |
|---|---|---|
| iPhone 6.7" | 1290x2796 | 6.7 英寸 |
| iPhone 6.5" | 1242x2688 | 6.5 英寸 |
| iPhone 5.5" | 1242x2208 | 5.5 英寸 |
| iPad Pro 12.9" | 2048x2732 | 3 代及以上 |
每台设备类型至少提供 1 张截图(最多 10 张),支持横向和纵向。
关键词与描述
- 关键词(Keywords):最多 100 个字符,用逗号分隔。
- 描述(Description):应用功能的详细说明,不建议包含用户评价内容。
- 支持 URL:必须提供有效的技术支持网站链接。
- 营销 URL:可选,Marketing URL。
App Review 审核指南
Apple App Store 审核指南包含五个主要类别:
2.1 App 完成度(Performance: App Completeness)
- 应用必须完成所有核心功能,不能包含空页面或未实现的功能。
- 应用不能展示测试数据或演示模式。
- 提交前需要删除所有的调试日志和断点。
4.0 设计(Design)
- 应用必须符合 Apple 的人机界面指南(HIG)。
- 应用不能只是简单的网页封装。
- UI 元素不能遮挡或模仿 App Store 界面。
5.0 法律(Legal)
- 遵守所有适用的法律法规。
- 不包含诽谤、歧视、暴力内容。
- 涉及用户数据必须提供隐私政策。
审核周期
- 通常审核周期为 24-48 小时。
- 紧急审核(Expedited Review)在特殊情况下可申请。
- 新账号或大量更新的应用审核时间可能更长。
- 审核状态变化可通过 App Store Connect 通知和邮件获取。
拒绝原因处理
常见被拒条款:
| 条款 | 常见原因 | 解决方案 |
|---|---|---|
| 2.1 | 应用不完整、崩溃、存在占位内容 | 完善功能后重新提交 |
| 2.3 | 误导性信息、刷榜行为 | 修改元数据,移除违规内容 |
| 4.0 | UI 不符合 HIG、iPad 适配问题 | 修改设计,适配不同屏幕 |
| 4.2 | 功能过于简单、只是网页封装 | 增加原生功能 |
| 5.1.1 | 数据收集未获同意 | 补充隐私权限弹窗 |
| 5.1.2 | 位置信息使用不当 | 说明位置使用场景和必要性 |
被拒后提交解决方法(Resolution Center)回复,说明修复措施或争议理由,审核团队会重新评估。
2.4 TestFlight
TestFlight 是 Apple 官方提供的应用测试分发平台。
内部测试
- 最多 100 名测试人员(需加入 Apple Developer 团队)。
- 无需审核,构建版本上传后即可邀请测试。
- 测试人员通过 TestFlight App 安装测试版本。
外部测试
- 测试人员数量不限(通过邮件邀请)。
- 首次构建版本需要经过 Beta App Review 审核。
- 审核通过后,后续同一版本的构建更新无需重新审核(除非有重大变更)。
- 外部测试人员不需要在开发者账号中注册设备 UDID。
Export Compliance 加密声明
提交到 TestFlight 或 App Store 时,需要声明应用是否使用加密:
- 使用包含加密的 API(如 HTTPS 除外):需要填写出口合规文件。
- 仅使用标准 HTTPS:选择 "No" 即可。
- 使用自定义加密或第三方加密库:可能需要提交 CCAT 文档。
在 Xcode Info.plist 中声明:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>如果设置为 false,在 App Store Connect 中导出合规页面选择 "No"。
三、App 通用性能优化
3.1 启动优化
启动时间直接影响用户的第一印象和留存率。
冷启动与热启动
| 启动类型 | 定义 | 发生条件 |
|---|---|---|
| 冷启动(Cold Start) | 应用进程不存在,从头开始初始化 | 首次启动、被系统杀死后重启 |
| 热启动(Warm Start) | 应用已在后台,切入前台 | Home 键切回、多任务切换 |
| 温启动(Lukewarm Start) | 进程存在但部分资源被回收 | 内存不足时部分释放后恢复 |
启动耗时测量
Android 启动时间测量:
# ADB 命令测量启动时间
adb shell am start -W com.example.app/.MainActivity
# 输出包含 ThisTime、TotalTime、WaitTimeiOS 启动时间测量:
# Xcode 环境变量
# 在 Scheme 的 Run Arguments 中添加
DYLD_PRINT_STATISTICS=1
DYLD_PRINT_STATISTICS_DETAILS=1启动阶段
iOS 启动阶段:
- pre-main 阶段:内核加载、dyld 动态链接器加载动态库、ObjC runtime 初始化。
- main 阶段:调用
main()函数,UIApplicationMain初始化。 - didFinishLaunch 阶段:在
application:didFinishLaunchingWithOptions:中执行应用初始化。
Android 启动阶段:
- Zygote 进程 fork:创建应用进程。
- Application 初始化:
Application.onCreate()执行。 - Activity 创建:
Activity.onCreate()、onStart()、onResume()。
优化策略
动态库优化(iOS):
- 减少不必要的动态库依赖。
- 将动态库合并为静态库。
- 减少
+load方法的使用,改用+initialize懒加载。
减少 +load 方法:
// 不推荐:+load 会在应用启动时执行
+ (void)load {
// 注册通知或方法交换
}
// 推荐:使用 +initialize 懒加载
+ (void)initialize {
if (self == [MyClass class]) {
// 仅在首次使用时执行
}
}二进制重排(iOS):
- 利用 Linker Order File 重排方法在二进制中的位置。
- 将启动路径上调用的方法排列在一起,减少 Page Fault 次数。
# Xcode Build Settings → Order File
# 文件路径:$(SRCROOT)/App.order生成 Order File 的工具:
- Clang SanitizerCoverage
- 静态扫描产物的启动调用链
启动闪屏与预加载:
Android 通过 SplashScreen API 优化冷启动体验:
// 使用 SplashScreen API(Android 12+)
// 在 themes.xml 中配置启动主题
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
</style>懒加载与预加载平衡:
- 首屏必要的资源预加载。
- 非首屏功能使用懒加载。
- 使用依赖注入容器实现按需加载。
3.2 内存优化
内存泄漏检测
iOS 内存泄漏检测:
- MLeakFinder:WeRead 团队开源的 iOS 内存泄漏检测工具,通过 hook UIViewController 和 UIView 的 dealloc 方法来检测泄漏。
// 集成 MLeakFinder
// CocoaPods: pod 'MLeaksFinder'
// 在 Debug 模式下默认启用,检测 VC 和 View 是否正常释放- Xcode Memory Graph Debugger:可视化查看内存引用关系和引用循环。
Android 内存泄漏检测:
- LeakCanary:Square 开源的 Android 内存泄漏检测库,自动检测 Activity、Fragment、ViewModel 等对象的泄漏。
// 集成 LeakCanary
// build.gradle
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
// LeakCanary 2.x 自动集成,不需要手动初始化- MAT(Memory Analyzer Tool):分析 Android Heap Dump,查找泄漏根。
# 获取 Heap Dump
adb shell am dumpheap <pid> /data/local/tmp/heapdump.hprof
adb pull /data/local/tmp/heapdump.hprof
# 使用 MAT 打开 .hprof 文件,分析 Suspects 和 Dominator Tree大图优化
图片格式对比:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| WebP | 有损/无损压缩,比 PNG 小 26% | 解码稍慢 | Android Web 图片 |
| HEIC | 比 JPEG 小 50%,高质量 | iOS 原生,兼容性差 | iOS 设备存储 |
| SVG | 矢量,无限缩放 | 复杂图形渲染慢 | 图标、Logo |
图片解压优化:
// Android: 使用 BitmapFactory.Options 控制解压
val options = BitmapFactory.Options().apply {
inSampleSize = 2 // 缩放到 1/2
inPreferredConfig = Bitmap.Config.RGB_565 // 减少颜色深度
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image, options)// iOS: 使用 ImageIO 逐步解码
func downsampleImage(at url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { return nil }
let maxDimension = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return nil }
return UIImage(cgImage: downsampledImage)
}线程管理
iOS 线程管理:
// GCD 并发控制
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 4 // 最大并发数
// GCD 队列
DispatchQueue.global(qos: .background).async {
// 后台任务
DispatchQueue.main.async {
// 主线程更新 UI
}
}Android 协程调度器:
// Android 协程调度器
viewModelScope.launch(Dispatchers.IO) {
// IO 密集型任务
val data = repository.fetchData()
withContext(Dispatchers.Main) {
// 主线程更新 UI
updateUI(data)
}
}
// 限制并发数
val limitedDispatcher = Dispatchers.IO.limitedParallelism(4)
viewModelScope.launch(limitedDispatcher) {
// 最多 4 个并发
}3.3 渲染优化
GPU 过度绘制
过度绘制(Overdraw)指一个像素点在同一帧内被绘制多次。
Android:通过开发者选项中的 "调试 GPU 过度绘制" 查看:
- 蓝色:1 次(最佳)
- 绿色:2 次
- 淡红:3 次
- 深红:4 次及以上(需要优化)
优化方法:
- 减少布局层次,使用 ConstraintLayout 替代嵌套 LinearLayout。
- 移除多余的背景色设置。
- 使用
ViewStub懒加载不常用视图。
离屏渲染(iOS)
离屏渲染(Off-Screen Rendering)发生在 GPU 在当前屏幕缓冲区外创建渲染内容,会触发额外的上下文切换。
触发离屏渲染的操作:
- 圆角(
cornerRadius+masksToBounds) - 阴影(
shadowPath未设置) - 图层蒙版(
mask) - 不透明组(
allowsGroupOpacity)
优化方案:
// 圆角优化:使用贝塞尔路径裁剪
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 8)
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
// 阴影优化:指定 shadowPath
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 8).cgPathCPU 栅格化
使用 shouldRasterize 可以将复杂层缓存为位图,避免重复渲染:
// 仅对静态内容启用
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale注意:频繁变化的内容不应开启栅格化,否则会频繁重建缓存,性能更差。
图层混合
图层混合(Blending)指 GPU 将多个半透明图层叠加渲染。不透明层不需要混合。
优化策略:
- 确保背景视图不透明(
opaque = true)。 - 减少半透明视图的层级。
- 使用与父视图背景色相同的背景色,避免透明穿透。
异步绘制
对于大量文本和复杂内容的视图,使用异步绘制避免主线程阻塞:
// iOS 异步绘制文本
DispatchQueue.global().async {
let text = "大量文本内容"
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 14)]
let textRect = text.boundingRect(with: size,
options: .usesLineFragmentOrigin,
attributes: attributes, context: nil)
// 在后台线程创建位图上下文
UIGraphicsBeginImageContextWithOptions(textRect.size, true, 0)
text.draw(in: textRect, withAttributes: attributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
DispatchQueue.main.async {
// 使用生成的图片更新 UI
imageView.image = image
}
}卡顿监控
FPS 监控(iOS):
// 使用 CADisplayLink 监控帧率
class FPSCounter {
private var displayLink: CADisplayLink?
private var lastTimestamp: CFTimeInterval = 0
private var frameCount = 0
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink?.add(to: .main, forMode: .common)
}
@objc func update() {
frameCount += 1
let elapsed = displayLink?.timestamp ?? 0 - lastTimestamp
if elapsed >= 1.0 {
let fps = Double(frameCount) / elapsed
print("FPS: \(Int(fps))")
frameCount = 0
lastTimestamp = displayLink?.timestamp ?? 0
}
}
}ANR 监控(Android):
// 使用 BlockCanary 监控主线程卡顿
// build.gradle
debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0'
// 初始化
BlockCanary.install(BlockCanaryContext()).start()
// 自定义配置,可设置阈值(默认 1000ms)主线程卡顿堆栈(iOS):
使用第三方工具如 PerformanceMonitor 或 GodEye 检测卡顿时自动记录主线程堆栈,定位问题代码:
// 卡顿时捕获主线程调用栈
func captureMainThreadStack() -> [String] {
var callStack = [String]()
let thread = Thread.main
let stackSymbols = Thread.callStackSymbols
// 解析堆栈符号,定位问题方法
return stackSymbols
}四、包体积优化
4.1 Android 包体积
APK Analyzer
Android Studio 内置的 APK Analyzer 可以分析 APK 中各部分的占比:
Build → Analyze APK → 选择 APK 文件查看:
- DEX 文件大小(代码)
- Resources 大小(资源文件)
- Assets 大小(静态资源)
- lib 大小(原生库)
- META-INF 大小(签名信息)
ABI 分包
通过在 build.gradle 中配置 ABI 拆分,减少单包体积:
android {
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk false // 不生成通用包
}
}
}资源压缩 shrinkResources
在 release 构建中开启资源压缩,移除未使用的资源:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true // 开启资源压缩
}
}
}注意:shrinkResources 需要 minifyEnabled = true。
资源混淆
使用 Android Gradle Plugin 的资源混淆功能,将资源路径缩短:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}图片压缩 WebP
Android Studio 支持将 PNG/JPEG 转为 WebP:
右键点击资源文件 → Convert to WebP支持有损(lossy)和无损(lossless)两种模式。
剔除无用资源
使用 Lint 工具扫描未使用的资源:
./gradlew lint在 lint-results.html 中查看 UnusedResources 警告。
R8 混淆
R8 是 ProGuard 的替代方案,提供代码压缩、混淆和优化:
// gradle.properties
android.enableR8 = true
// build.gradle
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}R8 优势:
- 更好的内联和常量折叠。
- 更小的输出 DEX 大小。
- 更短的构建时间(相比 ProGuard)。
AndResGuard
微信团队开源的资源混淆工具,将资源路径从 res/drawable/icon.png 缩短为 r/d/a.png:
// build.gradle (root)
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
// app/build.gradle
apply plugin: 'AndResGuard'
andResGuard {
mappingFile = file("./resource_mapping.txt")
use7zip = true
keepRoot = false
whiteList = [
"R.mipmap.ic_launcher",
]
}动态交付
使用 Play Feature Delivery 和 Play Asset Delivery 按需下载功能模块,减少初始安装包体积。
4.2 iOS 包体积
App Thinning
App Thinning 是 Apple 提供的一套包体积优化机制,包含三种技术:
- Slicing:App Store 根据用户的设备特性(CPU、屏幕等)生成个性化版本。
- Bitcode:上传中间表示(Intermediate Representation),Apple 可以后续优化而不需要开发者重新提交。
- On-Demand Resources:按需下载资源包。
// Xcode 中启用 Bitcode
// Build Settings → Enable Bitcode → YesAsset Catalog
使用 Asset Catalog 管理图片资源,Xcode 自动为不同设备生成最佳分辨率版本。
<!-- Images.xcassets 中自动管理 @1x @2x @3x 资源 -->
<!-- 支持矢量 PDF 资源,编译时自动生成位图 -->图片压缩
- 使用 ImageOptim 工具无损压缩 PNG 图片。
- 将 PNG 转为 HEIC 格式(iOS 11+ 支持)。
- 使用矢量 PDF 替代位图(适用于图标和简单图形)。
无用类与无用方法扫描
LinkMap 分析:
通过 Xcode 生成的 LinkMap 文件分析各符号的大小:
# 在 Build Settings 中设置
-WriteLinkMapFile $(BUILD_DIR)/$(CONFIGURATION)/$(PRODUCT_NAME)-LinkMap.txt
# 分析 LinkMap 文件
# 查看 __TEXT, __DATA, __LINKEDIT 各段大小无用方法扫描:
- 使用 AppCode 的代码检查工具扫描未使用的代码。
- 使用 Periphery(Swift)扫描未使用的声明。
# Periphery 扫描
periphery scan --workspace App.xcworkspace --schemes AppCocoaPods 二进制化
将 Pod 依赖预编译为 Framework,减少源码编译时间,同时通过 XCFramework 支持多平台:
# Podfile 中使用 prebuild 插件
plugin 'cocoapods-binary-cache'
# 或使用 cocoapods-packager 打包为静态库
pod 'Alamofire', :binary => trueXCFramework
XCFramework 是 Apple 推荐的跨平台 Framework 格式,支持 iOS、macOS、tvOS、watchOS 统一分发:
# 创建 XCFramework
xcodebuild -create-xcframework \
-framework ios/MyFramework.framework \
-framework ios-simulator/MyFramework.framework \
-output MyFramework.xcframework4.3 双平台优化策略对比
| 优化项 | Android | iOS |
|---|---|---|
| 图片压缩 | WebP | HEIC/Asset Catalog |
| 无用资源检测 | Lint UnusedResources | LinkMap/Periphery |
| 代码混淆 | R8/ProGuard | 编译器优化 |
| 模块化交付 | Play Feature Delivery | On-Demand Resources |
| 平台精简 | ABI 拆分 | App Thinning/Slicing |
| 资源混淆 | AndResGuard | Asset Catalog 编译优化 |
| 动态库处理 | 减少 so 库 | 合并动态库为静态库 |
| 冗余类清理 | ProGuard shrink | Periphery/AppCode 扫描 |
| 二进制预编译 | 较少使用 | CocoaPods 二进制化/XCFramework |
五、持续集成
5.1 Fastlane 自动化
Fastlane 是移动端最流行的自动化工具,覆盖构建、签名、测试、发布全流程。
安装与配置
# 安装 Fastlane
sudo gem install fastlane -NV
# 或者使用 Bundler
# Gemfile
source "https://rubygems.org"
gem "fastlane"
bundle install
# 初始化
cd project
fastlane initFastlane 核心组件
| 组件 | 功能 |
|---|---|
gym | 构建 iOS 应用 |
scan | 运行测试 |
match | 管理证书和描述文件 |
deliver | 上传到 App Store Connect |
sigh | 下载/管理描述文件 |
cert | 创建/管理证书 |
pem | 创建推送证书 |
produce | 在 App Store Connect 创建应用 |
snapshot | 自动生成多语言截图 |
boarding | TestFlight 测试员邀请页面 |
基础 Fastlane 配置
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "构建并上传到 TestFlight"
lane :beta do
match(type: "appstore")
gym(scheme: "App", configuration: "Release")
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: "本次更新内容..."
)
end
desc "构建并提交 App Store 审核"
lane :release do
match(type: "appstore")
gym(scheme: "App", configuration: "Release")
deliver(
force: true,
submit_for_review: true,
automatic_release: false
)
end
desc "自动截图"
lane :screenshots do
snapshot(
devices: ["iPhone 15 Pro", "iPhone 15 Plus", "iPad Pro (12.9-inch)"],
languages: ["zh-Hans", "en-US", "ja-JP"]
)
deliver(
skip_metadata: true,
skip_screenshots: false
)
end
desc "注册新设备"
lane :register_device do |options|
register_devices(
devices: {
"设备名称" => options[:udid]
}
)
end
end
platform :android do
desc "构建 Release APK/AAB"
lane :release do
gradle(task: "clean bundleRelease")
upload_to_play_store(
track: "internal",
release_status: "draft"
)
end
desc "构建 Debug APK"
lane :debug do
gradle(task: "assembleDebug")
end
endmatch 自动签名
match 是 Fastlane 的证书管理工具,将证书和描述文件加密存储在 Git 仓库或云存储中:
# 初始化 match
fastlane match init
# match 配置 (Matchfile)
git_url("https://github.com/company/certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.example.app", "com.example.app.extension"])
# 创建并存储证书
fastlane match appstore
# 在 CI 中使用
lane :beta do
match(type: "appstore", readonly: true)
gym(scheme: "App")
endsnapshot 自动化截图
snapshot 支持多语言、多设备的 UI 截图自动化:
# Snapshotfile
devices([
"iPhone 15 Pro Max",
"iPhone 15 Pro",
"iPad Pro (12.9-inch) (6th generation)"
])
languages(["zh-Hans", "en-US", "ja-JP"])
scheme("AppUITests")
output_directory("./screenshots")
clear_previous_screenshots(true)5.2 GitHub Actions 移动端 CI
Matrix 构建
使用 Matrix 策略并行构建不同平台和变体:
name: Mobile CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
android:
name: Android Build
runs-on: ubuntu-latest
strategy:
matrix:
abi: [armeabi-v7a, arm64-v8a]
build-type: [debug, release]
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build APK
run: ./gradlew assemble${{ matrix.build-type == 'release' && 'Release' || 'Debug' }}
ios:
name: iOS Build
runs-on: macos-14
strategy:
matrix:
scheme: [App]
configuration: [Debug, Release]
steps:
- uses: actions/checkout@v4
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install CocoaPods
run: pod install
- name: Build
run: |
xcodebuild clean build \
-workspace App.xcworkspace \
-scheme ${{ matrix.scheme }} \
-configuration ${{ matrix.configuration }} \
-sdk iphonesimulator \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NOFastlane 集成
在 GitHub Actions 中调用 Fastlane:
name: Publish to TestFlight
on:
push:
tags:
- 'v*'
jobs:
deploy-ios:
name: Deploy iOS to TestFlight
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Import Code Signing
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
run: |
bundle exec fastlane beta
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: IPA
path: ./build/*.ipa
deploy-android:
name: Deploy Android to Google Play
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/keystore.jks
- name: Fastlane Deploy
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
bundle exec fastlane android release版本号自增
在 CI 中自动递增版本号:
# Fastlane 中版本号自增
lane :increment_version do
increment_version_number(
xcodeproj: "App.xcodeproj",
bump_type: "patch" # major/minor/patch
)
increment_build_number(
xcodeproj: "App.xcodeproj",
build_number: ENV["GITHUB_RUN_NUMBER"]
)
endAndroid 版本号自增:
// build.gradle 中使用 CI 环境变量
def versionMajor = 2
def versionMinor = 1
def versionPatch = System.getenv("GITHUB_RUN_NUMBER") ?: "0"
android {
defaultConfig {
versionCode versionMajor * 100000 + versionMinor * 1000 + versionPatch.toInteger()
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
}
}分发到 Firebase App Distribution
name: Distribute via Firebase
on:
push:
branches: [develop]
jobs:
firebase-distribution:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build APK
run: ./gradlew assembleDebug
- name: Deploy to Firebase
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: qa-team
file: app/build/outputs/apk/debug/app-debug.apkSlack 通知
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"channel": "#build-notifications",
"text": "构建成功: ${{ github.repository }} (${{ github.ref_name }})",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*构建成功*: ${{ github.repository }}\n分支: ${{ github.ref_name }}\n提交: ${{ github.sha }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}APK 上传到 Google Play
name: Upload to Google Play
on:
release:
types: [published]
jobs:
upload-play-store:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/keystore.jks
- name: Build AAB
run: ./gradlew bundleRelease
- name: Upload to Google Play
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
bundle exec fastlane android deploy \
track:production \
release_status:completed5.3 移动端 CI 最佳实践
密钥管理
- 使用 CI 平台的内置密钥管理(GitHub Secrets / GitLab CI Variables)。
- 签名文件(jks、keystore、p12)Base64 编码后存入 Secrets。
- 敏感字符串(密码、API Token)直接存入 Secrets,不在代码中出现。
签名文件加密
# 使用 OpenSSL 加密签名文件
openssl enc -aes-256-cbc -salt -in keystore.jks -out keystore.jks.enc -pass pass:加密密码
# CI 中解密
openssl enc -aes-256-cbc -d -in keystore.jks.enc -out keystore.jks -pass pass:${{ secrets.ENC_PASSWORD }}构建矩阵
利用 Matrix 策略并行执行多个构建任务:
strategy:
matrix:
os: [ubuntu-latest, macos-14]
include:
- os: ubuntu-latest
task: android
- os: macos-14
task: ios缓存策略
合理缓存可大幅减少构建时间:
- Android:缓存
~/.gradle/caches和~/.gradle/wrapper。 - iOS:缓存
Pods/目录和~/Library/Caches/CocoaPods。 - 版本缓存 Key 基于锁文件的 Hash(
build.gradle/Podfile.lock)变化自动失效。
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: |
Pods
~/Library/Caches/CocoaPods
key: ${{ runner.os }}-pods-${{ hashFiles('Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-条件发布
仅对特定分支或 Tag 执行发布流程:
jobs:
deploy:
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
runs-on: macos-14
steps:
# 仅当推送版本标签时才执行发布手动审批
使用 GitHub Environments 实现手动审批:
jobs:
deploy-to-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://play.google.com/console
steps:
- name: Deploy to Production
run: bundle exec fastlane android deploy在 GitHub Repository Settings → Environments 中配置 production 环境,开启 Required reviewers 和 Wait timer。
Fastlane 全流程示例
# fastlane/Fastfile 完整示例
default_platform(:ios)
platform :ios do
before_all do
# 在 lane 执行前运行的公共操作
setup_ci if ENV["CI"]
end
desc "完整 CI 流程:测试 + 构建 + 分发"
lane :ci_build do |options|
# 1. 运行测试
scan(
scheme: "App",
devices: ["iPhone 15 Pro"],
code_coverage: true
)
# 2. 版本号自增
increment_build_number(
xcodeproj: "App.xcodeproj",
build_number: ENV["GITHUB_RUN_NUMBER"]
)
# 3. 同步证书
match(
type: options[:type] || "appstore",
readonly: true
)
# 4. 构建
gym(
scheme: "App",
configuration: options[:config] || "Release",
clean: true,
export_method: "app-store"
)
# 5. 分发
if options[:destination] == "testflight"
upload_to_testflight(
changelog: options[:changelog] || "自动化构建",
skip_waiting_for_build_processing: true
)
elsif options[:destination] == "firebase"
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID"],
groups: "qa",
firebase_cli_token: ENV["FIREBASE_TOKEN"]
)
end
end
after_all do |lane|
# lane 执行后的公共操作
slack(
message: "iOS 构建成功: #{lane}",
success: true,
default_payloads: [:git_branch, :git_author]
)
end
error do |lane, exception|
slack(
message: "iOS 构建失败: #{exception.message}",
success: false,
default_payloads: [:git_branch, :git_author]
)
end
end
platform :android do
desc "Android 完整 CI 流程"
lane :ci_build do |options|
gradle(
task: "clean",
)
# 运行测试
gradle(task: "test")
# 构建
gradle(
task: "assemble",
build_type: options[:build_type] || "Release"
)
# 分发到 Firebase
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID"],
groups: "qa",
firebase_cli_token: ENV["FIREBASE_TOKEN"]
)
end
endAppfile 配置
# fastlane/Appfile
app_identifier("com.example.app") # Bundle ID
apple_id("developer@example.com") # Apple ID
team_id("TEAMID") # Apple Developer Team ID
itc_team_id("ITCTEAMID") # App Store Connect Team ID
# Android
package_name("com.example.app")
json_key_file("path/to/google-play-api-key.json")