正如我们知道的,android是不让在子线程中更新ui的。在子线程中更新ui会直接抛出异常

  • Only the original thread that created a view hierarchy can touch its views
  • 那么这种检查机制在什么时候发生的呢?
  • 那么真的不能在子线程中更新ui么?我们带着这个疑问来看一下系统代码

我们知道android中的view的更新(大小,位置,内容)全部都交给了WindowManager,那么我们带着疑问来看下WindowMagager接口的实现类WindowManagerImpl,中如何控制对view的更新的

  • 我们知道WindowManager中有三个常用方法 addView(),removeView()和updateViewLayout();
  • 接下来我们只分析updateViewLayout()方法。

    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {    applyDefaultToken(params);    mGlobal.updateViewLayout(view, params);}
    • applyDefaultToken(params);方法和Window的层级有关系,这里和我们探讨的view的跟新没有关系,因此跳过
    • mGlobal.updateViewLayout(view, params); 发现windowManager的更新其实是交给了mGlobal来操作了,那么mGlobal是什么呢?

      private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    • 发现mGlobal其实是WindowManaerImpl一个成员变量,而且还是单例。其实WindowManagerImpl的跟新委托给了WindowManagerGlobal
    • 那么WindowManagerGlobal的updateViewLayout()方法里面完成了什么功能呢?

        public void updateViewLayout(View view, ViewGroup.LayoutParams params) {if (view == null) {    throw new IllegalArgumentException("view must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {    throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;view.setLayoutParams(wparams);synchronized (mLock) {    int index = findViewLocked(view, true);    ViewRootImpl root = mRoots.get(index);    mParams.remove(index);    mParams.add(index, wparams);    root.setLayoutParams(wparams, false);}

      }

    • 前半部分是异常判断,跳过
    • 下面是给view设置布局参数,新的布局参数。

       final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;view.setLayoutParams(wparams);
    • 下面是找到viewRootImpl,给root重新设置布局参数。

          int index = findViewLocked(view, true);    ViewRootImpl root = mRoots.get(index);    mParams.remove(index);    mParams.add(index, wparams);    root.setLayoutParams(wparams, false);
    • 那么ViewRootImpl是什么呢?其实是android系统中view和WindowManager通讯的桥梁。比如测量 布局 绘制 时间分发 都是在这里传递给view的
    • 接下来我们分析 root.setLayoutParams(wparams, false);这段代码。

      if (newView) {    mSoftInputMode = attrs.softInputMode;    requestLayout();}
    • 代码比较长,这里截取部分代码 requestLayout();
    • 那么requestLayout中做了什么操作呢?

      public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {    checkThread();    mLayoutRequested = true;    scheduleTraversals();}

      }

    • 终于到了重点 checkThread(),在这个方法中做了一个判断,就是当前更新ui的线程是否和ViewRootImpl创建的线程是否是同一个,不是则抛出异常
    • 下面是checkThread代码

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

      }

    • 那么mThread是什么时候创建的呢?下面我们看下ViewRootImpl的构造方法


* 那么viewRootImpl对象什么时候创建的呢?其实在WindowManagerImpl的addview中调用了WindowManagerGlobal的addview。在WindowManagerGlobal的addView的时候创建了ViewRootImpl对象

  • 现在我们终于理清楚了,不能在子线程中更新ui的原因。
    • 如果ViewRootImpl是在更新ui的时候,做了一个判断。判断创建自己的线程和更新ui的线程是否是同一个,不是,直接异常。
    • 那么我们能否手动的创建一个子线程,在这个线程中创建一个viewRootImpl呢?
    • 下面我们带着疑问写一个demo
    • 先看效果图

  • 下面是我们点击之后。在子线程中更新ui的效果图

  • 代码的原理是,我们在子线程中通过WindowManager添加一个view,而这个window所有的层级是系统层级。因此有悬浮效果。而我们创建的这个view因为是在子线程中直接创建了一个window,这个window的级别比较高,所以能显示在其他应用上面。而这个window又没有父window,因此其会单独创建ViewRootImpl对象,而这个对象又是在子线程中创建的,那么我们更新ui的时候,在这个子线程中更新能够成功
  • 下面是核心代码,我们将会一步一步对其进行分析

       new Thread() {    @Override    public void run() {        Looper.prepare();        wm = (WindowManager) MyApplication.ctx.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);        view.setOnTouchListener(new View.OnTouchListener() {            private int y;            private int x;            @Override            public boolean onTouch(View v, MotionEvent event) {                switch (event.getAction()) {                    case MotionEvent.ACTION_DOWN:                        x = (int) event.getRawX();                        y = (int) event.getRawY();                        break;                    case MotionEvent.ACTION_MOVE:                        int minX = (int) (event.getRawX() - x);                        int minY = (int) (event.getRawY() - y);                        params.x = Math.min(width - params.width, Math.max(0, minX + params.x));                        params.y = Math.min(height - params.height, Math.max(0, minY + params.y));                        wm.updateViewLayout(view, params);                        x = (int) event.getRawX();                        y = (int) event.getRawY();                        break;                    case MotionEvent.ACTION_UP:                        if (params.x > 0 && params.x < width - params.width) {                            int x = params.x;                            if (x > (width - params.width) / 2) {                                params.x = width - params.width;                            } else {                                params.x = 0;                            }                            wm.updateViewLayout(view, params);                        } else if (params.x == 0 || params.x == (width - params.width)) {                            Toast.makeText(MainActivity.this, "被电击了", Toast.LENGTH_SHORT).show();                            tv.setText("abcd");                        }                        break;                }                return true;            }        });        Looper.loop();    }}.start();
    • 首先准备Looper,之后loop。因为更新view的时候会在当前的子线程中使用handler。而使用handler必须要looper。
    • 接下来拿到windowManager wm = (WindowManager) MyApplication.ctx.getSystemService(WINDOW_SERVICE);
    • 填充view
    • WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 设置type,将window的级别设置较大,能够显示在其他的window之上
    • params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;这里是设置window透传,也就是当前view所在的window不阻碍底层的window获得触摸事件。
    • 接下来设置window的宽度和高度
    • params.format = PixelFormat.TRANSPARENT;设置透明 否则的话 圆形view后面显示一层黑色,默认效果是黑色。需要设置,才能体现出圆形。
    • 接下来就是设置Gravity了,这里比较简单,因为想实现悬浮窗口的拖拽效果,因此需要修改WindowManager的LayoutParams的x,y值。因此需要和gravity配合使用
    • 接下来就是将view添加到WindowManager中了
    • 剩下的就是触摸事件了
    • 在松手的时候判断了,更新了view中显示的ui
    • 下面是更新效果图

  • 初始文本为Click

因此能否在子线程中更新ui,由ViewRootImpl在哪个线程中创建决定。因此我们更应该将能更新ui的线程成为ui线程而不是主线程。

更多相关文章

  1. Android(安卓)studio 设置签名
  2. Redis的自白:我为什么在单线程的这条路上越走越远?
  3. Redis 6.0 正式版终于发布了!除了多线程还有什么新功能?
  4. Android中AsyncTask的使用与源码分析+3.0以前的缺陷(并发->逐一)
  5. Android之单线程下载与多线程下载
  6. Android:(11)消息机制,异步和多线程
  7. Android(安卓)基础总结:( 十六)Android(安卓)Thread
  8. Android热更新之AndFix就是个大坑
  9. Android在灭屏的情况下实现长按音量键切换歌曲

随机推荐

  1. 【译】Android 多媒体扫描过程(Android Me
  2. JavaDoc不显示 &Android中HttpGet和HttpP
  3. Android的设置界面:SharedPreferences和Pr
  4. Android(安卓)Studio 慢吗?No!!你还不懂她·
  5. msm8909编译环境搭建
  6. Android ApiDemos示例解析(136):Views->L
  7. Android - SQLite in Android
  8. android常用样式
  9. 【android】Eclipse集成android开发环境(I
  10. Android之JSON全面解析与使用