zoukankan      html  css  js  c++  java
  • Android -- ViewRoot,关于子线程刷新UI

    Android在4.0之后执行线程更新UI操作会报异常:CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.那么你肯定能看到很多文章说android里子线程不能刷新UI。这句话不能说错,只是有些不太严谨。其实线程能否刷新UI的关键在于ViewRoot是否属于该线程。

    首先,CalledFromWrongThreadException这个异常是有下面的代码抛出的:

    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/ViewRoot.java

    其次,看看RootView的构造函数:

    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();
    
            mLocation = new WindowLeaked(null);
    
            mLocation.fillInStackTrace();
    
            mWidth = -1;
    
            mHeight = -1;
    
            mDirty = new Rect();
    
            mTempRect = new Rect();
    
            mVisRect = new Rect();
    
            mWinFrame = new Rect();
    
            mWindow = new W(this, context);
    
            mInputMethodCallback = new InputMethodCallback(this);
    
            mViewVisibility = View.GONE;
    
            mTransparentRegion = new Region();
    
            mPreviousTransparentRegion = new Region();
    
            mFirst = true; // true for the first time the view is added
    
            mAdded = false;
    
            mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this);
    
            mViewConfiguration = ViewConfiguration.get(context);
    
            mDensity = context.getResources().getDisplayMetrics().densityDpi;
        }

    最后,我们看看ViewRoot.checkThread的调用顺序:

    com.david.test.helloworld.MainActivity$TestThread2.run
    
      -> 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

    到这里相信网友已经明白CalledFromWrongThreadException为什么出现了。那到底非主线程以外的线程能否刷新UI呢?答案当然是能,前提条件是它要拥有自己的ViewRoot。如果你要直接创建ViewRoot的实例的话,你会失望的发现不能找到这个类。那么我们要如何做呢?让我们用实例来说说吧,代码如下:

    class TestThread1 extends Thread{
                  @Override
                  public void run() {
                         Looper.prepare();                    
                         TextView tx = new TextView(MainActivity.this);
                         tx.setText("test11111111111111111");                         
                         WindowManager wm = MainActivity.this.getWindowManager();
                      WindowManager.LayoutParams params = new WindowManager.LayoutParams(
    250, 250, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                            WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);
                       
                         wm.addView(tx, params);
                         Looper.loop();
                  }
        }

    MainActivity是建立android工程时生成的入口类,TestThread1是MainActivity的内部类。感兴趣的话,试试吧!看看是不是在屏幕上看到了"test11111111111111111"?

    最后,说说那里创建了ViewRoot,这里:wm.addView(tx, params)。还是看看具体流程吧:

    WindowManagerImpl.addView(View view, ViewGroup.LayoutParams params)

    -> WindowManagerImpl.addView(View view, ViewGroup.LayoutParams params, boolean nest)

    奥妙就在这里,具体看看代码吧!

    private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
    {
            if (Config.LOGV) Log.v("WindowManager", "addView view=" + view); 
    
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException(
                        "Params must be WindowManager.LayoutParams");
            }
    
            final WindowManager.LayoutParams wparams
                    = (WindowManager.LayoutParams)params;
           
            ViewRoot root;
            View panelParentView = null;
         
            synchronized (this) {
    
                // Here's an odd/questionable case: if someone tries to add a
                // view multiple times, then we simply bump up a nesting count
                // and they need to remove the view the corresponding number of
                // times to have it actually removed from the window manager.
                // This is useful specifically for the notification manager,
                // which can continually add/remove the same view as a
                // notification gets updated.
                int index = findViewLocked(view, false);
    
                if (index >= 0) {
                    if (!nest) {
                        throw new IllegalStateException("View " + view
                                + " has already been added to the window manager.");
                    }
                    root = mRoots[index];
                    root.mAddNesting++;
    
                    // Update layout parameters.
                    view.setLayoutParams(wparams);
                    root.setLayoutParams(wparams, true);
                    return;
                }
             
                // 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 != null ? mViews.length : 0;
                    for (int i=0; i<count; i++) {
                        if (mRoots[i].mWindow.asBinder() == wparams.token) {
                            panelParentView = mViews[i];
                        }
                    }
                }
              
                root = new ViewRoot(view.getContext());
                root.mAddNesting = 1;
                view.setLayoutParams(wparams);
               
                if (mViews == null) {
                    index = 1;
                    mViews = new View[1];
                    mRoots = new ViewRoot[1];
                    mParams = new WindowManager.LayoutParams[1];
                } else {
                    index = mViews.length + 1;
                    Object[] old = mViews;
                    mViews = new View[index];
                    System.arraycopy(old, 0, mViews, 0, index-1);
                    old = mRoots;
                    mRoots = new ViewRoot[index];
                    System.arraycopy(old, 0, mRoots, 0, index-1);
                    old = mParams;
                    mParams = new WindowManager.LayoutParams[index];
                    System.arraycopy(old, 0, mParams, 0, index-1);
                }
                index--;
                mViews[index] = view;
                mRoots[index] = root;
                mParams[index] = wparams;
            
            // do this last because it fires off messages to start doing things
            root.setView(view, wparams, panelParentView);
    }

    出自:frameworks/base/core/java/android/view/WindowManagerImpl.java

    Ok,相信到了这里,大家都已经明白了:子线程是能够刷新UI的!!!

    public class TestActivity extends Activity {
    
        Button btn = null;
    
        /** Called when the activity is first created. */
        public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.main); 
           btn = (Button) findViewById(R.id.Button01);
           TestThread2 t = new TestThread2(btn);
           t.start();
        }
    
        class TestThread2 extends Thread {
           Button btn = null;
           public TestThread2(Button btn) {
               this.btn = btn;
           }
           @Override
           public void run() {
               btn.setText("TestThread2.run");
           }
        }
    }

    建立一个工程,将上述代码拷贝进去,运行看看吧! Btn的文本一定改变为"TestThread2.run"了。

    那么这到底是怎么回事呢?当我发现这个问题时,也困惑了。经过一番调查后,真相大白。现在和大家分享一下。奥秘在于ViewRoot的建立时间,它是在ActivityThread.java的final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward)里创建的。

    看看代码吧:

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
            // If we are getting ready to gc after going to the background, well
           // we are back active so skip it.
            unscheduleGcIdler();
            ActivityRecord r = performResumeActivity(token, clearHide);
            if (r != null) {
               final Activity a = r.activity;
                if (localLOGV) Slog.v(
                    TAG, "Resume " + r + " started activity: " +
                    a.mStartedActivity + ", hideForNow: " + r.hideForNow
                    + ", finished: " + a.mFinished); 
                final int forwardBit = isForward ?
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
                // If the window hasn't yet been added to the window manager,
                // and this guy didn't finish itself or start another activity,
                // then go ahead and add the window.
                boolean willBeVisible = !a.mStartedActivity;
                if (!willBeVisible) {
                    try {
                        willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                                a.getActivityToken());
                    } catch (RemoteException e) {
                    }
                }
                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;
                }
                // The window is now visible if it has been added, we are not
                // simply finishing, and we are not starting another activity.
                if (!r.activity.mFinished && willBeVisible
                        && r.activity.mDecor != null && !r.hideForNow) {
                    if (r.newConfig != null) {
                        if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                                + r.activityInfo.name + " with newConfig " + r.newConfig);
                        performConfigurationChanged(r.activity, r.newConfig);
                        r.newConfig = null;
                    }
                    if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                            + isForward);
                    WindowManager.LayoutParams l = r.window.getAttributes();
                    if ((l.softInputMode
                            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                            != forwardBit) {
                        l.softInputMode = (l.softInputMode
                                & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                                | forwardBit;
                        if (r.activity.mVisibleFromClient) {
                            ViewManager wm = a.getWindowManager();
                            View decor = r.window.getDecorView();
                            wm.updateViewLayout(decor, l);
                        }
                    }
                    r.activity.mVisibleFromServer = true;
                    mNumVisibleActivities++;
                    if (r.activity.mVisibleFromClient) {
                        r.activity.makeVisible();
                    }
                }
                r.nextIdle = mNewActivities;
                mNewActivities = r;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            } else {
                // If an exception was thrown when trying to resume, then
                // just end this activity.
                try {
                    ActivityManagerNative.getDefault()
                        .finishActivity(token, Activity.RESULT_CANCELED, null);
                } catch (RemoteException ex) {
                }
            }
    }

    在Activity.onResume前,ViewRoot实例没有建立,所以没有ViewRoot.checkThread检查。而btn.setText时设定的文本却保留了下来,所以当ViewRoot真正去刷新界面时,就把"TestThread2.run"刷了出来!

    注意onStart,因为onStart在oncreate->onStart->onResume过程中,子线程刷新UI没问题的,但是在onPause->onRestart->onStart过程中,就有问题了。

    我是天王盖地虎的分割线                                                                

    参考:http://blog.csdn.net/imyfriend/article/details/6877962

  • 相关阅读:
    《Forward团队-爬虫豆瓣top250项目-开发文档》
    结对-网页贪吃蛇游戏-项目总结
    Forward团队-爬虫豆瓣top250项目-模块测试过程
    Forward团队-爬虫豆瓣top250项目-模块开发过程
    Forward团队-爬虫豆瓣top250项目-项目总结
    《软件工程课程总结》
    课后作业-阅读任务-阅读提问-4
    《20171201-构建之法:现代软件工程-阅读笔记》
    Forward团队-爬虫豆瓣top250项目-最终程序
    课后作业-阅读任务-阅读提问-3
  • 原文地址:https://www.cnblogs.com/yydcdut/p/3864072.html
Copyright © 2011-2022 走看看