1. 程式人生 > >演化理解 Android 非同步載入圖片

演化理解 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);  } }}

參考資料: