说到 Android 开发,列表组件绝对是咱们每天打交道最多的“老熟人”了。但很多新手(甚至一些有一定经验的开发者)在写列表时,常常会遇到滑动卡顿、内存泄漏、权限拒绝崩溃这些让人头秃的问题。今天我们就把这些坑一个个踩平,从 ListView 的优化讲到 RecyclerView 的进阶,再聊聊权限请求那些容易被忽视的细节,最后配上完整可用的代码,让你下次写列表时心里有底。
先说说 ListView。虽然官方早就推荐用 RecyclerView 了,但市面上还有很多老项目在用 ListView,而且理解 ListView 的优化原理对后续学习 RecyclerView 很有帮助。ListView 最让人头疼的就是滑动卡顿,主要原因在于每次滚动时都要重新计算视图,而且如果没有复用机制,内存开销会爆炸。解决这个问题的核心就是 convertView 复用和 ViewHolder 模式。
public class ListViewAdapter extends BaseAdapter {
private Context mContext;
private List<String> mDataList;
public ListViewAdapter(Context context, List<String> dataList) {
this.mContext = context;
this.mDataList = dataList;
}
@Override
public int getCount() {
return mDataList.size();
}
@Override
public Object getItem(int position) {
return mDataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
// 关键优化点1:复用convertView,避免重复创建View
if (convertView == null) {
viewHolder = new ViewHolder();
// 关键优化点2:使用inflate而不是new,确保布局属性正确
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.item_list_view, parent, false);
viewHolder.textView = convertView.findViewById(R.id.tv_item);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
// 关键优化点3:只更新变化的内容,避免不必要的操作
String data = mDataList.get(position);
viewHolder.textView.setText(data);
return convertView;
}
// ViewHolder模式,避免每次findViewById
static class ViewHolder {
TextView textView;
}
}
这里有个很多人会忽略的细节:inflate 的第三个参数 attachToRoot 一定要传 false。如果传 true,View 会被直接添加到父容器中,导致重复添加。这个坑我见过太多人踩了,调试半天才发现是布局嵌套问题。
接下来进入正题——RecyclerView。这是 Google 官方推荐的列表组件,比 ListView 灵活得多,但学习曲线也稍微陡一些。RecyclerView 的核心思想是“视图回收复用”,和 ListView 类似,但实现更加模块化。
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private Context mContext;
private List<String> mDataList;
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
public RecyclerViewAdapter(Context context, List<String> dataList) {
this.mContext = context;
this.mDataList = dataList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 注意这里用的是RecyclerView,和ListView不同
View view = LayoutInflater.from(mContext)
.inflate(R.layout.item_recycler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String data = mDataList.get(position);
holder.textView.setText(data);
// 设置点击事件
if (listener != null) {
holder.itemView.setOnClickListener(v -> {
int adapterPosition = holder.getAdapterPosition();
if (adapterPosition != RecyclerView.NO_POSITION) {
listener.onItemClick(adapterPosition);
}
});
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
// 更新数据的方法,注意使用notifyItemRangeChanged而不是notifyDataSetChanged
// notifyDataSetChanged会触发所有item的重新绑定,性能较差
public void updateData(List<String> newData) {
mDataList.clear();
mDataList.addAll(newData);
notifyDataSetChanged(); // 简单场景用这个
// 或者更精细的更新:
// notifyItemRangeChanged(0, newData.size());
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.tv_recycler_item);
}
}
}
Activity 中的使用也很简单,但要注意布局管理器的选择:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private RecyclerViewAdapter adapter;
private List<String> dataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recycler_view);
// 初始化数据
dataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
dataList.add("Item " + i);
}
// 关键:必须设置LayoutManager,否则RecyclerView无法工作
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 可选:添加分隔线
recyclerView.addItemDecoration(new DividerItemDecoration(
this, DividerItemDecoration.VERTICAL));
adapter = new RecyclerViewAdapter(this, dataList);
recyclerView.setAdapter(adapter);
// 设置点击监听
adapter.setOnItemClickListener(position -> {
Toast.makeText(this, "Clicked: " + position, Toast.LENGTH_SHORT).show();
});
}
}
现在说说异步加载,这是列表组件的标配功能。很多开发者喜欢直接在 onBindViewHolder 里加载图片,但这会导致严重的性能问题,因为每次滚动都会触发网络请求。
public class ImageLoaderTask extends AsyncTask<String, Void, Bitmap> {
private WeakReference<ImageView> imageViewReference;
public ImageLoaderTask(ImageView imageView) {
imageViewReference = new WeakReference<>(imageView);
}
@Override
protected Bitmap doInBackground(String... urls) {
try {
// 实际项目中应该使用Glide、Picasso等成熟库
// 这里只是演示异步加载的基本思路
InputStream in = new java.net.URL(urls[0]).openStream();
return BitmapFactory.decodeStream(in);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
ImageView imageView = imageViewReference.get();
// 关键:检查ImageView是否还在屏幕上,避免内存泄漏和显示错乱
if (imageView != null && bitmap != null) {
// 再次检查position是否匹配,因为复用可能导致错位
Integer position = (Integer) imageView.getTag(R.id.tag_position);
if (position != null && position == imageView.getTag(R.id.tag_real_position)) {
imageView.setImageBitmap(bitmap);
}
}
}
}
在实际绑定图片时,要这样写:
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String data = mDataList.get(position);
holder.textView.setText(data);
// 先设置占位图
holder.imageView.setImageResource(R.drawable.placeholder);
// 保存position到View上,用于后续校验
holder.imageView.setTag(R.id.tag_position, position);
// 异步加载图片
new ImageLoaderTask(holder.imageView)
.execute("https://example.com/image" + position + ".jpg");
}
不过说实话,现在基本没人自己写异步加载了,直接用 Glide 或 Coil 就行:
// 使用Glide加载图片,一行代码搞定
Glide.with(holder.imageView.getContext())
.load(imageUrl)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(holder.imageView);
Glide 内置了缓存、复用校验、生命周期管理,比手写异步任务靠谱多了。
接下来聊聊权限请求,这是 Android 开发中另一个大坑。很多人以为调一下 requestPermissions 就完了,结果在 Android 10+ 上各种奇葩问题。
public class PermissionHelper {
// 权限请求码,自己定义一个唯一的值
private static final int PERMISSION_REQUEST_CODE = 1001;
// 需要请求的权限列表
private static final String[] PERMISSIONS = {
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
public static void requestPermissions(Activity activity) {
// 关键:检查是否已经授予权限
if (hasPermissions(activity, PERMISSIONS)) {
// 权限已授予,直接执行业务逻辑
onPermissionsGranted(activity);
return;
}
// 关键:检查是否需要显示权限说明
// 如果用户之前拒绝过权限,应该先解释为什么需要这个权限
if (shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA)) {
showPermissionExplanation(activity);
return;
}
// 直接请求权限
activity.requestPermissions(PERMISSIONS, PERMISSION_REQUEST_CODE);
}
private static boolean hasPermissions(Activity activity, String... permissions) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(activity, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private static void showPermissionExplanation(Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("需要权限")
.setMessage("我们需要相机权限来拍摄照片,这是应用的核心功能。")
.setPositiveButton("去设置", (dialog, which) -> {
// 跳转到应用设置页面
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
})
.setNegativeButton("取消", null)
.show();
}
// 在Activity中重写onRequestPermissionsResult
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
// 关键:检查是否所有权限都被授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
onPermissionsGranted(this);
} else {
// 关键:检查是否勾选了"不再询问"
if (!shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
// 用户勾选了"不再询问",需要引导去设置页面
showPermissionDeniedDialog();
} else {
// 用户拒绝了,可以再试一次
requestPermissions(this);
}
}
}
}
private void showPermissionDeniedDialog() {
new AlertDialog.Builder(this)
.setTitle("权限被拒绝")
.setMessage("您已在系统设置中拒绝了权限,请手动前往设置开启。")
.setPositiveButton("去设置", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
})
.setNegativeButton("取消", null)
.show();
}
private void onPermissionsGranted(Activity activity) {
// 权限已授予,执行业务逻辑
Toast.makeText(activity, "权限已授予", Toast.LENGTH_SHORT).show();
}
}
这里有个特别容易踩的坑:在 Android 11(API 30)及以上,WRITE_EXTERNAL_STORAGE 权限已经被废弃了,媒体访问改用 READ_MEDIA_IMAGES 等新权限。所以做权限判断时一定要区分 API 版本:
@TargetApi(Build.VERSION_CODES.R)
private boolean checkStoragePermissionV30(Activity activity) {
// Android 11+ 使用新的存储权限
if (Environment.getExternalStorageProfile() != null) {
return Environment.isExternalStorageManager();
}
return true;
}
private boolean checkStoragePermission(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return checkStoragePermissionV30(activity);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 使用分区存储,不需要WRITE_EXTERNAL_STORAGE
return true;
} else {
// Android 9及以下
return ContextCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
}
再说说 RecyclerView 的常见坑点。第一个是 notifyDataSetChanged 的性能问题。这个方法会通知适配器所有数据都变了,导致所有可见的 item 都会重新绑定。如果你的列表有 1000 条数据,虽然只变了 1 条,但它还是会触发 1000 次绑定,这在大数据量下非常卡顿。
// 错误示范:数据量大时性能极差
adapter.notifyDataSetChanged();
// 正确做法:精确通知变化
adapter.notifyItemChanged(position); // 单个item变化
adapter.notifyItemInserted(position); // 插入item
adapter.notifyItemRemoved(position); // 删除item
adapter.notifyItemRangeChanged(start, count); // 范围变化
adapter.notifyItemRangeInserted(start, count); // 范围插入
adapter.notifyItemRangeRemoved(start, count); // 范围删除
第二个坑是 layoutManager 没有正确初始化。很多新手忘记设置 layoutManager,或者设置错了类型,导致列表不显示或显示异常。
// 错误的 layoutManager 设置
recyclerView.setLayoutManager(new StackLayout()); // StackLayout不存在!
// 正确的设置
recyclerView.setLayoutManager(new LinearLayoutManager(this)); // 线性布局
// 或
recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); // 网格布局,2列
// 或
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL)); // 瀑布流布局
第三个坑是 item 点击事件失效或错位。这通常是因为没有正确处理 adapterPosition 和 bindingAdapterPosition 的区别。
holder.itemView.setOnClickListener(v -> {
// 错误:使用getPosition(),这个方法已经废弃,且可能返回错误的位置
int position = holder.getPosition();
// 正确:使用getAdapterPosition()或getBindingAdapterPosition()
int adapterPosition = holder.getAdapterPosition();
int bindingPosition = holder.getBindingAdapterPosition();
// 注意:在数据更新过程中,这些位置可能返回NO_POSITION
if (adapterPosition != RecyclerView.NO_POSITION) {
// 安全地使用position
doSomething(adapterPosition);
}
});
最后一个大坑是权限请求的生命周期问题。如果在 Activity 销毁后还在等待权限结果,就会崩溃。
public class PermissionManager implements LifecycleObserver {
private Activity activity;
private PermissionCallback callback;
private int requestCode;
public PermissionManager(Activity activity, int requestCode, PermissionCallback callback) {
this.activity = activity;
this.requestCode = requestCode;
this.callback = callback;
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestory() {
// 关键:在Activity销毁时清理引用,防止内存泄漏
activity = null;
callback = null;
}
public interface PermissionCallback {
void onGranted();
void onDenied();
}
}
实际使用时,要把这个管理器绑定到生命周期:
PermissionManager permissionManager = new PermissionManager(
this, PERMISSION_REQUEST_CODE, new PermissionManager.PermissionCallback() {
@Override
public void onGranted() {
// 权限已授予
}
@Override
public void onDenied() {
// 权限被拒绝
}
});
getLifecycle().addObserver(permissionManager);
好了,今天把这些坑都讲完了。总结一下关键点:ListView 要注意 convertView 复用和 ViewHolder;RecyclerView 要正确设置 layoutManager 并精确通知数据变化;异步加载图片一定要用库而不是手写;权限请求要考虑版本兼容和生命周期管理。记住这些,你的列表开发之路会顺畅很多。还有啥问题随时问我!
