在学习"Android异步加载图像小结"这篇文章时, 发现有些地方没写清楚,我就根据我的理解,把这篇文章的代码重写整理了一遍,下面就是我的整理。
原创整理不易,转载请注明出处:Android异步加载图片中UI是否被阻塞的测试
代码下载地址:http://www.zuidaima.com/share/1724477350611968.htm
下面测试使用的layout文件:
简单来说就是 LinearLayout 布局,其下放了5个ImageView。
01
|
<?xml
version= "1.0" encoding= "utf-8" ?>
|
02
|
<LinearLayout
xmlns:android= "http://schemas.android.com/apk/res/android"
|
03
|
android:orientation= "vertical" android:layout_width= "fill_parent"
|
04
|
android:layout_height= "fill_parent" >
|
05
|
<TextView
android:text= "图片区域开始" android:id= "@+id/textView2"
|
06
|
android:layout_width= "wrap_content" android:layout_height= "wrap_content" ></TextView>
|
07
|
<ImageView
android:id= "@+id/imageView1"
|
08
|
android:layout_height= "wrap_content" android:src= "@drawable/icon"
|
09
|
android:layout_width= "wrap_content" ></ImageView>
|
10
|
<ImageView
android:id= "@+id/imageView2"
|
11
|
android:layout_height= "wrap_content" android:src= "@drawable/icon"
|
12
|
android:layout_width= "wrap_content" ></ImageView>
|
13
|
<ImageView
android:id= "@+id/imageView3"
|
14
|
android:layout_height= "wrap_content" android:src= "@drawable/icon"
|
15
|
android:layout_width= "wrap_content" ></ImageView>
|
16
|
<ImageView
android:id= "@+id/imageView4"
|
17
|
android:layout_height= "wrap_content" android:src= "@drawable/icon"
|
18
|
android:layout_width= "wrap_content" ></ImageView>
|
19
|
<ImageView
android:id= "@+id/imageView5"
|
20
|
android:layout_height= "wrap_content" android:src= "@drawable/icon"
|
21
|
android:layout_width= "wrap_content" ></ImageView>
|
22
|
<TextView
android:text= "图片区域结束" android:id= "@+id/textView1"
|
23
|
android:layout_width= "wrap_content" android:layout_height= "wrap_content" ></TextView>
|
24
|
</LinearLayout>
|
我们将演示的逻辑是异步从服务器上下载5张不同图片,依次放入这5个ImageView。上下2个TextView 是为了方便我们看是否阻塞了UI的显示。
当然 AndroidManifest.xml 文件中要配置好网络访问权限。
1
|
<uses-permission
android:name= "android.permission.INTERNET" ></uses-permission>
|
Handler+Runnable模式
我们先看一个并不是异步线程加载的例子,使用 Handler+Runnable模式。
这里为何不是新开线程的原因请参看这篇文章:Android Runnable 运行在那个线程 这里的代码其实是在UI 主线程中下载图片的,而不是新开线程。
我们运行下面代码时,会发现他其实是阻塞了整个界面的显示,需要所有图片都加载完成后,才能显示界面。
01
|
package com.zuidaima.AndroidTest;
|
02
|
03
|
import java.io.IOException;
|
04
|
import java.net.URL; import android.app.Activity;
|
05
|
import android.graphics.drawable.Drawable;
|
06
|
import android.os.Bundle;
|
07
|
import android.os.Handler; import android.os.SystemClock;
|
08
|
import android.util.Log;
|
09
|
import android.widget.ImageView;
|
10
|
11
|
public class MainActivity extends Activity
{
|
12
|
@Override
|
13
|
public void onCreate(Bundle
savedInstanceState) {
|
14
|
super .onCreate(savedInstanceState);
|
15
|
setContentView(R.layout.main);
|
16
|
loadImage( "http://www.baidu.com/img/baidu_logo.gif" ,
R.id.imageView1);
|
17
|
loadImage(http: //www.chinatelecom.com.cn/images/logo_new.gif",
|
18
|
R.id.imageView2);
|
19
|
loadImage("http: //cache.soso.com/30d/img/web/logo.gif,
R.id.imageView3);
|
20
|
loadImage( "http://csdnimg.cn/www/images/csdnindex_logo.gif" ,
|
21
|
R.id.imageView4);
|
22
|
loadImage( "http://images.cnblogs.com/logo_small.gif" ,
|
23
|
R.id.imageView5);
|
24
|
}
|
25
|
26
|
private Handler
handler= new Handler();
|
27
|
28
|
private void loadImage( final String
url, final int id)
{
|
29
|
handler.post( new Runnable()
{
|
30
|
public void run()
{
|
31
|
Drawable
drawable= null ;
|
32
|
try {
|
33
|
drawable=Drawable.createFromStream(
|
34
|
new URL(url).openStream(), "image.gif" );
|
35
|
} catch (IOException
e) {
|
36
|
Log.d( "test" ,
e.getMessage());
|
37
|
}
|
38
|
if (drawable== null )
{
|
39
|
Log.d( "test" , "null
drawable" );
|
40
|
} else {
|
41
|
Log.d( "test" , "not
null drawable" );
|
42
|
}
|
43
|
//
为了测试缓存而模拟的网络延时
|
44
|
SystemClock.sleep( 2000 );
|
45
|
46
|
(ImageView)
MainActivity. this .findViewById(id))
|
47
|
.setImageDrawable(drawable);
|
48
|
}
|
49
|
});
|
50
|
}
|
51
|
}
|
Handler+Thread+Message模式
这种模式使用了线程,所以可以看到异步加载的效果。
核心代码:
01
|
package com.zuidaima.AndroidTest;
|
02
|
03
|
import java.io.IOException;
|
04
|
import java.net.URL;
|
05
|
import android.app.Activity;
|
06
|
import android.graphics.drawable.Drawable;
|
07
|
import android.os.Bundle;
|
08
|
import android.os.Handler;
|
09
|
import android.os.Message;
|
10
|
import android.os.SystemClock;
|
11
|
import android.util.Log;
|
12
|
import android.widget.ImageView;
|
13
|
14
|
public class MainActivity extends Activity
{
|
15
|
@Override
|
16
|
public void onCreate(Bundle
savedInstanceState) {
|
17
|
super .onCreate(savedInstanceState);
|
18
|
setContentView(R.layout.main);
|
19
|
loadImage2( "http://www.baidu.com/img/baidu_logo.gif" ,
R.id.imageView1);
|
20
|
loadImage2( "http://www.chinatelecom.com.cn/images/logo_new.gif" ,
|
21
|
R.id.imageView2);
|
22
|
loadImage2( "http://cache.soso.com/30d/img/web/logo.gif" ,
R.id.imageView3);
|
23
|
loadImage2( "http://csdnimg.cn/www/images/csdnindex_logo.gif" ,
|
24
|
R.id.imageView4);
|
25
|
loadImage2( "http://images.cnblogs.com/logo_small.gif" ,
|
26
|
R.id.imageView5);
|
27
|
}
|
28
|
29
|
final Handler
handler2= new Handler()
{
|
30
|
@Override
|
31
|
public void handleMessage(Message
msg) {
|
32
|
((ImageView)
MainActivity. this .findViewById(msg.arg1))
|
33
|
.setImageDrawable((Drawable)
msg.obj);
|
34
|
}
|
35
|
};
|
36
|
37
|
//
采用handler+Thread模式实现多线程异步加载
|
38
|
private void loadImage2( final String
url, final int id)
{
|
39
|
Thread
thread= new Thread()
{
|
40
|
@Override
|
41
|
public void run()
{
|
42
|
Drawable
drawable= null ;
|
43
|
try {
|
44
|
drawable=Drawable.createFromStream(
|
45
|
new URL(url).openStream(), "image.png" );
|
46
|
} catch (IOException
e) {
|
47
|
Log.d( "test" ,
e.getMessage());
|
48
|
}
|
49
|
50
|
//
模拟网络延时
|
51
|
SystemClock.sleep( 2000 );
|
52
|
53
|
Message
message=handler2.obtainMessage();
|
54
|
message.arg1=id;
|
55
|
message.obj=drawable;
|
56
|
handler2.sendMessage(message);
|
57
|
}
|
58
|
};
|
59
|
thread.start();
|
60
|
thread= null ;
|
61
|
}
|
62
|
63
|
}
|
这时候我们可以看到实现了异步加载, 界面打开时,五个ImageView都是没有图的,然后在各自线程下载完后才把图自动更新上去。
Handler+ExecutorService(线程池)+MessageQueue模式
能开线程的个数毕竟是有限的,我们总不能开很多线程,对于手机更是如此。
这个例子是使用线程池。Android拥有与Java相同的ExecutorService实现,我们就来用它。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
线程池的信息可以参看这篇文章: Java&Android的线程池-ExecutorService
下面的演示例子是创建一个可重用固定线程数的线程池。
核心代码
01
|
package com.zuidaima.AndroidTest;
|
02
|
03
|
import java.io.IOException;
|
04
|
import java.net.URL;
|
05
|
import java.util.concurrent.ExecutorService;
|
06
|
import java.util.concurrent.Executors;
|
07
|
08
|
import android.app.Activity;
|
09
|
import android.graphics.drawable.Drawable;
|
10
|
import android.os.Bundle;
|
11
|
import android.os.Handler;
|
12
|
import android.os.Message;
|
13
|
import android.os.SystemClock;
|
14
|
import android.util.Log;
|
15
|
import android.widget.ImageView;
|
16
|
17
|
public class MainActivity extends Activity
);
|
18
|
19
|
//
引入线程池来管理多线程
|
20
|
private void loadImage3( final String
url, final int id)
{
|
21
|
executorService.submit( new Runnable()
{
|
22
|
public void run()
{
|
23
|
try {
|
24
|
final Drawable
drawable=Drawable.createFromStream(
|
25
|
new URL(url).openStream(), "image.png" );
|
26
|
|
27
|
//
模拟网络延时
|
28
|
SystemClock.sleep( 2000 );
|
29
|
30
|
handler.post( new Runnable()
{
|
31
|
public void run()
{
|
32
|
((ImageView)
MainActivity. this .findViewById(id))
|
33
|
.setImageDrawable(drawable);
|
34
|
}
|
35
|
});
|
36
|
} catch (Exception
e) {
|
37
|
throw new RuntimeException(e);
|
38
|
}
|
39
|
}
|
40
|
});
|
41
|
}
|
42
|
}
|
这里我们象第一步一样使用了 handler.post(new Runnable() { 更新前段显示当然是在UI主线程,我们还有 executorService.submit(new Runnable() { 来确保下载是在线程池的线程中。
Handler+ExecutorService(线程池)+MessageQueue+缓存模式
下面比起前一个做了几个改造:
把整个代码封装在一个类中
为了避免出现同时多次下载同一幅图的问题,使用了本地缓存 封装的类:
01
|
package com.zuidaima.AndroidTest;
|
02
|
03
|
import java.lang.ref.SoftReference;
|
04
|
import java.net.URL; import java.util.HashMap;
|
05
|
import java.util.Map;
|
06
|
import java.util.concurrent.ExecutorService;
|
07
|
import java.util.concurrent.Executors;
|
08
|
import android.graphics.drawable.Drawable;
|
09
|
import android.os.Handler;
|
10
|
import android.os.SystemClock;
|
11
|
12
|
public class AsyncImageLoader3
);
|
13
|
14
|
//
固定五个线程来执行任务
|
15
|
private final Handler
handler= new Handler();
|
16
|
17
|
/**
*/ /**
*/ /***/ /**
|
18
|
*
|
19
|
*
@param imageUrl
|
20
|
*
图像url地址
|
21
|
*
@param callback
|
22
|
*
回调接口
|
23
|
*
@return 返回内存中缓存的图像,第一次加载返回null
|
24
|
*/
|
25
|
public Drawable
loadDrawable( final String
imageUrl,
|
26
|
final ImageCallback
callback) {
|
27
|
|
28
|
//
如果缓存过就从缓存中取出数据
|
29
|
if (imageCache.containsKey(imageUrl))
{
|
30
|
SoftReference<Drawable>
softReference=imageCache.get(imageUrl);
|
31
|
if (softReference.get()
!= null )
{
|
32
|
return softReference.get();
|
33
|
}
|
34
|
}
|
35
|
//
缓存中没有图像,则从网络上取出数据,并将取出的数据缓存到内存中
|
36
|
executorService.submit( new Runnable()
{
|
37
|
public void run()
{
|
38
|
try {
|
39
|
final Drawable
drawable=loadImageFromUrl(imageUrl);
|
40
|
|
41
|
imageCache.put(imageUrl, new SoftReference<Drawable>(
|
42
|
drawable));
|
43
|
44
|
handler.post( new Runnable()
{
|
45
|
public void run()
{
|
46
|
callback.imageLoaded(drawable);
|
47
|
}
|
48
|
});
|
49
|
} catch (Exception
e) {
|
50
|
throw new RuntimeException(e);
|
51
|
}
|
52
|
}
|
53
|
});
|
54
|
return null ;
|
55
|
}
|
56
|
57
|
//
从网络上取数据方法
|
58
|
protected Drawable
loadImageFromUrl(String imageUrl) {
|
59
|
try {
|
60
|
|
61
|
//
测试时,模拟网络延时,实际时这行代码不能有
|
62
|
SystemClock.sleep( 2000 );
|
63
|
64
|
return Drawable.createFromStream( new URL(imageUrl).openStream(),
|
65
|
"image.png" );
|
66
|
67
|
} catch (Exception
e) {
|
68
|
throw new RuntimeException(e);
|
69
|
}
|
70
|
}
|
71
|
72
|
//
对外界开放的回调接口
|
73
|
public interface ImageCallback
{
|
74
|
|
75
|
//
注意 此方法是用来设置目标对象的图像资源
|
76
|
public void imageLoaded(Drawable
imageDrawable);
|
77
|
}
|
78
|
}
|
说明:
final参数是指当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
参看:Java关键字final、static使用总结
这里使用SoftReference 是为了解决内存不足的错误(OutOfMemoryError)的,更详细的可以参看:
内存优化的两个类:SoftReference 和 WeakReference
前端调用:
01
|
package com.zuidaima.AndroidTest;
|
02
|
03
|
import android.app.Activity;
|
04
|
import android.graphics.drawable.Drawable;
|
05
|
import android.os.Bundle;
|
06
|
import android.widget.ImageView;
|
07
|
08
|
public class MainActivity extends Activity
{
|
09
|
@Override
|
10
|
public void onCreate(Bundle
savedInstanceState) {
|
11
|
super .onCreate(savedInstanceState);
|
12
|
setContentView(R.layout.main);
|
13
|
loadImage4( "http://www.baidu.com/img/baidu_logo.gif" ,
R.id.imageView1);
|
14
|
loadImage4( "http://www.chinatelecom.com.cn/images/logo_new.gif" ,
|
15
|
R.id.imageView2);
|
16
|
loadImage4( "http://cache.soso.com/30d/img/web/logo.gif" ,
|
17
|
R.id.imageView3);
|
18
|
loadImage4( "http://csdnimg.cn/www/images/csdnindex_logo.gif" ,
|
19
|
R.id.imageView4);
|
20
|
loadImage4( "http://images.cnblogs.com/logo_small.gif" ,
|
21
|
R.id.imageView5);
|
22
|
}
|
23
|
24
|
private AsyncImageLoader3
asyncImageLoader3= new AsyncImageLoader3();
|
25
|
26
|
//
引入线程池,并引入内存缓存功能,并对外部调用封装了接口,简化调用过程
|
27
|
28
|
private void loadImage4( final String
url, final int id)
{
|
29
|
//
如果缓存过就会从缓存中取出图像,ImageCallback接口中方法也不会被执行
|
30
|
Drawable
cacheImage=asyncImageLoader3.loadDrawable(url,
|
31
|
new AsyncImageLoader3.ImageCallback()
{
|
32
|
//
请参见实现:如果第一次加载url时下面方法会执行
|
33
|
public void imageLoaded(Drawable
imageDrawable) {
|
34
|
((ImageView)
findViewById(id))
|
35
|
.setImageDrawable(imageDrawable);
|
36
|
}
|
37
|
});
|
38
|
if (cacheImage
!= null )
{
|
39
|
((ImageView)
findViewById(id)).setImageDrawable(cacheImage);
|
40
|
}
|
41
|
}
|
42
|
43
|
}
|