Android 真的不能在子线程更新UI吗?
写过Android 代码的同学应该都听过Android不能在子线程更新UI,只能在主线程即UI线程处理视图。
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(tv);
new Thread(){
@Override
public void run() {
super.run();
textView.setText("not from UI thread!!!");
}
}.start();
}
}
猜一下运行结果呢? 抛出CalledFromWrongThreadException
吗? No!No!No!
不信的话可以试下。
这是为啥?说好的不能子线程更新UI呢! 这个我们等下再说
CalledFromWrongThreadException
首先我们测试一下:
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.btn);
textView = (TextView) findViewById(R.id.tv);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(){
@Override
public void run() {
super.run();
textView.setText("not from UI Thread");
}
}.start();
}
});
}
}
我们在点击button
时,给textView
设置文字,运行App,点击button时我们会看到这个异常
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
说的很清楚,只有创建视图层次结构的原始线程可以访问它的视图。
通过查看Android是源码,我们发现这个异常是在 android.view.ViewRootImpl
抛出来的
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
当Activity对象被创建完毕后,会创建ViewRootImpl
对象,mThread
是在ViewRootImpl
的构造方法里这样初始化的
所以mThread
被赋值成主线程。
public ViewRootImpl(Context context, Display display) {
mContext = context;
······
mThread = Thread.currentThread();
......
}
checkThread()
方法是怎样被调用的呢
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
at android.view.View.invalidateInternal(View.java:13656)
at android.view.View.invalidate(View.java:13620)
at android.view.View.invalidate(View.java:13604)
at android.widget.TextView.checkForRelayout(TextView.java:7347)
at android.widget.TextView.setText(TextView.java:4480)
at android.widget.TextView.setText(TextView.java:4337)
at android.widget.TextView.setText(TextView.java:4312)
at com.acemurder.test.MainActivity$1$1.run(MainActivity.java:27)
这样更直白一点
com.acemurder.test.MainActivity$1$1.run
-> android.widget.TextView.setText
-> android.widget.TextView.checkForRelayout
-> android.view.View.invalidate
-> android.view.ViewGroup.invalidateChild
-> android.view.ViewRootImpl.invalidateChildInParent
-> android.view.ViewRootImpl.invalidateChild
-> android.view.ViewRootImpl.checkThread
当运行到``checkThread()时候,
Thread.currentThread()我们在
OnCreate()`方法里创建的一个子线程,所以抛出来了异常。
通过创建ViewRootImpl在子线程更新UI
通过方才的分析,我们发现异常的原因是TextView
的 ViewRootImpl
是在我们创建Activity的时候创建的,所以我们能不能通过给一个View单独的一个ViewRootImpl
呢?
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(){
@Override
public void run() {
super.run();
Looper.prepare();
TextView tv = new TextView(MainActivity.this);
tv.setText("not from UI Thread");
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
windowManager.addView(tv, params);
Looper.loop();
}
}.start();
}
});
看结果
等等,不是说好的单独创建一个ViewRootImpl
吗?别急,我们来理一下
通过看源码发现,WindowManager
是一个抽象类,我们看他的子类WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
addView方法最终调用了mGlobal
的addView
方法这里的mGlobal
是private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
这样得到的,我们看下它的addView
方法
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
······
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
看见了吧,在这里创建了一个ViewRootImpl对象,所以在子线程更新UI成功。
不创建ViewRootImpl在子线程更新UI
逼逼了这么久,终于说到文章开始那个问题了。
我们OnCreate()
里直接开启一个子线程去更新UI,并没有创建单独的ViewRootImpl
对象啊?
原因就在于ViewRootImpl
的建立时间,它是在ActivityThread.Java
的final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward)
里创建的。
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
······
if (r != null) {
final Activity a = r.activity;
if (localLOGV) Slog.v(
TAG, "Resume " + r + " started activity: " +
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished);
final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
}
······
}
原因就是在Activity的onResume
之前ViewRootImpl实例没有建立,所以没有ViewRootImpl.checkThread检查。而textView.setText
时设定的文本却保留了下来,所以当ViewRootImpl真正去刷新界面时,就把"not from UI Thread"刷了出来!