Activity 显示原理

1,841 阅读8分钟

Android中的Activity的显示原理会经历一连串的调用过程,通过了解这个流程,许多平时开发中遇到的问题的真正原理也就迎刃而解了。下面我们就一起探寻一下这显示背后的秘密吧。

直观的来讲,我们实现一个Activity的时候都会在onCreate()方法中调用super.onCreate(),然后紧接着调用一句setContentView(R.layout.activity_XXX)对吧。大家都知道我们调用这一句是将定制的Activity的布局页面xml加载进入Activity中,最终能够呈现。但是,setContentView到底做了什么,才能够最终显示出来呢。这就是我们今天这里想要探索的重点内容。下面我们先提出一些问题:

  1. Activity的显示过程中都经历了哪些重要步骤?
  2. Activity的显示过程都有哪些重量级的类参与其中?
  3. Activity的显示过程中,Window、DecorView、ViewRootImpl是如何各司其职的?

首先,大家都至少有所耳闻,Activity、Window、DecorView之间的大致的位置关系吧,这里贴一张自制的图。

记住这个空间关系,我们就从setContentView()开始吧。

setContentView中直接调用了window对象的setContentView()

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);//直接拿window操作。
        initWindowDecorActionBar();
    }

那么getWindow()做了什么呢?不用纠结了,Activity中持有的变量mWindow,我们只要记住它的初始化赋值在ActivityThread中,而且时机是Activity被attach到Application的时候,将Window的子类PhoneWindow传入的。这个咱们以后在探索Activity启动原理和流程的时候再聊。

追踪到PhoneWindow中,我们看到了setContentView()方法内容。在这个过程中,主要经历了两个步骤:1. 建立DecorView,并得到大名鼎鼎的android.R.id.content对象:mContentParent。2. 通过LayoutInflater实例化R.layout.xxx对象,并关联到一整个以DecorView为根节点的View树结构中

//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,得到mContentParent。
        } 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);//LayoutInflate不用多说了吧,实例化View,关联到View树。
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

那我们先看installDecor()做了什么。我们发现重要的赋值和初始化其实都集中在方法上半部:如果首次执行,则decorView是空,走generateDecor()。自此,Window和DecorView这两大角色历史性的关联就完成了。 然后调用generateLayout(),得到我们大名鼎鼎的contentParent:android.R.id.content对象

//PhoneWindow代码

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);//1. 实例化DecorView
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);//2. 得到contentParent。
			//省略...
            } else {
                //省略...
            }
			//省略...
        }
    }
    
//PhoneWindow代码
protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());//实例化全新的DecorView,并将自身(也就是PhoneWindow)绑定到DecorView中去。
    }
    
    
//PhoneWindow代码
protected ViewGroup generateLayout(DecorView decor) {
		//省略...

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
		//省略...
        mDecor.finishChanging();
        return contentParent;//返回android.R.id.content
    }

好了,到这里我们发现,phoneWindow.setContentView()方法执行完了。那后面咋办呢?什么时候开始渲染啊?什么时候能够展示啊?

别急,我们仔细回忆一下,Activity的生命周期有onCreate、onStart、onResume等几个重要生命周期回调对吧。onStart只能说明Activity处于前台,但并不可见,更不能交互。真正可见且能交互的时候是在onResume回调之后,请注意,这里说的是之后哦!那我们就直接来看onResume()和之后都做了什么吧。去哪里看呢?显然,对于四大组件生命周期的执行几乎都在ActivityThread中调用的。

ActivityThread  代码

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        //省略...
        r = performResumeActivity(token, clearHide, reason);	//1. 调用activity的onResume

        if (r != null) {
            final Activity a = r.activity;
        //省略...
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManager.getService().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);			//2. 将decorView设置为不可见
                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) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);					//3. WindowManagerImpl 添加View。
                    } 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.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                if (r.newConfig != null) {
                    performConfigurationChangedForActivity(r, r.newConfig);
                    
                    r.newConfig = null;
                }
                
                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();				//3.最终令activity内容可见
                }
            }

            if (!r.onlyLocalRequest) {
                r.nextIdle = mNewActivities;
                mNewActivities = r;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            }
            r.onlyLocalRequest = false;

            // Tell the activity manager we have resumed.
            if (reallyResume) {
                try {
                    ActivityManager.getService().activityResumed(token);
                } catch (RemoteException ex) {
                    throw ex.rethrowFromSystemServer();
                }
            }

        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try {
                ActivityManager.getService()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

结论很明显了吧,handleResumeActivity阶段,1. performResumeActivity(),执行对应Activity的onResume()方法。2. decor.setVisible(INVISIBLE),将DecorView设置为不可见状态。3. wm.addView(decor, l),将通过WindowManager(WindowManagerImpl),将DecorView与ViewRootImpl进行关联绑定。这一步就是将DecorView纳入到了ViewRootImpl的管控当中了。 4. r.activity.makeVisible(); ,最后调用activity的makeVisible(),调用mDecor.setVisibility(View.VISIBLE);,得以显示。

//WindowManagerGlobal代码
//3. wm.addView(decor, l),实例化ViewRootImpl,使其与DecorView关联,并记录于WindowManagerGlobal。
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
		//省略...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
		//省略...
            root = new ViewRootImpl(view.getContext(), display);//3.1 进行了ViewRootImpl的实例化

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);//3.2. 将DecorView与ViewRootImpl绑定。
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

走到这一步之后,ViewRootImpl.setView()就会通过调用requestLayout()方法触发View从根到叶的绘制操作了。

//ViewRootImpl 代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
				//省略。。。
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // 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();//3.2.1 触发View的绘制流程
                //省略。。。
            }
        }
    }

好了,走到这里,基于Activity的显示流程就完整了。我们来回顾一下:

  1. Activity的显示流程起始于setContentView(),调用的是PhoneWindow的setContentView(),建立DecorView实例并将layout资源文件实例化并绑定到decorview中。
  2. 经过AMS(ActivityManagerService)调度,流程来到ActivityThread中的handleResumeActivity(),调用onResume(),然后设置DecorView为不可见INVISIBLE,并通过WindowManagerGlobal.addView(decor,l),实例化ViewRootImpl,关联DecorView,建立了WindowManagerGlobal对ViewRootImpl的管控。最后,调用activity.makeVisible(),令DecorView为可见状态。

最后,我们能够回答最开始的问题了:

  1. Activity的显示过程中都经历了哪些重要步骤? Activity.setContentView()->PhoneWindow.setContentView(){1. 实例化DecorView 2. 得到mParentContent 3. 实例化layout资源文件,关联到DecorView树中 } ActivityThread.handleResumeActivity() {1. Activity.onResume() 2. DecorView设置不可见 3. WindowManagerGlobal.setView()实例化ViewRootImpl,关联ViewRootImpl与DecorView,调用ViewRootImpl启动绘制流程。 4. 调用Activity.makeVisible()令DecorView可见。}

  2. Activity的显示过程都有哪些重量级的类参与其中? 按照基本出场顺序:Activity、PhoneWindow、DecorView、ActivityThread、WindowManagerGlobal、ViewRootImpl。如果在往后续流程说,还会涉及到Choreographer等等。

  3. Activity的显示过程中,Window、DecorView、ViewRootImpl是如何各司其职的? 这些就不用多说了,上面的流程分析已经说得比较明确了。

最后的最后,我们开始说道,了解了以上的流程信息,平时开发中遇到的疑难杂症也就迎刃而解了,具体有哪些呢?下面就举几个例子吧:

  1. 为什么在onResume回调中无法获得某个View的measuredHeight和measuredWidth? 因为View的真正启动绘制的流程起始于ViewRootImpl的requestLayout(),这个时机是在ActivityThread调用Activity.onResume()之后。因此,在onResume中的时候,View都还没有开始绘制。所以无法获得宽高。

  2. 深一步想,如果我真的需要在onResume的时机去获取View的宽高,应该如何处理? a. 使用View.post() b. 自定义Handler.post()

  3. 子线程真的不能更新UI吗? 如上述分析可知,我们在ViewRootImpl.requestLayout()方法中,首先调用的就是检查线程checkThread()。若报错,则报错内容为"Only the original thread that created a view hierarchy can touch its views.",原意其实是说,只有创建了View树的线程才能更新对应的View,并没有说只有主线程才能更新View。什么意思呢?其实就是说,通过只有通过ViewRootImpl.setView()绑定View树的线程,才能够更新这些View。所以退一步讲,若ViewRootImpl实例并没有跑在主线程上而是某个子线程thread#1,那么只要调用方也在thread#1,就能够更新该UI。具体看下代码:

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

mThread变量是ViewRootImpl在构造器中被赋值的mThread = Thread.currentThread();。再看检测线程的逻辑,是不是就全明白了?那既然这样,为什么我们一般会说,只有主线程才能更新UI呢?这个说法也没有错,因为几乎我们所有的View都最终会依赖Activity去承载呈现,而Activity伴随着ActivityThread的调度,他们都是直接运行在主线程的,因此在绝对大多数的情况下,我们会说只有主线程才能更新UI这句话也是没错的。