您的位置:首页 > 产品设计 > UI/UE

android多线程编程详解,关于Handler ,Looper , Message , MessageQueue

2015-02-06 17:53 691 查看
为什么要用多线程

这里列出几个原因:

a) 提高用户体验或者避免ANR

在事件处理代码中需要使用多线程,否则会出现ANR(Application is not responding),或者因为响应较慢导致用户体验很差。

b) 异步

应用中有些情况下并不一定需要同步阻塞去等待返回结果,可以通过多线程来实现异步,例如:上一点中提到的,你的应用中的某个Activity需要从云端获取一些图片,加载图片比较耗时,这时需要使用异步加载,加载完成一个图片刷新一个。

c) 多任务

例如多线程下载。

后两点与Java中的多线程应用没有太大区别,不细说。

下面重点说明第一点,即如何减少事件响应的时间从而提高用户体验,以及如何避免ANR。

2. 为什么通过多线程可以提高用户体验、避免ANR

大家还记得我在群里说过的移动开发的“三不要”原则吗?即:不要让我想、不要让我等、不要让我烦。响应慢了用户需要等,等的次数多了就会烦,你的应用离被卸载不远了。

首先我们来了解一下Android应用程序的main线程,它负责处理UI的绘制,Android系统为了防止应用程序反应较慢导致系统无法正常运行做了一个处理,一种情况是当用户输入事件在5秒内无法得到响应,那么系统会弹出ANR对话框,由用户决定继续等待还是强制结束应用程序(另一种情况是BroadcastReciever 超过10秒没执行完也会弹出ANR对话框)。

即使你的程序中某个事件响应不超过5秒钟,人眼可以分辨的时间是0.1秒,小于0.1秒基本感觉不出来,超过0.2秒用户就能感觉到有点儿卡了,俗称打嗝现象,2秒以上就很慢了,用户体验会很差。有同学说我可以用进度条啊,但你的程序中不能到处都是进度条,否则那个圈圈会把用户转晕的,好像在对用户说,画个圈圈烦死你……

比如某些应用,它要显示很多图片,还好它是异步的,不过在图片加载完成前每个图片的位置上都有一个圈圈,让人看了很烦。你可以变通一下,图片加载成功之前显示一个默认的图片,加载成功后再刷新一下即可,何必弄那么多进度条呢?

事件处理的原则:所有可能耗时的操作都放到其他线程去处理。

Android中的Main线程的事件处理不能太耗时,否则后续的事件无法在5秒内得到响应,就会弹出ANR对话框。那么哪些方法会在 Main线程执行呢?

1) Activity的生命周期方法,例如:onCreate()、onStart()、onResume()等

2) 事件处理方法,例如onClick()、onItemClick()等

通常Android基类中以on开头的方法是在Main线程被回调的。

提高应用的响应性,可以从这两方面入手。

一般来说,Activity的onCreate()、onStart()、onResume()方法的执行时间决定了你的应用首页打开的时间,这里要尽量把不必要的操作放到其他线程去处理,如果仍然很耗时,可以使用SplashScreen。使用SplashScreen最好用动态的,这样用户知道你的应用没有死掉。

当用户与你的应用交互时,事件处理方法的执行快慢决定了应用的响应性是否良好,这里分两种情况:

1) 同步,需要等待返回结果。例如用户点击了注册按钮,需要等待服务端返回结果,那么需要有一个进度条来提示用户你的程序正在运行没有死掉。一般与服务端交互的都要有进度条,例如系统自带的浏览器,URL跳转时会有进度条。

2) 异步,不需要等待返回结果。例如微博中的收藏功能,点击完收藏按钮后是否成功执行完成后告诉我就行了,我不想等它,这里最好实现为异步的。

无论同步异步,事件处理都可能比较耗时,那么需要放到其他线程中处理,等处理完成后,再通知界面刷新。

这里有一点要注意,不是所有的界面刷新行为都需要放到Main线程处理,例如TextView的setText()方法需要在Main线程中,否则会抛出CalledFromWrongThreadException,而ProgressBar的setProgress()方法则不需要在Main线程中处理。

当然你也可以把所有UI组件相关行为都放到Main线程中处理,没有问题。可以减轻你的思考负担,但你最好了解他们之间的差别,掌握事物之间细微差别的是专家。把事件处理代码放到其他线程中处理,如果处理的结果需要刷新界面,那么需要线程间通讯的方法来实现在其他线程中发消息给Main线程处理。

如何实现线程间通讯

在Android中有多种方法可以实现其他线程与Main线程通讯,我们这里介绍常见的两种。

1) 使用AsyncTask

AsyncTask是Android框架提供的异步处理的辅助类,它可以实现耗时操作在其他线程执行,而处理结果在Main线程执行,对于开发者而言,它屏蔽掉了多线程和后面要讲的Handler的概念。你不了解怎么处理线程间通讯也没有关系,AsyncTask体贴的帮你做好了。不过封装越好越高级的API,对初级程序员反而越不利,就是你不了解它的原理。当你需要面对更加复杂的情况,而高级API无法完成得很好时,你就杯具了。所以,我们也要掌握功能更强大,更自由的与Main线程通讯的方法:Handler的使用。

2) 使用Handler

这里需要了解Android SDK提供的几个线程间通讯的类。

2.1 Handler

Handler在android里负责发送和处理消息,通过它可以实现其他线程与Main线程之间的消息通讯。

2.2 Looper

Looper负责管理线程的消息队列和消息循环

2.3 Message

Message是线程间通讯的消息载体。两个码头之间运输货物,Message充当集装箱的功能,里面可以存放任何你想要传递的消息。

2.4 MessageQueue

MessageQueue是消息队列,先进先出,它的作用是保存有待线程处理的消息。

它们四者之间的关系是,在其他线程中调用Handler.sendMsg()方法(参数是Message对象),将需要Main线程处理的事件添加到Main线程的MessageQueue中,Main线程通过MainLooper从消息队列中取出Handler发过来的这个消息时,会回调Handler的handlerMessage()方法。

除了以上两种常用方法之外,还有几种比较简单的方法

3) Activity.runOnUiThread(Runnable)

4) View.post(Runnable)

View.postDelayed(Runnable, long)

5) Handler.post

Handler.postDelayed(Runnable, long)

利用线程池提高性能

这里我们建议使用线程池来管理临时的Thread对象,从而达到提高应用程序性能的目的。

线程池是资源池在线程应用中的一个实例。了解线程池之前我们首先要了解一下资源池的概念。在JAVA中,创建和销毁对象是比较消耗资源的。我们如果在应用中需要频繁创建销毁某个类型的对象实例,这样会产生很多临时对象,当失去引用的临时对象较多时,虚拟机会进行垃圾回收(GC),CPU在进行GC时会导致应用程序的运行得不到相应,从而导致应用的响应性降低。

资源池就是用来解决这个问题,当你需要使用对象时,从资源池来获取,资源池负责维护对象的生命周期。

了解了资源池,就很好理解线程池了,线程池就是存放对象类型都是线程的资源池。

以下是android通信的代码:

Handler+Runnable模式

我们先看一个并不是异步线程加载的例子,使用 Handler+Runnable模式。

这里为何不是新开线程的原因请参看这篇文章:Android Runnable 运行在那个线程 这里的代码其实是在UI 主线程中下载图片的,而不是新开线程。

我们运行下面代码时,会发现他其实是阻塞了整个界面的显示,需要所有图片都加载完成后,才能显示界面。

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(",
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://images.cnblogs.com/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模式

这种模式使用了线程,所以可以看到异步加载的效果。

核心代码:`

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://images.cnblogs.com/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实现,我们就来用它。

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

线程池的信息可以参看这篇文章:Java&Android的线程池-ExecutorService 下面的演示例子是创建一个可重用固定线程数的线程池。

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://images.cnblogs.com/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+缓存模式

下面比起前一个做了几个改造:

把整个代码封装在一个类中

为了避免出现同时多次下载同一幅图的问题,使用了本地缓存

封装的类:

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<String, SoftReference<Drawable>> imageCache = new HashMap<String, SoftReference<Drawable>>();

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<Drawable> 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>(
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

前段调用:

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://images.cnblogs.com/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);
}
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: