WindowManager实现真正意义上的子线程更新View
我们日常工作时不会写子线程中更新View的代码,因为我们都知道子线程不能更新View。
在这里和大家同步一下,准确的来说不是子线程不能更新View,准确的来说是View不能在非View创建出的线程中更新。
具体原因我给大家讲一下。
这次发现主要是因为要实现一个一直动的悬浮窗,又不能使用SurfaceView,才发现的。
先从View开始讲
我们如果使用子线程更新View 会抛出这样的异常
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
可以看出异常是从 ViewRootImpl中抛出方法是requestLayout中的checkThread
熟知自定义 View的话应该知道requestLayout是什么方法,那么我们来看一下checkThread
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
可以看的 如果mThread != Thread.currentThread() ,mThread不等于当前方法调用线程时抛出异常
那么接着看看 mThread是如何创建的
public ViewRootImpl(Context context, Displaydisplay) {
/* ① 从WindowManagerGlobal中获取一个IWindowSession的实例。它是ViewRootImpl和
WMS进行通信的代理 */
mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper());
// **②保存参数display**,在后面setView()调用中将会把窗口添加到这个Display上
mDisplay= display;
CompatibilityInfoHolder cih = display.getCompatibilityInfo();
mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder();
/* **③ 保存当前线程到mThread。**这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主程。
在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检
查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常*/
mThread= Thread.currentThread();
...
}
代码比较长我就放一点,可以看的mThread是在创建ViewRootImpl时,赋予的当前线程,此时是不是懂了一点,代码不是直接赋予的主线程,而是当前方法调用线程
那么ViewRootImpl是什么时候创建的呢,具体源码不细讲了,是在Activity onResume时,调用WindowManager 实际上是WindowManagerImp类是的AddView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
}
}
所以我们正常创建的View都是从主线程创建的,那么ViewRootImpl所持有的线程也就是主线程
此时答案和解决方式已经很明了,我们想办法从子线程里创建一个ViewRootImpl不就行了
接着思考就是ViewRootImpl是WindowManager中addView方法调用的,那我们直接从子线程里addView就行了,其实就是这么简单
new Thread() {
@Override
public void run() {
Looper.prepare();
WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
view = View.inflate(MainActivity.this, R.layout.item, null);
tv = (TextView) view.findViewById(R.id.tv);
params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.width = (int) (60 * getResources().getDisplayMetrics().density);
params.height = (int) (60 * getResources().getDisplayMetrics().density);
params.gravity = Gravity.LEFT | Gravity.TOP;
params.format = PixelFormat.TRANSPARENT;
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
params.y = height / 2 - params.height / 2;
wm.addView(view, params);
});
Looper.loop();
}
}.start();
就是我们常用的WindowManager创建View的方式
注意要写Looper,因为View更新的时候要使用子线程中的handler
此时就可以子线程更新View了,做多复杂的操作都不会影响主线程,是不是很新奇很简单
仔细封装封装又一个主线程呢。。。