Android 与 iOS 核心差异
专为 Android 开发者整理,需要特别注意的设计理念和实现差异
核心理念差异
Android:组件化 vs iOS:单入口
| 方面 |
Android |
iOS |
| 应用入口 |
多个 Activity/Service/BroadcastReceiver |
单一 App 入口 |
| 页面跳转 |
Activity 栈、Broadcast、Intent |
View 切换、Navigation |
| 后台运行 |
多组件可独立运行 |
依赖 App 生命周期 |
| 应用模型 |
四大组件自由组合 |
单一 ViewController 树 |
特别注意:
- iOS 没有 Intent 机制
- iOS 没有 BroadcastReceiver
- iOS 的后台能力非常受限
- 页面跳转通过 Navigation 或 NavigationLink,不是新建实例
生命周期差异
Activity/Fragment vs UIViewController
Android Activity 生命周期:
onCreate → onStart → onResume → [运行] → onPause → onStop → onDestroy
iOS ViewController 生命周期:
init → loadView → viewDidLoad → viewWillAppear → viewDidAppear
→ [运行] → viewWillDisappear → viewDidDisappear → deinit
关键差异
| Android |
iOS |
说明 |
| onCreate |
viewDidLoad |
视图加载完成 |
| onStart |
viewWillAppear |
即将显示 |
| onResume |
viewDidAppear |
已显示 |
| onPause |
viewWillDisappear |
即将消失 |
| onStop |
viewDidDisappear |
已消失 |
| onDestroy |
deinit |
释放资源 |
特别注意:
- iOS 没有 onDestroy 的精确对应,
viewDidDisappear 不代表页面被销毁
- ViewController 可能被系统回收(内存不足时)
- SwiftUI 使用
@StateObject 和 @ObservedObject 替代生命周期管理
SwiftUI 生命周期
// SwiftUI 没有传统生命周期,用这些替代
struct ContentView: View {
@State private var data = ""
// 视图出现时调用 (类似 onResume)
.onAppear {
loadData()
}
// 视图消失时调用 (类似 onPause)
.onDisappear {
saveData()
}
}
内存管理差异
ARC vs GC
| Android |
iOS |
| 垃圾回收器 (GC) |
自动引用计数 (ARC) |
| 何时回收不确定 |
引用计数归零立即释放 |
| 内存峰值可以较高 |
内存更可控但可能过早释放 |
强引用 vs 弱引用
// Swift 内存管理
class Person {
var name: String
// init
init(name: String) {
self.name = name
}
// deinit (类似 onDestroy)
deinit {
print("\(name) 被释放")
}
}
// 强引用 - 对象不会被释放
var strongRef: Person? = Person(name: "Alice")
// 弱引用 - 不增加引用计数,可为 nil
weak var weakRef: Person? = Person(name: "Bob")
// 无主引用 - 不增加引用计数,不能为 nil(类似 Kotlin lateinit var)
unowned var unownedRef: Person
特别注意:
- 循环引用是 iOS 最常见的内存泄漏原因
- ViewController 和 Delegate 之间常用
weak 避免循环引用
- 闭包中的 self 需要使用
[weak self] 捕获
// 闭包中的循环引用
class MyViewController: UIViewController {
var data: [String] = []
func loadData() {
// ❌ 错误:闭包捕获 self,造成循环引用
network.request { response in
self.data = response
}
// ✅ 正确:使用 weak self
network.request { [weak self] response in
self?.data = response
}
// ✅ 更好:使用 weak self 并 guard
network.request { [weak self] response in
guard let self = self else { return }
self.data = response
}
}
}
Android 对比
// Android 内存管理更简单
class MyActivity : AppCompatActivity() {
// Kotlin 有 GC,不需要手动处理循环引用
fun loadData() {
network.request { response ->
data = response // 直接用,不用担心泄漏
}
}
}
UI 架构差异
命令式 vs 声明式
| Android |
iOS |
| 命令式 UI (XML/Kotlin) |
声明式 UI (SwiftUI) |
| 手动更新视图 |
状态驱动自动更新 |
| findViewById |
数据绑定 |
| RecyclerView 手动刷新 |
列表自动响应数据变化 |
状态管理
// SwiftUI 状态管理
struct CounterView: View {
// @State - 值类型,用于简单状态
@State private var count = 0
// @StateObject - 引用类型,用于 ViewModel
@StateObject private var viewModel = CounterViewModel()
// @Published - 属性发布变化,类似 LiveData
// @ObservedObject - 观察外部 ViewModel
// @EnvironmentObject - 从环境获取共享对象
}
// ViewModel
class CounterViewModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
}
Android 对比
// Android LiveData + ViewBinding
class CounterActivity : AppCompatActivity() {
private lateinit var binding: ActivityCounterBinding
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCounterBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.count.observe(this) { count ->
binding.textView.text = count.toString()
}
binding.button.setOnClickListener {
viewModel.increment()
}
}
}
特别注意:
- SwiftUI 的
@Published 类似 LiveData,但自动在主线程发布
- SwiftUI 数据流是单向的:View → Action → ViewModel → State → View
- 不能直接修改 View 中的
@State,必须通过 ViewModel
导航系统差异
Android Navigation vs iOS Navigation
| Android |
iOS |
说明 |
| Activity 栈 |
UINavigationController 栈 |
前进/后退 |
| Intent 跳转 |
NavigationLink / pushViewController |
页面跳转 |
| startActivity |
NavigationLink |
启动新页面 |
| finish() |
popViewController |
返回 |
| FragmentTransaction |
sheet/fullScreenCover |
弹窗/模态页面 |
SwiftUI 导航
// 方式 1: NavigationStack (iOS 16+)
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
// 方式 2: NavigationView + NavigationLink (旧版)
NavigationView {
List(items) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
// 方式 3: 编程式导航
@State private var showDetail = false
Button("查看详情") {
showDetail = true
}
.sheet(isPresented: $showDetail) {
DetailView()
}
Android 对比
// Android Navigation Component
val navController = findNavController(R.id.nav_host_fragment)
navController.navigate(R.id.action_list_to_detail)
navController.popBackStack()
// 或传统方式
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("item", item)
startActivity(intent)
finish()
特别注意:
- iOS 没有 Intent 的 extras 机制,传值方式不同
- SwiftUI 传值:初始化时传入、Environment 传递
- NavigationLink destination 是视图,不是字符串路径
- 返回手势 iOS 系统自动处理
权限管理差异
Android 6.0+ vs iOS
| Android |
iOS |
说明 |
| 运行时请求 |
运行时请求 |
大部分权限 |
| ActivityCompat.requestPermissions |
present(UNAuthorizationAlert) |
请求方式 |
| onRequestPermissionsResult |
CLLocationManagerDelegate 等 |
结果回调 |
| 拒绝后可再请求 |
拒绝后需跳转设置 |
用户拒绝后 |
| 分组权限 |
单一权限 |
权限粒度 |
iOS 权限请求
import CoreLocation
import Photos
import AVFoundation
class PermissionManager {
let locationManager = CLLocationManager()
func requestLocation() {
let status = locationManager.authorizationStatus
switch status {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse, .authorizedAlways:
// 已授权
locationManager.requestLocation()
case .denied, .restricted:
// 被拒绝,跳转设置
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
@unknown default:
break
}
}
func requestPhotos() {
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
DispatchQueue.main.async {
switch status {
case .authorized, .limited:
// 访问照片
break
case .denied, .restricted:
// 跳转设置
break
default:
break
}
}
}
}
}
Android 对比
class PermissionActivity : AppCompatActivity() {
private val locationPermission = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
fun requestLocation() {
if (checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
// 已授权
} else {
requestPermissions(locationPermission, REQUEST_CODE)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 授权成功
}
}
}
}
特别注意:
- iOS 权限说明必须写在 Info.plist 的对应 key 下
- 用户拒绝后只能跳转设置,无法再次弹出请求
- iOS 蓝牙权限需要
NSBluetoothAlwaysUsageDescription
线程模型差异
Main Thread vs UI Thread
| Android |
iOS |
| 主线程 = UI 线程 |
主线程 = UI 线程 |
| 所有 UI 操作在主线程 |
所有 UI 操作在主线程 |
| Handler / runOnUiThread |
DispatchQueue.main |
| Coroutines (多线程) |
async/await / GCD |
| 后台线程无限制 |
后台执行有限制 |
Swift 并发
// Swift 5.5+ async/await (类似 Kotlin Coroutines)
func fetchUser() async throws -> User {
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// 调用
Task {
do {
let user = try await fetchUser()
await MainActor.run {
// 更新 UI - 必须回到主线程
self.nameLabel.text = user.name
}
} catch {
print("Error: \(error)")
}
}
// GCD 方式 (类似 Handler)
DispatchQueue.global(qos: .background).async {
// 后台执行
let result = self.doHeavyWork()
DispatchQueue.main.async {
// 回到主线程更新 UI
self.label.text = result
}
}
Android 对比
// Kotlin Coroutines
GlobalScope.launch(Dispatchers.IO) {
val user = api.fetchUser()
withContext(Dispatchers.Main) {
// 更新 UI
nameText.text = user.name
}
}
// 或
runOnUiThread {
nameText.text = user.name
}
特别注意:
- SwiftUI 的 View 更新自动在主线程
@Published 属性发布默认在主线程
- 长时间后台任务 iOS 会被系统挂起
- Background Tasks API 有限制
包管理差异
Gradle vs CocoaPods / SPM
| Android |
iOS |
说明 |
| build.gradle |
Podfile / Package.swift |
依赖声明 |
| Gradle |
CocoaPods / SPM |
包管理器 |
| Maven/Gradle Repository |
CocoaPods trunk / Git |
依赖源 |
| implementation 'com.android...' |
pod 'Alamofire', '~> 5.0' |
声明方式 |
| sync project |
pod install |
安装依赖 |
| aar |
framework |
产物格式 |
Swift Package Manager
// Package.swift
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v15)],
products: [
.library(
name: "MyApp",
targets: ["MyApp"]),
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "Alamofire", package: "Alamofire")
])
]
)
CocoaPods
# Podfile
platform :ios, '15.0'
use_frameworks!
target 'MyApp' do
pod 'Alamofire', '~> 5.0'
pod 'SnapKit', '~> 5.0'
pod 'Kingfisher', '~> 7.0'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
特别注意:
- Xcode 需要打开
.xcworkspace 而不是 .xcodeproj(使用 CocoaPods 时)
- SPM 和 CocoaPods 不能同时使用做主依赖管理
- 版本号写法不同:
~> 5.0 等于 >= 5.0 and < 6.0
其他重要差异
1. 文件系统
| Android |
iOS |
说明 |
| /data/data/package/ |
App Sandbox |
应用私有目录 |
| getExternalFilesDir() |
Documents/ |
外部存储 |
| SAF (Storage Access Framework) |
UIDocument / FileManager |
文件访问 |
2. 应用签名
| Android |
iOS |
说明 |
| debug.keystore / release.jks |
Certificate + Provisioning |
签名文件 |
| v1 / v2 / v3 签名 |
代码签名 (Code Sign) |
签名方式 |
| Play Store 自动签名 |
必须手动管理证书 |
证书管理 |
# iOS 签名相关命令
security find-identity -v -p codesigning # 列出证书
3. 应用审查
| Android |
iOS |
说明 |
| 审核 1-7 天 |
审核 1-3 天 (首次 7-14 天) |
审核时间 |
| 分级制度 |
评分制度 |
内容分级 |
| 权限声明可选 |
权限说明必须完整 |
权限说明 |
4. 后台执行
| Android |
iOS |
说明 |
| 多任务后台 |
严格限制 |
后台能力 |
| JobScheduler / WorkManager |
Background Tasks (有限) |
后台任务 |
| 推送唤醒 |
APNs 推送 |
推送机制 |
// iOS 后台刷新
BGAppRefreshTask.shared.register(
forTaskWithIdentifier: "com.example.refresh"
) { task in
// 执行后台刷新
task.setTaskCompleted(success: true)
}
5. 深链接 (Deep Link)
| Android |
iOS |
| Intent Filter |
URL Scheme |
| App Links |
Universal Links |
| Deferred Deep Link |
Universal Links |
// iOS Universal Links 配置
// 需要在 Apple Developer 配置 associated domains
// applinks:yourdomain.com
// 处理
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
// 处理 deep link
return handleDeepLink(url)
}
常见踩坑清单
必踩坑及解决方案
| # |
踩坑点 |
问题描述 |
解决方案 |
| 1 |
闭包循环引用 |
内存泄漏 |
始终使用 [weak self] |
| 2 |
UI 在后台线程更新 |
崩溃 |
用 DispatchQueue.main |
| 3 |
权限 Info.plist 缺失 |
崩溃 |
添加对应 UsageDescription |
| 4 |
忘记处理 Optional |
编译错误 |
用 guard let / if let |
| 5 |
Navigation 返回手势 |
意外返回 |
navigationBarBackButtonHidden(true) |
| 6 |
@Published 默认主线程 |
行为不符预期 |
注意线程切换 |
| 7 |
async/await 主线程假象 |
死锁 |
await MainActor.run {} |
| 8 |
xcworkspace vs xcodeproj |
找不到文件 |
用 workspace |
| 9 |
CocoaPods 冲突 |
编译失败 |
pod deintegrate && pod install |
| 10 |
真机签名失效 |
无法运行 |
更新证书和描述文件 |
快速对比速查表
| 方面 |
Android |
iOS |
| 语言 |
Kotlin / Java |
Swift |
| UI 框架 |
Jetpack Compose / XML |
SwiftUI / UIKit |
| 架构组件 |
ViewModel, LiveData, Room |
ObservableObject, @Published, SQLite |
| 依赖管理 |
Gradle |
CocoaPods / SPM |
| 包格式 |
APK / AAB |
IPA |
| 应用商店 |
Play Store |
App Store |
| 导航 |
NavController / Intent |
NavigationStack / NavigationLink |
| 权限请求 |
requestPermissions |
框架特定 API |
| 后台任务 |
WorkManager |
BackgroundTasks (受限) |
| 生命周期 |
onCreate/onDestroy |
viewDidLoad/deinit |
| 内存管理 |
GC |
ARC |
| 线程 |
Coroutines |
async/await / GCD |
| 网络 |
Retrofit / OkHttp |
Alamofire / URLSession |
| 图片加载 |
Coil / Glide |
Kingfisher / AsyncImage |
| 布局 |
ConstraintLayout |
SwiftUI Stacks / Auto Layout |
| 列表 |
RecyclerView |
List / LazyVStack |
| 调试 |
Logcat / Android Studio |
Xcode / Console |
这份文档帮助 Android 开发者快速识别 iOS 开发中的关键差异,避免常见陷阱。