android工作线程更新ui,Android 真的不能在子线程更新 UI 吗?让你搞懂很多底层原理...

原标题:Android 真的不能在子线程更新 UI 吗?让你搞懂很多底层原理

热文导读 | 点击标题阅读

来源:http://www.cnblogs.com/lao-liang/p/5108745.html

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

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

如果在其它线程访问 UI 线程,也就是非主线程,Android 提供了以下的方式:

Activity.runOnUiThread(Runnable)

View.post(Runnable)

View.postDelayed(Runnable, long)

Handler

为什么在子线程中就不能操作 UI 呢?

当 App 第一次启动的时候,Android 会同时启动一个对应的主线程(Main Thread),这个主线程就是 UI 线程,也就是 ActivityThread。UI 线程主要负责处理与 UI 相关的事件,如用户的按键点击、用户触摸屏幕以及屏幕绘图等。

系统不会为每个 UI 组件单独创建一个线程,在同一个进程里的 UI 组件都会在 UI 线程里实例化,系统对每一个组件的调用都从 UI 线程分发出去。

所以,响应系统回调的方法永远都是在 UI 线程里运行,如响应用户动作的 onKeyDown() 的回调。

那为什么选择一个主线程干这些活呢?换个说法,Android 为什么使用单线程模型,它有什么好处?

要明白这点,就得先看下单线程化的事件队列模型是怎么定义的:

采用一个专门的线程从队列中抽取事件,并把他们转发给应用程序定义的事件处理器。

这看起来就是 Android 的消息队列、Looper 和 Handler嘛

其实现代 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 不能在子线程中使用的原因。

而 postInvalidate 可以在子线程中使用,它是怎么做到的?

看看源码是怎么实现的:

public voidpostInvalidate() {

postInvalidateDelayed( 0);

}

public voidpostInvalidateDelayed(long delayMilliseconds) {

// We try only with the AttachInfo because there's no point in invalidating

// if we are not attached to our window

if(mAttachInfo != null) {

Message msg = Message.obtain();

msg.what = AttachInfo.INVALIDATE_MSG;

msg.obj = this;

mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);

}

}

说到底还是通过 Handler 的 sendMessageDelayed 啊,还是逃不过消息队列,最终还是交给 UI 线程处理。所以 View 的更新只能由 UI 线程处理。

那么问题来了:如果我非要在子线程中更新UI,那会出现什么情况呢?

会抛出一个 CalledFromWrongThreadException 异常:

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

相信很多人遇到这个异常后,就会通过前面的四种方式中的其中一种解决:

Activity.runOnUiThread(Runnable)

View.post(Runnable)

View.postDelayed(Runnable, long)

Handler

说到这儿还是没触发到根本,不禁会问:为什么会出现这个异常呢,这个异常在哪里抛出来的呢?

在 framework/base/core/java/android/view/ViewRootImpl.java 类中,有这么一个方法:

voidcheckThread() {

if(mThread != Thread.currentThread()) {

thrownewCalledFromWrongThreadException(

"Only the original thread that created a view hierarchy can touch its views.");

}

}

再看下 ViewRootImpl 的构造函数,mThread 就是在这初始化的:

public ViewRootImpl(Context context, Display display) { mContext = context; mWindowSession = WindowManagerGlobal.getWindowSession(); mDisplay = display; mBasePackageName = context.getBasePackageName(); mDisplayAdjustments = display.getDisplayAdjustments(); mThread = Thread.currentThread(); ......}

再研究一下这个 CalledFromWrongThreadException 异常的去量堆栈信息,会发现最后到了invalidateChild 和 invalidateChildInParent 方法中:

@Override

public voidinvalidateChild(View child, Rect dirty) {

invalidateChildInParent( null, dirty);

}

@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

checkThread();

......

}

最终在 checkThread() 时形成了这个异常。说到底,非 UI 线程是可以刷新 UI 的呀,前提是它要拥有自己的 ViewRoot。如果想直接创建 ViewRoot 实例,你会发现找不到这个类。那怎么做呢?通过 WindowManager:

classNonUiThread extends Thread{

@Override

public voidrun() {

Looper.prepare();

TextView tx = newTextView(MainActivity.this);

tx.setText( "non-UiThread update textview");

WindowManager windowManager = MainActivity.this.getWindowManager();

WindowManager.LayoutParams params = newWindowManager.LayoutParams(

200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,

WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);

windowManager.addView(tx, params);

Looper.loop();

}

}

就是通过 windowManager.addView() 创建了 ViewRoot,WindowManagerImpl.java 中的 addView() 方法如下:

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override

public voidaddView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

applyDefaultToken(params);

mGlobal.addView(view, params, mDisplay, mParentWindow);

}

mGlobal 是一个 WindowManagerGlobal 实例,代码在 frameworks/base/core/java/android/view/WindowManagerGlobal.java 中,具体实现如下:

public voidaddView(View view, ViewGroup.LayoutParams params,

Display display, Window parentWindow) {

if(view == null) {

thrownewIllegalArgumentException( "view must not be null");

}

if(display == null) {

thrownewIllegalArgumentException( "display must not be null");

}

if(!(params instanceofWindowManager.LayoutParams)) {

thrownewIllegalArgumentException( "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 = newRunnable() {

@Override public voidrun() {

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{

thrownewIllegalStateException( "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 = newViewRootImpl(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);

}

}

throwe;

}

}

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

延伸阅读

Android Activity 本身是在什么时候创建 ViewRoot 的呢?

既然是单线程模型,就要先找到这个 UI 线程实现类 ActivityThread,看里面哪里 addView()了。没错,是在 onResume() 里面,对应 ActivityThread 就是 handleResumeActivity 这个方法:

final voidhandleResumeActivity(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.

} elseif(!willBeVisible) {

if(localLOGV) Slog.v(

TAG, "Launch "+ r + " mStartedActivity set");

r.hideForNow = true;

}

......

}

所以,如果在 onCreate() 中通过子线程直接更新UI(也就是在 onresume() 之前),并不会抛 CalledFromWrongThreadException 异常。但是一般情况下,我们不会在 onCreate() 中做这样的事情。

这就是Android为我们设计的单线程模型,核心就是一句话:

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

但这一句话背后,却隐藏着我们平时看不见的代码实现,只有搞懂这些,我们才能知其然知其所以然。

看完本文有收获?请分享给更多人

Java和Android架构

公众号:JANiubility返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值