Android的UI是单线程的,所以对于运行时间长的程序必须异步运行。实现异步任务的一个很方便的工具是AsyncTask。它完全隐藏了运行任务的线程的很多详细信息。
以一个例子来说明AsyncTask:
一个非常简单的应用中,有需要初始化游戏引擎,当加载内容时,显示一些插播广告图形。假设,我们希望在用户等待游戏启动时,显示一个动画背景(类似于Windows Phone 8)上的加载程序的等待界面。
当用户在点击启动按钮以后,会执行多个初始化。
问题:如果在UI线程中执行远程服务调用初始化时,整个UI界面无法执行任何其他操作。
解决:使用AsyncTask解决这个问题,代码如下:
private final class AsyncInitGame extends AsyncTask<String, void, String> { private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInitGame(View root, Drawable bg, Game game, TextView msg) { this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 当UI线程调用任务的execute方法时,会首先调用该方法,这里要做的操作时该任务能够对其本身和环境执行初始化,在这个例子中是安装等待启动的背景动画 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 当doInBackground方法完成时,就删除背景线程,再在UI线程中调用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on a background thread //2. 在onPreExecute方法完成后AsyncTask创建新的背景线程,并发执行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); } }private final class AsyncInitGame extends AsyncTask<String, void, String> { private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInitGame(View root, Drawable bg, Game game, TextView msg) { this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 当UI线程调用任务的execute方法时,会首先调用该方法,这里要做的操作时该任务能够对其本身和环境执行初始化,在这个例子中是安装等待启动的背景动画 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 当doInBackground方法完成时,就删除背景线程,再在UI线程中调用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on a background thread //2. 在onPreExecute方法完成后AsyncTask创建新的背景线程,并发执行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); } }假设AsyncTask的实现是正确的,我们单击按钮“启动”按钮只需要创建一个实例并调用它,如下所示:
((Button)findViewById(R.id.start)).setOnClickListener( new View.OnClickListener(){ @Override public void onClick(View v){ new AsyncInitGame(root, bg, game, msg).execute("basic"); } } );//注:该文中的代码并不全面,只是为了阐述AsyncTask
doInBackground是类Game的代理(proxy)。
解释:
一般来说AsyncTask需要一组参数并返回一个结果。因为需要在线程之间传递该参数并返回结果,所以就需要一些握手机制用来确保线程安全性。
1. 通过参数传递调用execute方法来调用AsyncTask。
2. 当线程在后台执行时,这些参数最终通过AsyncTask机制传递给doInBackground方法的,doInBackground返回结果。
3. AsyncTask把该结果作为参数传递给doPostExecute方法,doPostExecute方法和最初的execute方法在同一个线程中运行。
AsyncTask不但会确保数据流安全也会确保类型安全。该抽象基类(AsyncTask)使用Java反省,使得实现能够制定任务参数和结果的类型,下面以一个例子来说明:
public class AsyncDBReq extends AsyncTask<PreparedStatement, Void, ResultSet> { @Override protected ResultSet doInBackground(PreparedStatement...q){ //implementation..... } @Override protected onPostExecute(ResultSet result){ //implementation } } public class AsyncHttpReq extends AsyncTask<HttpRequest, Void, HttpResponse> { @Override protected HeepResponse doInBackground(HttpRequest.....req){ //implementaion..... } @Override protected void onPostExecute(HttpResponse result){ //implementaion } }第一个类,AsyncDBReq实例的execute方法参数是一个或者多个PreparedStatement变量。
该类中AsyncDBReq实例的doInBackground方法会把这些PreparedStatement参数作为其参数,返回结果是ResultSet。onPostExecute方法会把该ResultSet作为其参数使用。
第二个类也是如此。
AsyncTask的一个实例只能运行一次!!!,第二次执行execute方法会抛出IllegalStateException一场。所以每个任务调用都需要一个新的实例。
虽然AsyncTask简化并行处理,但它有很强的限制约束条件且无法自动验证这些条件。注意不要违反这些约束条件是非常有必须要的,
对于这些约束条件,最明显的是doInBackground方法,因为它是在另一个线程上执行的,只能引用作用域内的变量!!!这样才是线程安全的
OK难点来了,如果实际使用中可能还是会发生下面两个这样的错误,所以,可能需要大量的练习来熟悉和理解了。
案例一:
//易犯错误一、 //....some clas int mCOunt; public void initButton1(Button button){ mCOunt = 0; button.setonClickListener( new View.OnClickListener(){ @SuppressWarnings("unchecked") @Override public void onCLick(View v){ new AsyncTask<Void, Void, Void>(){ @Override protected Void doInBackground(Void...args){ mCount++; //!!! not thread safe!! return null; } }.execute(); }}); }这里在编译时不会产生编译错误,也没有运行警告,可能甚至在bug被触发时也不会立即失败,但该代码绝对是错误的。有两个不同的线程访问变量mCount,而这两个线程之间却没有执行同步。
鉴于这种情况,在本文的第一段代码中的mInFlight的访问执行同步时,你可能会感到奇怪。事实上它是正确的,AsyncTask约束会确保onPreExecute方法和onPostExecute方法在同一个线程中执行,即execute方法被调用的线程。和mCount不同,mInFlight只有一个线程访问,不需要执行同步。
案例二:可能会导致最致命的的并发问题是在用完某个参数变量后,没有释放其引用。如下代码:
//易犯错误二、 public void initButton(Button button, final Map<String, String> vals){ button.setOnClickListener( new View.OnClickListener(){ @Override public void onClick(View v){ new AsyncTask<Map<String, String>, Void, Void>(){ @Override protected Void doInBackground(Map<String,String>...params){ //implementation, uses the params Map } }.execute(vals); vals.clear();//this is not thread safe!!!! }}); }错误原因:initButton的参数valse被并发引用,却没有执行同步!!!当调用AsyncTask时,它作为参数传递给execute方法。syncTask框架可以确保当调用doInBackground方法时,该引用会正确地传递给后台线程。但是对于在initButton方法中所保存并使用的vals引用,却没有办法处理。调用vals.caear修改了在另一个线程上正在使用的状态,但没有执行同步。因此,不是线程安全的。
最佳解决办法:确保AsyncTask的参数是不可变的。如果这些参数不可变,类似String、Integer或只包含final变量的POJO对象,那么它们都是线程安全的,不需要更多的操作。要保证传递给AsyncTask的可变对象是程序安全的唯一办法是确保只有AsyncTask持有引用。
在案例二中参数vals是传递给initButton方法,我们完全无法保证它不存在悬空的引用(dangline references)。即使删掉代码vals.clear,也无法保证该代码正确,因为调用initButton方法的实例可能会保存其参数map的引用,它最终传递的是参数vals。使该代码正确的唯一方式是完全复制(深拷贝)map及其包含的对象!!!
最后还有AsyncTask还有一个方法没有使用到:onProgressUpdate。作用:使长时间运行的任务可以周期性安全地把状态返回给UI线程。
用一个例子来说明并结束本文吧,哎,打字不容易啊,妹子的电脑上没有eclipse,只能用editplus,木有智能提示伤不起的。
该例子说明了如何使用onProgressUpdate方法实现进度条,向用户显示游戏初始化进程还需要多久的时间。
public class AsyncTaskDemoWithProgress extends Activity{ private final class AsyncInit extends AsyncTask<String, Integer, String> implements Game.InitProgressLIstener { private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInit(View root, Drawable bg, Game game, TextView msg){ this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 当UI线程调用任务的execute方法时,会首先调用该方法 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 当doInBackground方法完成时,就删除背景线程,再在UI线程中调用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on its own thread //2. 在onPreExecute方法完成后AsyncTask创建新的背景线程,并发执行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); } //runs on its UI thread @Override protected void onProgressUpdate(Integer... vals){ updateProgressBar(vals[0].intValue()); } //runs on the UI thread @Override public void onInitProgress(int pctComlete){ //为了正确地给UI线程发布进程状态,onInitProgress调用的是AsyncTask的publicProgress。 //AsyncTask处理UI线程的publicProgress调度细节,从而onProgressUpdate可以安全地使用View方法。 publicProgress(Integer.valueOf(pctComplete)); } } int mInFlight,mComplete; /** @see android.app.Activity#onCreate(android.os.Bundle) */ @Override public void onCreate(Bundle state){ super.onCreate(state); setContentView(R.layout.asyncdemoprogress); final View root = findViewById(R.id.root); final Drawable bg = root.getBackground(); final TextView msg = ((TextView)findViewById(R.id.msg)); final Game game = Game.newGame(); ((Button)findViewById(R.id.start)).setOnClickListener( new View.OnClickListener(){ @Override public void onCLick(View v){ mComplete=0; new AsyncInit(root, bg, game, msg).execute("basic"); }}); } void updateProgressBar(int progress){ int p = progress; if(mComplete < p){ mComplete = p; (ProgressBar)findViewById(R.id.progress)).setprogress(p); } } }.......嗯,其实还没结束,来个总结:
· Android UI线程是单线程的。为了熟练使用Android UI,开发人员必须对任务队列概念很熟悉。
· 为了保证UI的及时响应,需要运行的任务的执行时间超过几毫秒,或者需要好几百条指令,都不应该在UI线程中执行。
· 并发编程很棘手,容易犯错,并难以找出错误。
· AsyncTask是运行简单、异步任务的很便捷的工具。要记住的是doInBackground运行在另一个线程上。它不能写任何其他线程可见的状态,也不能读任何其他线程可写的状态,这也包括其参数!
· 不可改变的对象时在并线程之间传递信息的重要工具。
Mr.傅:学习笔记
欢迎转载,转载注明出处,谢谢
《Android程序设计》
引用说明:Programming Android by Zigurd Mednieks, Laird Dornin, G.Blake Meike, and Masumi Nakamura. Copyright 2011 O‘Reilly Media, Inc., 978-1-449-38969-7