Android 子线程更新UI那些事儿

Posted Coder随笔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 子线程更新UI那些事儿相关的知识,希望对你有一定的参考价值。

android 子线程真的不能更新UI么


如果在其他线程更新UI,Android系统提供了以下几种方式:

  1. Activity.runonUIthread(Runnable)

  2. view.post(Runnable)

  3. view.postDelay(Runnable,long)

  4. Handler


为什么在子线程就不能更新UI呢。

我们知道在子线程更新UI会抛出一个

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

当碰到这个异常的时候,很多人会用上面的4种方式中的一种来解决。那么我们来查看下这个异常是在哪里抛出的呢?

从异常堆栈中,我们可以定位到,是ViewRootImpl.invalidateChildInParent()方法抛出的这个异常。

2019-03-31 20:52:30.234 18524-18970/com.msxf.cash.internal.dev E/AndroidRuntime: FATAL EXCEPTION: testthread 
Process: com.msxf.cash.internal.dev, PID: 18524
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6920)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1116)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:5244)
at android.view.View.invalidateInternal(View.java:13593)
at android.view.View.invalidate(View.java:13557)
at android.view.View.invalidate(View.java:13541)
at android.widget.TextView.checkForRelayout(TextView.java:7357)
at android.widget.TextView.setText(TextView.java:4482)
at android.widget.TextView.setText(TextView.java:4339)
at android.widget.TextView.setText(TextView.java:4314)
at com.msxf.cash.ui.account.LoginActivity$1.handleMessage(LoginActivity.java:149)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:163)
at android.os.HandlerThread.run(HandlerThread.java:61)

再来查看checkThread方法的实现,

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

该代码出自 framework/base/core/java/android/view/ViewRootImpl.java


然后在ViewRootImpl的构造方法里 可以找到mThread 的初始化

public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
mBasePackageName = context.getBasePackageName();

mDisplayAdjustments = display.getDisplayAdjustments();

mThread = Thread.currentThread();
......
}


那么,我们能通过某种方式绕过这个异常么。答案是肯定的。

我们可以给要刷新的View绑定自己的ViewRoot。

就是通过windowManager.addView创建了ViewRoot.

class NonUiThread extends Thread{
@Override
public void run() {
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
tx.setText("non-UiThread update textview");

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(tx, params);
Looper.loop();
}
}


WindowManagerImpl.java

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}


private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow
)
{
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}

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);
}

// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}

所以,非UI线程能更新UI,只要它有自己的ViewRoot。


ps:

通过自己实验,发现在oncreate方法中启动线程操作ui并不会报异常,这是为什么呢?

既然Android的UI相关操作是单线程模型,那么先找到UI线程实现类ActivityThread来看看,是在哪里addview的。

没错,是在onResume里面,对应ActivityThread就是handleResumeActivity这个方法。

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume)
{
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;

// TODO Push resumeArgs into the activity for consideration
ActivityClientRecord r = performResumeActivity(token, clearHide);
......
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 (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}

// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(
TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
......
}




扩展阅读:

Android单线程模型是这样描述的:

Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。


当程序启动的时候,Android会同时启动一个主线程,这个主线程就是UI线程。UI线程主要负责与UI相关的计算和事件,比如点击事件,控件绘制。

那么为什么要选择单线程模型呢?


其实现代GUI框架就是使用了类似这样的模型:模型创建一个专门的线程,事件派发线程来处理GUI事件。

单线程化也不单单存在Android中,Qt、XWindows等都是单线程化。当然,也有人试图用多线程的GUI,最终由于竞争条件和死锁导致的稳定性问题等,又回到单线程化的事件队列模型老路上来。单线程化的GUI框架通过限制来达到线程安全:所有GUI中的对象,包括可视组件和数据模型,都只能被事件线程访问。


这就解释了Android为什么使用单线程模型。


那Android的UI操作并不是线程安全的又是怎么回事?


Android实现View更新有两组方法,分别是invalidate和postInvalidate。前者在UI线程中使用,后者在非UI线程中使用。换句话说,Android的UI操作不是线程安全可以表述为invalidate在子线程中调用会导致线程不安全。作一个假设,现在我用invalidate在子线程中刷新界面,同时UI线程也在用invalidate刷新界面,这样会不会导致界面的刷新不能同步?既然刷新不同步,那么invalidate就不能在子线程中使用。这就是invalidate不能在子线程中使用的原因。


以上是关于Android 子线程更新UI那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

Android开发——实现子线程更新UI

为啥loop之后就可以子线程更新ui

Android 子线程更新UI 异常

Android编程:如何在子线程中更新TextView控件

Android 在子线程中更新UI的几种方法示例

为什么我的子线程更新了 UI 没报错?借此,纠正一些Android 程序员的一个知识误区