演化理解 Android 非同步載入圖片
在學習"Android非同步載入影象小結"這篇文章時, 發現有些地方沒寫清楚,我就根據我的理解,把這篇文章的程式碼重寫整理了一遍,下面就是我的整理。
下面測試使用的layout檔案:
簡單來說就是 LinearLayout 佈局,其下放了5個ImageView。
<?xml version="1.0" encoding="utf-8"?>http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> 圖片區域開始" android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content">@+id/imageView1" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="wrap_content">@+id/imageView2" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="wrap_content">@+id/imageView3" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="wrap_content">@+id/imageView4" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="wrap_content">@+id/imageView5" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="wrap_content">圖片區域結束" android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content">
我們將演示的邏輯是非同步從伺服器上下載5張不同圖片,依次放入這5個ImageView。上下2個TextView 是為了方便我們看是否阻塞了UI的顯示。
當然 AndroidManifest.xml 檔案中要配置好網路訪問許可權。
Handler+Runnable模式
我們先看一個並不是非同步執行緒載入的例子,使用 Handler+Runnable模式。
這裡為何不是新開執行緒的原因請參看這篇文章:Android Runnable 執行在那個執行緒 這裡的程式碼其實是在UI 主執行緒中下載圖片的,而不是新開執行緒。
我們執行下面程式碼時,會發現他其實是阻塞了整個介面的顯示,需要所有圖片都載入完成後,才能顯示介面。
package ghj1976.AndroidTest;import java.io.IOException;import java.net.URL;import android.app.Activity;import android.graphics.drawable.Drawable;import android.os.Bundle;import android.os.Handler;import android.os.SystemClock;import android.util.Log;import android.widget.ImageView;public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); loadImage("http://www.baidu.com/img/baidu_logo.gif", R.id.imageView1); loadImage(http://www.chinatelecom.com.cn/images/logo_new.gif", R.id.imageView2); loadImage("http://cache.soso.com/30d/img/web/logo.gif, R.id.imageView3); loadImage("http://csdnimg.cn/www/images/csdnindex_logo.gif", R.id.imageView4); loadImage("http://www.cnblogs.com/images/logo_small.gif", R.id.imageView5); } private Handler handler = new Handler(); private void loadImage(final String url, final int id) { handler.post(new Runnable() { public void run() { Drawable drawable = null; try { drawable = Drawable.createFromStream( new URL(url).openStream(), "image.gif"); } catch (IOException e) { Log.d("test", e.getMessage()); } if (drawable == null) { Log.d("test", "null drawable"); } else { Log.d("test", "not null drawable"); } // 為了測試快取而模擬的網路延時
SystemClock.sleep(2000);
((ImageView) MainActivity.this.findViewById(id)) .setImageDrawable(drawable); } }); }}
Handler+Thread+Message模式
這種模式使用了執行緒,所以可以看到非同步載入的效果。
核心程式碼:
package ghj1976.AndroidTest;import java.io.IOException;import java.net.URL;import android.app.Activity;import android.graphics.drawable.Drawable;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.os.SystemClock;import android.util.Log;import android.widget.ImageView;public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); loadImage2("http://www.baidu.com/img/baidu_logo.gif", R.id.imageView1); loadImage2("http://www.chinatelecom.com.cn/images/logo_new.gif", R.id.imageView2); loadImage2("http://cache.soso.com/30d/img/web/logo.gif", R.id.imageView3); loadImage2("http://csdnimg.cn/www/images/csdnindex_logo.gif", R.id.imageView4); loadImage2("http://www.cnblogs.com/images/logo_small.gif", R.id.imageView5); } final Handler handler2 = new Handler() { @Override public void handleMessage(Message msg) { ((ImageView) MainActivity.this.findViewById(msg.arg1)) .setImageDrawable((Drawable) msg.obj); } }; // 採用handler+Thread模式實現多執行緒非同步載入 private void loadImage2(final String url, final int id) { Thread thread = new Thread() { @Override public void run() { Drawable drawable = null; try { drawable = Drawable.createFromStream( new URL(url).openStream(), "image.png"); } catch (IOException e) { Log.d("test", e.getMessage()); } // 模擬網路延時 SystemClock.sleep(2000); Message message = handler2.obtainMessage(); message.arg1 = id; message.obj = drawable; handler2.sendMessage(message); } }; thread.start(); thread = null; }}
這時候我們可以看到實現了非同步載入, 介面開啟時,五個ImageView都是沒有圖的,然後在各自執行緒下載完後才把圖自動更新上去。
Handler+ExecutorService(執行緒池)+MessageQueue模式
能開執行緒的個數畢竟是有限的,我們總不能開很多執行緒,對於手機更是如此。
這個例子是使用執行緒池。Android擁有與Java相同的ExecutorService實現,我們就來用它。
執行緒池的基本思想還是一種物件池的思想,開闢一塊記憶體空間,裡面存放了眾多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆建立執行緒物件所帶來的效能開銷,節省了系統的資源。
核心程式碼
package ghj1976.AndroidTest;import java.io.IOException;import java.net.URL;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import android.app.Activity;import android.graphics.drawable.Drawable;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.os.SystemClock;import android.util.Log;import android.widget.ImageView;public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); loadImage3("http://www.baidu.com/img/baidu_logo.gif", R.id.imageView1); loadImage3("http://www.chinatelecom.com.cn/images/logo_new.gif", R.id.imageView2); loadImage3("http://cache.soso.com/30d/img/web/logo.gif", R.id.imageView3); loadImage3("http://csdnimg.cn/www/images/csdnindex_logo.gif", R.id.imageView4); loadImage3("http://www.cnblogs.com/images/logo_small.gif", R.id.imageView5); } private Handler handler = new Handler(); private ExecutorService executorService = Executors.newFixedThreadPool(5); // 引入執行緒池來管理多執行緒 private void loadImage3(final String url, final int id) { executorService.submit(new Runnable() { public void run() { try { final Drawable drawable = Drawable.createFromStream( new URL(url).openStream(), "image.png"); // 模擬網路延時 SystemClock.sleep(2000); handler.post(new Runnable() { public void run() { ((ImageView) MainActivity.this.findViewById(id)) .setImageDrawable(drawable); } }); } catch (Exception e) { throw new RuntimeException(e); } } }); }}
這裡我們象第一步一樣使用了 handler.post(new Runnable() { 更新前段顯示當然是在UI主執行緒,我們還有 executorService.submit(new Runnable() { 來確保下載是線上程池的執行緒中。
Handler+ExecutorService(執行緒池)+MessageQueue+快取模式
下面比起前一個做了幾個改造:
- 把整個程式碼封裝在一個類中
- 為了避免出現同時多次下載同一幅圖的問題,使用了本地快取
封裝的類:
package ghj1976.AndroidTest;import java.lang.ref.SoftReference;import java.net.URL;import java.util.HashMap;import java.util.Map;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import android.graphics.drawable.Drawable;import android.os.Handler;import android.os.SystemClock;public class AsyncImageLoader3 { // 為了加快速度,在記憶體中開啟快取(主要應用於重複圖片較多時,或者同一個圖片要多次被訪問,比如在ListView時來回滾動) public Map> imageCache = new HashMap>(); private ExecutorService executorService = Executors.newFixedThreadPool(5); // 固定五個執行緒來執行任務 private final Handler handler = new Handler(); /** * * @param imageUrl * 影象url地址 * @param callback * 回撥介面 * @return 返回記憶體中快取的影象,第一次載入返回null */ public Drawable loadDrawable(final String imageUrl, final ImageCallback callback) { // 如果快取過就從快取中取出資料 if (imageCache.containsKey(imageUrl)) { SoftReference softReference = imageCache.get(imageUrl); if (softReference.get() != null) { return softReference.get(); } } // 快取中沒有影象,則從網路上取出資料,並將取出的資料快取到記憶體中 executorService.submit(new Runnable() { public void run() { try { final Drawable drawable = loadImageFromUrl(imageUrl); imageCache.put(imageUrl, new SoftReference( drawable)); handler.post(new Runnable() { public void run() { callback.imageLoaded(drawable); } }); } catch (Exception e) { throw new RuntimeException(e); } } }); return null; } // 從網路上取資料方法 protected Drawable loadImageFromUrl(String imageUrl) { try { // 測試時,模擬網路延時,實際時這行程式碼不能有 SystemClock.sleep(2000); return Drawable.createFromStream(new URL(imageUrl).openStream(), "image.png"); } catch (Exception e) { throw new RuntimeException(e); } } // 對外界開放的回撥介面 public interface ImageCallback { // 注意 此方法是用來設定目標物件的影象資源 public void imageLoaded(Drawable imageDrawable); }}
說明:
final引數是指當函式引數為final型別時,你可以讀取使用該引數,但是無法改變該引數的值。參看:Java關鍵字final、static使用總結 這裡使用SoftReference 是為了解決記憶體不足的錯誤(OutOfMemoryError)的,更詳細的可以參看:記憶體優化的兩個類:SoftReference 和 WeakReference
前段呼叫:
package ghj1976.AndroidTest;import android.app.Activity;import android.graphics.drawable.Drawable;import android.os.Bundle;import android.widget.ImageView;public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); loadImage4("http://www.baidu.com/img/baidu_logo.gif", R.id.imageView1); loadImage4("http://www.chinatelecom.com.cn/images/logo_new.gif", R.id.imageView2); loadImage4("http://cache.soso.com/30d/img/web/logo.gif", R.id.imageView3); loadImage4("http://csdnimg.cn/www/images/csdnindex_logo.gif", R.id.imageView4); loadImage4("http://www.cnblogs.com/images/logo_small.gif", R.id.imageView5); } private AsyncImageLoader3 asyncImageLoader3 = new AsyncImageLoader3(); // 引入執行緒池,並引入記憶體快取功能,並對外部呼叫封裝了介面,簡化呼叫過程 private void loadImage4(final String url, final int id) { // 如果快取過就會從快取中取出影象,ImageCallback介面中方法也不會被執行 Drawable cacheImage = asyncImageLoader3.loadDrawable(url, new AsyncImageLoader3.ImageCallback() { // 請參見實現:如果第一次載入url時下面方法會執行 public void imageLoaded(Drawable imageDrawable) { ((ImageView) findViewById(id)) .setImageDrawable(imageDrawable); } }); if (cacheImage != null) { ((ImageView) findViewById(id)).setImageDrawable(cacheImage); } }}
參考資料: