Activity展示流程源码阅读

前言

前面查看了Activity启动的整体流程,现在来看一看Activity里定义的视图树是如何展示到手机屏幕上的。首先开发者通常都会在onCreate里定义setContentView(布局文件ID),再运行Activity就能够将布局文件中的视图展示出来,在底层实际做做展示以及与用户交互都是有WindowManagerService(简称WMS)服务来进行的,与WMS做交互的主要是Window对象,为了能够清楚它们之间如何交互我们需要从创建Activity后调用attach绑定Activity和Window对象的代码看起。

代码分析

查看Activity的attach方法,它主要负责将Activity和Window对象关联起来,这里需要特别注意的是WindowManager对象的初始化过程。我们知道WindowManager实际上是个接口,实现它的是WindowManagerImpl类,这个类也只是客户端与WMS通信的代理对象,真正的通信过程实际上要使用WindowManagerGlobal单例对象来实现。

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    // 创建PhoneWindow对象
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    // ActivityInfo中保存了从AndroidManifest文件中解析出来的信息,设置软键盘模式
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }
    if (info.uiOptions != 0) {
        mWindow.setUiOptions(info.uiOptions);
    }
    // 设置其他成员属性

    // 为Window对象初始化WindowManager对象
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    mCurrentConfig = config;

    mWindow.setColorMode(info.colorMode);
}

查看PhoneWindow的setWindowManager方法发现它会为每个Activity创建一个WindowManagerImpl对象,但是在每个WindowManagerImpl对象里都会使用单一的WindowManagerGlobal对象来实现和WMS通信。为什么要做这样的设计呢,其实主要是因为和PhoneWindow绑定的WindowManager还需要专门记录下它所维护的Window的父Window,这里就多加了一个中间层。

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);
}

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;

    private IBinder mDefaultToken;

    public WindowManagerImpl(Context context) {
        this(context, null);
    }

    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

    public WindowManagerImpl createPresentationWindowManager(Context displayContext) {
        return new WindowManagerImpl(displayContext, mParentWindow);
    }
    ......
}

和Activity向关联的Window对象已经初始化完毕,这时后就会调用Activity.onCreate方法,开发者会调用setContentView来添加自己的界面布局。查看setContentView的实现源码发现实际上是调用了PhoneWindow的setContentView方法,这里首先判断mContentParent是否为空,onCreate的时候一般mContentParent是空的,就会调用installDecor()方法向PhoneWindow中添加DecorView根部局。

// Activity源码
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

// PhoneWindow源码
@Override
public void setContentView(int layoutResID) {

    if (mContentParent == null) {
        installDecor();
    } 

    // 判断各种feature
}

在installDecor()方法中会首先创建DecorView根部局,它是继承自FrameLayout的一个布局对象,通常都只是作为Activity的根部局来使用。需要注意的是从Android7.0开始如果设置了mUseDecorContext传递给DecorView的context不再直接使用Activity对象了,可以看到实际上是一个DecorContext对象,如果之后直接使用View.getContext()获得的并不是Activity对象,强转会出现ClassCastException异常。

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    ...
}


protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            // 这里需要注意使用了DecorContext
            context = new DecorContext(applicationContext, getContext().getResources());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

生成完根部局DecorView之后就需要开始生成内部的包含有ActionBar和一个id为android.R.id.content的FrameLayout内容布局对象,我们在Activity中设置的布局资源就会被加载放到这个内容布局中,执行完这些代码之后Activity的所有布局就都已经准备完成,需要通知WMS进行展示操作。

在Activity启动源码分析中我们知道在onCreate方法调用之后会继续执行ActivityThread.handleResumeActivity,从源代码中可以看到在需要展示布局的时候会调用WindowManager.addView(view),通过前面的分析我们知道这个WindowManager就是WindowManageImpl类型的对象。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    .........
    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);
        }
    }
    .......
}

在WindowManagerImpl的addView方法中会直接调用WindowManagerGlobal的addView方法,在这里获取Display对象和WindowManagerImpl保存的父Window对象并且初始化ViewRootImpl对象,将它们的引用都保存到WindowManagerGlobal的缓存中,为以后的用户交互动作方便查找对应的界面。

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

// WindowManagerGlobal.java
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);

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

最后一步的ViewRootImpl的setView方法就是最重要的一步,我们看到最开始它会初始化一个mAttachInfo的属性,对View比较了解的读者应该对这个属性比较熟悉,之后它还会发送一个requestLayout请求,最后在调用mWindowSession.addToDisplay()真正的把Window里的DecorView添加到WMS当中,后面的代码会对addToDisplay操作的结果进行判定,可见这个跨进程通信是同步执行的。了解了整个过程之后我们在来查看requestLayout里的详细实现逻辑。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
    ......
    mAttachInfo.mRootView = view;
    mAttachInfo.mScalingRequired = mTranslator != null;
    mAttachInfo.mApplicationScale =
            mTranslator == null ? 1.0f : mTranslator.applicationScale;
    if (panelParentView != null) {
        mAttachInfo.mPanelParentWindowToken
                = panelParentView.getApplicationWindowToken();
    }
    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();

    #########################################################################################


    try {
        mOrigWindowType = mWindowAttributes.type;
        mAttachInfo.mRecomputeGlobalAttributes = true;
        collectViewAttributes();
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                getHostVisibility(), mDisplay.getDisplayId(),
                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                mAttachInfo.mOutsets, mInputChannel);
    } catch (RemoteException e) {
        mAdded = false;
        mView = null;
        mAttachInfo.mRootView = null;
        mInputChannel = null;
        mFallbackEventHandler.setView(null);
        unscheduleTraversals();
        setAccessibilityFocus(null, null);
        throw new RuntimeException("Adding window failed", e);
    } finally {
        if (restore) {
            attrs.restore();
        }
    }

    // 判断
    if (res < WindowManagerGlobal.ADD_OKAY) {
        mAttachInfo.mRootView = null;
        mAdded = false;
        mFallbackEventHandler.setView(null);
        unscheduleTraversals();
        setAccessibilityFocus(null, null);
        switch (res) {
            case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
            case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not valid; is your activity running?");
            case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not for an application");
    }
    ......
}

在requestLayout中首先会判定当前的线程是否是主线程,如果不是会直接抛出CalledFromWrongThreadException异常,如果在子线程中改变View的展示属性就会抛出这个异常。后面调用scheduleTraversals方法并且使用mChoreographer提交了一个TraversalRunnable对象。其中mChoreographer对象主要负责根据VSync信号或者系统帧率自动刷新屏幕功能,这里使用的是postCallback方法也就是说会立即执行doTraversal不会放到队列中延迟执行。

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

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

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

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal方法中会直接调用performTraversals方法,performTraversals方法在第一次被调用使会执行dispatchAttachedToWindow也就是将mAttachInfo设置到DecorView里面的所有子View中,后面executeActions就是执行在View.postXXX方法提交的回调。比如想要获取某个View的宽高,直接在onCreate方法中是无法获取的因为这时候还没有测量,可以使用view.postXXX回调里能够拿到宽高,需要注意的是这些回调是放到Handler里执行的,虽然它们在performMeasure之前其实真正的执行是performTraversals方法结束之后。后面的performMeasure是从DecorView根View开始递归测量视图树中的所有View的宽高,performLayout则是从DecorView开始递归的将View放到指定的位置,最后面的performDraw方法会调用View的draw方法将所有的View内容画到屏幕上,这样整个Activity就展示出来了。

private void performTraversals() {
    // mView也就是setView传进来的DecorView
    final View host = mView;
    if (mFirst) {
        // 如果是第一次执行performTraversals
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    }

    .....
    getRunQueue().executeActions(mAttachInfo.mHandler);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    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);
    }
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ....
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());   
    ....
}

前面还提到了WindowSession和WMS进程间通信,这个其实比较底层了,对于Android应用开发者而言了解到performTraversals方法的逻辑就足够了。

总结

Activity内部包含了PhoneWindow对象,PhoneWindow对象管理者DecorView对象,DecorView内部包含了用户自定义的所有展示布局;WindowManagerGlobal管理这整个应用中所有界面的展示,它通过ViewRootImpl实现和系统WMS服务的交互功能,ViewRootImpl只负责一个Window的交互操作;ViewRootImpl通过调用requestLayout方法实现DecorView整棵视图树的测量、布局和绘制工作,使用WindowSession对象将DecorView添加到WMS服务,WMS服务负责将DecorView的内容展示到手机屏幕上。

猜你喜欢

转载自blog.csdn.net/xingzhong128/article/details/81392479