Android开发实战避坑指南:从ListView卡顿到内存泄漏的常见错误与解决方案
一、ListView的卡顿:为什么你的列表滑动起来像在看PPT?
你肯定遇到过这种场景:APP里有个列表,手指一滑,画面直接卡成幻灯片,或者干脆就卡死不动了。用户心里估计已经在骂人了。我当年也踩过这个坑,后来慢慢摸清楚了一整套”为什么卡”的逻辑。
1.1 列表卡顿的根本原因
ListView卡顿的核心,通常就几个问题:主线程做了不该做的事、视图重复创建、图片加载阻塞。下面我一个个拆开来讲。
1.1.1 主线程做耗时操作
Android的UI更新必须在主线程(也叫UI线程)完成,但很多开发者喜欢在getView()里直接做耗时操作,比如网络请求、数据库查询、文件IO、图片解码。这些操作一旦在主线程上执行,就会阻塞整个UI渲染。
错误示例:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_list, parent, false);
holder = new ViewHolder();
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.ivImage = convertView.findViewById(R.id.iv_image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// ❌ 错误:在getView里直接查询数据库
User user = database.queryUser(position);
holder.tvName.setText(user.getName());
// ❌ 错误:在getView里直接加载网络图片
// 这会直接导致主线程ANR(Application Not Responding)
Bitmap bitmap = ImageLoader.downloadBitmap(user.getAvatarUrl());
holder.ivImage.setImageBitmap(bitmap);
return convertView;
}
上面的代码中,database.queryUser()和ImageLoader.downloadBitmap()都是耗时操作,放在getView()里执行,每次滑过一行都会触发一次,列表滑动时CPU和I/O几乎被完全占用,帧率直接跌到个位数。
正确做法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_list, parent, false);
holder = new ViewHolder();
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.ivImage = convertView.findViewById(R.id.iv_image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
User user = dataList.get(position);
holder.tvName.setText(user.getName());
// ✅ 正确:用AsyncTask或线程池异步加载图片
new AsyncTask<String, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(String... urls) {
return ImageLoader.downloadBitmap(urls[0]);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
// 还要检查position是否对应,避免错位
if (position < dataList.size()) {
holder.ivImage.setImageBitmap(bitmap);
}
}
}
}.execute(user.getAvatarUrl());
return convertView;
}
但实际上,现在更推荐用成熟的图片加载库,比如Glide或Picasso,它们内部已经处理好了缓存、线程池、生命周期等问题。
// ✅ 使用Glide加载图片,简洁且高效
Glide.with(context)
.load(user.getAvatarUrl())
.placeholder(R.drawable.placeholder) // 加载中显示的占位图
.error(R.drawable.error) // 加载失败显示的图
.into(holder.ivImage);
1.1.2 重复inflate布局
很多新手在getView()里每次都调用LayoutInflater.inflate(),这会导致大量的对象创建,GC频繁回收,进而引起卡顿。
// ❌ 错误:每次都inflate新View
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 每次调用都会创建新的View,非常浪费
View view = LayoutInflater.from(context).inflate(R.layout.item_list, parent, false);
TextView tv = view.findViewById(R.id.tv_name);
tv.setText(dataList.get(position).getName());
return view;
}
正确做法是复用convertView,配合ViewHolder模式:
// ✅ 正确:复用convertView + ViewHolder
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
// 只在第一次创建时inflate布局
convertView = LayoutInflater.from(context).inflate(R.layout.item_list, parent, false);
holder = new ViewHolder();
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.ivImage = convertView.findViewById(R.id.iv_image);
holder.tvTime = convertView.findViewById(R.id.tv_time);
convertView.setTag(holder); // 把ViewHolder绑定到View上
} else {
// 复用已有的View,通过tag取回ViewHolder
holder = (ViewHolder) convertView.getTag();
}
User user = dataList.get(position);
holder.tvName.setText(user.getName());
holder.tvTime.setText(user.getCreateTime());
Glide.with(context)
.load(user.getAvatarUrl())
.into(holder.ivImage);
return convertView;
}
// ViewHolder类,用于缓存View的引用
static class ViewHolder {
TextView tvName;
ImageView ivImage;
TextView tvTime;
}
这个模式是Android列表优化的经典套路,核心思想就是:能复用的东西就别创建新的。convertView的复用机制是Android系统自带的,它会把滚出屏幕的View缓存起来,等你滑回来的时候直接复用,避免了大量的内存分配和回收。
1.1.3 不必要的布局层级
你的列表项布局有多深?如果每一行都是一个嵌套了好几层的RelativeLayout,滑动时的测量和绘制开销会非常可观。
<!-- ❌ 错误:布局层级过深,嵌套了5层 -->
<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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/avatar_default"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"/>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</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"
android:padding="12dp">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/iv_avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_avatar"
app:layout_constraintBottom_toTopOf="@id/tv_content"/>
<TextView
android:id="@+id/tv_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/iv_avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name"
app:layout_constraintBottom_toBottomOf="@id/iv_avatar"/>
</androidx.constraintlayout.widget.ConstraintLayout>
布局层级越深,measure()和layout()的过程就越慢。你可以用Android Studio的Layout Inspector工具,一键查看当前页面的布局层级图,哪一层深一目了然。
1.1.4 数据集过大导致的内存问题
如果你的列表有几千条数据,而且每条都要在内存中维护一个完整的View对象,这本身就是一个问题。特别是当你使用notifyDataSetChanged()的时候,它会强制刷新整个列表,而不是只刷新变化的部分。
// ❌ 错误:数据量大时频繁调用notifyDataSetChanged()
for (User user : updatedUsers) {
dataList.add(user);
}
adapter.notifyDataSetChanged(); // 整个列表重绘,非常慢
// ✅ 正确:只刷新变化的部分
adapter.notifyItemRangeInserted(dataList.size(), updatedUsers.size());
dataList.addAll(updatedUsers);
另外,如果是超大列表(比如上万条),可以考虑分页加载或者虚拟列表(VirtualList)方案,每次只加载当前屏幕可见范围内的数据。
1.2 列表卡顿的调试工具
遇到卡顿,别急着改代码,先用工具定位问题。
Traceview / CPU Profile:Android Studio内置的性能分析工具,可以精确到每一行代码的耗时。在Profiler面板里选择”Trace CPU”,然后滑动列表,就能看到哪个方法占了大部分时间。
Layout Inspector:如前面提到的,查看布局层级,找出深层嵌套的问题。
Choreographer:通过Choreographer.getInstance().postFrameCallback()监听每一帧的渲染时间,如果某一帧超过了16ms(60fps),就会看到明显的卡顿。
// Kotlin示例:监听帧渲染时间
val choreographer = Choreographer.getInstance()
var lastFrameTime = 0L
choreographer.postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
val deltaTime = (frameTimeNanos - lastFrameTime) / 1_000_000 // 转换为毫秒
if (deltaTime > 16) {
Log.w("FrameRate", "卡顿!帧耗时: ${deltaTime}ms")
}
lastFrameTime = frameTimeNanos
choreographer.postFrameCallback(this)
}
})
二、内存泄漏:你的APP为什么越来越卡,最后直接OOM?
内存泄漏是Android开发中最隐蔽、最头疼的问题之一。它不像崩溃那样会直接让用户报错,而是让APP的内存占用一点点累积,最终触发OOM(OutOfMemory),导致APP崩溃或者被系统强杀。
2.1 什么是内存泄漏?
简单来说,内存泄漏就是:你不再需要某个对象了,但Java的GC回收不掉它,因为还有引用指向它。这个对象一直占据着内存,永远不会被释放。
内存泄漏的典型场景:
- Activity关闭了,但有个静态引用还指向它
- 非静态内部类持有了外部类的引用
- 单例对象引用了Context
- 注册了广播/监听器但没有注销
2.2 常见内存泄漏场景及解决方案
2.2.1 静态变量持有Activity/Context引用
这是最经典的内存泄漏案例。静态变量生命周期和APP进程一样长,如果你在里面存了Activity的引用,Activity就永远无法被回收。
// ❌ 错误:静态变量持有Activity引用
public class MySingleton {
private static MySingleton instance;
private Activity mActivity; // 这里有问题!
public static MySingleton getInstance(Activity activity) {
if (instance == null) {
instance = new MySingleton();
}
instance.mActivity = activity; // Activity的引用被永久持有
return instance;
}
}
// ✅ 正确:使用WeakReference
public class MySingleton {
private static MySingleton instance;
private WeakReference<Activity> mActivityRef;
public static MySingleton getInstance(Activity activity) {
if (instance == null) {
instance = new MySingleton();
}
instance.mActivityRef = new WeakReference<>(activity);
return instance;
}
public Activity getActivity() {
return mActivityRef != null ? mActivityRef.get() : null;
}
}
WeakReference的意思是:GC在回收时,如果发现一个对象只能被WeakReference引用到,它不会被犹豫,直接回收。这样Activity退出时就能正常被回收了。
不过要注意,使用WeakReference后,每次取值都要做null检查,因为对象可能已经被回收了。
2.2.2 非静态内部类和匿名类
非静态内部类会隐式持有外部类的引用,这在异步操作场景中特别容易出问题。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ❌ 错误:非静态匿名内部类,持有Activity引用
new Thread(new Runnable() {
@Override
public void run() {
// 假设这个操作需要几秒甚至更长时间
loadDataFromNetwork();
// 操作完成后更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
updateUI();
}
});
}
}).start();
}
}
线程执行完之前,Runnable对象一直被线程持有,而Runnable持有MainActivity的引用,MainActivity就无法被回收。即使用户已经退出界面了,内存里的Activity还”赖着不走”。
// ✅ 正确:使用静态内部类 + WeakReference
public class MainActivity extends AppCompatActivity {
private static class DataLoadTask extends AsyncTask<Void, Void, String> {
private final WeakReference<MainActivity> mActivityRef;
DataLoadTask(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
protected String doInBackground(Void... params) {
return loadDataFromNetwork();
}
@Override
protected void onPostExecute(String result) {
MainActivity activity = mActivityRef.get();
if (activity != null) {
activity.updateUI(result);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new DataLoadTask(this).execute();
}
}
用静态内部类的好处是它不会隐式持有外部类引用,再配合WeakReference,就能安全地在异步操作中使用Activity了。
2.2.3 未注销的广播接收器、监听器
BroadcastReceiver、View的OnClick监听器、观察者模式里的Observer,注册了就要记得注销,否则它们会一直持有引用。
// ❌ 错误:注册了但没有注销
public class MyFragment extends Fragment {
private BroadcastReceiver receiver;
@Override
public void onStart() {
super.onStart();
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
}
};
// 注册了,但 onStop 里没有注销!
getActivity().registerReceiver(receiver, new IntentFilter("com.example.MY_ACTION"));
}
}
这个Fragment一旦退出,receiver还挂着,它持有的Context(通过getActivity())也不会释放。
// ✅ 正确:注册和注销配对
public class MyFragment extends Fragment {
private BroadcastReceiver receiver;
@Override
public void onStart() {
super.onStart();
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
}
};
getActivity().registerReceiver(receiver, new IntentFilter("com.example.MY_ACTION"));
}
@Override
public void onStop() {
super.onStop();
// 注销广播接收器,防止内存泄漏
if (receiver != null) {
getActivity().unregisterReceiver(receiver);
receiver = null;
}
}
}
View的监听器也是一样的道理:
// ❌ 错误:注销不当
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 匿名内部类持有外部类引用
}
});
// ✅ 正确:使用弱引用或者在onDestroy中移除
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 逻辑
}
});
// 在合适的时机(比如界面销毁时)
button.setOnClickListener(null);
2.2.4 单例模式引用了Context
单例的生命周期和APP进程一样,用它来持有Activity或Context是非常危险的。
// ❌ 错误:单例持有Context
public class ApiManager {
private static ApiManager instance;
private Context context;
private ApiManager(Context context) {
this.context = context; // 持有Activity的Context
}
public static ApiManager getInstance(Context context) {
if (instance == null) {
instance = new ApiManager(context);
}
return instance;
}
}
// ✅ 正确:单例只持有Application Context
public class ApiManager {
private static ApiManager instance;
private Context context;
private ApiManager(Context context) {
this.context = context.getApplicationContext(); // 用Application Context
}
public static ApiManager getInstance(Context context) {
if (instance == null) {
instance = new ApiManager(context);
}
return instance;
}
}
Application Context的生命周期和APP进程一样长,但它不会绑定到任何Activity,所以用它来做全局性的操作是安全的。
2.2.5 Handler和Message的泄漏
Handler在非静态使用时会隐式持有外部类引用,而且Message也可能被消息队列持有。
// ❌ 错误:非静态Handler
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.sendEmptyMessageDelayed(1, 60_000); // 60秒后执行
// 如果用户60秒内退出了Activity,Handler和Message仍在队列中
// 它们持有MainActivity的引用,导致Activity无法回收
}
}
// ✅ 正确:静态Handler + WeakReference
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new MyHandler(this);
mHandler.sendEmptyMessageDelayed(1, 60_000);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null); // 清除所有消息
}
// 静态内部类,不隐式持有外部类引用
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivityRef;
MyHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity != null) {
// 处理消息
}
}
}
}
removeCallbacksAndMessages(null)这个调用很关键,它会把消息队列里所有未处理的消息都清除掉,避免Handler因为持有消息而间接持有Activity。
2.3 内存泄漏的检测工具
LeakCanary:这是Square公司开源的一款内存泄漏检测库,接入非常简单,只需要在debug包中添加依赖:
// build.gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
接入后,当检测到内存泄漏时,LeakCanary会在通知栏提示你,点击即可查看详细的泄漏引用链。这个工具对于日常开发来说几乎是必备的,它能帮你自动发现大部分内存泄漏问题,省去了手动排查的痛苦。
Android Studio Profiler:Android Studio内置的内存分析工具,可以手动创建Heap Dump,然后分析对象占用情况。
使用步骤:
- 运行APP,打开Profiler面板
- 执行一些操作(比如进入某个界面)
- 点击”Collect GC”触发垃圾回收
- 点击”Dump Java Heap”导出堆快照
- 在打开的窗口中,按”Package”或”Class”排序,查看哪些对象占用最多内存
- 右键选择”Show Dominator Tree”,追踪对象的引用链,找到是谁在持有它
MAT (Memory Analyzer Tool):更专业的内存分析工具,适合处理较大的heap dump文件。可以从Google的MAT官网下载,配合Android Studio的Profile功能使用。
三、ListView的其他常见坑
除了卡顿和内存泄漏,ListView还有一些其他常见的问题,下面我也一并讲一下。
3.1 notifyDataSetChanged的性能陷阱
每次数据变化都调用notifyDataSetChanged()是最简单粗暴的方式,但它会让ListView重新绘制整个列表,数据量大时性能非常差。
// ❌ 低效:数据量大时 notifyDataSetChanged() 非常慢
List<User> newUsers = fetchNewUsers();
dataList.clear();
dataList.addAll(newUsers);
adapter.notifyDataSetChanged();
// ✅ 高效:精确通知变化的区域
adapter.notifyItemRangeInserted(oldSize, newUsers.size());
// 或者使用DiffUtil(推荐)
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldList, newList));
diffResult.dispatchUpdatesTo(adapter);
DiffUtil是Android Support Library提供的工具,它可以自动计算出两个列表之间的差异,然后生成最小化的更新操作,比手动调用notifyItemInserted()更智能。
class MyDiffCallback extends DiffUtil.Callback {
private final List<User> oldList;
private final List<User> newList;
MyDiffCallback(List<User> oldList, List<User> 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 oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
User oldUser = oldList.get(oldItemPosition);
User newUser = newList.get(newItemPosition);
return oldUser.getName().equals(newUser.getName())
&& oldUser.getAvatarUrl().equals(newUser.getAvatarUrl());
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
// 可以返回部分更新的payload,实现更高效的部分刷新
return null;
}
}
3.2 RecyclerView:ListView的替代者
如果你还在用ListView,强烈建议迁移到RecyclerView。RecyclerView在设计上就解决了ListView的很多问题:
- ViewHolder模式强制使用:ListView可以通过convertView的tag来复用,但RecyclerView强制你使用ViewHolder,代码更清晰
- 灵活的布局管理器:通过
RecyclerView.LayoutManager可以方便地实现列表、网格、瀑布流等多种布局 - 动画支持:内置了添加、删除、移动等动画效果
- ItemDecoration和ItemAnimator可定制:可以通过自定义来扩展功能
// RecyclerView的基本使用
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
private List<User> dataList;
private Context context;
public static class ViewHolder extends RecyclerView.ViewHolder {
ImageView ivAvatar;
TextView tvName;
TextView tvTime;
public ViewHolder(View itemView) {
super(itemView);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
tvName = itemView.findViewById(R.id.tv_name);
tvTime = itemView.findViewById(R.id.tv_time);
}
}
public MyRecyclerViewAdapter(List<User> dataList, Context context) {
this.dataList = dataList;
this.context = context;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_list, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
User user = dataList.get(position);
holder.tvName.setText(user.getName());
holder.tvTime.setText(user.getCreateTime());
Glide.with(context)
.load(user.getAvatarUrl())
.into(holder.ivAvatar);
}
@Override
public int getItemCount() {
return dataList.size();
}
}
<!-- 在布局中使用RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
// Activity中初始化
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new MyRecyclerViewAdapter(dataList, this));
3.3 ListView的item点击事件重复触发
这是一个很容易被忽视的问题。如果你的ListView里每个item都有点击事件,而且item布局中包含Button、CheckBox等可以获取焦点的控件,点击事件可能会被这些子控件拦截,导致点击不灵敏或者重复触发。
<!-- ❌ 问题:子控件获取焦点,导致点击事件异常 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/btn_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="操作"/>
</LinearLayout>
解决方案有两种:
方案一:在根布局设置focusable=“false”
<!-- ✅ 方案一:根布局禁止焦点 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:descendantFocusability="blocksDescendants">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/btn_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="操作"/>
</LinearLayout>
descendantFocusability="blocksDescendants"表示根布局会拦截所有子控件的焦点请求,点击事件就会正确传递给ListView的item。
方案二:在Adapter中区分点击
// ✅ 方案二:在Adapter中精确处理点击
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
// 处理item点击
}
}
});
holder.btnAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
// 处理按钮点击
}
}
});
3.4 图片OOM问题
列表中有大量图片时,很容易触发OOM。除了使用Glide等库做缓存外,还要注意以下几点:
控制图片尺寸:不要直接加载原图,要根据ImageView的实际大小来缩放。
// ✅ 使用Glide的override方法指定加载尺寸
Glide.with(context)
.load(url)
.override(200, 200) // 指定目标尺寸,避免加载过大的图片
.centerCrop()
.into(imageView);
合理使用缓存策略:Glide默认有内存缓存和磁盘缓存,但你可以针对不同的图片设置不同的策略。
// 对于不太可能变化的头像,可以设置较强的缓存策略
Glide.with(context)
.load(avatarUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL) // 同时缓存原始和转换后的图片
.into(imageView);
// 对于经常变化的图片(如动态内容),不缓存原始图
Glide.with(context)
.load(dynamicImageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA) // 只缓存解码后的图片
.into(imageView);
及时回收Bitmap:如果你手动处理Bitmap,用完了一定要调用recycle()。
// ❌ 错误:用完Bitmap没有回收
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
imageView.setImageBitmap(bitmap);
// bitmap仍然占用内存,即使ImageView已经不在屏幕上
// ✅ 正确:用完回收
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
imageView.setImageBitmap(bitmap);
// 在适当的时候(比如Fragment/Activity销毁时)
imageView.setImageBitmap(null);
bitmap.recycle();
四、从ListView到ViewPager2:现代Android列表的完整避坑
现在的APP很少只用ListView了,通常会配合ViewPager做滑动切换的多个页面,或者用RecyclerView做复杂列表。下面讲几个这些场景中的坑。
4.1 ViewPager + Fragment的内存泄漏
ViewPager配合Fragment使用时,如果处理不好,很容易产生大量Fragment实例却无法回收。
// ❌ 错误:setRetainInstance(true)在某些场景下会导致Fragment泄漏
public class MyFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true); // 这个在某些场景下会导致问题
}
}
推荐使用FragmentStatePagerAdapter而不是FragmentPagerAdapter,前者会在页面不可见时销毁Fragment,只保留状态信息:
// ✅ 使用FragmentStatePagerAdapter
public class MyPagerAdapter extends FragmentStatePagerAdapter {
private final List<Fragment> fragments;
public MyPagerAdapter(@NonNull FragmentManager fm, List<Fragment> fragments) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.fragments = fragments;
}
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
}
4.2 ViewPager2的优化
ViewPager2是ViewPager的升级版,基于RecyclerView实现,性能更好,也更灵活。
// ✅ ViewPager2的基本使用
ViewPager2 viewPager2 = findViewById(R.id.view_pager_2);
viewPager2.setAdapter(new MyViewPager2Adapter(fragmentList));
// ViewPager2的Adapter
public class MyViewPager2Adapter extends RecyclerView.Adapter<MyViewPager2Adapter.ViewHolder> {
private final List<Fragment> fragmentList;
private final FragmentManager fragmentManager;
private final FragmentActivity fragmentActivity;
public MyViewPager2Adapter(List<Fragment> fragmentList,
FragmentActivity fragmentActivity,
FragmentManager fragmentManager) {
this.fragmentList = fragmentList;
this.fragmentActivity = fragmentActivity;
this.fragmentManager = fragmentManager;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(FragmentContainerView(parent.getContext()));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fragment fragment = fragmentList.get(position);
fragmentActivity.getSupportFragmentManager()
.beginTransaction()
.replace(holder.container.getId(), fragment)
.commit();
}
@Override
public int getItemCount() {
return fragmentList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
final FragmentContainerView container;
ViewHolder(FragmentContainerView container) {
super(container);
this.container = container;
}
}
}
<!-- 布局中的FragmentContainerView -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_2"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
4.3 滚动时暂停图片加载
列表滚动时,如果每一张图片都去网络加载,不仅流量浪费,还会导致列表滑动卡顿。常见的做法是:滚动时暂停加载,停止时恢复加载。
// ✅ 使用Glide的RequestManagerToken
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_SCROLLING -> {
// 滚动时暂停所有图片请求
Glide.with(recyclerView.context)
.pauseRequests()
}
RecyclerView.SCROLL_STATE_IDLE -> {
// 停止时恢复
Glide.with(recyclerView.context)
.resumeRequests()
}
}
}
})
五、总结:Android列表优化的核心思路
回顾一下这篇文章讲的内容,核心思路其实就一句话:尽量减少主线程的工作量,尽量复用已有的资源,尽量释放不再需要的引用。
具体来说:
- 卡顿优化:把耗时操作移到子线程,复用convertView和ViewHolder,扁平化布局,使用DiffUtil精确更新。
- 内存泄漏:静态类不持有Activity引用,使用WeakReference,注册和注销配对,善用LeakCanary。
- 图片加载:用Glide/Picasso等库,指定加载尺寸,合理使用缓存,滚动时暂停加载。
- 架构选型:能用RecyclerView就别用ListView,能用ViewPager2就别用ViewPager,能用ViewModel就别在Fragment里存状态。
Android开发里有很多”看起来能用但实际有问题”的代码,这些东西在开发阶段可能不会直接暴露出来,但一旦用户量上来、设备类型多样化,问题就会集中爆发。所以养成好的编码习惯,定期用Profiling工具检查自己的APP,是避免这些坑的最好方法。
希望这篇文章对你有所启发。如果还有具体的场景遇到坑了,欢迎继续讨论。
