美文网首页
Activty视图加载流程浅析

Activty视图加载流程浅析

作者: 留给时光吧 | 来源:发表于2018-05-26 10:11 被阅读0次

    通过本文你可能会了解以下几个方面

    • 1.Activity视图从准备到绘制显示的基本流程
    • 2.视图绘制和Activity生命周期的关系
    • 3.子线程不能更新UI的原因和原理
    • 4.invalidate和postInvalidate机制
    • 5.ViewRootImpl和View的绑定流程

    上篇文章中我们分析了一个Activity的视图结构,但是直到setContentView执行完毕,我们也仅仅是得到了一个布局,若不做任何处理,他是显示出来的,今天我们就来介绍一个视图的加载。首先说一下setContentView之后视图是在哪加载的。
    这还要从Activity的创建流程开始看,这里不多叙述,如果看我的的关于四大组件中Context的分析话应该清楚,在初始化Activity的地方,也就是ActivityThread中的performLaunchActivity方法中,从这个方法向前看,也就是调用的地方,在handleLaunchActivity方法中:

        private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
            ....
            Activity a = performLaunchActivity(r, customIntent);
    
            if (a != null) {
                ....
                handleResumeActivity(r.token, false, r.isForward,
                        !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
    
               ....
        }
    
    

    Activity初始化之后,就调用了handleResumeActivity方法:

        final void handleResumeActivity(IBinder token,
                boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
            ....
            r = performResumeActivity(token, clearHide, reason);
    
                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;
                        ViewRootImpl impl = decor.getViewRootImpl();
                        if (impl != null) {
                            impl.notifyChildRebuilt();
                        }
                    }
                    if (a.mVisibleFromClient) {
                        if (!a.mWindowAdded) {
                            a.mWindowAdded = true;
                            wm.addView(decor, l);
                        } else {
                            a.onWindowAttributesChanged(l);
                        }
                    }
                }
              ......
                    if (r.activity.mVisibleFromClient) {
                        r.activity.makeVisible();
                    }
                }
    
          ....
        }
    
    

    首先执行了performResumeActivity,在这里面回调了onResume方法,也就是一个activity创建完成前的最后一个生命周期回调。下面先获得了activity的DecorView,也就是一个完整的视图,之后先把他的可见性设为隐藏。然后调用了wm.addView(decor, l);wm就是ViewManager 。如果你了解或者看过我的Toast源码分析,应该知道Toast也是通过这种方式显示出来的。所以到这一步才能把一个activity的视图显示出来,随后调用了activity的makeVisible再把DecorView设为可见:

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

    这里的mDecor在Activity自身没有初始化的地方,只在handleResumeActivity方法中通过a.mDecor = decor;初始化。

    这里才是把一个activity显示出来。下面我么重点看一下wm.addView的实现。首先看一下wm 也就是ViewManager 来历,来自于getWindowManager:

        public WindowManager getWindowManager() {
            return mWindowManager;
        }
    

    只是简单返回一下,再看他初始化的地方,在attach方法中通过mWindowManager = mWindow.getWindowManager();初始化。

        final void attach(...) {
           ...
            mWindow = new PhoneWindow(this, window, activityConfigCallback);
            ....
            mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            ...
            mWindowManager = mWindow.getWindowManager();
            ...
        }
    

    mWindow是一个PhoneWindow对象,所以去看看他的getWindowManager方法,发现并没有实现,所以用的是父类Window中的:

        public WindowManager getWindowManager() {
            return mWindowManager;
        }
    

    又是简单取值,所以还要看初始化的地方:

        public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
                boolean hardwareAccelerated) {
            mAppToken = appToken;
            mAppName = appName;
            mHardwareAccelerated = hardwareAccelerated
                    || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
            if (wm == null) {
                wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            }
            mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
        }
    

    这个setWindowManager其实是在activity中的attch方法中调用的,先传入了一个WindowManager ,也就是获取的系统服务,然后调用了其内部方法createLocalWindowManager,在setWindowManager中的类型转换告诉了我们实际调用的是WindowManagerImpl:

    android\frameworks\base\core\java\android\view\WindowManagerImpl.java
    
        public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
            return new WindowManagerImpl(mContext, parentWindow);
        }
    

    发现实际上new了一个WindowManagerImpl对象。WindowManagerImpl实现了WindowManager接口。到这里基本上是跟踪清楚了,上文中调用的addView实际上调用的WindowManagerImpl的addView方法:

        @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            android.util.SeempLog.record_vg_layout(383,params);
            applyDefaultToken(params);
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    

    发现又调用了mGlobal的addview。mGlobal是WindowManagerGlobal 的单例,看他的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);
                try {
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
            }
        }
    

    这个方法主要是给要添加的view设置LayoutParams,然后将View,ViewRootImpl和LayoutParams分别存在三个集合中,然后调用ViewRootImpl的setView方法:

    android\frameworks\base\core\java\android\view\ViewRootImpl.java
    

    这个方法比较长,但是重要的地方是调用了 requestLayout():

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

    这里先检查了线程,所谓子线程不能跟新UI的原因就在这,这个问题在文末再具体讲。紧接着调用了scheduleTraversals方法

        void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    重点在mTraversalRunnable:

        final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
        final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
        void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

    这里执行了performTraversals,这个方法是ViewRootImpl的核心。ViewRootImpl是具体更新View的管理类,所有关于View的更新操作都是在这里执行的,performTraversals就是view绘制的起始,这个方法很长也和很复杂,需要详细了解的可以去看完整源码。这里仅简单介绍一下整个流程:

    • 1.预测量:对整个控件第一次策略,各个控件会上报各自期望的尺寸
    • 2.窗口布局:根据预测量的结果对窗口进行重新布局
    • 3.最终测量:根据布局结果,进行最后的测量,并回调view的measure
    • 4.布局阶段:根据最终测量结果进行布局,确定控件位置,回调view的layout
    • 5.绘制阶段:最后回调onDraw将一个view画出来

    在performTraversals方法中,会先后执行下面几个方法:

    private void performTraversals() {  
            ......  
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ......  
            if (triggerGlobalLayoutListener) {
                mAttachInfo.mRecomputeGlobalAttributes = false;
                mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
            }
            .....
            performDraw();
            }
            ......  
        }
    
    
        private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    

    在这里面调用了mView的measure方法,mView在setView中初始化,就是wm.addView中传进来的DecorView,但是DecorView并没有measure的实现,调用的是父类中的,这个方法中调用了onMeasure方法,在DecorView中又会调用父类也就是FrameLayout的onMeasure,在这里会遍历所有子view,并调用其的measure,就这样测量了整个视图的view。

        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            mLayoutRequested = false;
            mScrollMayChange = true;
            mInLayout = true;
    
            final View host = mView;
            if (host == null) {
                return;
            }
            if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
                Log.v(mTag, "Laying out " + host + " to (" +
                        host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
            }
    
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
            try {
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
               ....
        }
    

    同样调用了DecorView的layout,也是父类的layout,然后调用onLayout,在DecorView的onLayout中,先调用了FrameLayout的onLayout,FrameLayout中调用了layoutChildren来对每个子view进行布局,也是调用子view的layout方法。

        private void performDraw() {
            ...
            draw(fullRedrawNeeded);
            ...
        }
        private void draw(boolean fullRedrawNeeded) {
            Surface surface = mSurface;
            ....
            drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
            ....
         }
        private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                boolean scalingRequired, Rect dirty) {
    
            // Draw with software renderer.
            final Canvas canvas;
            try {
                ....
    
                canvas = mSurface.lockCanvas(dirty);
    
                ....
    
                    mView.draw(canvas);
    
                   ....
        }
    
    

    performDraw最后调用到了drawSoftware,先获取了canvas,然后执行了DecorView的draw,具体流程和上面两点类似。

    到这里就把DecorView绘制出来了,setView中在执行完requestLayout之后,又调用了addToDisplay从而把整个视图显示出来。。

    最后总结一下,一个Activity的视图界面,在setContentView时被准备出来,但是还没有绘制显示(界面的测量布局绘制都没有执行),一直到生命周期中onResume中也没有执行,而是在ActivityThread的handleResumeActivity中,回调完onResume后,在wm.addView时才做了测量布局绘制,所以我们在onCreate或者onResume都拿不到控件的某些信息,如getWidth。若要解决这个问题,可以添加一个回调,如view.getViewTreeObserver().addOnGlobalLayoutListener。这个回调在performTraversals中调起,他的位置在performMeasure及performLayout之后,在performDraw之前(见上文代码片段),所以能获得正常的数据。

    文末再来说一下文中遗留的问题,就是子线程为什么不能更新UI。

    首先我们如果在子线程中更新UI,直观的就会有这样错误:



    其实到这里需要纠正一点,出现这样错误并不能说子线程不能更新UI,或者UI界面只能在主线程内更新,好像主线程比子线程神圣很多似的。正如错误信息中所说的哪那样,只能在视图创建的那个线程中更新UI。

    首先先看一下这个错误的来历,在ViewRootImpl的checkThread方法中,也就是requestLayout中执行的第一个方法:

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

    我们说他检测了线程,就是检测当前线程是不是创建该视图的线程。mThread 在构造中被初始化,赋值为构造函数执行时的那个线程。为什么更新UI时会报错呢?一般更新一个view时会调用invalidate方法,我们看一下流程:

        public void invalidate() {
            invalidate(true);
        }
        public void invalidate(boolean invalidateCache) {
            invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
        }
        void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                boolean fullInvalidate) {
            ...
                final AttachInfo ai = mAttachInfo;
                final ViewParent p = mParent;
                if (p != null && ai != null && l < r && t < b) {
                    final Rect damage = ai.mTmpInvalRect;
                    damage.set(l, t, r, b);
                    p.invalidateChild(this, damage);
                }
    
               .........
            }
        }
    

    ViewParent 是一个接口,ViewRootImpl就实现了这个接口,最后会调用到ViewRootImpl的invalidateChild方法:

       @Override
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            checkThread();
             ....
        }
    

    可见这个方法第一步就在检查线程。至于为什么要原始线程呢,还是和线程安全相关的.

    为了打消大家的疑虑,我来给大家做一个关于在子线程创建视图,而不能在主线程更新的例子:

        public void click(View view) {
            System.out.println(bt.getWidth());
            if (f){
                f = false;
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        Looper.prepare();
                        level = new Level(MainActivity.this);
                        WindowManager mWindowManager = (WindowManager) getApplicationContext()
                                .getSystemService(Context.WINDOW_SERVICE);
    
                        View mView = level;
                        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
                        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
                        int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    
                        params.flags = flags;
                        params.format = PixelFormat.TRANSLUCENT;
                        params.width = WindowManager.LayoutParams.MATCH_PARENT;
                        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
                        params.gravity = Gravity.TOP;
    
                        mWindowManager.addView(mView, params);
                        level.setXY(10,10);
                        Looper.loop();
                    }
                }.start();
            }else{
                level.setXY(20,20);
            }
        }
    
        public void setXY(int x,int y){
            rx = x;
            ry = y;
            invalidate();
        }
    

    就是创建一个悬浮窗(目前能想到的在子线程创建UI的只有这个,activity界面都是在主线程创建的,我们干涉不了),第一次执行click,启动一个子线程创建一个悬浮窗,第二字执行click是调用level.setXY(20,20);,这个肯定是在主线程,但是报错了,还是一样的错误,就是只能在原始线程更新。同样在子线程中,我也调用了setXY,是没有问题的。

    这个例子大家可以自己尝试验证,level类是我项目中一个自定义view,大家尝试时随意一个view都可以。

    同样大家肯定都做过在子线程弹出Toast,也进一步验证了子线程也是可以执行UI操作的,只不过要一定条件。另外如果一定要在子线程更新,可以调用postInvalidate,可以看一下他的实现还有和Invalidate的区别。

        public void postInvalidate() {
            postInvalidateDelayed(0);
        }
        public void postInvalidateDelayed(long delayMilliseconds) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
            }
        }
    

    这里最后调用了attachInfo.mViewRootImpl.dispatchInvalidateDelayed。attachInfo是View内部类AttachInfo的一个对象,通过他将view和ViewRootImpl进行了绑定,关于它的绑定我们下面再说,先看dispatchInvalidateDelayed:

        public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
            Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
            mHandler.sendMessageDelayed(msg, delayMilliseconds);
        }
        
            public void handleMessage(Message msg) {
                switch (msg.what) {
                case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                    break;
                  .....
            }
    

    看到了吧,同Handler机制,进行了更新,mHandler随着ViewRootImpl创建,所以是一个线程,间接在一个线程调用view的invalidate,所以没问题。

    最后说一下ViewRootImpl和View的绑定。可见View中有很多方法都直接用到了mViewRootImpl,那么在哪里绑定的呢?还是看AttachInfo这个View的内部类,他在ViewRootImpl的构造中初始化:

        public ViewRootImpl(Context context, Display display) {
            ....
            mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                    context);
            ...
        }
    
    

    这里AttachInfo持有了ViewRootImpl对象,随后在setView中设置了根viewmAttachInfo.mRootView = view;,也就是DecorView。到这里viewmAttachInfo持有了ViewRootImpl和根view。然后在performTraversals方法中,调用了

    host.dispatchAttachedToWindow(mAttachInfo, 0);
    
    
        void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            mAttachInfo = info;
            ....
        }
    

    就这样DecorView获得了AttachInfo 对象,这样就可以获得ViewRootImpl

        public ViewRootImpl getViewRootImpl() {
            if (mAttachInfo != null) {
                return mAttachInfo.mViewRootImpl;
            }
            return null;
        }
    

    但具体某个view是如何和ViewRootImpl绑定的呢?不要忘了DecorView的所有孩子都是addView进去的,看一下addview的实现:

        public void addView(View child) {
            addView(child, -1);
        }
        public void addView(View child, int index) {
            ...
            addView(child, index, params);
        }
        public void addView(View child, int index, LayoutParams params) {
            ...
            addViewInner(child, index, params, false);
        }
        private void addViewInner(View child, int index, LayoutParams params,
                boolean preventRequestLayout) {
    
           ...
    
            AttachInfo ai = mAttachInfo;
            if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
                boolean lastKeepOn = ai.mKeepScreenOn;
                ai.mKeepScreenOn = false;
                child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
                ...
            }
        }
    
    

    看到了吧,子view把父view的mAttachInfo绑定到自己身上的。

    相关文章

      网友评论

          本文标题:Activty视图加载流程浅析

          本文链接:https://www.haomeiwen.com/subject/elesjftx.html