说到 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 点击事件失效或错位。这通常是因为没有正确处理 adapterPositionbindingAdapterPosition 的区别。

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 并精确通知数据变化;异步加载图片一定要用库而不是手写;权限请求要考虑版本兼容和生命周期管理。记住这些,你的列表开发之路会顺畅很多。还有啥问题随时问我!