在Android中,UI主线程并非线程安全的,所有UI相关的操作均需在UI主线程中完成。在默认情况下,开发者创建的Service、Activity、Broadcast均运行在UI主线程中,但将一些耗时操作,如网络下载、大文件读写、加解密计算、数据库操作等,也放在UI线程中执行,往往会阻塞UI线程,造成ANR异常,因此,在Android应用开发中,应特别注意线程的使用。
在Android中,实现多线程的方式有多种,开发者可以通过原生Java线程或Android对Java线程的封装即AsyncTask来实现多线程的效果。
为了开发出性能更好的应用,开发者必须对Android的多线程实现由清楚的了解,了解每种实现方式的优缺点和线程安全方面的问题,这样才能最大程度地发挥出Android的潜力。
1.Java线程实现
Android线程的实现本质上仍是Java线程,只是为了方便开发者进行实现,针对特定场景做了封装。本节将着重介绍基于Thread、Runnable的java线程的实现,并介绍Android的线程优先级。
(1)基于Thread的实现
基于Thread实现新线程和在Runnable构造器中实现新线程是传统Java实现线程的两种方式,基于Thread的实现非常简单。示例如下:
static class AThread extends Thread{
ActivityManagerService mService;
boolean mRead=false;
public AThread(){
super("ActivityManager");
}
public void run(){
Looper.prepare(); //初始化事件循环之后调用loop()
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_FOREGROUND):
android.os.Precess.setCanSelfBackground(false);
ActivityManagerService m=mew ActivityManagerService();
synchronized(this){
mSerice=m;
notifyAll();
}
synchronized(this){
while(!mReady){
try{
wait();
catch(InterruptedException e){
}
}
}
Looper.loop();
}
}
在Thread声明周期中,存在NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED等多个状态。线程的生命周期和状态迁移过程如下图:
下面是Thread中常用方法的说明,在实现Java线程时,务必掌握这些方法的用途。
getState() //获取线程的当前状态 interrupt() //中断当前线程
interrupted() //判断当前线程是否已中断 isAlive //判断线程是否仍在运行
isInterrupted() //判断是否处于“中断”状态 sleep() //线程睡眠指定的时间
join() //线程同步处理 run() //线程暂停一段时间
start() //开启线程 yield() //暂停当前线程,同优先级的其它线程获得时间片
(2)基于Runable的实现
对于APP开发来说,由于应用层本身的特点,Runnable使用更加广泛,下面是一个Runnable实现的示例:
private Runnable mForceCheckBoxRunnable =new Runnable(){
public void run(){
if(mCheckBoxPreference!=null){
mCheckBoxPreference.setChecked(!mCheckBoxPreference.isChecked());
}
mHandle.postDelayed(this, 1000);
}
};
另外,通过Handle的removeCallbacks()方法可以移除待运行的线程,通过postAtTime方法可以在特定时间将线程放入消息队列。
(3)线程优先级
Android线程优先级是以及Linux设置的,其优先级是用数字表示的,范围是-20~19,其中-20为最高优先级,19为最低优先级。优先级的设置通常用于处理并发线程产生的阻塞,防止重要性较低的线程占用大量的CPU资源。设置优先级的方法如下:
Process.setThreadPriority(priority);
根据适用场景的不同,Android提供了以下几种常用场景的线程优先级:
THREAD_PRIORITY_AUDIO //值为-16,用于音乐播放场景
THREAD_PRIORITY_BACKGROUND //值为10,用于普通后台程序
THREAD_PRIORITY_DEFAULT //值为0,默认优先级
THREAD_PRIORITY_DISPLAY //值为-4,用于普通显示线程
THREAD_PRIORITY_FOREGROUND //值为-2,用于普通前台程序
THREAD_PRIORITY_LESS_FAVORABLE //值为1,低于默认优先级
THREAD_PRIORITY_LOWEST //值为19,最低优先级
THREAD_PRIORITY_MORE_FAVORABLE //值为-1,最高默认优先级
THREAD_PRIORITY_URGENT_AUDIO //值为-19,重要的音频线程
THREAD_PRIORITY_UNGENT_DISPLAY //值为-8,重要的显示线程
2.Android线程封装
在Android中,为了简化开发者的工作,对Java线程进行了一定程度的封装,最主要的工作包括AsyncTask封装和接入UI线程。
(1)AsyncTask封装
AsyncTask通常用于后台线程和UI线程的交互。虽然AsyncTask本质上还是Thread加Handler的一个封装,但是由于采用了Java 1.5中的并发库FutureTask,故其在处理异步事务时表现卓越。使用AsyncTask的示例如下:
private class ReceicePushTask extends AsyncTask<Intent, Void, Void>{
protected Void doInBackground(Intent... instentd){
Intent intent-intents[0];
}
public void onReceive(Context context, Intent intent){}
}
AsyncTask中最基本的两个方法为doInBackground和onReceive,前者执行真正的后台计算,后者向UI主线程反馈计算结果。为了使用AsyncTask,开发者必须对AsyncTask进行继承。
AsyncTask的其他重要方法如下:
public final AsyncTask<Params, Progress,Result>execute(Params... params) //执行AsyncTask
public void onPostExecute(Result result) //在doInBackGround方法执行后执行,该方法在UI线程中执行
protected void onPreExecute() //在执行doInBackground方法前执行
protected final void publishProgress(Progress... values) //向UI线程反馈计算进度
public final boolean cancel(boolean mayInterruptIfRunning) //中断线程执行
public final boolean isCancelled() //判断AsyncTask是否已经被中断
在Android3.0中,Google又引入了execute(Runnable runnable)等多个方法。AsyncTask可以支持多个输入参数,甚至可变参数,其参数在execute方法中传入。
在中断开始执行时,系统将会调用AsyncTask的onCancelled方法,开发者可以在该方法中进行一些收尾工作,但是要注意,必须显式判断线程是否中断。假设在某个目录下寻找和关键字匹配的文件和文件夹,后台计算的工作被放置在AsyncTask中,显式判断线程是否中断的工作在dosth中进行,示例如下:
SearchTask mSearchTask=new SearchTask();
mSearchTask.execute(path) //执行线程
public void dosth(String value){
for(...){
if(mSearchTask.isCancelled()){
return;
}
}
}
下面是SearchTask的实现,其中包含了后台计算的入口、正常结束的入口、线程中断的入口等。
class SearchTask extends AsyncTask<String,Void, Void>{
public void doInBackground(String... value){
dosth(value[0]);
}
pretected void onCanceled(){
//执行中断的善后工作
}
protected void onPostExecute(Result result){ //该方法在UI线程执行
//执行正常的结束工作
}
}
(2)接入UI线程
在多线程编程中,执行后台计算的线程通常需要和UI线程进行交互,将计算的结果反馈给UI线程,呈现给用户,而传统的方法一般需要借组Handle。在Android中,充分考虑了开发者者方面的需要,从Activity、View和UI主线程3个层次提供了4种方式来接入UI线程。
1)Activity的runOnUiThread(Runnable)方法
Activity提供的接入UI线程的方法实际上是通过runOnUiThread(Runnable)方法进行的,从本质上讲,runOnUiThread方法时基于Handler的post(Runnable r)方法实现的。runOnUiThread(Runnable)的示例如下:
mActivity.runOnUiThread(new Runnable(){
public void run(){
mToast.setGravity(Gravity.BOTTOM, 0, 0);
mToast.show();
}
});
2)View的post(Runnable)方法
View的post(Runnable)方法从控件层面对多线程提供了支持,是开发更加灵活,其实现原理与基于Activity的runOnUiThread(Runnable)方法类似,均是借助Handle进行的。关于View的post(Runnable)方法的示例如下:
public void onClick(View v){
new Thread(new Runnable(){
public void run(){
finnal Bitmap bitmap=loadImageFromNetwork(http://example.com/image.png);
mImageView.post(new Runnable(){
public void run(){
mImageView.setImageBitmap(bitmap);
}
});
}
}).start
}
(3)View的postDelayed(Runnable, long)方法
View的postDelayed(Runnable, long)方法提供了延时处理的能力,示例如下:
final static int PAUSE_DELAY=100;
Runnable mRunnable=new Runnable(){
public void run(){
mView.postDelayed(mRunnable, PAUSE_DELAY);
}
};
(4)Handler方法
利用Message消息向UI主线程发送后台计算的消息,及时将计算结果反馈给实际用户,这一切均需要借助Handle进行消息处理。当UI线程接收到Message消息后,会调用其Handler进行消息处理。Handler处理消息的过程如下:
Message message=mHandle.obtainMessage(ORIENTATION_CHANGED);
mHandler.sendMessage(message);
Handler mHandler=new Handler(){
public void handleMessage(Message msg){
switch(msg.what){
case ORIENTATION_CHANGED;
break;
}
}
};
3.线程间的消息通信
相比Thread加Handler的封装,Async更为可靠,更易于维护。AsyncTask的缺点是,一旦线程开启即dobackground方法执行后无法向线程发送消息,仅能通过预先设置好的标记来控制,当然可以通过挂起线程并等待标志位的改变来进行通信。
对于某些应用,原生的Thread加Handler以及Looper可能更灵活。下图揭示了三者之间的关系。
(1)消息队列
消息队列的维护是在线程中进行的,但默认除UI主线程外,其他线程并不拥有消息队列。为了维护一个消息队列,需要在线程中调用Looper的prepare方法进行初始化。为了使线程可以接收发送过来的消息,通常需要借组Handler来接收消息。一个维护消息队列的示例如下:
private class Queryrunner extends Thread{
public Handler mHandle
public void run(){
Looper.prepare(); //loop()方法应随后调用
mHandler=new Handler(){
public void handleMessage(Message msg){//处理收到的消息}
};
Looper.loop();
};
}
通过Looper还可以判断当前线程是否为UI主线程,方法如下:
private void assertUiThread(){
if(!Looper.getMainLooper().equals(Looper.myLooper())){
throw new RuntimeException("Not on the UI thread!");
}
}
通过查看Message的实现即可发现,Message继承于Parcelable,在消息的传递过程中,实际上传递的是序列化数据。Message的实现如下:
public final class message implements Parcelable{
public int what; //消息表示符
public int arg1; //传递参数
public int arg2; //传递对象
public Object obj; //传递的对象
public Messager replyTo;
long when;
Bundle data;
Handle target; //目标Handler
Runnable callback;
Message next;
private static final int MAX_POOL_SIZE=10;
...
}
通过分析Message的源代码实现可以发现,一个线程会自动维护一个消息池,该消息池的大小为10.在需要生成消息时,首先从消息池中获取消息,只有当消息池中的消息均被使用时,才会重新创建新消息,当消息使用结束时,消息池会回收消息。
从obtain方法的实现中可以看出,在发送消息时,通过obtain方法获得Message比创建Message的效率更高。发送消息的示例如下:
public void progress(boolean progress){
android.os.Message msg=android.os.Message.obtain();
msg.what=MSG_PROGRESS;
msg.arg1=progress ? 1: 0;
sendMessage(msg);
}
(2)消息分发
消息的分发和接收均与Handler密切相关,对于消息的分发,Android目前提供了两种消息类型,一种是post消息,一种是send消息。其中post消息会在为了某一时间加入消息队列,而send消息则会立即加入到消息队列。
1)send消息
分发一个简单的send消息的方法如下:
Messagemsg=mHandler.obtainMessage(KILL_APPLICATION_MSG);
msg.arg1=uid;
msg.arg2=0;
msg.obj=pkg;
mHandler.sendMessage(msg);
如果没有参数需要传递,那么可以发送仅携带消息表示符的空消息,方法如下:
mHandler.sendEnptyMessage(CANCEL_HEAVY_NOTIFICATION_MSG);
2)post消息
通过post消息可以实现类似循环计算的功能,方法如下:
mTicker = new Runnable(){
public void run(){
if(mTickerStopped) return;
mCalender.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat(mFormat, mCalendar));
invalidate();
long now=SystemClock.uptimerMillis();
long next=now + (1000-now%1000);
mHandler.postAtTime(mTicker, next);
}
};
mTicker.run();
(3)消息接收
消息的接收相对简单,但是如果存在重入问题,应注意数据的同步。关于数据安全的内容。在Handler中消息接收的过程如下:
public void dispatchMessage(Message msg){
if(msg.callback !=null){
handleCallback(msg); //通过Message自身回调进行处理
}else{
if(mCallback.handleMessage(msg)){
return;
}
}
handleMessage(msg); //handle自己处理
}
对于消息队列而言,如何监听消息的来临呢,笔者的经验是,在Looper的loop方法中执行对消息的监听。loop方法的实现如下:
public static final void loop(){
Looper me=myLooper();
MessageQueue queue=me.mQueue;
Binder.clearCallingIdentity();
finnal long ident=Binder.clearCallingIdentity();
if(msg!=null){
if(msg.target==null){
return;
}
msg.target.dispatchMessage(msg); //调用Handler进行消息处理
msg.recycle(); //消息回收
}
}
4.线程安全处理
线程安全多是由多线程对共享资源的访问引起的,在线程安全层面,Android更多的是采用Java的实现,除了Java的join()、wait()、sleep()、notify()等安全方法和synchronized关键字外,还有Java的并发库。
除了synchronized关键字外,基于Java的多线程安全均比较复杂,对于不是十分复杂的场景,优先进行多线程处理。
(1)synchronized同步
在Android应用层,线程的并发很多是靠synchronized关键字来实现的,这样的实现非常简单。通过synchronized关键字,可以实现方法和语句块的同步。同步的本质是对特定的对象加锁,他们可以是来自调用方法的对象,也可以是开发者设计的对象。
另外,synchronized关键字并不能继承,对于父类的同步方法,在子类中必须再次显式声明才能成为同步方法。
synchronized关键字的局限在于在试图获得锁时无法设定超时和中断,每个锁只有一个条件,在某些场景下可能不够用。
另外,同步会带来较大的系统开销,甚至造成死锁,因此在进行开发时应避免无谓的同步。synchronize关键字可以实现方法同步和语句块同步。
1)方法同步
方法同步分一般方法同步和静态方法同步两种。一般方法同步的本质在于将synchronized关键字作用于对象引用(object referece),作用域仅限类的单个对象,下面是AbsSeekBar中的一个实现:
public synchronized void setMax(int max){
super.setMax(max);
}
以上实现等同于如下实现:
public void setMax(int max){
synchronized(this){
super.setMax(max);
}
}
静态方法同步的本质是将类本身作为所,其作用域是该类的所有对象。下面是BitmapManaget中利用synchronized实现单子模式的过程。
private static BitmapManager sManager=null;
public static synchronized BitmapManaget instance(){
if(sManager==null){
sManager=new BitmapManager();
}
}
2)语句块的同步
方法同步的作用域较大,但事实上需要同步的范围可能并没那么大。当需要同步的范围不是很大时,可采用语句块同步。下面是AccelerometerListener中的一个示例:
private void setOrientation(int orientation){
synchronized(this){
}
}
将对象引用本身作为锁,显然影响并发效率,更灵巧的设计是自定义锁。自定义锁可以是任何类型的对象,但通常将其类型设计为Object。AbstractPreferences中的一个示例如下:
public abstract class AbstractPreferences extends Preferences
{
protected final Object lock;
protected AbstractPreferences getChild(String name) throws
BackingStoreException{
synchronized(lock){
}
}
}
采用自定义锁可以带来更高的并发效率,当然具体采用什么样的锁,需要根据实际场景决定。
(2)RPC通信
在基于AIDL的通信中,由于允许多个客户端的存在,其实现必须是线程安全的,开发者要注意,对于IPC调用方法,如果是一种耗时的操作,应避免在UI主线程中调用,以免阻塞UI主线程。
(3)SQLite调用
对于SQLite的调用,可能会存在多处执行读写操作的场景,这种场景也需要考虑线程的安全性。为了方便开发者操作,Android提供了类似于AsyncTask的AsyncQueryHandler方法来解决这一问题,将耗时的查询等操作放置在非UI主线程中,操作结束后,通过Handler调用相应的UI主线程的方法处理操作执行的结果,示例如下:
AsyncQueryHandler mAsyncQueryHandler=new AsyncQueryHandler(getContectResolver()){
protected void onQueryComplete(int token, Object cookie, Cursor cursor){
if(cursor!=null && cursor.moveToFirst()){
}
}
}
mAsyncQueryHandler.startQuery(0, null, mUri, new String[] {
MediaStore.Audio.Media.TITLE,MediaStore.Audio.Media.ARTIST
},MediaStore.Audio.Media,DATA+”=?“, new string[]{path}, null());