做Android开发就像是在玩一场高难度的积木游戏,只不过这些积木随时可能因为重力(内存泄漏)、天气(网络波动)或者小朋友的乱按(用户交互)而崩塌。很多刚入行的朋友,看着官方文档觉得“这很简单啊”,结果一上手,App卡顿、闪退、耗电快,连自己都不知道问题出在哪。
别担心,今天我们就把这些坑一个个填平。我不讲那些枯燥的理论,咱们直接看代码,看那些让你深夜抓狂的实际场景,看看高手是怎么避坑并写出优雅代码的。
一、 UI布局:别让视图树变成“巨无霸”
新手最容易犯的错误,就是把所有东西都塞进一个 LinearLayout 里,或者为了省事,嵌套层级深得像俄罗斯套娃。
1. 过度嵌套与性能陷阱
想象一下,你有一个列表项,里面包含头像、名字、简介、标签、时间……如果每个部分都用一个 LinearLayout 包裹,再套一个 ConstraintLayout,层级可能轻松超过10层。Android绘制UI时是从上到下递归测量的,层级越深,测量时间越长,滑动起来就会掉帧。
错误示范:
<!-- 这种写法在复杂界面中是性能杀手 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView ... /> <!-- 头像 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView ... /> <!-- 名字 -->
<TextView ... /> <!-- 简介 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView ... /> <!-- 标签1 -->
<TextView ... /> <!-- 标签2 -->
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
专家解法:拥抱 ConstraintLayout
ConstraintLayout 允许你在同一层级通过约束关系定位视图,极大地减少了嵌套。
<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="wrap_content">
<!-- 头像 -->
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="16dp"/>
<!-- 名字 -->
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/ivAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/ivAvatar"
android:layout_marginStart="8dp"
android:textSize="16sp"/>
<!-- 简介 -->
<TextView
android:id="@+id/tvDesc"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/tvName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName"
android:layout_marginTop="4dp"
android:textColor="#666666"/>
<!-- 标签组:使用 Flow 或简单的横向排列,避免额外嵌套 -->
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flexboxTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/tvDesc"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDesc"
app:flexWrap="wrap"
android:layout_marginTop="8dp">
<!-- 标签项 -->
<TextView ... />
<TextView ... />
</com.google.android.flexbox.FlexboxLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
注意:对于复杂的标签排列,推荐使用 FlexboxLayout 库,它比手动计算宽度要高效且灵活得多。
2. 静态引用导致内存泄漏
在 Adapter 或 Fragment 中,很多新手喜欢直接持有 Activity 的引用,甚至把 Context 存为静态变量。
错误示范:
public class MyAdapter extends RecyclerView.Adapter {
// 绝对不要这样做!Context 会持有整个 Activity 的生命周期引用
private static Context sContext;
public MyAdapter(Context context) {
sContext = context;
}
}
正确做法:
始终使用 Application Context 或者确保在 onDestroy 中清理引用。如果是 View 相关的操作,必须使用 Activity Context,但要小心生命周期。
二、 后台任务:别在主线程上“搬砖”
这是新手踩雷最多的地方。Android 规定,任何耗时操作(网络请求、数据库读写、大文件解压)都不能放在主线程(UI线程),否则 ANR(应用无响应)警告马上就到,App 直接卡死。
1. 滥用 Thread 和 Handler
虽然 new Thread() 能用,但管理线程池、回调、内存泄漏非常麻烦。
错误示范:
// 在主线程中直接发起网络请求 -> 崩溃!
new Thread(new Runnable() {
@Override
public void run() {
String data = fetchDataFromNetwork(); // 耗时操作
// 试图在这里更新UI -> 崩溃!
textView.setText(data);
}
}).start();
2. 现代方案:Kotlin Coroutines + Retrofit + Room
现在做 Android 开发,如果不学 Kotlin 协程,就像开法拉利还骑单车。协程让异步代码看起来像同步代码一样简洁,同时自动处理线程切换。
场景:加载用户列表并显示
第一步:定义数据源 (Repository)
class UserRepository @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) {
// 获取在线数据
suspend fun fetchUsers(): List<User> {
return apiService.getUsers()
}
// 保存数据到本地
suspend fun saveUsers(users: List<User>) {
userDao.insertAll(users)
}
}
第二步:ViewModel 中处理逻辑
ViewModel 是生命周期感知的,即使屏幕旋转,数据也不会丢失。
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
// 使用 StateFlow 暴露状态给 UI
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
_isLoading.value = true
try {
// 这里默认运行在 IO 线程
val fetchedUsers = repository.fetchUsers()
// 保存本地
repository.saveUsers(fetchedUsers)
// 更新 UI 状态
_users.value = fetchedUsers
} catch (e: Exception) {
// 处理错误
Log.e("UserViewModel", "Load failed", e)
} finally {
_isLoading.value = false
}
}
}
}
第三步:Activity/Fragment 观察状态
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[UserViewModel::class.java]
// 收集状态流
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.isLoading.collect { loading ->
progressBar.visibility = if (loading) View.VISIBLE else View.GONE
}
}
launch {
viewModel.users.collect { userList ->
adapter.submitList(userList)
}
}
}
}
viewModel.loadUsers()
}
}
关键点: repeatOnLifecycle 确保了当页面不可见(如切换到后台)时,不再消耗资源去收集数据,这是防止内存泄漏和提升性能的关键细节。
三、 图片加载:内存溢出的元凶
新手经常直接用 ImageView.setImageBitmap(bitmap) 加载大图,或者不设置图片缓存。这不仅慢,而且极易导致 OutOfMemoryError。
解决方案:使用 Glide 或 Coil
以 Coil (Kotlin Only) 为例,它是目前最推荐的轻量级图片加载库,完美支持协程。
添加依赖:
implementation("io.coil-kt:coil:2.4.0")
implementation("io.coil-kt:coil-gif:2.4.0") // 如果需要加载 GIF
XML 中使用:
<ImageView
android:id="@+id/ivUserAvatar"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/default_avatar" />
Kotlin 代码加载:
// 简单一行代码搞定,自动处理下载、缓存、缩放、生命周期感知
imageView.load("https://example.com/avatar.jpg") {
placeholder(R.drawable.loading_spinner)
error(R.drawable.error_image)
crossfade(true)
transformations(CircleCropTransformation()) // 圆形裁剪
}
// 在 RecyclerView 中更是如此,Glide/Coil 会自动回收图片,防止复用错乱
holder.imageView.load(user.avatarUrl)
为什么这很重要? 如果你手动管理 Bitmap,当用户快速滑动列表时,你会创建成千上万个 Bitmap 对象,GC(垃圾回收器)会疯狂工作,导致界面卡顿。图片库会在图片不在视野内时自动释放内存。
四、 数据库操作:Room 的正确姿势
很多新手喜欢用 SQLite 原始 API,或者直接在主线程查数据库。
常见错误
// 错误:在主线程执行数据库查询 -> ANR
Cursor cursor = db.rawQuery("SELECT * FROM users", null);
专家建议:使用 Room + Coroutines
Room 是 Google 官方的 SQLite 抽象层,编译时检查 SQL 语法,防止运行时崩溃。
1. 定义 Entity:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
2. 定义 Dao:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<User>> // 返回 Flow,实现实时数据更新
}
3. 使用:
viewModelScope.launch {
userDao.getAllUsers().collect { users ->
// 数据库变化时自动触发 UI 更新,无需手动刷新
adapter.submitList(users)
}
}
五、 权限与隐私:尊重用户,也保护自己
从 Android 10 (API 29) 开始,分区存储(Scoped Storage)成为强制要求。新手常常忘记申请权限,或者在 Manifest 中声明了权限却在代码中没做动态请求。
最佳实践流程
- Manifest 声明: 明确你需要什么权限。
- 运行时请求: 在需要使用功能前,检查权限。
- 优雅拒绝: 如果用户拒绝,提供解释为什么需要这个权限,而不是直接崩溃。
private fun requestStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 检查是否需要向用户解释为什么需要权限
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
// 弹出一个对话框,告诉用户:“我们需要读取照片以便您选择头像...”
showPermissionExplanationDialog()
} else {
// 直接请求
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_CODE_STORAGE
)
}
} else {
// 已有权限,执行操作
openGallery()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_STORAGE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openGallery()
} else {
Toast.makeText(this, "权限被拒绝,无法选择图片", Toast.LENGTH_SHORT).show()
}
}
}
六、 测试与调试:让代码自己说话
最后,也是区分初级和中级开发者的关键:不要只靠眼睛看代码,要靠日志和测试。
使用 Timber 代替 Log:
Log.d在发布版本中会被移除,而 Timber 可以方便地配置输出策略。// 初始化 if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } else { // 生产环境可以接入 Crashlytics 或其他日志服务 Timber.plant(object : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { // 发送日志到服务器 } }) } // 使用 Timber.d("User loaded: %s", userName)LeakCanary 监控内存泄漏: 在
debugImplementation中添加 LeakCanary。当你意外持有 Context 时,它会直接弹窗告诉你哪里泄漏了。这是新手的神器。debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'Jetpack Compose 预览: 如果你使用 Compose,利用
@Preview注解在 IDE 中直接查看 UI 效果,无需真机运行。@Composable fun Greeting(name: String) { Text(text = "Hello $name!") } @Preview(showBackground = true) @Composable fun DefaultPreview() { Greeting("Android Developer") }
结语:从“能跑”到“优雅”
Android 开发的门槛看似不高,但要写出高质量、高性能、易维护的代码,需要积累大量的实战经验。
记住这几个核心原则:
- UI 要扁平:减少嵌套,提升渲染效率。
- 异步要规范:主线程只做 UI 更新,后台任务用协程或 RxJava。
- 资源要释放:图片加载用库,数据库用 Room,时刻警惕内存泄漏。
- 权限要尊重:动态申请,友好交互。
- 工具要善用:LeakCanary、Profiler、Timber 是你的好朋友。
希望这些实例和建议能帮你避开那些让人头秃的陷阱。编程是一场马拉松,不是短跑,保持好奇心,多写多练,你一定能写出让同事点赞的优秀代码。加油!
