View的工作流程

702 阅读5分钟

老规矩,先从几个问题入手,看Android Framwork的源码。基于源码9.0

1.Activity的界面是如何显示出来的? 2.为什么说Activity 在onResume 才显示界面,才可进行交互 3.为什么说ViewRootImpl 不单单是渲染的中转站,还是触摸事件的中转站。 4.不能在子线程操作View? 5.view是何时被挂载的? 6.View.post()的Runnable最终在哪执行了? 7.为什么View.post 可以获取宽高?

第一点:我们知道Activity设置布局问价是setContentView,那setContentView到底做了什么,View是怎么显示出来的 看下Activity的setConteview() 方法

public void setContentView(@LayoutRes int layoutResID) {    
     getWindow().setContentView(layoutResID); initWindowDecorActionBar(); 
}

这里个getWindow() 其实是 PhoneWindow,PhoneWindow继承于Window(抽象类)

接着往下看PhoneWindow

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();        //创建decorView
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);   // 第一点
        }
        mContentParent.requestApplyInsets();  
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

第一点:mContentParent 这里的mContentParent是个ViewGroup,其实就是DecorView中的 com.android.internal.R.id.content 经过第一步,setContentView就被 加载到DecorView的Content上了 接着看下mContentParent.requestApplyInsets(), mContentParent为ViewGroup,ViewGroup继承于View, 所以会调用View 的 requestApplyInsets

public void requestApplyInsets() {
        requestFitSystemWindows();
    }
    
    @Deprecated
    public void requestFitSystemWindows() {
        if (mParent != null) {
            mParent.requestFitSystemWindows(); //第一点
        }
}

第一点:这里的mParent,其实是ViewParent,ViewParent是接口,实现类ViewRootImpl,往下看会到scheduleTraversals

    @Override
    public void requestFitSystemWindows() {
        checkThread();
        mApplyInsetsRequested = true;
        scheduleTraversals();
    }
   
   
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);     //mChoreographer 协调动画、输入和绘图的时间安排
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
           
           
 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();     //开始遍历View树
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

接着看doTraversal()

void doTraversal() {
   if (mTraversalScheduled) {
       mTraversalScheduled = false;
       mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
       performTraversals();
   }
}

真正的执行在performTraversals()

       private void performTraversals() {    
       
       
         #省略多余代码
         windowSizeMayChange |= measureHierarchy(host, lp, res,
                        desiredWindowWidth, desiredWindowHeight);        }
          performLayout(lp, mWidth, mHeight);
          performDraw(); 
       }

事实上在setContentView()中,并不会执行到绘制过程。只是将布局文件inflate,即,将整个布局view进行了解析,我们结合第二个问题来看就明白了

为什么说Activity 在onResume 才显示界面,才可进行交互? AMS 可以处理显示界面了,会调用 ActivityThread的handleResumeActivity()

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
     #省略多余代码     
   final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
   if (r == null) {
            // We didn't actually resume the activity, so skipping any follow-up actions.
            return;
   }       
   
   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 (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);      第二点
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(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;
        }
   if (r.activity.mVisibleFromClient) {
       r.activity.makeVisible();
   }
}


void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());  //第三点
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
}

第一点:这里将记录的Activity.class的window下的decorView 赋值给Activity的mDecor,这样第三点mDecor就不为null,然后将 mWindowAdded 不为null mDecor添加到window上, 第二点和第三点,都是当还没添加到window 上,就会将mDecor添加到window上,第三点将mDecor 设置为可见状态,其中这个vm的实现类是WindowMangerImpl

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

mGlobal 为WindowMangerGlobal, 在往下看,离真相不远了

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            
            #省略部分代码    
            root = new ViewRootImpl(view.getContext(), display);  //创建ViewRootImpl
            view.setLayoutParams(wparams);
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);   //传递decorView给ViewRootImpl
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }            
    }

这里会创建ViewRootImpl对象,最终decorView 会被传递到ViewRootImpl里面

     /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        
        #省略部分代码    
          // Schedule the first layout -before- adding to the window
          // manager, to make sure we do the relayout before receiving
          // any other events from the system.
          requestLayout();    //关键代码  到了这里,就会去绘制view ( measure,layout,draw)
     }       
         @Override
    public void requestLayout() {       
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();           //这里会进行线程检测,也就是第四点的问题
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

这样第一,第二点就能得出原因了。在onCreate()中,setContentView()方法只是加载了布局文件,创建了布局,而在onResum()方法时,会在WindowMangerGlobal创建ViewRootImpl,然后WindowMangerGlobal将DrecorView传递给ViewRootImpl,并执行view的整个绘制流程,并且结束后将decorView设置为显示状态

为什么说ViewRootImpl 不单单是渲染的中转站,还是触摸事件的中转站

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        
             if (mInputChannel != null) {
                    if (mInputQueueCallback != null) {
                        mInputQueue = new InputQueue();
                        mInputQueueCallback.onInputQueueCreated(mInputQueue);
                    }
                    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
                }

     }

硬件传感器接收到触摸事件经过层层传递分发到应用窗口的第一站就是ViewRootImpl,然后将事件向下分发

5.view是何时被挂载的? view在被绘制前会被绑定window相关信息,在performTraversals方法中,相当于view被挂载到window上了

private void performTraversals() {
    final View host = mView;
    host.dispatchAttachedToWindow(mAttachInfo, 0);  //mAttachInfo 包含window ,缓存,handler等相关信息
}

6.View.post()的Runnable最终在哪执行了?

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);   //当已经被挂载了,直接post消息
        }

        // Postpone the runnable until we know on which thread it needs to run.
        // Assume that the runnable will be successfully placed after attach.
        getRunQueue().post(action);     //否则,放入队列等待执行
        return true;
    }
    
    void dispatchAttachedToWindow(AttachInfo info, int visibility){
            // Transfer all pending runnables.
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);   //挂载后,执行所有的消息
            mRunQueue = null;
        }
   }
    

代码中的分析已经知道,如果已经挂载就直接post消息,否则会加入队列,直到挂载后,执行post消息。 也就能解释,第七个问题,因为view.post是在挂载后的,执行了测量流程,所以可以正常获取宽高

总结:image.png