前几天看到一个帖子,有位朋友列出了一段代码1:
/********************代码1:在非UI线程里面操作UI*****************************/
class myThread extends Thread {
private Handler myHandler;
public void run() {
Looper myLooper, mainLooper;
Looper.prepare();
myLooper = Looper.myLooper();
mainLooper = Looper.getMainLooper();
String obj;
if (myLooper == null) {
Log.d("ALOOP", "myLooper == null " + Thread.currentThread().getId());
} else {
myHandler = new Handler(myLooper) {//DebugPoint 2
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case MSG_TIMER2:
String obj1 = "MSG_TIMER MainThread";
Log.d("ALOOP",
"Print1 myHandler::handleMessage begin thread id= "
+ Thread.currentThread().getId());
vText.setText(obj1);
Log.d("ALOOP",
"Print2 myHandler::handleMessage end thread id= "
+ Thread.currentThread().getId());
break;
}
}
};
myHandler.sendEmptyMessage(MSG_TIMER2);
}
Looper.loop();
};
}
显然,这段代码是在在非UI线程里面操作UI。Android 的UI元素不是线程安全的。也就是说,不应该在非UI线程里面操作UI元素。
但是这位朋友很困惑的是,这段代码确实运行正常。而且运行了若干次依然正常。
其实多线程编程,有时候你运行若干次,结果正确,并不表明你的逻辑就是对的。
那为什么这里运行多次一直正常呢?根据作者的以往的调试经验,这类代码通常会在运行的时候报
CalledFromWrongThreadException: “Only the original thread that created a view hierarchy can touch its views.” 。可是这段代码我运行多次,确实没有发现这个异常出来。
为了刨根究底, 我们不妨跟着vText.setText代码往下看,看看这段代码的执行路径:
TextView::setText
View::invalidate
ViewRoot::invalidateChild
作者在 View::invalidate里面加入了打印信息,发现在myHandler.handleMessage函数的打印信息“Print1”和“Print1”之间,
判断条件mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)是不成立的,也就是说并没有调用p.invalidateChild(this, r)。
public void invalidate() {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
}
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);
}
}
}
p.invalidateChild的调用路径是:
View::invalidateChild
ViewGroup::invalidateChild
...(ViewGroup::invalidateChild若干)
ViewGroup::invalidateChild
ViewRoot::invalidateChild
ViewRoot::checkThread //这个地方会抛出CalledFromWrongThreadException
所以既然在 “Print1”和“Print1”之间没有调用p.invalidateChild,那自然就不会抛出CalledFromWrongThreadException的异常。
从实验结果来看,这里调用的 TextView::setText,仅仅在于修改了TextView::mText的成员变量。但是并未对TextView 发起一次刷新。也就是说,并没有完成对textView的修改,修改的仅仅是他的模型(数据)。可是做过实验的朋友会发现TextView的文本在这句话之后界面确实会显示“MSG_TIMER MainThread” 。这个界面(视图)的显示其实不是TextView::setText的结果,而是Activity::setContentView的结果。
这部分代码的实际运行路径分为两条线,UI修改线和textView数据修改线。
UI修改线程:
Activity::setContentView
ViewRoot::invalidateChild
textView数据修改线程:
TextView::setText
View::invalidate(并未实际修改UI)
ViewRoot::invalidateChild(因为状态不对,没有进入)
这两条线是并行进行的。UI修改线程会将mPrivateFlags 置为DRAWN | HAS_BOUNDS。但是“textView数据修改线程”的执行路径很短,所以他很可能在mPrivateFlags被UI修改线程修改之前到达View::invalidate,从而导致判断的失败,代码也并没有进入ViewRoot::invalidateChild。
所以,如果我们在代码vText.setText(obj1);之前加上一个延时,让textView数据修改线程稍微等待UI修改线程一会会,系统基本上会一直报异常CalledFromWrongThreadException:
try {//DebugPoint 3
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
vText.setText(obj1);
综上,这个代码本身是有问题的。之所以没有报异常,是UI线程和数据修改线程在时序上错过了。
要根本上解决这个问题,就要把数据修改线程的Looper修改为UI主线程的Looper:
//myHandler = new Handler(mainLooper) { //DebugPoint 1
完整代码如下:
/*TempTestActivity.java*/
package com.test;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;
public class TempTestActivity extends Activity {
private final int MSG_TIMER2 = 14;
private TextView vText = null;
myThread t;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
vText = (TextView) findViewById(R.id.tv01);
//t.myHandler.sendEmptyMessage(MSG_TIMER2);
//vText.setText("adnb");
t = new myThread();
t.start();
}
class myThread extends Thread {
private Handler myHandler;
public void run() {
//Looper myLooper,
Looper mainLooper;
//Looper.prepare();
//myLooper = Looper.myLooper();
mainLooper = Looper.getMainLooper();
String obj;
//if (myLooper == null) {
Log.d("ALOOP", "myLooper == null " + Thread.currentThread().getId());
//} else {
myHandler = new Handler(mainLooper) { //DebugPoint 1
//myHandler = new Handler(myLooper) {//DebugPoint 2
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case MSG_TIMER2:
String obj1 = "MSG_TIMER MainThread";
Log.d("ALOOP",
"myHandler::handleMessage begin thread id= "
+ Thread.currentThread().getId());
/*
try { //DebugPoint 3
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
*/
vText.setText(obj1);
//vText.setVisibility(View.VISIBLE);
//vText.invalidate();
Log.d("ALOOP",
"myHandler::handleMessage end thread id= "
+ Thread.currentThread().getId());
break;
}
}
};
myHandler.sendEmptyMessage(MSG_TIMER2);
//}
//Looper.loop();
};
}
}
主要Layout文件: