不使用Picasso/Glide,GridView/ListView加载400张图片性能优化求助
Hey there! Let's tackle this image loading slowdown issue you're facing with your GridView/ListView—totally get why you'd want to avoid third-party libs like Picasso or Glide, so let's break down what's going wrong and how to fix it.
The Root Cause
Both of your current approaches are hitting the same bottlenecks:
- Synchronous network calls on the main thread: Whether you're converting all Bitmaps upfront or doing it in
getView, you're blocking the UI thread while waiting for network responses. This doesn't just make loading slow—it risks ANRs (Application Not Responding) too. - No caching: Every time you load an image, you're making a fresh network request instead of reusing a previously downloaded copy. That's 400+ redundant calls to the server, which kills speed.
Practical Fixes (No Third-Party Libs Required)
Here are actionable steps to speed things up drastically:
1. Offload Network Work to Background Threads
Never do network operations on the main thread. Use AsyncTask, Coroutines (if you're using Kotlin), or a dedicated HandlerThread to fetch and process images in the background. Also, handle view recycling properly to avoid image misalignment:
- Cancel pending tasks when a view is reused (so old requests don't overwrite new ones)
- Use
WeakReferencefor ImageViews to prevent memory leaks
2. Add Memory Caching with LruCache
Store frequently used Bitmaps in memory to avoid reloading them from disk or network. LruCache is perfect for this—it automatically evicts least recently used items when memory runs low:
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // Allocate ~1/8 of your app's available memory for the cache final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // Return the Bitmap's size in KB return bitmap.getByteCount() / 1024; } }; } // Add bitmap to memory cache public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap); } } // Fetch bitmap from memory cache public Bitmap getBitmapFromMemCache(String key) { return memoryCache.get(key); }
3. Implement Disk Caching
Save downloaded Bitmaps to your app's cache directory so you don't have to re-download them. Use the image URL's hash as a filename to avoid conflicts:
// Load bitmap from disk cache private Bitmap getBitmapFromDiskCache(String imageUrl) { String fileName = String.valueOf(imageUrl.hashCode()); File cacheFile = new File(getApplicationContext().getCacheDir(), fileName); return cacheFile.exists() ? BitmapFactory.decodeFile(cacheFile.getAbsolutePath()) : null; } // Save bitmap to disk cache private void saveBitmapToDiskCache(String imageUrl, Bitmap bitmap) { String fileName = String.valueOf(imageUrl.hashCode()); File cacheFile = new File(getApplicationContext().getCacheDir(), fileName); try (FileOutputStream fos = new FileOutputStream(cacheFile)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); } catch (IOException e) { e.printStackTrace(); } }
4. Compress Bitmaps Before Loading
Don't load full-size images into memory—resize them to match your GridView item's dimensions. This cuts down memory usage and speeds up rendering:
private Bitmap decodeSampledBitmapFromStream(InputStream inputStream, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); // First pass: get image dimensions without loading the full bitmap options.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, options); // Calculate sample size to resize the image options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Second pass: load the resized bitmap options.inJustDecodeBounds = false; // Reopen the stream since the first decode consumed it try { URL url = new URL(imageUrl); inputStream = ((HttpURLConnection) url.openConnection()).getInputStream(); } catch (IOException e) { e.printStackTrace(); } return BitmapFactory.decodeStream(inputStream, null, options); } private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Double the sample size until the resized image fits the target dimensions while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
5. Combine Everything in getView
Put it all together to load images efficiently:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(context).inflate(R.layout.grid_item, parent, false); holder = new ViewHolder(); holder.imageView = convertView.findViewById(R.id.item_image); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); // Cancel pending tasks to avoid image misalignment if (holder.loadTask != null) { holder.loadTask.cancel(true); } } String imageUrl = imageUrls.get(position); // Check memory cache first Bitmap cachedBitmap = getBitmapFromMemCache(imageUrl); if (cachedBitmap != null) { holder.imageView.setImageBitmap(cachedBitmap); } else { holder.imageView.setImageResource(R.drawable.placeholder); // Start background task to load image holder.loadTask = new LoadImageTask(holder.imageView, imageUrl, itemWidth, itemHeight); holder.loadTask.execute(); } return convertView; } private class LoadImageTask extends AsyncTask<Void, Void, Bitmap> { private final WeakReference<ImageView> imageViewRef; private final String imageUrl; private final int reqWidth; private final int reqHeight; public LoadImageTask(ImageView imageView, String imageUrl, int reqWidth, int reqHeight) { imageViewRef = new WeakReference<>(imageView); this.imageUrl = imageUrl; this.reqWidth = reqWidth; this.reqHeight = reqHeight; } @Override protected Bitmap doInBackground(Void... voids) { if (isCancelled()) return null; // Check disk cache next Bitmap bitmap = getBitmapFromDiskCache(imageUrl); if (bitmap != null) { addBitmapToMemoryCache(imageUrl, bitmap); return bitmap; } // Fetch from network if no cache exists try { URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); InputStream inputStream = connection.getInputStream(); bitmap = decodeSampledBitmapFromStream(inputStream, reqWidth, reqHeight); if (bitmap != null) { saveBitmapToDiskCache(imageUrl, bitmap); addBitmapToMemoryCache(imageUrl, bitmap); } inputStream.close(); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { ImageView imageView = imageViewRef.get(); if (imageView != null && bitmap != null && !isCancelled()) { imageView.setImageBitmap(bitmap); } } } static class ViewHolder { ImageView imageView; LoadImageTask loadTask; }
Final Notes
Combining these optimizations will eliminate redundant network calls, keep your UI responsive, and drastically speed up image loading. The key is prioritizing cache layers (memory → disk → network) and never blocking the main thread.
内容的提问来源于stack exchange,提问作者Adrian

