Android 原生开发文档
Android 是 Google 开发的基于 Linux 内核的开源移动操作系统,广泛应用于手机、平板、可穿戴设备、车载系统等。本文档系统介绍 Android 原生开发的核心概念、架构演进、Jetpack Compose 声明式 UI、网络与数据、后台任务与推送、性能优化等内容。
1. Android 架构演进
1.1 系统架构
Android 系统采用分层架构,从上到下依次为:
1.1.1 应用层(System Apps)
系统内置应用和用户安装的第三方应用,包括电话、短信、浏览器、联系人等。应用层使用 Java/Kotlin 语言开发,运行在 Android Runtime 之上。
1.1.2 Java API Framework
Google 提供的 Android 框架 API,包括:
- View System:UI 组件体系(TextView、Button、RecyclerView 等)
- Content Providers:数据共享机制
- Resource Manager:资源文件管理
- Notification Manager:通知管理
- Activity Manager:Activity 生命周期管理
- Window Manager:窗口管理
- Location Manager:位置服务
- Package Manager:包管理
1.1.3 Android Runtime 与系统库(Android Runtime / Native Libraries)
Android Runtime (ART):
- Android 5.0 起取代 Dalvik 成为官方运行时
- 支持 Ahead-of-Time (AOT) 和 Just-in-Time (JIT) 编译
- 引入 Profile-guided compilation (PGO) 优化
- 支持压缩垃圾回收 (Concurrent Compact GC)
- Android 12+ 支持 ART Mainline 模块化更新
Native Libraries:
- C/C++ 原生库,通过 JNI 调用
- WebView:基于 Chromium 的渲染引擎
- MediaCodec:音视频编解码
- OpenGL ES / Vulkan:图形渲染
- SQLite:本地数据库引擎
- SSL/TLS:加密通信(BoringSSL)
1.1.4 硬件抽象层(Hardware Abstraction Layer, HAL)
HAL 为上层框架提供统一的硬件访问接口,屏蔽不同硬件厂商的驱动差异。每种硬件模块(Camera、Audio、Sensor、GPS 等)对应一个 HAL 接口实现。
应用层 (System Apps)
|
Java API Framework
|
Android Runtime (ART) / Native Libraries
|
硬件抽象层 (HAL)
|
Linux Kernel1.1.5 Linux Kernel
Android 基于 Linux 长期支持版(LTS)内核,包含以下增强:
- Binder IPC:进程间通信机制
- wakelocks:电源管理
- ashmem:匿名共享内存
- Low Memory Killer:低内存时杀进程
- SELinux:强制访问控制
- Treble 架构:Android 8.0+ 引入 Project Treble,将系统框架与厂商 HAL 解耦,加速系统更新
1.2 应用组件
1.2.1 Activity
Activity 是用户界面的入口,管理一个窗口的内容。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
}
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
}
override fun onStop() {
super.onStop()
}
override fun onDestroy() {
super.onDestroy()
}
// 处理配置变更(如屏幕旋转)
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
}
// 处理返回结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}
}Activity 启动模式:
| 模式 | 说明 |
|---|---|
| standard | 默认模式,每次启动创建新实例 |
| singleTop | 如果栈顶已有该 Activity 实例,复用不重建 |
| singleTask | 如果栈中已有实例,将其上方的 Activity 全部出栈,复用该实例 |
| singleInstance | 该 Activity 单独在一个任务栈中 |
// AndroidManifest.xml 中配置启动模式
<activity
android:name=".ui.DetailActivity"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize" />
// 传递参数
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("KEY_ID", 1001)
putExtra("KEY_TITLE", "Detail")
}
startActivity(intent)
// 或使用 Activity Result API
startActivityForResult(intent, REQUEST_CODE)1.2.2 Fragment
Fragment 是 Activity 内可重用的 UI 片段,有自己的生命周期。
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.text = "Home Fragment"
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
// Activity 中添加 Fragment
supportFragmentManager.commit {
replace(R.id.fragment_container, HomeFragment())
addToBackStack(null)
}Fragment 生命周期: onAttach -> onCreate -> onCreateView -> onViewCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
1.2.3 Service
Service 用于执行后台长时间运行的操作,不提供用户界面。
// 前台服务
class DownloadService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Downloading")
.setContentText("File is downloading...")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.build()
startForeground(NOTIFICATION_ID, notification)
// 执行下载任务
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Download Service",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
override fun onDestroy() {
super.onDestroy()
stopForeground(STOP_FOREGROUND_REMOVE)
}
companion object {
const val CHANNEL_ID = "download_service_channel"
const val NOTIFICATION_ID = 1
}
}绑定服务(Bound Service):
class MyBoundService : Service() {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): MyBoundService = this@MyBoundService
}
override fun onBind(intent: Intent?): IBinder = binder
fun getData(): String = "Hello from BoundService"
}
// 客户端绑定
class MainActivity : AppCompatActivity() {
private var myService: MyBoundService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
myService = (service as MyBoundService.LocalBinder).getService()
}
override fun onServiceDisconnected(name: ComponentName?) {
myService = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindService(Intent(this, MyBoundService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
unbindService(connection)
}
}1.2.4 BroadcastReceiver
BroadcastReceiver 用于接收系统或应用发送的广播事件。
// 静态注册(AndroidManifest.xml)
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// 设备启动完成后的处理
}
}
}
// 动态注册
class MainActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
val isEnabled = intent.getBooleanExtra("state", false)
}
}
}
override fun onResume() {
super.onResume()
val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
registerReceiver(receiver, filter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
}Android 14+ 广播限制:
- 应用无法向清单注册的非上下文广播发送隐式广播
- 必须使用
Context.registerReceiver()动态注册 - 导出广播需声明
RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED
1.2.5 ContentProvider
ContentProvider 用于跨进程数据共享。
class MyContentProvider : ContentProvider() {
private lateinit var dbHelper: MyDbHelper
override fun onCreate(): Boolean {
dbHelper = MyDbHelper(context)
return true
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
return dbHelper.readableDatabase.query(
"my_table", projection, selection, selectionArgs, null, null, sortOrder
)
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val id = dbHelper.writableDatabase.insert("my_table", null, values)
context?.contentResolver?.notifyChange(uri, null)
return ContentUris.withAppendedId(uri, id)
}
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
return dbHelper.writableDatabase.update("my_table", values, selection, selectionArgs)
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return dbHelper.writableDatabase.delete("my_table", selection, selectionArgs)
}
override fun getType(uri: Uri): String? = null
}1.2.6 Intent
Intent 是组件间通信的载体,支持显式 Intent 和隐式 Intent。
// 显式 Intent:启动特定 Activity
val explicitIntent = Intent(this, DetailActivity::class.java)
startActivity(explicitIntent)
// 隐式 Intent:系统根据 action 匹配
val implicitIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://www.example.com")
}
startActivity(implicitIntent)
// Intent 传递数据
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("user_id", 123)
putExtra("user_name", "John")
putExtra("is_premium", true)
putExtra("items", arrayListOf("item1", "item2"))
}
startActivity(intent)
// 接收数据
val userId = intent.getIntExtra("user_id", 0)
val userName = intent.getStringExtra("user_name")
// Intent 中 Bundle 的限制
// Intent 内的 Bundle 数据大小限制约 500KB-1MB(不同 ROM 不同)
// 大数据量建议使用持久化存储 + 传递 ID1.2.7 Application
Application 是应用全局的单例,在应用进程创建时初始化。
class MyApplication : Application() {
lateinit var appContainer: AppContainer
private set
override fun onCreate() {
super.onCreate()
instance = this
appContainer = AppContainer(this)
// 初始化第三方 SDK
initThirdPartySDKs()
}
private fun initThirdPartySDKs() {
// LeakCanary
// if (LeakCanary.isInAnalyzerProcess(this)) return
// LeakCanary.install(this)
}
override fun onLowMemory() {
super.onLowMemory()
// 低内存时的清理
}
companion object {
lateinit var instance: MyApplication
private set
}
}
// AndroidManifest.xml 中声明
// <application android:name=".MyApplication" ... />1.3 Build 系统
1.3.1 Gradle + Kotlin DSL
Android 使用 Gradle 构建系统,推荐使用 Kotlin DSL 替代 Groovy。
// settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyApp"
include(":app")
include(":core:network")
include(":core:ui")
include(":feature:home")
include(":feature:profile")// 根目录 build.gradle.kts
plugins {
id("com.android.application") version "8.2.0" apply false
id("com.android.library") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
}1.3.2 Version Catalogs (libs.versions.toml)
使用 TOML 文件统一管理依赖版本。
# gradle/libs.versions.toml
[versions]
kotlin = "1.9.20"
agp = "8.2.0"
compose-bom = "2024.01.00"
compose-compiler = "1.5.5"
retrofit = "2.9.0"
okhttp = "4.12.0"
room = "2.6.1"
hilt = "2.48.1"
navigation-compose = "2.7.6"
coroutines = "1.7.3"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
compose-ui = { module = "androidx.compose.ui:ui" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.1.0" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }// app/build.gradle.kts 引用
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.example.myapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
}
create("production") {
dimension = "environment"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.5"
}
}
dependencies {
implementation(project(":core:network"))
implementation(project(":core:ui"))
implementation(project(":feature:home"))
implementation(project(":feature:profile"))
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.compose.navigation)
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose)
implementation(libs.coroutines.core)
implementation(libs.coroutines.android)
}1.3.3 构建变体(Build Variants)
每个构建变体 = 构建类型 (Build Type) x 产品风味 (Product Flavor)。
构建类型: debug, release
产品风味: dev, staging, production
构建变体: devDebug, devRelease, stagingDebug, stagingRelease, productionDebug, productionRelease// 每个变体可配置独立参数
android {
productFlavors {
create("dev") {
dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
}
create("production") {
dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
}
}
}
// 代码中使用
// val baseUrl = BuildConfig.API_BASE_URL1.3.4 签名配置 (SigningConfig)
android {
signingConfigs {
create("release") {
storeFile = file("keystore/release.keystore")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}1.3.5 ProGuard / R8 混淆
# proguard-rules.pro
# 保留实体类
-keep class com.example.myapp.data.model.** { *; }
# 保留 Retrofit 接口
-keep,allowobfuscation interface com.example.myapp.data.remote.*Api
# 保留 Gson 序列化类
-keep class com.google.gson.** { *; }
-keepattributes Signature
-keepattributes *Annotation*
# 保留 Kotlin 协程相关
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# 移除日志
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
public static int i(...);
}// R8 全局配置 (gradle.properties)
android.enableR8.fullMode=true
android.enableR8.libraries=false1.4 AndroidManifest
1.4.1 基本结构
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 权限声明 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 硬件功能声明 -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<!-- SDK 版本 -->
<uses-sdk android:minSdkVersion="26"
android:targetSdkVersion="34"
android:maxSdkVersion="34" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApp"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="false"
tools:targetApi="34">
<!-- Activity 注册 -->
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.DetailActivity"
android:exported="false"
android:parentActivityName=".ui.MainActivity" />
<!-- Service 注册 -->
<service
android:name=".service.DownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- Receiver 注册 -->
<receiver
android:name=".receiver.BootCompletedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Provider 注册 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup" />
</provider>
<!-- 元数据 -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
</application>
</manifest>1.4.2 Application 重要属性
| 属性 | 说明 |
|---|---|
android:allowBackup | 是否允许应用数据备份 |
android:dataExtractionRules | Android 12+ 数据提取规则 |
android:networkSecurityConfig | 网络安全配置(明文 HTTP、证书锁定) |
android:hardwareAccelerated | 是否启用硬件加速 |
android:largeHeap | 是否请求大堆内存 |
android:theme | 应用全局主题 |
android:supportsRtl | 是否支持从右到左布局 |
android:requestLegacyExternalStorage | Android 10+ 旧版存储模式(targetSdk 30+ 不应使用) |
1.4.3 intent-filter 匹配规则
<!-- 隐式 Intent 匹配 -->
<activity android:name=".ui.ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<!-- 深层链接 (Deep Link) -->
<activity android:name=".ui.DeepLinkActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="www.example.com"
android:pathPrefix="/product" />
</intent-filter>
</activity>2. Jetpack Compose 声明式 UI
2.1 Compose 基础
2.1.1 @Composable 函数
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Composable
fun UserProfile(user: User) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = user.name, style = MaterialTheme.typography.headlineMedium)
Text(text = user.email, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(8.dp))
Text(text = user.bio)
}
}2.1.2 基本布局组件
@Composable
fun LayoutDemo() {
// Column: 垂直排列
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
// Row: 水平排列
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Favorite, contentDescription = null)
Text("Like")
Button(onClick = { }) { Text("Action") }
}
// Box: 层叠布局(类似 FrameLayout)
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Gray)
) {
Text(
text = "Center Text",
modifier = Modifier.align(Alignment.Center)
)
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
)
}
}2.1.3 Modifier 链
@Composable
fun ModifierChainDemo() {
Text(
text = "Styled Text",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp))
.padding(12.dp)
.clickable { /* handle click */ }
.then(Modifier.shadow(4.dp))
)
}2.1.4 常用组件
@Composable
fun CommonComponents() {
// Text
Text(
text = "Hello Compose",
color = MaterialTheme.colorScheme.primary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
// Image
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "Avatar",
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
// Button
Button(
onClick = { /* action */ },
enabled = true,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
shape = RoundedCornerShape(8.dp)
) {
Text("Submit")
}
// TextField
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Username") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
isError = text.length < 3
)
// Icon
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}2.1.5 Scaffold 与 TopAppBar / BottomBar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val navController = rememberNavController()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = { Text("My App") },
navigationIcon = {
IconButton(onClick = { /* open drawer */ }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
actions = {
IconButton(onClick = { /* search */ }) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
IconButton(onClick = { /* settings */ }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
},
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("profile") { ProfileScreen() }
composable("settings") { SettingsScreen() }
}
}
}
}2.1.6 列表:LazyColumn / LazyRow
@Composable
fun UserList(users: List<User>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 固定头部
item {
Text(
text = "Users (${users.size})",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
)
}
// 使用索引
itemsIndexed(users) { index, user ->
UserCard(user = user, isEven = index % 2 == 0)
}
// 分隔线
items(users.size) { index ->
UserRow(user = users[index])
if (index < users.size - 1) {
Divider()
}
}
// 底部加载状态
item {
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
}
// LazyRow 水平滚动列表
@Composable
fun CategoryRow(categories: List<Category>) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categories) { category ->
CategoryChip(category = category)
}
}
}2.2 状态管理
2.2.1 State / MutableState
@Composable
fun Counter() {
// remember: 在重组时保持状态
var count by remember { mutableStateOf(0) }
Column(modifier = Modifier.padding(16.dp)) {
Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}2.2.2 rememberSaveable
在配置变更(如屏幕旋转)后仍然保持状态。
@Composable
fun SaveableStateDemo() {
// rememberSaveable 通过 Bundle 保存状态,支持配置变更后的恢复
var text by rememberSaveable { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Input") }
)
}
// 自定义 Saver
data class UserState(val name: String, val age: Int)
val UserStateSaver = run {
val nameKey = "name"
val ageKey = "age"
mapSaver(
save = { mapOf(nameKey to it.name, ageKey to it.age) },
restore = { UserState(it[nameKey] as String, it[ageKey] as Int) }
)
}
@Composable
fun CustomSaverDemo() {
var userState by rememberSaveable(stateSaver = UserStateSaver) {
mutableStateOf(UserState("", 0))
}
}2.2.3 derivedStateOf
从其他状态派生新状态,避免不必要的重组。
@Composable
fun TodoList(todos: List<Todo>) {
val completedCount by remember {
derivedStateOf { todos.count { it.isCompleted } }
}
val progress by remember {
derivedStateOf {
if (todos.isEmpty()) 0f
else completedCount.toFloat() / todos.size
}
}
Text("Completed: $completedCount / ${todos.size}")
LinearProgressIndicator(progress = progress)
}2.2.4 StateFlow 与 collectAsState
// ViewModel
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
private val _loadingState = MutableStateFlow(false)
val loadingState: StateFlow<Boolean> = _loadingState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_loadingState.value = true
try {
val data = repository.fetchData()
_uiState.update { it.copy(data = data, error = null) }
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message) }
} finally {
_loadingState.value = false
}
}
}
}
data class MainUiState(
val data: List<String> = emptyList(),
val error: String? = null
)
// Composable
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
val isLoading by viewModel.loadingState.collectAsState()
Box(modifier = Modifier.fillMaxSize()) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
uiState.error?.let { error ->
Text("Error: $error", color = MaterialTheme.colorScheme.error)
}
LazyColumn {
items(uiState.data) { item ->
Text(item)
}
}
}
}2.2.5 ViewModel 与 viewModel()
// 使用 hiltViewModel()
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
_user.value = userRepository.getCurrentUser()
}
}
}
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel()
) {
val user by viewModel.user.collectAsState()
user?.let {
Text("Welcome, ${it.name}")
}
}2.2.6 SideEffect
@Composable
fun SideEffectDemo() {
// LaunchedEffect: 进入 Composition 时启动协程,离开时取消
LaunchedEffect(Unit) {
// 相当于 lifecycleScope.launch
val data = repository.fetchData()
// 更新 UI 状态
}
// LaunchedEffect 的 key 变化时会重启协程
var userId by remember { mutableStateOf("") }
LaunchedEffect(userId) {
if (userId.isNotBlank()) {
val user = repository.getUser(userId)
// 更新 UI
}
}
// DisposableEffect: 进入时执行,离开时清理
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> { /* 前台 */ }
Lifecycle.Event.ON_PAUSE -> { /* 后台 */ }
else -> {}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
// rememberCoroutineScope: 获取一个作用域,在 Composable 外部启动协程
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
val result = repository.performAction()
// 处理结果
}
}) {
Text("Execute")
}
// snapshotFlow: 将 Composable 状态转换为 Flow
var searchQuery by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { searchQuery }
.debounce(300)
.filter { it.length >= 2 }
.collect { query ->
// 防抖搜索
repository.search(query)
}
}
// SideEffect: 在每次重组后执行,不影响 Composition
SideEffect {
// 告诉外部系统当前的状态
analytics.trackScreen("Profile")
}
}2.3 导航(Navigation Compose)
2.3.1 基本导航
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetail = { itemId ->
navController.navigate("detail/$itemId")
}
)
}
composable(
route = "detail/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.IntType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailScreen(
itemId = itemId,
onNavigateBack = { navController.popBackStack() }
)
}
}
}
// 传递复杂参数
composable(
route = "profile/{userId}?source={source}",
arguments = listOf(
navArgument("userId") { type = NavType.IntType },
navArgument("source") {
type = NavType.StringType
defaultValue = "direct"
}
)
) { entry ->
ProfileScreen(
userId = entry.arguments?.getInt("userId") ?: 0,
source = entry.arguments?.getString("source") ?: "direct"
)
}
// 导航
navController.navigate("profile/123?source=notification")2.3.2 深层链接(Deep Link)
// 定义深层链接
composable(
route = "product/{productId}",
arguments = listOf(navArgument("productId") { type = NavType.IntType }),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://product/{productId}" },
navDeepLink { uriPattern = "https://www.example.com/product/{productId}" }
)
) { entry ->
val productId = entry.arguments?.getInt("productId") ?: 0
ProductScreen(productId)
}
// Activity 中处理深层链接
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// intent.data 中携带深层链接 URI
val deepLinkUri = intent.data?.toString()
}
}2.3.3 Bottom Navigation 集成
sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object Home : Screen("home", "Home", Icons.Default.Home)
object Explore : Screen("explore", "Explore", Icons.Default.Explore)
object Profile : Screen("profile", "Profile", Icons.Default.Person)
}
val screens = listOf(Screen.Home, Screen.Explore, Screen.Profile)
@Composable
fun BottomNavApp() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
screens.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = screen.label) },
label = { Text(screen.label) },
selected = currentDestination?.route == screen.route,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) { HomeScreen() }
composable(Screen.Explore.route) { ExploreScreen() }
composable(Screen.Profile.route) { ProfileScreen() }
}
}
}
}2.3.4 导航动画
NavHost(
navController = navController,
startDestination = "list"
) {
composable(
route = "list",
enterTransition = { fadeIn(animationSpec = tween(300)) },
exitTransition = { fadeOut(animationSpec = tween(300)) }
) { ListScreen() }
composable(
route = "detail/{id}",
enterTransition = {
slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(300)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -it },
animationSpec = tween(300)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -it },
animationSpec = tween(300)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(300)
)
}
) { entry ->
DetailScreen(id = entry.arguments?.getInt("id") ?: 0)
}
}2.4 主题
2.4.1 MaterialTheme
// 自定义颜色
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
primaryContainer = Color(0xFFBBDEFB),
secondary = Color(0xFF43A047),
onSecondary = Color.White,
background = Color(0xFFF5F5F5),
surface = Color.White,
error = Color(0xFFD32F2F),
onBackground = Color(0xFF212121),
onSurface = Color(0xFF212121)
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF0D47A1),
primaryContainer = Color(0xFF1565C0),
secondary = Color(0xFFA5D6A7),
onSecondary = Color(0xFF1B5E20),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
error = Color(0xFFEF5350),
onBackground = Color(0xFFE0E0E0),
onSurface = Color(0xFFE0E0E0)
)
// 自定义字体
private val AppTypography = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp
),
titleLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
)
)
// 自定义形状
private val AppShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(16.dp)
)2.4.2 暗黑主题与动态颜色
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
// Android 12+ 动态颜色
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
// 使用
@Composable
fun App() {
MyAppTheme(dynamicColor = true) {
// 应用内容
Scaffold {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
// 主题中各颜色的使用
@Composable
fun ThemeUsage() {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Text(
text = "Themed Text",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge
)
}
}3. Android 资源与 UI
3.1 资源系统
3.1.1 资源目录结构
res/
drawable/ # 图片资源(PNG, JPG, WebP, XML vector drawable)
mipmap/ # 应用图标(mipmap-mdpi, mipmap-hdpi, mipmap-xhdpi 等)
values/ # 字符串、颜色、尺寸、样式
strings.xml
colors.xml
dimens.xml
themes.xml
arrays.xml
layout/ # XML 布局文件
menu/ # 菜单定义
xml/ # 任意 XML 配置文件
anim/ # 补间动画
animator/ # 属性动画
raw/ # 原始文件(音频、视频等)
font/ # 字体文件
navigation/ # Navigation Graph 定义3.1.2 资源引用
// 在 Kotlin 中引用资源
val appName: String = getString(R.string.app_name)
val primaryColor: Int = getColor(R.color.primary)
val icon: Drawable = getDrawable(R.drawable.ic_launcher)
val layout: Int = R.layout.activity_main
val id: Int = R.id.button_submit
val dimension: Float = resources.getDimension(R.dimen.margin_normal)
// 在 XML 中引用资源
// @string/app_name, @color/primary, @drawable/icon, @dimen/margin_normal
// 在 Composable 中引用
@Composable
fun ResourceUsage() {
val context = LocalContext.current
val appName = stringResource(R.string.app_name)
val icon = painterResource(R.drawable.ic_avatar)
val primaryColor = colorResource(R.color.primary)
val dimension = dimensionResource(R.dimen.margin_normal)
}3.1.3 资源限定符
Android 通过资源限定符机制支持不同配置下的资源适配:
| 限定符类型 | 示例 | 说明 |
|---|---|---|
| 语言 | values-zh values-en values-zh-rCN | 本地化 |
| 屏幕密度 | drawable-mdpi drawable-hdpi drawable-xhdpi drawable-xxhdpi drawable-xxxhdpi | 不同密度图片 |
| 屏幕尺寸 | layout-sw600dp layout-sw720dp | 最小宽度 |
| 方向 | layout-land layout-port | 横屏/竖屏 |
| 夜间模式 | values-night drawable-night | 暗黑模式 |
| API 级别 | values-v26 drawable-v24 | API 版本适配 |
| 屏幕比例 | layout-long layout-notlong | 长屏适配 |
<!-- 本地化示例 -->
<!-- values/strings.xml (默认,英文) -->
<string name="welcome">Welcome</string>
<string name="login">Login</string>
<!-- values-zh/strings.xml (中文) -->
<string name="welcome">欢迎</string>
<string name="login">登录</string>
<!-- values-zh-rTW/strings.xml (繁体中文) -->
<string name="welcome">歡迎</string>
<string name="login">登入</string>// 在代码中获取设备语言
val currentLocale = resources.configuration.locales.get(0)
val languageCode = currentLocale.language // "zh", "en", etc.
// 动态设置语言(需要 recreate Activity)
fun setAppLocale(context: Context, languageCode: String) {
val locale = Locale(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}3.1.4 资源文件示例
<!-- res/values/colors.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#FF1976D2</color>
<color name="primary_dark">#FF1565C0</color>
<color name="accent">#FF43A047</color>
<color name="background">#FFF5F5F5</color>
<color name="surface">#FFFFFFFF</color>
<color name="text_primary">#DE000000</color>
<color name="text_secondary">#8A000000</color>
<color name="divider">#1F000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
<!-- res/values/dimens.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="margin_small">8dp</dimen>
<dimen name="margin_normal">16dp</dimen>
<dimen name="margin_large">24dp</dimen>
<dimen name="text_size_small">12sp</dimen>
<dimen name="text_size_normal">14sp</dimen>
<dimen name="text_size_large">16sp</dimen>
<dimen name="text_size_title">20sp</dimen>
<dimen name="app_bar_height">56dp</dimen>
<dimen name="fab_size">56dp</dimen>
</resources>3.2 布局
3.2.1 ConstraintLayout
ConstraintLayout 是 Google 推荐的 View 体系布局容器,使用约束关系定位子 View。
<!-- activity_constraint.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<!-- 基本约束 -->
<TextView
android:id="@+id/textTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 相对其他 View 约束 -->
<TextView
android:id="@+id/textSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Subtitle"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@+id/textTitle"
app:layout_constraintTop_toBottomOf="@+id/textTitle"
app:layout_constraintBottom_toBottomOf="@+id/imageAvatar" />
<!-- 宽高比 -->
<ImageView
android:id="@+id/imageAvatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 链 (Chain) -->
<Button
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnConfirm"
app:layout_constraintTop_toBottomOf="@+id/imageAvatar"
app:layout_constraintHorizontal_chainStyle="spread" />
<Button
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Confirm"
app:layout_constraintStart_toEndOf="@+id/btnCancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageAvatar" />
<!-- Guideline (辅助线) -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<!-- Barrier (屏障) -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="textTitle,textSubtitle" />
<!-- 匹配父容器 -->
<View
android:id="@+id/viewDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>3.2.2 LinearLayout
<!-- 基础 LinearLayout -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Item 1"
android:textSize="16sp"
android:padding="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Item 2"
android:textSize="16sp"
android:padding="8dp" />
<!-- weight 分配剩余空间 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Left"
android:layout_marginEnd="8dp" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Right"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>3.2.3 FrameLayout 与 RelativeLayout
<!-- FrameLayout: 层叠布局,适合单子 View 或叠加 -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/background" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:text="Overlay Text"
android:textColor="@android:color/white"
android:padding="8dp" />
</FrameLayout>
<!-- RelativeLayout: 相对定位(已不被推荐,应使用 ConstraintLayout) -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/avatar" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/avatar"
android:layout_alignTop="@+id/avatar"
android:text="Name"
android:layout_marginStart="12dp" />
<TextView
android:id="@+id/email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/avatar"
android:layout_below="@+id/name"
android:text="email@example.com"
android:layout_marginStart="12dp" />
</RelativeLayout>3.2.4 布局性能比较
| 布局 | 测量次数 | 嵌套影响 | 推荐场景 |
|---|---|---|---|
| ConstraintLayout | 2 次(线性测量) | 低 | 复杂布局,推荐首选 |
| LinearLayout | 1 次/方向 | 高(嵌套深度大时) | 简单线性排列 |
| FrameLayout | 1 次 | 低 | 单子 View / 叠加层 |
| RelativeLayout | 2 次 | 中 | 已被 ConstraintLayout 替代 |
| RecyclerView | 按需测量 | 低 | 列表/网格大数据 |
3.2.5 XML vs Compose 对比
| 维度 | XML View System | Jetpack Compose |
|---|---|---|
| 声明方式 | XML + Kotlin findViewById/binding | Kotlin @Composable 函数 |
| 布局 | ConstraintLayout、LinearLayout 等 | Column、Row、Box、ConstraintLayout |
| 状态管理 | LiveData、DataBinding、ViewModel + Observable | State、MutableState、StateFlow |
| 更新方式 | 通过 setText/setVisibility 等手动更新状态 | 状态变化自动重组 |
| 生命周期 | 开发者手动管理 | 自动跟踪 Composition |
| 列表 | RecyclerView(Adapter、ViewHolder) | LazyColumn(无需 Adapter) |
| 性能 | 成熟稳定,复杂列表场景更优 | 重组优化持续改进 |
| 动画 | Property Animation、View Animation | 动画 API 更直观 |
| 互操作性 | — | AndroidView 嵌入 View |
| 学习曲线 | 低 | 中高 |
3.3 本地化
<!-- values/strings.xml (默认/英文) -->
<resources>
<string name="app_name">MyApp</string>
<string name="welcome_message">Welcome to MyApp</string>
<string name="login">Login</string>
<string name="logout">Logout</string>
<string name="settings">Settings</string>
<string name="item_count">%d items</string>
<string name="welcome_user">Welcome, %s!</string>
<string name="unread_messages">You have %d unread messages</string>
</resources>
<!-- values-zh/strings.xml (简体中文) -->
<resources>
<string name="app_name">我的应用</string>
<string name="welcome_message">欢迎使用我的应用</string>
<string name="login">登录</string>
<string name="logout">退出</string>
<string name="settings">设置</string>
<string name="item_count">%d 个项目</string>
<string name="welcome_user">欢迎,%s!</string>
<string name="unread_messages">您有 %d 条未读消息</string>
</resources>
<!-- values-ja/strings.xml (日语) -->
<resources>
<string name="app_name">マイアプリ</string>
<string name="welcome_message">マイアプリへようこそ</string>
<string name="login">ログイン</string>
<string name="logout">ログアウト</string>
<string name="settings">設定</string>
<string name="item_count">%d 個のアイテム</string>
<string name="welcome_user">ようこそ、%sさん!</string>
<string name="unread_messages">未読メッセージが %d 件あります</string>
</resources>// 使用参数化字符串
val message = getString(R.string.unread_messages, count)
// Composable 中使用
@Composable
fun LocalizedUI() {
val count = 5
Text(text = stringResource(R.string.unread_messages, count))
}4. 网络与数据
4.1 网络请求(Retrofit + OkHttp)
4.1.1 接口定义
// API 接口定义
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): Response<User>
@GET("users")
suspend fun getUsers(
@Query("page") page: Int,
@Query("limit") limit: Int = 20
): Response<List<User>>
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): Response<User>
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") userId: Int,
@Body user: UpdateUserRequest
): Response<User>
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") userId: Int): Response<Unit>
@GET("users/search")
suspend fun searchUsers(
@QueryMap filters: Map<String, String>
): Response<List<User>>
@Multipart
@POST("users/{id}/avatar")
suspend fun uploadAvatar(
@Path("id") userId: Int,
@Part avatar: MultipartBody.Part
): Response<User>
@Streaming
@GET("files/{fileId}/download")
suspend fun downloadFile(@Path("fileId") fileId: Int): Response<ResponseBody>
@Headers("Cache-Control: max-age=60")
@GET("config")
suspend fun getConfig(): Response<Config>
}4.1.2 Retrofit 与 OkHttp 配置
// OkHttpClient 配置
val okHttpClient = OkHttpClient.Builder().apply {
// 超时配置
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
writeTimeout(30, TimeUnit.SECONDS)
// 连接池
connectionPool(ConnectionPool(
maxIdleConnections = 5,
keepAliveDuration = 5, TimeUnit.MINUTES
))
// 日志拦截器
if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
}
// 请求头拦截器
addInterceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("Authorization", "Bearer $authToken")
.header("Accept", "application/json")
.header("X-Request-Id", UUID.randomUUID().toString())
.build()
chain.proceed(request)
}
// 错误重试拦截器
addInterceptor(RetryInterceptor(3))
// CookieJar
cookieJar(object : CookieJar {
private val cookieStore = mutableMapOf<String, List<Cookie>>()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookieStore[url.host] = cookies
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return cookieStore[url.host] ?: emptyList()
}
})
// 网络拦截器(在请求和响应之间)
addNetworkInterceptor { chain ->
val request = chain.request()
val startTime = System.nanoTime()
val response = chain.proceed(request)
val duration = (System.nanoTime() - startTime) / 1_000_000
Log.d("OkHttp", "${request.method} ${request.url} completed in ${duration}ms")
response
}
}.build()
// Retrofit 实例
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create(ApiService::class.java)
// 错误重试拦截器
class RetryInterceptor(private val maxRetries: Int) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var retryCount = 0
var response: Response
while (true) {
try {
response = chain.proceed(chain.request())
if (response.isSuccessful || retryCount >= maxRetries) {
return response
}
} catch (e: IOException) {
if (retryCount >= maxRetries) throw e
}
retryCount++
Thread.sleep(1000L * retryCount)
}
}
}
// 统一错误处理
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String, val code: Int = -1) : ApiResult<Nothing>()
}
suspend fun <T> safeApiCall(call: suspend () -> Response<T>): ApiResult<T> {
return try {
val response = call()
if (response.isSuccessful) {
ApiResult.Success(response.body()!!)
} else {
val errorBody = response.errorBody()?.string()
ApiResult.Error(
message = errorBody ?: "Unknown error",
code = response.code()
)
}
} catch (e: IOException) {
ApiResult.Error(message = "Network error: ${e.localizedMessage}")
} catch (e: Exception) {
ApiResult.Error(message = "Unexpected error: ${e.localizedMessage}")
}
}
### 4.2 依赖注入(Hilt / Dagger)
Hilt 是基于 Dagger 的 Android 依赖注入框架,简化了 Dagger 在 Android 项目中的配置和使用。
#### 4.2.1 Hilt 配置
```kotlin
// Application 类添加 @HiltAndroidApp 注解
@HiltAndroidApp
class MyApplication : Application()
// Activity 添加 @AndroidEntryPoint 注解
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var analyticsHelper: AnalyticsHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
// Fragment 添加 @AndroidEntryPoint 注解
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
}4.2.2 Module 与 Provides / Binds
// @Module 声明依赖提供方式
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
// @Provides 提供实例
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
// 自定义限定符
@Provides
@Singleton
@Named("base_url")
fun provideBaseUrl(): String = BuildConfig.API_BASE_URL
}
// @Binds 用于接口绑定
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
@Binds
@Singleton
abstract fun bindAuthRepository(
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository
}4.2.3 HiltViewModel
@HiltViewModel
class HomeViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadUsers()
}
private fun loadUsers() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
when (val result = userRepository.getUsers()) {
is ApiResult.Success -> {
_uiState.update {
it.copy(users = result.data, isLoading = false)
}
}
is ApiResult.Error -> {
_uiState.update {
it.copy(error = result.message, isLoading = false)
}
}
}
}
}
}
data class HomeUiState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// Composable 中使用
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
// ...
}4.2.4 EntryPoint 非 Android 类注入
// 对于非 Android 类(如 ContentProvider),使用 EntryPoint
class MyContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
val entryPoint = EntryPointAccessors.fromApplication(
context!!,
MyEntryPoint::class.java
)
val apiService = entryPoint.apiService()
return true
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun apiService(): ApiService
}4.3 数据持久化(Room DB)
Room 是 Android Jetpack 的 ORM 数据库库,提供 SQLite 的抽象层。
4.3.1 Entity 定义
@Entity(
tableName = "users",
indices = [
Index(value = ["email"], unique = true),
Index(value = ["name", "age"])
],
foreignKeys = [
ForeignKey(
entity = Department::class,
parentColumns = ["id"],
childColumns = ["department_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "name")
val name: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "age")
val age: Int,
@ColumnInfo(name = "department_id")
val departmentId: Long?,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "is_active")
val isActive: Boolean = true,
@ColumnInfo(name = "avatar_url")
val avatarUrl: String?
)
@Entity(tableName = "departments")
data class Department(
@PrimaryKey val id: Long,
@ColumnInfo(name = "name") val name: String
)4.3.2 Dao 定义
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
fun getActiveUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
@Query("SELECT * FROM users WHERE name LIKE '%' || :query || '%'")
fun searchUsers(query: String): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>): List<Long>
@Update
suspend fun updateUser(user: User): Int
@Delete
suspend fun deleteUser(user: User): Int
@Query("DELETE FROM users WHERE is_active = 0")
suspend fun deleteInactiveUsers(): Int
@Query("SELECT COUNT(*) FROM users")
fun getUserCount(): Flow<Int>
// 关联查询
@Transaction
@Query("SELECT * FROM departments")
fun getDepartmentsWithUsers(): Flow<List<DepartmentWithUsers>>
}
// 关系数据类
data class DepartmentWithUsers(
@Embedded val department: Department,
@Relation(
parentColumn = "id",
entityColumn = "department_id"
)
val users: List<User>
)4.3.3 Database 定义
@Database(
entities = [User::class, Department::class],
version = 2,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun departmentDao(): DepartmentDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"myapp_database"
)
.fallbackToDestructiveMigration()
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 数据库创建时的初始化
}
})
.build()
.also { INSTANCE = it }
}
}
}
}
// 类型转换器
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromStringList(value: String): List<String> {
return value.split(",").filter { it.isNotEmpty() }
}
@TypeConverter
fun toStringList(list: List<String>): String {
return list.joinToString(",")
}
}4.3.4 数据库迁移与 AutoMigration
// 手动迁移(从版本 1 到版本 2)
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `posts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT, `content` TEXT)")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "myapp_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
// AutoMigration(Android 13+ / Room 2.5+)
@Database(
entities = [User::class],
version = 3,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3)
]
)
abstract class AppDatabaseV3 : RoomDatabase() {
abstract fun userDao(): UserDao
}
// 使用 KSP 替代 kapt(性能更好)
// build.gradle.kts
// ksp(libs.room.compiler) // 替代 kapt(libs.room.compiler)4.3.5 Room + Flow 使用
@HiltViewModel
class UserViewModel @Inject constructor(
private val userDao: UserDao
) : ViewModel() {
val users: StateFlow<List<User>> = userDao.getActiveUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun addUser(name: String, email: String) {
viewModelScope.launch {
val user = User(name = name, email = email)
userDao.insertUser(user)
}
}
}4.3.6 SQLCipher 加密
// 添加依赖
// implementation("net.zetetic:android-database-sqlcipher:4.5.6")
// implementation("androidx.sqlite:sqlite-ktx:2.4.0")
// 使用加密数据库
val passphrase = SQLiteDatabase.getBytes("user_supplied_passphrase".toCharArray())
val factory = SupportFactory(passphrase)
val db = Room.databaseBuilder(context, AppDatabase::class.java, "encrypted_db")
.openHelperFactory(factory)
.build()4.4 数据存储(DataStore)
4.4.1 Preferences DataStore
// 创建 DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
// 写入数据
suspend fun saveDarkModePreference(context: Context, isDarkMode: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.DARK_MODE_KEY] = isDarkMode
}
}
// 读取数据
val darkModeFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DARK_MODE_KEY] ?: false
}
// Composable 中使用
@Composable
fun SettingsScreen() {
val context = LocalContext.current
val isDarkMode by context.dataStore.data
.map { it[PreferencesKeys.DARK_MODE_KEY] ?: false }
.catch { emit(false) }
.collectAsState(initial = false)
Switch(
checked = isDarkMode,
onCheckedChange = { enabled ->
CoroutineScope(Dispatchers.IO).launch {
saveDarkModePreference(context, enabled)
}
}
)
}
object PreferencesKeys {
val DARK_MODE_KEY = booleanPreferencesKey("dark_mode")
val LOGIN_STATUS_KEY = booleanPreferencesKey("is_logged_in")
val USERNAME_KEY = stringPreferencesKey("username")
val LAST_SYNC_TIME_KEY = longPreferencesKey("last_sync_time")
}
// 登录状态存储
suspend fun saveLoginState(context: Context, isLoggedIn: Boolean, username: String) {
context.dataStore.edit { prefs ->
prefs[PreferencesKeys.LOGIN_STATUS_KEY] = isLoggedIn
prefs[PreferencesKeys.USERNAME_KEY] = username
}
}4.4.2 Proto DataStore
// 定义 Protobuf 序列化对象 (user_preferences.proto)
// syntax = "proto3";
// option java_package = "com.example.myapp.data.local";
// option java_multiple_files = true;
//
// message UserPreferences {
// bool dark_mode = 1;
// bool is_logged_in = 2;
// string username = 3;
// int32 font_size = 4;
// }
// 创建 Serializer
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
// 创建 DataStore
val Context.userPreferencesDataStore: DataStore<UserPreferences> by
dataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
// 读取
val userPreferencesFlow: Flow<UserPreferences> = context.userPreferencesDataStore.data
.catch { emit(UserPreferences.getDefaultInstance()) }
// 写入
suspend fun updateDarkMode(context: Context, enabled: Boolean) {
context.userPreferencesDataStore.updateData { preferences ->
preferences.toBuilder()
.setDarkMode(enabled)
.build()
}
}4.4.3 SharedPreferences vs DataStore 对比
| 维度 | SharedPreferences | DataStore |
|---|---|---|
| API | 同步阻塞 API | 异步 Flow API(协程) |
| 线程安全 | 不保证 | 保证(使用 Dispatchers.IO) |
| 进程安全 | 不安全 | 不安全(需要 MultiProcessDataStore) |
| 错误处理 | 无,运行时崩溃 | Flow catch 操作符 |
| 写入类型 | 原子写入(apply/commit) | 事务性写入 |
| 支持类型 | 基本类型 + String + Set | Preferences: 基本类型 + String; Proto: 自定义类型 |
| 合并写入 | 不支持 | 支持(updateData 原子合并) |
| 性能 | 可能导致 ANR(在主线程读写) | 异步操作,无 ANR 风险 |
4.4.4 存储使用示例
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val isDarkMode: StateFlow<Boolean> = dataStore.data
.map { preferences ->
preferences[PreferencesKeys.DARK_MODE_KEY] ?: false
}
.catch { emit(false) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
fun toggleDarkMode(enabled: Boolean) {
viewModelScope.launch {
dataStore.edit { preferences ->
preferences[PreferencesKeys.DARK_MODE_KEY] = enabled
}
}
}
}5. 后台任务与推送
5.1 后台任务(WorkManager)
WorkManager 是 Android Jetpack 推荐的异步任务调度 API,用于需要保证执行的后台工作(即使应用退出或设备重启)。
5.1.1 Worker 定义
// 简单 Worker
class SyncWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
return try {
// 执行后台任务
val syncManager = SyncManager(applicationContext)
syncManager.syncData()
Result.success()
} catch (e: IOException) {
Result.retry()
} catch (e: Exception) {
Result.failure()
}
}
}
// 带输入的 Worker
class UploadWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
val fileUri = inputData.getString("file_uri") ?: return Result.failure()
return try {
uploadFile(Uri.parse(fileUri))
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
private fun uploadFile(uri: Uri): Boolean {
// 上传逻辑
return true
}
}5.1.2 协程 Worker
// CoroutineWorker(推荐)
class SyncCoroutineWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
try {
val repository = Repository()
repository.syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
// 监听进度
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
NOTIFICATION_ID,
NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setContentTitle("Syncing")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.build()
)
}
}5.1.3 WorkRequest 配置
// OneTimeWorkRequest(一次性任务)
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(workDataOf("file_uri" to "content://..." ))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
.setRequiresBatteryNotLow(true) // 电量不低
.setRequiresCharging(true) // 充电中
.setRequiresStorageNotLow(true) // 存储空间充足
.setRequiresDeviceIdle(true) // 设备空闲(API 23+)
.build()
)
.setBackoffCriteria(
BackoffPolicy.LINEAR, // 线性退避
OneTimeWorkRequest.MIN_BACKOFF_MILLIS, // 最少间隔
TimeUnit.MILLISECONDS
)
.addTag("upload")
.build()
// PeriodicWorkRequest(周期任务)
val syncRequest = PeriodicWorkRequestBuilder<SyncCoroutineWorker>(
15, TimeUnit.MINUTES // 最小周期 15 分钟
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10, TimeUnit.SECONDS
)
.build()
// 调度任务
WorkManager.getInstance(context).apply {
enqueue(uploadRequest)
enqueue(syncRequest)
enqueueUniquePeriodicWork(
"sync_work",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}5.1.4 链式任务
// 任务链:下载 -> 处理 -> 上传
val downloadWork = OneTimeWorkRequestBuilder<DownloadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
val processWork = OneTimeWorkRequestBuilder<ProcessWorker>()
.build()
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context)
.beginWith(downloadWork)
.then(processWork)
.then(uploadWork)
.enqueue()
// 并行 + 链式组合
val imageWork1 = OneTimeWorkRequestBuilder<ImageWorker>().build()
val imageWork2 = OneTimeWorkRequestBuilder<ImageWorker>().build()
val mergeWork = OneTimeWorkRequestBuilder<MergeWorker>().build()
WorkManager.getInstance(context)
.beginWith(listOf(imageWork1, imageWork2))
.then(mergeWork)
.enqueue()5.1.5 观察任务状态
// WorkManager 状态观察
val workManager = WorkManager.getInstance(context)
val workInfoLiveData = workManager.getWorkInfoByIdLiveData(uploadRequest.id)
workInfoLiveData.observe(this) { workInfo ->
when (workInfo.state) {
WorkInfo.State.ENQUEUED -> { /* 已排队 */ }
WorkInfo.State.RUNNING -> { /* 运行中 */ }
WorkInfo.State.SUCCEEDED -> { /* 成功 */ }
WorkInfo.State.FAILED -> { /* 失败,获取输出 */ }
WorkInfo.State.BLOCKED -> { /* 被阻塞(依赖前置任务) */ }
WorkInfo.State.CANCELLED -> { /* 已取消 */ }
}
// 获取输出数据
val output = workInfo.outputData.getString("result")
}
// 按 Tag 观察
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("upload")
.observe(this) { workInfos ->
// 所有 upload tag 的任务
}
// 唯一工作链观察
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData("sync_work")
.observe(this) { workInfos -> }5.2 推送通知(Firebase Cloud Messaging)
5.2.1 FCM 配置
// 添加依赖
// implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
// implementation("com.google.firebase:firebase-messaging-ktx")
// FCM Service
class MyFirebaseMessagingService : FirebaseMessagingService() {
// 获取新 Token
override fun onNewToken(token: String) {
super.onNewToken(token)
sendTokenToServer(token)
}
// 接收消息
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
// 数据消息
message.data.forEach { (key, value) ->
Log.d("FCM", "$key: $value")
}
// 通知消息
message.notification?.let { notification ->
showNotification(
title = notification.title ?: "",
body = notification.body ?: ""
)
}
}
private fun sendTokenToServer(token: String) {
// 将 Token 发送到应用服务器
CoroutineScope(Dispatchers.IO).launch {
try {
// apiService.registerFcmToken(token)
} catch (e: Exception) {
// 保存 Token,下次重试
}
}
}
private fun showNotification(title: String, body: String) {
createNotificationChannel()
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(createPendingIntent())
.build()
val manager = getSystemService(NotificationManager::class.java)
manager.notify(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Messages",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "In-app message notifications"
enableVibration(true)
setShowBadge(true)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
private fun createPendingIntent(): PendingIntent {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
companion object {
const val CHANNEL_ID = "fcm_messages"
const val NOTIFICATION_ID = 1001
}
}5.2.2 通知渠道(NotificationChannel)
fun createNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = context.getSystemService(NotificationManager::class.java)
// 默认消息渠道
val defaultChannel = NotificationChannel(
"default_channel",
"Default Notifications",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Default notification channel"
enableVibration(true)
setShowBadge(true)
}
// 重要消息渠道
val importantChannel = NotificationChannel(
"important_channel",
"Important Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Important notifications"
enableVibration(true)
setShowBadge(true)
enableLights(true)
lightColor = Color.RED
}
// 静默消息渠道
val silentChannel = NotificationChannel(
"silent_channel",
"Silent Notifications",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Silent notifications (no sound)"
setSound(null, null)
}
manager.createNotificationChannels(
listOf(defaultChannel, importantChannel, silentChannel)
)
}
}
// 通知分类
class MyNotificationManager(private val context: Context) {
fun showMessageNotification(title: String, body: String) {
val notification = NotificationCompat.Builder(context, "default_channel")
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.addAction(R.drawable.ic_reply, "Reply", replyPendingIntent)
.build()
NotificationManagerCompat.from(context).notify(
System.currentTimeMillis().toInt(),
notification
)
}
}5.2.3 Android 13 POST_NOTIFICATIONS 运行时权限
// Android 13+ 需要运行时权限
class MainActivity : AppCompatActivity() {
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限已授予
} else {
// 权限被拒绝
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestNotificationPermission()
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
notificationPermissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS
)
}
}
}
}5.2.4 FCM Token 管理
@HiltViewModel
class FCMViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _fcmToken = MutableStateFlow<String?>(null)
val fcmToken: StateFlow<String?> = _fcmToken.asStateFlow()
init {
// 获取 FCM Token
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
_fcmToken.value = token
registerToken(token)
}
}
}
private fun registerToken(token: String) {
viewModelScope.launch {
try {
apiService.registerFcmToken(FcmTokenRequest(token))
} catch (e: Exception) {
// 失败重试逻辑
}
}
}
}5.3 前台服务(Foreground Service)
5.3.1 前台服务定义
// AndroidManifest.xml 声明
// <service
// android:name=".service.MusicPlaybackService"
// android:enabled="true"
// android:exported="false"
// android:foregroundServiceType="mediaPlayback" />
class MusicPlaybackService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
// 执行前台任务
when (intent?.action) {
ACTION_PLAY -> playMusic()
ACTION_PAUSE -> pauseMusic()
ACTION_STOP -> stopSelf()
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Now Playing")
.setContentText("Song Title")
.setSmallIcon(android.R.drawable.ic_media_play)
.setOngoing(true)
.setForegroundServiceBehavior(
NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
)
.addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent)
.addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent)
.addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent)
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1, 2))
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Music Playback",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Music playback controls"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
override fun onDestroy() {
super.onDestroy()
stopForeground(STOP_FOREGROUND_REMOVE)
}
companion object {
const val CHANNEL_ID = "music_playback"
const val NOTIFICATION_ID = 2
const val ACTION_PLAY = "com.example.action.PLAY"
const val ACTION_PAUSE = "com.example.action.PAUSE"
const val ACTION_STOP = "com.example.action.STOP"
}
}5.3.2 Android 14 前台服务类型声明
// Android 14 (API 34) 要求必须声明前台服务类型
// AndroidManifest.xml
// <service
// android:name=".service.DataSyncService"
// android:foregroundServiceType="dataSync"
// android:exported="false" />
//
// <service
// android:name=".service.LocationService"
// android:foregroundServiceType="location"
// android:exported="false" />
//
// <service
// android:name=".service.CameraService"
// android:foregroundServiceType="camera"
// android:exported="false" />
//
// 可用的 foregroundServiceType:
// - camera: 相机应用
// - connectedDevice: 蓝牙/USB/NFC 外设
// - dataSync: 数据同步/上传/下载
// - health: 健康应用
// - location: 位置服务
// - mediaPlayback: 媒体播放
// - mediaProjection: 屏幕录制/投屏
// - microphone: 麦克风录制
// - phoneCall: 电话
// - remoteMessaging: 远程消息
// - shortService: 短时重要服务(几分钟)
// - systemExempted: 系统豁免6. 性能优化
6.1 启动优化
6.1.1 App Startup
// 使用 App Startup 减少 ContentProvider 初始化开销
// 在 AndroidManifest 中合并多个 Initializer
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.MyInitializer"
android:value="androidx.startup" />
</provider>
// 自定义 Initializer
class MyInitializer : Initializer<Unit> {
override fun create(context: Context) {
// 初始化第三方 SDK
LeakCanary.install(context.applicationContext as Application)
// 其他初始化
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// 依赖其他 Initializer,保证执行顺序
return listOf(OtherInitializer::class.java)
}
}
// 延迟初始化(不需要在启动时立即初始化)
// AndroidManifest.xml 中设置 tools:node="remove"
// 在代码中手动初始化
// AppInitializer.getInstance(context).initializeComponent(MyInitializer::class.java)6.1.2 启动耗时追踪
// 使用 Systrace / Perfetto 追踪启动过程
// adb shell perfetto --trace :bus:/systrace -t 10 sched freq idle am wm
// 代码中标记启动阶段
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 启动 Tracing
// Debug.startMethodTracing("app_start")
initSDKs()
// 停止 Tracing
// Debug.stopMethodTracing()
}
}
// 使用 startActivityTimeline / reportFullyDrawn
class MainActivity : AppCompatActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
// 报告首次绘制完成
reportFullyDrawn()
}
}
}6.1.3 SplashScreen API
// Android 12+ SplashScreen API
// themes.xml
// <style name="Theme.MyApp" parent="Theme.SplashScreen">
// <item name="windowSplashScreenBackground">@color/white</item>
// <item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_logo</item>
// <item name="windowSplashScreenAnimationDuration">500</item>
// <item name="postSplashScreenTheme">@style/Theme.MyApp.Main</item>
// </style>
// 代码中控制 SplashScreen 显示
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 等待数据加载
splashScreen.setKeepOnScreenCondition {
// 返回 true 则保持 Splash 显示
isLoadingData
}
}
}6.1.4 冷启动 vs 热启动
| 类型 | 系统行为 | 优化目标 |
|---|---|---|
| 冷启动 | 进程不存在,完整启动流程 | 减少 Application.onCreate 耗时,延迟初始化 |
| 热启动 | 进程在后台,Activity 重建 | 减少 Activity.onCreate 耗时 |
| 温启动 | Activity 被回收但进程存活 | 优化 View 重建速度 |
优化建议:
- 使用 App Startup 合并 ContentProvider 初始化
- 延迟非必要的 SDK 初始化
- 使用异步初始化(如
AsyncInitTask) - 减少启动时的布局层级
- 使用 Baseline Profiles 加速编译
6.2 内存优化
6.2.1 内存泄漏检测
// LeakCanary 集成
// 添加依赖:debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary 自动检测(debug 版本)
// if (!LeakCanary.isInAnalyzerProcess(this)) {
// LeakCanary.install(this)
// }
}
}
// 常见内存泄漏场景:
// 1. 匿名内部类持有 Activity 引用
// 2. 静态变量持有 Context/View
// 3. Handler / Runnable 未及时移除
// 4. 注册的监听器未注销
// 5. 单例持有 Activity 引用
// 6. 资源未关闭(Cursor、Stream、IO)
// 使用 LruCache 管理内存缓存
val imageCache = LruCache<String, Bitmap>(maxMemory / 8) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.allocationByteCount
}
}6.2.2 Android Profiler 使用
// Android Studio Profiler 工具:
// - Memory Profiler: 查看 Java/Kotlin 堆内存分配
// - CPU Profiler: 追踪方法执行时间
// - Network Profiler: 监控网络请求
// - Energy Profiler: 跟踪电量消耗
// 手动触发 GC 和 Heap Dump
// adb shell am broadcast -a android.intent.action.DUMP_HEAP
// 使用 Allocation Tracker 分析内存抖动
// 内存抖动示例:在循环中创建临时对象
fun processItems(items: List<String>) {
val result = mutableListOf<String>()
for (item in items) {
// 避免在循环中创建临时对象
result.add(item.trim().toLowerCase())
}
}6.2.3 图片内存优化
// Glide 图片加载
// implementation("com.github.bumptech.glide:glide:4.16.0")
Glide.with(context)
.load(url)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.override(300, 300) // 指定加载尺寸
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false) // 使用内存缓存
.into(imageView)
// 大图加载(BitmapFactory)
fun loadLargeBitmap(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap? {
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, this)
// 计算采样率
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
inJustDecodeBounds = false
inPreferredConfig = Bitmap.Config.RGB_565 // 减少内存(无透明通道用 RGB_565)
BitmapFactory.decodeFile(filePath, this)
}
}
private fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
val (height: Int, width: Int) = options.outHeight to options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth
) {
inSampleSize *= 2
}
}
return inSampleSize
}6.3 包体积优化
6.3.1 R8 混淆与收缩
// build.gradle.kts 配置
android {
buildTypes {
release {
isMinifyEnabled = true // 启用 R8 混淆
isShrinkResources = true // 启用资源收缩
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
// gradle.properties 启用 R8 full 模式
// android.enableR8.fullMode=true6.3.2 资源收缩
<!-- res/raw/keep.xml 保留特定资源 -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/kept_layout,@drawable/kept_drawable"
tools:shrinkMode="strict" />// build.gradle.kts 资源收缩配置
android {
buildTypes {
release {
isShrinkResources = true
// 保留特定语言资源
resConfigs("en", "zh", "ja")
}
}
}6.3.3 其他优化手段
| 手段 | 说明 | 预期效果 |
|---|---|---|
| R8 混淆 | 代码混淆、收缩、优化 | 减少约 20-50% DEX 大小 |
| shrinkResources | 移除未使用的资源 | 减少约 10-30% 资源大小 |
| resConfigs | 保留需要的语言/密度资源 | 减少约 20-40% 资源大小 |
| WebP 图片 | 使用 WebP 格式替代 PNG/JPG | 减少约 25-35% 图片大小 |
| Vector Drawable | 使用矢量图替代位图 | 减少约 50-90% 图片大小 |
| ABI 分包 | 按架构拆分包(APK splits) | 减少约 30-50% 包大小 |
| Dynamic Feature | 按需下载模块 | 减少初始安装包大小 |
| AndResGuard | 资源路径混淆 | 减少约 5-10% 资源大小 |
// ABI 分包
android {
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a", "x86_64")
isUniversalApk = false
}
}
}
// APK Analyzer 命令行
// adb shell pm path com.example.myapp
// aapt dump badging <apk_path>
// build/ 目录下使用 Android Studio -> Build -> Analyze APK6.4 Jetpack 基准库(Baseline Profiles)
Baseline Profiles 是一组预编译的 AOT(Ahead-of-Time)配置文件,包含应用启动时常用的类和方法,设备在首次安装后即可将这些代码预编译为机器码,显著提升启动速度和运行性能。
6.4.1 生成 Baseline Profiles
// 1. 添加依赖
// build.gradle.kts
// implementation("androidx.profileinstaller:profileinstaller:1.3.1")
// 2. 创建基准测试(在 app/src/main 下)
// app/src/main/baseline-prof.txt
// 3. 手动编写 Baseline Profile
// app/src/main/baseline-prof.txt
// 规则语法:
// HSPLandroidx/compose/runtime/Composer;->Compose()V
// HSPLandroidx/compose/ui/Modifier;->fillMaxWidth()Landroidx/compose/ui/Modifier;
// HSPLandroidx/compose/material3/Text;-><init>(...)V
// HSPLandroidx/recyclerview/widget/RecyclerView;->onMeasure(II)V
// 4. 使用 Macrobenchmark 自动生成
// 添加依赖:
// androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
// 基准测试代码示例
// @RunWith(AndroidJUnit4ClassRunner::class)
// class BaselineProfileGenerator {
// @get:Rule
// val benchmarkRule = MacrobenchmarkRule()
//
// @Test
// fun generateBaselineProfile() {
// benchmarkRule.collectBaselineProfile(
// packageName = "com.example.myapp",
// maxIterations = 10
// ) {
// startActivityAndWait()
// // 模拟用户操作启动流程
// }
// }
// }6.4.2 使用与验证
// 将生成的 baseline-prof.txt 放入 app/src/main/ 目录
// 在 release 构建中自动包含该配置文件
// 验证生效:
// adb shell cmd package dump-profiles --dex-location <path_to_dex>
// 启动加速效果
// Baseline Profiles 可减少约 15-30% 的冷启动时间
// 与 Cloud Profiles 配合使用:Google Play 服务会收集云端配置数据
// 安装后首次启动即可享受 AOT 编译加速,无需等待 JIT 预热
// 预编译热门代码路径:
// - Activity/Fragment 生命周期方法
// - 首页列表数据绑定的关键方法
// - Compose 初始渲染路径
// - 网络请求框架的初始化路径
// - 导航与路由的核心方法