在android开发中经常听到这样一句话——“android的UI操作不是线程安全的,同时也只有主线程才能够操作UI”。对于这句话,一直感觉不是太理解。当初心里想既然android的UI操作只能在UI线程即主线程中操作,别的线程不允许操作。所以是不会出现线程的同步问题的,这不应该是很安全的吗?为什么不是线程安全的呢?最近才想明白这句话什么意思:正是因为android的UI操作不是线程安全的,所以才不允许在非UI线程中进行UI操作。试想假如允许在其它工作线程中允许直接进行UI操作,会带来一个问题:多个线程同时操作一个控件可能会有冲突发生。所以android就限定了只能在UI线程中操作各种view。那么如果其它线程需要操作view应该怎么办呢?答案是通过Handler,Handler是子线程与UI主线程通信的桥梁,子线程通过Handler去通知主线程操作UI.
主线程的主要任务就是负责处理与UI相关的事件,比如按钮的按下,并且负责分发相应的事件到相应的组件上。鉴于此,其他一些比较耗时的操作比如:从网络上下载东西,数据库操作等最好不要放到主线程中进行。否则可能导致主线程的阻塞,给用户的感觉就是程序的卡顿。我们要做的就是新开线程处理这些比较耗时的操作,然后在适当的时机通知主线程进行UI的更新操作。
看下面的例子:
主界面上有一个按钮,点击此按钮从网络上下载一个图片,然后显示到一个ImageView上。
解决方案1:
新开一个线程完成下载操作,然后再利用handler.post(runnable())方法将进行UI的操作post到主线程中执行。
代码如下:
1 package learn.again.android; 2 3 import java.io.IOException; 4 5 import android.os.Bundle; 6 import android.os.Handler; 7 import android.view.View; 8 import android.view.View.OnClickListener; 9 import android.widget.ImageView; 10 import android.app.Activity; 11 import android.graphics.Bitmap; 12 13 public class MainActivity extends Activity { 14 protected static final int SHOW_IMAGE = 0; 15 private ImageView image; 16 17 18 private Handler myHandler = new Handler(); 19 20 @Override 21 protected void onCreate(Bundle savedInstanceState) { 22 super.onCreate(savedInstanceState); 23 setContentView(R.layout.activity_main); 24 image = (ImageView) findViewById(R.id.image); 25 findViewById(R.id.download_image).setOnClickListener( 26 new OnClickListener() { 27 28 @Override 29 public void onClick(View v) { 30 // download image to simulate LongOperation 31 downloadImage("http://www.android.com/images/logo.png"); 32 } 33 34 }); 35 } 36 37 // cut-off rule................................................................................ 38 39 /** 40 * start a new thread to download image 41 * 42 * @param url 43 * image address 44 * @throws IOException 45 */ 46 // 方法1 47 private void downloadImage(final String url) { 48 49 new Thread(new Runnable() { 50 51 @Override 52 public void run() { 53 try { 54 final Bitmap bitmap = ImageUtil.getImageByUrl(url); 55 myHandler.post(new Runnable() { 56 @Override 57 public void run() { 58 image.setImageBitmap(bitmap); 59 } 60 }); 61 } catch (IOException e) { 62 e.printStackTrace(); 63 } 64 } 65 }).start(); 66 67 } 68 }
解决方案2:
我们可以重写Handler类复写handleMessage方法,在子线程中通过handler.sendMessage()方法把下载好的bitmap发回主线程,再在主线程中通过image.setImageBitmap(bitmap)完成。
代码如下:
1 package learn.again.android; 2 3 import java.io.IOException; 4 5 import android.os.Bundle; 6 import android.os.Handler; 7 import android.os.Message; 8 import android.view.View; 9 import android.view.View.OnClickListener; 10 import android.widget.ImageView; 11 import android.app.Activity; 12 import android.graphics.Bitmap; 13 14 public class MainActivity extends Activity { 15 protected static final int SHOW_IMAGE = 0; 16 private ImageView image; 17 18 /* 19 * Handler类应该为static类型,否则有可能造成泄露。 在程序消息队列中排队的消息保持了对目标Handler类的应用。 20 * 如果Handler是个内部类,那么它也会保持它所在的外部类的引用。 21 * 为了避免泄露这个外部类,应该将Handler声明为static嵌套类,并且使用对外部类的弱应用。 22 */ 23 private Handler myHandler = new Handler() { 24 @Override 25 public void handleMessage(Message msg) { 26 super.handleMessage(msg); 27 switch (msg.what) { 28 case SHOW_IMAGE: 29 Bitmap bm = (Bitmap) msg.obj; 30 image.setImageBitmap(bm); 31 break; 32 } 33 } 34 35 }; 36 37 @Override 38 protected void onCreate(Bundle savedInstanceState) { 39 super.onCreate(savedInstanceState); 40 setContentView(R.layout.activity_main); 41 image = (ImageView) findViewById(R.id.image); 42 findViewById(R.id.download_image).setOnClickListener( 43 new OnClickListener() { 44 45 @Override 46 public void onClick(View v) { 47 // download image to simulate LongOperation 48 downloadImage_2("http://www.android.com/images/logo.png"); 49 } 50 51 }); 52 } 53 54 // cut-off rule................................................................... 55 private void downloadImage_2(final String url) { 56 57 new Thread(new Runnable() { 58 59 @Override 60 public void run() { 61 try { 62 final Bitmap bitmap = ImageUtil.getImageByUrl(url); 63 Message msg = Message.obtain(); 64 msg.what = SHOW_IMAGE; 65 msg.obj = bitmap; 66 myHandler.sendMessage(msg); 67 } catch (IOException e) { 68 e.printStackTrace(); 69 } 70 } 71 }).start(); 72 73 } 74 }
总结:
第一种方法是通过handler的post方法将一个Runnable对象置于UI线程中去执行,达到目的。第二种方法是能过handler的sendMessage方法,将主线程更新view所需要的东西发回到主线程中。两种方法的对比:第一种发送的是Runnable对象,第二种发送的是Message对象。这两种方法都能达到目的但使用第一种时要注意所发送的Runnable中的代码不能太复杂,否则还是会导致UI的卡顿。第二种方法要自己重写Handler的handleMessage方法。
最后附上ImageUtil类的代码:
1 package learn.again.android; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.net.HttpURLConnection; 6 import java.net.URL; 7 8 import android.graphics.Bitmap; 9 import android.graphics.BitmapFactory; 10 11 public class ImageUtil { 12 public static Bitmap getImageByUrl(String url) throws IOException { 13 URL url_image = new URL(url); 14 HttpURLConnection con = (HttpURLConnection) url_image.openConnection(); 15 con.setRequestMethod("GET"); 16 con.setReadTimeout(5000); 17 InputStream is = con.getInputStream(); 18 Bitmap bitmap = BitmapFactory.decodeStream(is); 19 return bitmap; 20 } 21 }
方案3:
还有一个比较轻量级的方法,Handler有一个以Callback接口为参数的构造函数
1 Handler myHandler = new Handler(new Callback() { 2 3 @Override 4 public boolean handleMessage(Message msg) { 5 Bitmap bm = (Bitmap) msg.obj; 6 image.setImageBitmap(bm); 7 return true; 8 } 9 });
其实,和第一种方案类似。
方案4:
我们翻看View的方法会发现View也有一个Post方法:
public boolean post (Runnable action)
因为这个方法是View的所以调用这个方法,它会将action封装成一个Message然后投递到UI线程的消息队列中。
1 private void downloadImage3(final String url) { 2 3 new Thread(new Runnable() { 4 5 @Override 6 public void run() { 7 try { 8 final Bitmap bitmap = ImageUtil.getImageByUrl(url); 9 image.post(new Runnable() { 10 @Override 11 public void run() { 12 image.setImageBitmap(bitmap); 13 } 14 }); 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } 18 } 19 }).start(); 20 21 }
这样的话我们就不需要自己new Handler对象了,通过翻看源码我们可以看到其实它也先获取view线程即主线程的handler对象然后再用此handler投递此action对象封装的Message.