其实在某些情况下是可以在子线程中更新UI的!
比如:在一个activity的xml文件中中随便写一个TextView文本控件,然后在Activity的onCreate方法中开启一个子线程并在该子线程的run方法中更新TextView文本控件,你会发现根本没有任何问题。
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.threadcrashui_act);
textView = (TextView) findViewById(R.id.textView);
new Thread(new Runnable() {
@Override
public void run() {
// 更新TextView文本内容
textView.setText("update TextView");
}
}).start();
}
但是如果你让子线程休眠2秒钟如下面代码:
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.threadcrashui_act);
textView = (TextView) findViewById(R.id.textView);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
//更新TextView文本内容
textView.setText("update TextView");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
程序直接挂掉了,好吧,我们先去看看log日志,如图提示
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。
程序不允许在非UI线程中更新UI线程
这个ViewRootImpl我们可能在Eclipse或者Android Studio中我们无法查看,大家可以使用Source Insight 去查看源码:打开Source Insight 打开ViewRootImpl类,找到 invalidateChild这个方法
public void invalidateChild(View child, Rect dirty) {
//关键的地方就是这个方法
checkThread();
//后边的全部省略
}
void checkThread() {
//在这里mThread表示的是主线程,程序作了判断,检查当前线程是不是主线程,如果不是就会抛出异常
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
看到上边方法中抛出的异常是不是感觉很熟悉,对,没错,就是我log中截出来的那句话!!那么我们现在懵逼了,为什么我们在不让子线程休眠的情况下去更新TextView文本可以,而让线程休眠两秒后就出抛异常呢?根本原因就是ViewRootImpl到底是在哪里被初始化的!ViewRootImpl是在onResume中初始化的,而我们开启的子线程是在onCreat方法中,这个时候程序没有去检测当前线程是不是主线程,所以没有抛异常!!下边我们去看ActivityThread源码,去找出原因!!
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//ViewPrent实现类
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//问题的根源在这里,addView方法在这里对ViewRootImpl进行初始化,大家可以去看看ViewGroup的源码,找里边的addView方法,你会发现,最后又回到View的invalidate(true)方法;
wm.addView(decor, l);
}
}
}
子线程更新UI界面的2种方法
方式一:Handler和Message
① 实例化一个Handler并重写handlerMessage()方法
private Handler handler = newHandler() {
public void handleMessage(Message msg) {
// 处理消息
super.handleMessage(msg);
switch (msg.what) {
case 1:
button1.setText("点击安装");
break;
case 2:
button1.setText("打开");
break;
}
};
};
② 在子线程中获取或创建消息,并使用handler对象发送。
//子线程一
doInBackground1=new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//1.访问数据库或者互联网,不在UI进程,所以不卡
Message msg=new Message();
//对消息一个识别号,便于handler能够识别
msg.what=1;
handler.sendMessage(msg);
Log.i("dd","子线程一已发送消息给handler");
}};
方式二:在子线程中直接调用Activity.runOnUiThread(Runnable action)方法
new Thread() {
public void run() {
//这儿是耗时操作,完成之后更新UI;
runOnUiThread(new Runnable(){
@Override
public void run() {
//更新UI
imageView.setImageBitmap(bitmap);
}
});
}
}.start();
如果在非上下文类中(Activity),可以通过传递上下文实现调用;
Activity activity = (Activity) imageView.getContext();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
扩展:
ViewRoot对应ViewViewRootImpl类,它是连接WindowManager和DecorView的纽带,view的三大流程(measure,layout,draw)均是通过ViewRoot来完成的,ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象并和DecorView建立联系。DecorView作为顶级的View一般情况下它内部会包含一个竖向的LinearLayout,这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。