在加载大量数据的时候,经常会用到异步加载,所谓异步加载,就是把耗时的工作放到子线程里执行,当数据加载完毕的时候再到主线程进行UI刷新。在数据量非常大的情况下,我们通常会使用两种技术来进行异步加载,一是通过AsyncTask来实现,另一种方式则是通过ThreadPool来实现,今天我们就通过一个例子来讲解和对比这两种实现方式。

    本文原创,如需转载,请注明转载地址

    项目的结构如下所示:


Android 协程多异步下载同步回调进度_java

    在今天这个例子里,我们用到了之前一篇文章中写过的一个自定义控件,如果有同学感兴趣的话可以点击这里来先研究下这个控件的实现,为了配合异步加载的效果,我针对这个控件做了一点修改,下面会对修改的地方进行解释。

    接下来我们就分别针对ThreadPool和AsyncTask两种实现方式进行讲解,我会顺着实现的思路贴出关键的代码,在文章最后会贴出实现效果和源码下载,感兴趣的同学可以下载下来对比来看。

    首先来讲解ThreadPool(线程池)的实现方式。

    我们首先需要来实现一个线程池管理器,这个管理器内部包含一个独立的轮询子线程,它的工作是不时的检查工作队列,如果队列中有未执行的任务,就将任务交给线程池来执行。此外,线程池管理器还负责管理线程池和维护任务队列。具体实现代码如下:


[java]  view plain copy

1. package com.carrey.asyncloaddemo;  
2.   
3. import java.util.LinkedList;  
4. import java.util.concurrent.ExecutorService;  
5. import java.util.concurrent.Executors;  
6.   
7. import android.util.Log;  
8.   
9. /**
10.  * 线程池管理类
11.  * @author carrey
12.  *
13.  */  
14. public class ThreadPoolManager {  
15.   
16. private static final String TAG = "ThreadPoolManager";  
17.       
18. /** 线程池的大小 */  
19. private int poolSize;  
20. private static final int MIN_POOL_SIZE = 1;  
21. private static final int MAX_POOL_SIZE = 10;  
22.       
23. /** 线程池 */  
24. private ExecutorService threadPool;  
25.       
26. /** 请求队列 */  
27. private LinkedList<ThreadPoolTask> asyncTasks;  
28.       
29. /** 工作方式 */  
30. private int type;  
31. public static final int TYPE_FIFO = 0;  
32. public static final int TYPE_LIFO = 1;  
33.       
34. /** 轮询线程 */  
35. private Thread poolThread;  
36. /** 轮询时间 */  
37. private static final int SLEEP_TIME = 200;  
38.       
39. public ThreadPoolManager(int type, int poolSize) {  
40. this.type = (type == TYPE_FIFO) ? TYPE_FIFO : TYPE_LIFO;  
41.           
42. if (poolSize < MIN_POOL_SIZE) poolSize = MIN_POOL_SIZE;  
43. if (poolSize > MAX_POOL_SIZE) poolSize = MAX_POOL_SIZE;  
44. this.poolSize = poolSize;  
45.                   
46. this.poolSize);  
47.           
48. new LinkedList<ThreadPoolTask>();  
49.     }  
50.       
51. /**
52.      * 向任务队列中添加任务
53.      * @param task
54.      */  
55. public void addAsyncTask(ThreadPoolTask task) {  
56. synchronized (asyncTasks) {  
57. "add task: " + task.getURL());  
58.             asyncTasks.addLast(task);  
59.         }  
60.     }  
61.       
62. /**
63.      * 从任务队列中提取任务
64.      * @return
65.      */  
66. private ThreadPoolTask getAsyncTask() {  
67. synchronized (asyncTasks) {  
68. if (asyncTasks.size() > 0) {  
69. this.type == TYPE_FIFO) ?   
70.                         asyncTasks.removeFirst() : asyncTasks.removeLast();  
71. "remove task: " + task.getURL());  
72. return task;  
73.             }  
74.         }  
75. return null;  
76.     }  
77.       
78. /**
79.      * 开启线程池轮询
80.      * @return
81.      */  
82. public void start() {  
83. if (poolThread == null) {  
84. new Thread(new PoolRunnable());  
85.             poolThread.start();  
86.         }  
87.     }  
88.       
89. /**
90.      * 结束轮询,关闭线程池
91.      */  
92. public void stop() {  
93.         poolThread.interrupt();  
94. null;  
95.     }  
96.       
97. /**
98.      * 实现轮询的Runnable
99.      * @author carrey
100.      *
101.      */  
102. private class PoolRunnable implements Runnable {  
103.   
104. @Override  
105. public void run() {  
106. "开始轮询");  
107.               
108. try {  
109. while (!Thread.currentThread().isInterrupted()) {  
110.                     ThreadPoolTask task = getAsyncTask();  
111. if (task == null) {  
112. try {  
113.                             Thread.sleep(SLEEP_TIME);  
114. catch (InterruptedException e) {  
115.                             Thread.currentThread().interrupt();  
116.                         }  
117. continue;  
118.                     }  
119.                     threadPool.execute(task);  
120.                 }  
121. finally {  
122.                 threadPool.shutdown();  
123.             }  
124.               
125. "结束轮询");  
126.         }  
127.           
128.     }  
129. }


    注意在上面的代码中,我们自定义了任务单元的实现,任务单元是一系列的Runnable对象,最终都将交给线程池来执行,任务单元的实现代码如下:


    ThreadPoolTask.java:


[java]  view plain copy


1. package com.carrey.asyncloaddemo;  
2. /**
3.  * 任务单元
4.  * @author carrey
5.  *
6.  */  
7. public abstract class ThreadPoolTask implements Runnable {  
8.   
9. protected String url;  
10.       
11. public ThreadPoolTask(String url) {  
12. this.url = url;  
13.     }  
14.       
15. public abstract void run();  
16.       
17. public String getURL() {  
18. return this.url;  
19.     }  
20. }


    ThreadPoolTaskBitmap.java:



[java]  view plain copy



    1. package com.carrey.asyncloaddemo;  
    2.   
    3. import com.carrey.customview.customview.CustomView;  
    4.   
    5. import android.graphics.Bitmap;  
    6. import android.os.Process;  
    7. import android.util.Log;  
    8. /**
    9.  * 图片加载的任务单元
    10.  * @author carrey
    11.  *
    12.  */  
    13. public class ThreadPoolTaskBitmap extends ThreadPoolTask {  
    14.       
    15. private static final String TAG = "ThreadPoolTaskBitmap";  
    16.   
    17. private CallBack callBack;  
    18.       
    19. private CustomView view;  
    20.       
    21. private int position;  
    22.       
    23. public ThreadPoolTaskBitmap(String url, CallBack callBack, int position, CustomView view) {  
    24. super(url);  
    25. this.callBack = callBack;  
    26. this.position = position;  
    27. this.view = view;  
    28.     }  
    29.   
    30. @Override  
    31. public void run() {  
    32.         Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);  
    33.           
    34.         Bitmap bitmap = ImageHelper.loadBitmapFromNet(url);  
    35.           
    36. "loaded: " + url);  
    37.           
    38. if (callBack != null) {  
    39. this.position, this.view);  
    40.         }  
    41.     }  
    42.   
    43. public interface CallBack {  
    44. public void onReady(String url, Bitmap bitmap, int position, CustomView view);  
    45.     }  
    46. }



        上面代码中的回调实现位于MainActivity.java中,在任务单元的run方法中主要做的事情就是从服务器得到要加载的图片的Bitmap,然后调用回调,在回调中会将得到的图片加载到UI界面中。


        在加载服务器图片的时候,用到了ImageHelper这个工具类,这个类主要的功能就是提供获得服务器图片地址和解析图片的方法,具体代码如下:


    [java]  view plain copy


    1. package com.carrey.asyncloaddemo;  
    2.   
    3. import java.io.IOException;  
    4. import java.io.InputStream;  
    5. import java.net.URL;  
    6. import java.net.URLConnection;  
    7.   
    8. import android.graphics.Bitmap;  
    9. import android.graphics.BitmapFactory;  
    10. import android.util.Log;  
    11.   
    12. /**
    13.  * 工具类,用于获得要加载的图片资源
    14.  * @author carrey
    15.  *
    16.  */  
    17. public class ImageHelper {  
    18.       
    19. private static final String TAG = "ImageHelper";  
    20.       
    21. public static String getImageUrl(String webServerStr, int position) {  
    22. return "http://" + webServerStr + "/" + (position % 50) + ".jpg";  
    23.     }  
    24.       
    25. /**
    26.      * 获得网络图片Bitmap
    27.      * @param imageUrl
    28.      * @return
    29.      */  
    30. public static Bitmap loadBitmapFromNet(String imageUrlStr) {  
    31. null;  
    32. null;  
    33.           
    34. if (imageUrlStr == null || imageUrlStr.length() == 0) {  
    35. return null;  
    36.         }  
    37.           
    38. try {  
    39. new URL(imageUrlStr);  
    40.             URLConnection conn = imageUrl.openConnection();  
    41. true);  
    42.             conn.connect();  
    43.             InputStream is = conn.getInputStream();  
    44. int length = conn.getContentLength();  
    45. if (length != -1) {  
    46. byte[] imgData = new byte[length];  
    47. byte[] temp = new byte[512];  
    48. int readLen = 0;  
    49. int destPos = 0;  
    50. while ((readLen = is.read(temp)) != -1) {  
    51. 0, imgData, destPos, readLen);  
    52.                     destPos += readLen;  
    53.                 }  
    54. 0, imgData.length);  
    55.             }  
    56. catch (IOException e) {  
    57.             Log.e(TAG, e.toString());  
    58. return null;  
    59.         }  
    60.           
    61. return bitmap;  
    62.     }  
    63. }

        到这里准备的工作基本就完成了,接下来的工作就是启动线程池管理器并向任务队列添加我们的加载任务了,这部分工作我们放在GridView的Adapter的getView方法中来执行,其中关键的代码如下:



    [java]  view plain copy

    1. holder.customView.setTitleText("ThreadPool");  
    2. holder.customView.setSubTitleText("position: " + position);  
    3. poolManager.start();  
    4. String imageUrl = ImageHelper.getImageUrl(webServerStr, position);  
    5. poolManager.addAsyncTask(new ThreadPoolTaskBitmap(imageUrl, MainActivity.this, position, holder.customView));

        下面我们来接着讲解AsyncTask的实现方式。


        相对线程池的实现方式,AsyncTask的实现方式要简单一些

        我们首先来定义好我们的AsyncTask子类,在其中我们将在doInBackground中加载图片数据,在onPostExecute中来刷新UI。代码如下:


    [java]  view plain copy


    1. package com.carrey.asyncloaddemo;  
    2.   
    3. import com.carrey.customview.customview.CustomView;  
    4.   
    5. import android.graphics.Bitmap;  
    6. import android.os.AsyncTask;  
    7. import android.util.Log;  
    8. import android.util.Pair;  
    9.   
    10. public class AsyncLoadTask extends AsyncTask<Integer, Void, Pair<Integer, Bitmap>> {  
    11.   
    12. private static final String TAG = "AsyncLoadTask";  
    13.       
    14. /** 要刷新的view */  
    15. private CustomView view;  
    16.           
    17. public AsyncLoadTask(CustomView view) {  
    18. this.view = view;  
    19.     }  
    20.       
    21. @Override  
    22. protected void onPreExecute() {  
    23. super.onPreExecute();  
    24.     }  
    25.       
    26. @Override  
    27. protected Pair<Integer, Bitmap> doInBackground(Integer... params) {  
    28. int position = params[0];  
    29.         String imageUrl = ImageHelper.getImageUrl(MainActivity.webServerStr, position);  
    30. "AsyncLoad from NET :" + imageUrl);  
    31.         Bitmap bitmap = ImageHelper.loadBitmapFromNet(imageUrl);  
    32. return new Pair<Integer, Bitmap>(position, bitmap);  
    33.     }  
    34.       
    35. @Override  
    36. protected void onPostExecute(Pair<Integer, Bitmap> result) {  
    37. if (result.first == view.position) {  
    38.             view.setImageBitmap(result.second);  
    39.         }  
    40.     }  
    41.   
    42. }



        在Adapter中调用AsyncTask异步加载的代码如下:



    [java]  view plain copy


    1. holder.customView.setTitleText("AsyncTask");  
    2. holder.customView.setSubTitleText("position: " + position);  
    3. new AsyncLoadTask(holder.customView).execute(position);


        写到这里,关键的代码基本都讲完了,我们不妨先来看一下两种实现方式的效果:


    Android 协程多异步下载同步回调进度_android_02

        通过对比可以发现,ThreadPool相比AsyncTask,并发能力更强,加载的速度也更快,AsyncTask在加载过程中明显变现出顺序性,加载的速度要慢一些。

        下面是调用两种加载方式的MainActivity的所有代码:


    [java]  view plain copy


    1. package com.carrey.asyncloaddemo;  
    2.   
    3. import android.app.Activity;  
    4. import android.content.Context;  
    5. import android.graphics.Bitmap;  
    6. import android.graphics.BitmapFactory;  
    7. import android.os.Bundle;  
    8. import android.util.Log;  
    9. import android.view.LayoutInflater;  
    10. import android.view.Menu;  
    11. import android.view.View;  
    12. import android.view.View.OnClickListener;  
    13. import android.view.ViewGroup;  
    14. import android.widget.BaseAdapter;  
    15. import android.widget.Button;  
    16. import android.widget.GridView;  
    17.   
    18. import com.carrey.customview.customview.CustomView;  
    19. /**
    20.  * 异步加载的两种方式:AsyncTask与ThreadPool
    21.  * @author carrey
    22.  *
    23.  */  
    24. public class MainActivity extends Activity implements ThreadPoolTaskBitmap.CallBack {  
    25.   
    26. /** 服务器地址 */  
    27. public static String webServerStr;  
    28.       
    29. private static final String TAG = "MainActivity";  
    30.   
    31. private LayoutInflater inflater;  
    32.       
    33. private Button btnAsync;  
    34.       
    35. private Button btnPool;  
    36.       
    37. private GridView gridView;  
    38. private GridAdapter adapter;  
    39.       
    40. /** 加载方式 */  
    41. private int loadWay;  
    42. private static final int LOAD_ASYNC = 1;  
    43. private static final int LOAD_POOL = 2;  
    44.       
    45. private ThreadPoolManager poolManager;  
    46.       
    47. @Override  
    48. protected void onCreate(Bundle savedInstanceState) {  
    49. super.onCreate(savedInstanceState);  
    50.         setContentView(R.layout.activity_main);  
    51.           
    52.         loadWay = LOAD_ASYNC;  
    53.         webServerStr = getResources().getString(R.string.web_server);  
    54.         inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
    55.           
    56.         btnAsync = (Button) findViewById(R.id.btn_async);  
    57. new AsyncButtonClick());  
    58.         btnPool = (Button) findViewById(R.id.btn_pool);  
    59. new PoolButtonClick());  
    60.         gridView = (GridView) findViewById(R.id.gridview);  
    61.           
    62. new GridAdapter();  
    63.         gridView.setAdapter(adapter);  
    64.           
    65. new ThreadPoolManager(ThreadPoolManager.TYPE_FIFO, 5);  
    66.     }  
    67.   
    68. private class AsyncButtonClick implements OnClickListener {  
    69.   
    70. @Override  
    71. public void onClick(View v) {  
    72.             loadWay = LOAD_ASYNC;  
    73.             adapter.notifyDataSetChanged();  
    74.         }  
    75.           
    76.     }  
    77.       
    78. private class PoolButtonClick implements OnClickListener {  
    79.   
    80. @Override  
    81. public void onClick(View v) {  
    82.             loadWay = LOAD_POOL;  
    83.             adapter.notifyDataSetChanged();  
    84.         }  
    85.           
    86.     }  
    87.       
    88. private class GridAdapter extends BaseAdapter {  
    89.   
    90. private Bitmap mBackgroundBitmap;  
    91.           
    92. public GridAdapter() {  
    93.             mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.item_bg);  
    94.         }  
    95.           
    96. @Override  
    97. public int getCount() {  
    98. return 999;  
    99.         }  
    100.   
    101. @Override  
    102. public Object getItem(int position) {  
    103. return null;  
    104.         }  
    105.   
    106. @Override  
    107. public long getItemId(int position) {  
    108. return 0;  
    109.         }  
    110.   
    111. @Override  
    112. public View getView(int position, View convertView, ViewGroup parent) {  
    113. null;  
    114. if (convertView == null) {  
    115. new ViewHolder();  
    116. null);  
    117.                 holder.customView = (CustomView) convertView.findViewById(R.id.customview);  
    118.                 convertView.setTag(holder);  
    119. else {  
    120.                 holder = (ViewHolder) convertView.getTag();  
    121.             }  
    122.             holder.customView.position = position;  
    123. null);  
    124.             holder.customView.setBackgroundBitmap(mBackgroundBitmap);  
    125.               
    126. if (loadWay == LOAD_ASYNC) {  
    127. "AsyncTask");  
    128. "position: " + position);  
    129. new AsyncLoadTask(holder.customView).execute(position);  
    130. else if (loadWay == LOAD_POOL) {  
    131. "ThreadPool");  
    132. "position: " + position);  
    133.                 poolManager.start();  
    134.                 String imageUrl = ImageHelper.getImageUrl(webServerStr, position);  
    135. new ThreadPoolTaskBitmap(imageUrl, MainActivity.this, position, holder.customView));  
    136.             }  
    137.               
    138. return convertView;  
    139.         }  
    140.           
    141.     }  
    142.       
    143. static class ViewHolder {  
    144.         CustomView customView;  
    145.     }  
    146.       
    147. @Override  
    148. protected void onDestroy() {  
    149.         poolManager.stop();  
    150. super.onDestroy();  
    151.     }  
    152.       
    153. @Override  
    154. public boolean onCreateOptionsMenu(Menu menu) {  
    155. // Inflate the menu; this adds items to the action bar if it is present.  
    156.         getMenuInflater().inflate(R.menu.activity_main, menu);  
    157. return true;  
    158.     }  
    159.   
    160. @Override  
    161. public void onReady(String url, Bitmap bitmap, int position, CustomView view) {  
    162. "thread pool done task: " + url);  
    163. if (view.position == position) {  
    164.             view.setImageBitmap(bitmap);  
    165.         }  
    166.     }  
    167.   
    168. }        、

    在文章开头我们提到过,我对CustomView做了一些修改,下面做一些说明:

        我在控件里添加了一个mDrawableBackground属性,这个Drawable会在渲染图片内容之前渲染,异步加载的时候我会首先将其设置为一个空白的背景,这样在图片加载完成之前我们就会先看到一个白色的背景(或者你自定义一个沙漏或者时钟之类的图片),给用户一种视觉上的延时效果。

        除此之外我添加了一个pisition属性,并在最终刷新UI的时候用来做判断,分别位于线程池实现方式中的任务单元执行到最后的回调实现和AsyncTask实现方式的onPostExecute中,如下的代码:



    [java]  view plain copy


    1. @Override  
    2. public void onReady(String url, Bitmap bitmap, int position, CustomView view) {  
    3. "thread pool done task: " + url);  
    4. if (view.position == position) {  
    5.             view.setImageBitmap(bitmap);  
    6.         }  
    7.     }


    [java]  view plain copy


    1. @Override  
    2. protected void onPostExecute(Pair<Integer, Bitmap> result) {  
    3. if (result.first == view.position) {  
    4.             view.setImageBitmap(result.second);  
    5.         }  
    6.     }


        为什么要做这样一个判断呢?这是因为BaseAdapter的convertView是不停复用的,如果我们的滑动非常快,那就会存在这样一种情况,有一个convertView还没有加载完,就会第二次复用了,如果这个时候第二次加载慢于第一次,那结果就会被第一次覆盖,这样就不准确了,所以我们要加一个判断,以确保刷新的准确。