Android中RecyclerView加载900首歌曲耗时过长的优化方案咨询
Hey there! Loading 900 songs shouldn't take 9-10 seconds—let's dig into the bottlenecks in your code and fix them to get that load time way down.
1. 移除同步网络请求(最大的性能杀手)
Your biggest issue right now is probably the getAlbumArtFromInternet call inside the cursor loop. Doing a synchronous network request for every single song blocks the background thread until each request completes—with 900 songs, this adds massive overhead that's almost certainly causing the long load time.
Instead, move album art loading to your SongAdapter using an image loading library like Glide or Picasso. These libraries handle async loading, caching, and placeholder images automatically:
- In your
SongInfoModel, store just the album ID/artist/title instead of the pre-fetched URI. - In your Adapter's ViewHolder, load the art when binding the item:
// Example with Glide (Java implementation) Glide.with(itemView.getContext()) .load(getLocalAlbumArtUri(albumId)) // Try local art first .placeholder(R.drawable.default_album_art) .error(Glide.with(itemView.getContext()) .load(getInternetAlbumArtUrl(artist, title))) // Async fallback to internet .into(albumArtImageView);
This way, album art loads in the background after your song list is already displayed, so the RecyclerView populates instantly.
2. 优化Cursor解析效率
Your cursor loop is doing unnecessary repetitive work that adds up:
- Precompute column indices: Calling
cursor.getColumnIndex()9 times per song (900 songs = 8100 calls) is inefficient. Get all indices once before the loop:
// Inside the background thread, after confirming cursor is not null int idCol = cursor.getColumnIndex(MediaStore.Audio.Media._ID); int titleCol = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE); int artistCol = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST); // ... repeat this for all columns you need to fetch
Then use these precomputed indices in your loop instead of calling getColumnIndex every time.
Initialize your list with fixed capacity: When creating
SongList, usenew ArrayList<>(cursor.getCount())to avoid expensive resizing as the list grows. The cursor's count tells you exactly how many songs you'll add, so pre-allocating space eliminates reallocation overhead.Simplify the cursor loop: You don't need
moveToFirst()+do-while—just usewhile (cursor.moveToNext())which is cleaner and slightly more efficient.
3. 替换过时的AsyncTask
AsyncTask is deprecated and has limitations with thread pool management. Switch to ExecutorService (for Java) or Coroutines (for Kotlin) for better control over background threading.
Here's a Java example using ExecutorService:
private void queryForSongsInTheBg(final Uri uri, final String selection, final String[] whereVal) { ProgressBar progressBar = getView().findViewById(R.id.querySongsProgressBar); progressBar.setVisibility(View.VISIBLE); ExecutorService executor = Executors.newSingleThreadExecutor(); Handler mainHandler = new Handler(Looper.getMainLooper()); executor.execute(() -> { // Your cursor query and optimized data parsing goes here mainHandler.post(() -> { progressBar.setVisibility(View.GONE); songAdapter = new SongAdapter(getContext(), SongList, listener); recyclerView.setAdapter(songAdapter); }); }); }
4. RecyclerView基础优化
Make sure your RecyclerView is configured for maximum performance:
- Call
recyclerView.setHasFixedSize(true)if the size of your RecyclerView doesn't change after loading (most music apps fit this case). This tells the RecyclerView it doesn't need to re-measure items every time data changes. - Ensure your Adapter's
onCreateViewHolderonly handles inflating the layout and finding views—no heavy processing should happen here. - Use
RecyclerView.RecycledViewPoolif you have multiple RecyclerViews (like in tabbed interfaces) to reuse ViewHolders across them.
修改后的核心代码示例
Here's how your optimized background processing code would look:
// Inside the background thread (ExecutorService runnable) String[] projection = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATE_ADDED, MediaStore.Audio.Media.YEAR }; Cursor cursor = getActivity().getContentResolver().query(uri, projection, selection, whereVal, null); if (cursor != null) { // Precompute all column indices once int idCol = cursor.getColumnIndex(MediaStore.Audio.Media._ID); int titleCol = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE); int artistCol = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST); int albumCol = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM); int durationCol = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION); int dataCol = cursor.getColumnIndex(MediaStore.Audio.Media.DATA); int albumIdCol = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID); int dateAddedCol = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED); int yearCol = cursor.getColumnIndex(MediaStore.Audio.Media.YEAR); // Initialize list with fixed capacity SongList = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { long id = cursor.getLong(idCol); String name = cursor.getString(titleCol); String artist = cursor.getString(artistCol); String album = cursor.getString(albumCol); Long duration = cursor.getLong(durationCol); String data = cursor.getString(dataCol); Long albumId = cursor.getLong(albumIdCol); String date_added = cursor.getString(dateAddedCol); Integer year = cursor.getInt(yearCol); // Only handle local album art here—internet load goes to Adapter String albumArtUri = String.valueOf(ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId)); String checkAlbumArt = getCoverArtPath(albumId); if (checkAlbumArt == null || checkAlbumArt.isEmpty()) { albumArtUri = null; // Flag to trigger internet load in Adapter } SongInfoModel s = new SongInfoModel(id, name, artist, date_added, album, year, duration, data, albumId, albumArtUri); SongList.add(s); } cursor.close(); }
内容的提问来源于stack exchange,提问作者Rektirino

