从源码分析 对 非UI线程不允许访问UI元素的 理解
2016-03-04 10:16
337 查看
做Android开发初期大部分开发者都会遇到下面这个问题.
原因大半都是 由于在异步线程中视图 更新界面中的元素
百度一搜,可以搜到大量解决办法.
结果提炼一下: 大致就是讲 UI元素/视图元素 必须在 UI线程(主线程)中更新修改.
新手这样理解就够了.
但是仔细看这个Exception说明
里面并没有提到 主线程/UI 线程,只是提到,必须是创建
!注意区分
创建View的线程和创建View hierarchy的不一定是同一个线程,不要理解错了,下面在源码中具体分析
观察ViewRoot源码中抛出calledfromwrongthreadexception的位置
可以注意到 这里只检查了 当前执行线程和 mThread是否为同一个线程
由ViewRoot的构造函数可以看到,mThread被绑定在了 创建
当然大部分情况下,
mTextView.setText(“123”);为例子.
跟踪一下调用栈
可以看到 view会不断遍历去获取 ViewParent
一个视图上的View遍历到最后 就是 获取到了 ViewRoot
(ViewRoot本身就是ViewParent的一个子类)
然后就会调用到上面提到的 checkThread()
到这里有一个结论:
UI修改操作确实会验证操作的线程
回到标题上来说, 因为 ViewRoot是由UI线程创建的,所以所有被添加到ViewRoot下的子View都必须在UI线程中修改,看起来也很合理.
那么,有可能在非UI线程中创建ViewRoot吗?
其实是可以的.
这里就涉及到ViewRoot和WindowManager之间的关系
demo代码如下:
可以看到,我在异步线程中将 mButton添加到 WindowManager中
查看 WindowManangerImpl中addView的代码:
可以发现在addView的过程中 WindowMananger创建了 ViewRoot
这就是上面提到的在 异步线程中创建 ViewRoot
上面Demo代码中的 runOnUiThread是为了保证下面的
mButton.setText( “22222222” );
是在主线程中执行
可以看到,这段代码即使是在主线程中执行,依然抛出了CalledFromWrongThreadException
在特意构造的这个特殊场景下,主线程/UI线程中 不能更新在异步线程中”创建/添加”的UI元素!!
从源码和设计的角度上分析,我倾向于认为这其实是设计上的一个漏洞,我通过这个漏洞创建了一个非主线程绑定的ViewRoot对象,才会导致上面的结果.
android.view.viewroot$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.
原因大半都是 由于在异步线程中视图 更新界面中的元素
百度一搜,可以搜到大量解决办法.
结果提炼一下: 大致就是讲 UI元素/视图元素 必须在 UI线程(主线程)中更新修改.
新手这样理解就够了.
但是仔细看这个Exception说明
only the original thread that created a view hierarchy can touch its views.
里面并没有提到 主线程/UI 线程,只是提到,必须是创建
View hierarchy的线程
!注意区分
创建View的线程和创建View hierarchy的不一定是同一个线程,不要理解错了,下面在源码中具体分析
观察ViewRoot源码中抛出calledfromwrongthreadexception的位置
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
可以注意到 这里只检查了 当前执行线程和 mThread是否为同一个线程
public ViewRoot(Context context) { super(); if (MEASURE_LATENCY && lt == null) { lt = new LatencyTimer(100, 1000); } // For debug only //++sInstanceCount; // Initialize the statics when this class is first instantiated. This is // done here instead of in the static block because Zygote does not // allow the spawning of threads. getWindowSession(context.getMainLooper()); mThread = Thread.currentThread();
由ViewRoot的构造函数可以看到,mThread被绑定在了 创建
viewRoot的线程上.
当然大部分情况下,
ViewRoot都是在主线程中创建的,所以在异步线程中修改view会造成checkThread失败.
mTextView.setText(“123”);为例子.
跟踪一下调用栈
-- android.widget.TextView.setText -- android.widget.TextView.checkForRelayout -- android.view.View.invalidate -- android.view.ViewGroup.invalidateChild -- android.view.ViewRoot.invalidateChildInParent -- android.view.ViewRoot.invalidateChild -- android.view.ViewRoot.checkThread
可以看到 view会不断遍历去获取 ViewParent
一个视图上的View遍历到最后 就是 获取到了 ViewRoot
(ViewRoot本身就是ViewParent的一个子类)
然后就会调用到上面提到的 checkThread()
到这里有一个结论:
UI修改操作确实会验证操作的线程
* * *
回到标题上来说, 因为 ViewRoot是由UI线程创建的,所以所有被添加到ViewRoot下的子View都必须在UI线程中修改,看起来也很合理.
那么,有可能在非UI线程中创建ViewRoot吗?
其实是可以的.
这里就涉及到ViewRoot和WindowManager之间的关系
demo代码如下:
package com.dk.asyncui; import android.app.Activity; import android.graphics.PixelFormat; import android.os.Bundle; import android.os.Looper; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.Button; public class MainActivity extends Activity { private Button mButton ; private WindowManager mWindowManager ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout. activity_main); mWindowManager = (WindowManager) getSystemService("window" ); mButton = new Button(this ); mButton.setText( "HelloWorld"); Thread mthread = new Thread(new Runnable() { @Override public void run() { Looper. prepare(); WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams(); wmParams. type = WindowManager.LayoutParams.TYPE_APPLICATION ; wmParams. format = PixelFormat. RGBA_8888; wmParams. flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL ; wmParams. width = 320; wmParams. height = 140; mWindowManager.addView(mButton , wmParams); Looper. loop(); } }); mthread.start(); mButton.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread( new Runnable() { public void run() { mButton.setText( "22222222"); } }); } }); } }
可以看到,我在异步线程中将 mButton添加到 WindowManager中
查看 WindowManangerImpl中addView的代码:
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); } 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; } }
可以发现在addView的过程中 WindowMananger创建了 ViewRoot
这就是上面提到的在 异步线程中创建 ViewRoot
上面Demo代码中的 runOnUiThread是为了保证下面的
mButton.setText( “22222222” );
是在主线程中执行
可以看到,这段代码即使是在主线程中执行,依然抛出了CalledFromWrongThreadException
在特意构造的这个特殊场景下,主线程/UI线程中 不能更新在异步线程中”创建/添加”的UI元素!!
从源码和设计的角度上分析,我倾向于认为这其实是设计上的一个漏洞,我通过这个漏洞创建了一个非主线程绑定的ViewRoot对象,才会导致上面的结果.
相关文章推荐
- UIAutomator remote debug方法
- Uiautomator使用
- EasyUI 的各种提示框消息框配置
- Android Studio 中遇到的Gradle build插件版本太低的问题:Plugin is too old
- SVN UUID 变更导致提交更新错误
- UIScrollView之检测滚动方向
- iOS学习笔记67-UIBezierPath精讲
- 慎用PHP$_REQUEST数组
- web开发中,需要用到的脚本---jque…
- PHP $_SERVER["QUERY_ST…
- question_018-JAVA之Map之TreeMap键为自定义对象
- mysqli->multi_query()多条语句的…
- mysqli_query和mysql_query有何区…
- include require include_on…
- UISearchController使用方法及注意事项
- 用xib自定义UITableViewCell的注意事项——重用问题
- LeetCode-60-Permutation Sequence(找规律)-Medium
- 自定义UITabBarController标签视图控制器
- 自定义UITabBarController标签视图控制器
- 获取手机唯一标识符(UUID+KeyChain)