关于Android中的消息机制和异步

来源:互联网 发布:数据库设计有哪些步骤 编辑:程序博客网 时间:2024/06/01 09:12

转载地址:http://blog.csdn.net/zhangjg_blog/article/details/12949785

Android中的异步

android中的应用开发,不像是写控制台程序,他是一种和UI相关的程序。几乎所有的UI应用程序都会有这样的要求:不能在主线程(即UI线程)中做耗时的操作。因为一般情况下,主线程负责处理消息和更新界面。其实更新界面也是基于消息驱动的。

在android设备上, 我们做的每个操作,比如按下菜单键或返回键,或者点击了界面上的一个按钮,这些事件 都会被封装成一个消息,发送到主线程的消息队列中。而主线程监听在他的消息队列上, 如果消息队列中进入了一个消息,那么主线程便取出这个消息,调用这个消息上的回调方法,如果主线程的消息队列中没有消息,那么主线程便会阻塞在队列上,直到一个消息的到来。这种消息机制可以用下面的一张图来解释(该图片来自百度):


从这张图中可以看到android消息机制的几个角色:

  • MessageQueue:消息队列。和线程绑定,用于存储当前线程的消息
  • Looper:循环器。和线程绑定,用于控制消息循环。例如在消息队列为空时阻塞当前线程。
  • Message:消息实体。
  • Handler:句柄。和线程绑定,用于发送消息,并且负责消息的回调处理。

其实主线程中的所有代码都是由这种消息机制驱动的。比如我们熟悉的onCreate等回调方法,是框架向该应用程序的主线程的消息队列中发送了一个消息,然后由主线程基于这个消息,调用onrCreate等回调方法。

如果在主线程中做耗时的操作,比如IO和网络,那么主线程就会被长时间的占用,他的消息队列中还有其他消息就不能被即使处理,导致应用程序崩溃,这就是著名的ANR(application no response)错误。举个例子,主线程正在从数据库中读取大量的数据,这时你点击了界面上的一个按钮,这个事件被封装成消息发送到主线程的消息队列,等待主线程处理,由于主线程正在读数据,所以这个消息得不到及时的处理。

所以,在安卓应用开发中, 为了避免主线程被阻塞,将耗时的操作放到子线程中是非常重要的。最主要的处理方式是:

  1. 主线程创建一个Handler对象,这个Handler对象在创建完成后就和主线程绑定在一起,他将消息发送到主线程的消息队列中,并且负责这个消息的处理。
  2. 将耗时的操作放到一个新开的子线程中执行,并且传入主线程的Handler,在子线程执行完毕时,使用这个Handler发送一个消息到主线程的消息队列
  3. 主线程的Looper(主线程创建时建立)控制主线程读取到这个消息
  4. 主线程执行这个消息上的回调方法(一般情况下会回调Handler中的handleMessage方法)
代码的形式如下:
[java] view plaincopyprint?
  1. Handler handlerMain = new Handler(){  
  2.       
  3.     public void handleMessage(Message msg) {  
  4.         switch (msg.what) {  
  5.           
  6. case 1:  
  7.     // ...  
  8.     break;  
  9.   
  10. case 2:  
  11.     // ...  
  12.     break;  
  13.   
  14. case 3:  
  15.     // ...  
  16.     break;  
  17. default:  
  18.     break;  
  19. }  
  20.           
  21.     };  
  22.  };  
  23.   
  24.  private void downloadFile(){  
  25.       
  26.     new Thread(new Runnable() {  
  27.   
  28. @Override  
  29. public void run() {  
  30.     // 在子线程下载文件  
  31.       
  32.     //...  
  33.     //...  
  34.     //...  
  35.       
  36.       
  37.     //下载完成,发送通知  
  38.       
  39.     Message msg = handlerMain.obtainMessage();  
  40.     msg.what = 1;  
  41.       
  42.     //msg.sendToTarget();发送消息, 也可以这样写  
  43.     handlerMain.sendMessage(msg);  
  44.       
  45. }  
  46. ).start();  
  47.  }  


除此之外,为了方便于利用消息机制更新界面,Android特意创造了AsyncTask这个类。这个类的底层也是使用上述的消息机制,只不过进行了一些封装而已,此外AsyncTask中还使用了线程池技术。AsyncTask的使用方式如下:
[java] view plaincopyprint?
  1. AsyncTask<String, String, String> task = new AsyncTask<String, String, String>(){  
  2.   
  3.         @Override  
  4.         protected String doInBackground(String... params) {  
  5.               
  6.             // 在子线程下载文件  
  7.               
  8.             //...  
  9.             //...  
  10.             //...  
  11.               
  12.             //下载任务完成后, 会自动发送消息  
  13.               
  14.             return null;  
  15.         }  
  16.           
  17.         protected void onPostExecute(String result) {  
  18.               
  19.             //主线程得到子线程发送的消息后,会回调到这个方法  
  20.             //该方法在主线程中执行  
  21.               
  22.             //处理消息或更新界面  
  23.               
  24.             //...  
  25.             //...  
  26.             //...  
  27.               
  28.         };  
  29.           
  30.     };  
  31.       
  32.     private void downloadFileAndUpdateUI(){  
  33.         task.execute(null);  
  34.     }  

较新的android版本中, 还引入了一些用于异步加载的API,这个异步加载的工具其实底层都是利用的Android的消息机制。

异步 or 同步

异步, 顾名思义就是不是同时执行的:这件事我干不了, 交给你来干, 你干完了之后通知我一下, 我再做一些后续工作。这样你来我往, 各自负责一部分事情, 就达到了异步的效果。
可是, 从上面的代码可以看出,这种根据消息机制实现的异步, 在代码上比较混乱, 阅读时需要跳转来阅读, 有时候阅读一个逻辑还需要跨越好几个文件, 也会在同一个文件的不同地方跳来跳去。
所以, 我们可不可以实现这样一种逻辑:这件事我干不了, 交给你来干,你快点干,干完之后也不用通知我了, 我等着你, 你干完之后, 我再干其他相关的事情。这是一种同步的机制,适用于执行时间不长的任务。
举个例子, 在上一篇博客Android4.0网络操作必须放在子线程中中,有一个登录验证的网络操作,在4.0中只能放在子线程中执行,验证通过后要跳转到其他界面。要实现跳转到其他界面, 必须依赖于验证的结果,而验证是在子线程中进行的,跳转必须在主线程中进行,所以就必须使用异步, 再验证完成之后发送消息到主线程,在主线程中跳转。
其实这只是一个非常简单的http请求, 不会耗费很长时间,其实我们可以在主线程中等待这个操作完成后直接跳转界面。
在jdk5中的线程并发库中可以很方便的实现这种线程等待的逻辑。
  1. 首先创建线程池ExecutorService
  2. 调用ExecutorService的submit方法,传入一个任务对象Callable,返回一个结果Future
  3. 在当前线程中调用Future对象的get方法, 等待后台任务执行完成返回结果
代码如下:
[java] view plaincopyprint?
  1. /** 
  2.      * 登陆验证, 在主线程中直接调用, 主线程会等待后台线程验证的结果返回 
  3.      * @param context 
  4.      * @return 验证成功返回true, 反之返回false 
  5.      */  
  6.     public static boolean userLoginCheckWaited(final Context context){  
  7.           
  8.         //创建单个线程池, 将验证的网络操作放到子线程中  
  9.         ExecutorService singleTheadPool = Executors.newSingleThreadExecutor();  
  10.           
  11.         //将验证任务提交到线程池中  
  12.         Future<Boolean> fu = singleTheadPool.submit(new Callable<Boolean>() {  
  13.   
  14.             @Override  
  15.             public Boolean call() throws Exception {  
  16.                 return ESDKUtils.userLoginCheck1(context);  
  17.             }  
  18.               
  19.         });  
  20.           
  21.         try {  
  22.             return fu.get();                //等待验证结果的返回  
  23.         } catch (Exception e) {  
  24.             e.printStackTrace();  
  25.             return false;  
  26.         }   
  27.     }  
  28.   
  29. /** 
  30. * 网络操作放到后台线程中执行 
  31. */  
  32. private static boolean userLoginCheck1( Context context){  
  33.           
  34.   
  35.         //设置登录验证的各项参数  
  36.         List<BasicNameValuePair> params = new LinkedList<BasicNameValuePair>();  
  37.         params.add(new BasicNameValuePair("yhid", userName));  
  38.         params.add(new BasicNameValuePair("yhkl", passwd));  
  39.         params.add(new BasicNameValuePair("sbid", DeviceTool.getDeviceId(context)));  
  40.         params.add(new BasicNameValuePair("clientIp", IPTool.getPsdnIp()));  
  41.         params.add(new BasicNameValuePair("ywxtbm""BGPTNEW"));  
  42.         params.add(new BasicNameValuePair("ywxtmc""办公平台升级"));  
  43.   
  44.   
  45.         URL url = null;  
  46.         StringBuilder sb = new StringBuilder();  
  47.         BufferedReader reader = null;  
  48.   
  49.         try{  
  50.               
  51.             //设置url地址      
  52.             url = new URL(URLConstant.USER_LOGIN_CHEAK_ADDRESS);  
  53.               
  54.             String paramString = URLEncodedUtils.format(params, "GBK"); //请求参数编码为GBK  
  55.               
  56.             byte[] dataToSend = paramString.getBytes();                 //post请求中的实体数据  
  57.   
  58.             HttpURLConnection connection = (HttpURLConnection) url.openConnection();  
  59.               
  60.             connection.setRequestMethod("POST");  
  61.             connection.setDoOutput(true);  

这样的话, 可以直接在主线程中调用userLoginCheckWaited方法, 而不用再写异步相关的代码, 可以使代码大大简化。调用代码如下:
[java] view plaincopyprint?
  1. //开始业务登陆验证, 验证用户名和密码的正确性,在主线程直接调用  
  2. if(ESDKUtils.userLoginCheckWaited(this)){  
  3.       
  4.     //跳转到界面  
  5. }  


我们还要明白一点, fu.get()方法是会阻塞的, 它等待后台任务的完成。所以要注意,在主线程中调用时, 它同样也会阻塞主线程。因此这种方式只适用于耗时很少的方法,比如验证登陆只是一个http请求,并且数据量很少,可以使用这种方法。如果是上传下载文件这类的操作,就不能使用这种方式了。

原创粉丝点击