初学者的第一个HelloWorld,踩了哪些坑?
每个Android开发者都写过这样一段代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.hello_text);
tv.setText("Hello World!");
}
}
看上去没问题,但正是这些”没问题”的代码,埋下了无数后续麻烦。
坑一:直接在主线程做耗时操作
// ❌ 新手常犯:在主线程下载数据
protected void onCreate(Bundle savedInstanceState) {
String data = downloadFromNetwork(); // 可能卡死UI线程
tv.setText(data);
}
为什么是坑: Android规定,主线程(UI线程)负责渲染界面和响应事件。任何耗时操作(网络请求、文件IO、数据库查询)超过5秒,系统会抛出ANR(Application Not Responding)错误,应用直接崩溃。
正确做法: 用协程、Thread或RxJava:
// ✅ 用协程在IO线程执行
lifecycleScope.launch(Dispatchers.IO) {
val data = withContext(Dispatchers.IO) { downloadFromNetwork() }
withContext(Dispatchers.Main) {
tv.setText(data)
}
}
坑二:忘记处理Configuration Change
// ❌ 旋转屏幕后,数据全丢
private String userData;
protected void onCreate(Bundle savedInstanceState) {
userData = fetchUserData(); // 旋转后Activity重建,数据丢失
}
为什么是坑: 手机旋转、切换语言、分屏模式都会触发Activity重建(onDestroy → onCreate)。如果你在onCreate里重新获取数据,不仅浪费资源,还会导致用户看到空白界面。
正确做法: 用ViewModel保存状态:
// ✅ ViewModel跨配置变更保留数据
class MyViewModel : ViewModel() {
val userData = MutableLiveData<String>()
fun loadUser() {
// 数据持久化在ViewModel中,旋转屏幕不丢失
userData.postValue("user_data_from_network")
}
}
// Activity里
val viewModel: MyViewModel by viewModels()
viewModel.userData.observe(this) { text ->
tv.text = text
}
坑三:用findViewById重复获取View
// ❌ 每帧都重新findView
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
TextView title = holder.itemView.findViewById(R.id.title); // 每次bind都执行
title.setText(data.get(position));
}
为什么是坑: findViewById是树形遍历操作,在RecyclerView的onBindViewHolder里调用,等于每渲染一屏就遍历一次View树,性能极差。
正确做法: 在ViewHolder构造时缓存:
// ✅ 只在构造时find一次
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.title) // 只执行一次
val subtitle: TextView = itemView.findViewById(R.id.subtitle)
fun bind(item: Item) {
title.text = item.title
subtitle.text = item.subtitle
}
}
第一个RecyclerView,80%的人写错了
当你要展示列表数据时,新手通常这样写:
// ❌ 最典型的错误写法
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<String> data = new ArrayList<>();
// 每次notifyDataSetChanged都重建设置整个列表
public void setData(List<String> newData) {
data.clear();
data.addAll(newData);
notifyDataSetChanged(); // 最差的更新方式
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_layout, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
ViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.text);
}
}
}
问题一:notifyDataSetChanged()是性能杀手
// ❌ 告知系统"整个列表都变了"
data.add("新数据");
notifyDataSetChanged(); // 系统会重新绑定所有可见项
为什么是坑: 当你只插入一条数据时,notifyDataSetChanged()会让RecyclerView重新调用onBindViewHolder绑定所有可见item,即使它们的数据根本没有变。
正确做法: 使用精确的更新方法:
// ✅ 精确更新,只绑定变化的item
data.add("新数据");
notifyItemInserted(data.size() - 1); // 只绑定新增的那一个
// 删除时
data.remove(position);
notifyItemRemoved(position);
// 更新某一项
data.set(position, newData);
notifyItemChanged(position);
问题二:没有设置setHasStableIds(true)
// ❌ 没有稳定ID,系统无法优化重排
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
// 缺少:setHasStableIds(true);
}
为什么是坑: RecyclerView在数据变化时需要重新计算每个item的position和viewType。如果有稳定的ID,系统可以直接复用已有的ViewHolder,不需要重新创建。
正确做法:
// ✅ 设置稳定ID
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
public MyAdapter() {
setHasStableIds(true); // 告诉系统"我有稳定的ID"
}
@Override
public long getItemId(int position) {
return data.get(position).getId(); // 返回唯一ID
}
@Override
public int getItemViewType(int position) {
return data.get(position).getType(); // 返回视图类型
}
}
问题三:没有预取机制
// ❌ 没有预取,滑动时会有短暂卡顿
recyclerView.setItemPrefetchEnabled(false); // 默认关闭
为什么是坑: RecyclerView默认关闭了预取机制。当你快速滑动时,系统只有在item进入屏幕后才会创建ViewHolder,导致滑动过程中出现短暂卡顿。
正确做法:
// ✅ 开启预取,提前创建即将进入屏幕的item
recyclerView.setItemPrefetchEnabled(true);
// 或者设置预取数量
recyclerView.setInitialPrefetchItemCount(3); // 提前预取3个
RecyclerView性能优化的完整方案
第一步:使用DiffUtil精确更新
// ✅ 用DiffUtil计算精确的差异
class MyDiffCallback extends DiffUtil.Callback {
private final List<String> oldList;
private final List<String> newList;
MyDiffCallback(List<String> oldList, List<String> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() { return oldList.size(); }
@Override
public int getNewListSize() { return newList.size(); }
@Override
public boolean areItemsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).getId() == newList.get(newPos).getId();
}
@Override
public boolean areContentsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).equals(newList.get(newPos));
}
@Override
public Object getChangePayload(int oldPos, int newPos) {
// 返回变化的payload,用于部分更新
return null;
}
}
// 使用
DiffUtil.calculateDiff(new MyDiffCallback(oldData, newData)).dispatchUpdatesTo(adapter);
第二步:使用ViewBinding替代findViewById
// ❌ 传统方式:每次都要find
TextView title = itemView.findViewById(R.id.title);
// ✅ ViewBinding:编译时生成类型安全的binding
class MyViewHolder(MyItemBinding binding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.title.text = item.title // 编译时检查,不会出错
binding.subtitle.text = item.subtitle
}
}
第三步:启用RecyclerView缓存
// ✅ 配置多级缓存
recyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool());
recyclerView.setItemViewCacheSize(5); // 缓存5个View
// 或者使用默认缓存策略
recyclerView.setHasFixedSize(true); // 告诉系统"item大小不变",跳过测量
第四步:使用Glide/Picasso优化图片加载
// ❌ 直接加载大图,内存爆炸
ImageView iv = itemView.findViewById(R.id.image);
iv.setImageResource(R.drawable.large_image); // oom风险
// ✅ 用Glide异步加载并缓存
Glide.with(itemView.getContext())
.load(item.imageUrl)
.override(200, 200) // 指定大小
.centerCrop() // 裁剪适应
.into(iv); // 自动内存+磁盘缓存
常见性能陷阱的完整检查清单
| 问题 | 现象 | 解决方案 |
|---|---|---|
notifyDataSetChanged() |
滑动卡顿,CPU占用高 | 使用notifyItemInserted/Removed/Changed |
重复findViewById |
每帧都遍历View树 | 在ViewHolder构造时缓存 |
| 没有稳定ID | 数据变化时大量重绘 | 设置setHasStableIds(true) |
| 大图直接加载 | OOM崩溃 | 用Glide/Picasso加载 |
| 没有预取 | 滑动时短暂卡顿 | 开启setItemPrefetchEnabled |
| ViewHolder复用不当 | 列表错乱 | 实现onViewRecycled清空数据 |
| 复杂layout嵌套 | 测量耗时 | 用ConstraintLayout替代多层嵌套 |
| 动画没优化 | 掉帧 | 用RecyclerView.ItemAnimator自定义 |
总结:从HelloWorld到专业级的关键跃迁
从最初写TextView.setText("Hello World!")到实现一个高性能的RecyclerView列表,核心是理解Android的生命周期和线程模型。
记住这三个原则:
- 不在主线程做耗时操作(用协程/线程池)
- 只在必要时更新UI(用DiffUtil精确更新)
- 缓存可复用的东西(ViewHolder、图片、网络数据)
当你能够说出”为什么不用notifyDataSetChanged()“而不是”知道它不好用”时,你就已经跨过了新手到进阶的门槛。
