Activity布局繪制流程源碼解析

Activity布局加載流程源碼解析一文中,我們分析了Activity布局加載流程,通過(guò)分析我們了解到Activity通過(guò)Window來(lái)控制界面的展示,一個(gè)Activity包含一個(gè)Window對(duì)象(具體由PhoneWindow來(lái)實(shí)現(xiàn)),并且PhoneWindow將DecorView作為整個(gè)應(yīng)用窗口的根View锡搜。當(dāng)時(shí),我們?cè)诜治龅臅r(shí)候遺留了一個(gè)問(wèn)題瞧掺,那就是DecorView是什么時(shí)候添加到Window上去的?那么凡傅,這篇文章就來(lái)具體介紹一下Activity布局繪制流程的源碼分析辟狈,而且源碼版本基于Android 8.0。

一夏跷、從ActivityThread的handleResumeActivity方法說(shuō)起

在Activity的onCreate()方法中哼转,我們通過(guò)setContentView()方法完成了xml布局解析以及DecorView的創(chuàng)建。之后槽华,DecorView會(huì)被添加到Window上去壹蔓,這個(gè)過(guò)程的起始點(diǎn)在ActivityThread的handleResumeActivity方法中。

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
            return;
        }

        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            final Activity a = r.activity;
            ... ...
            // 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.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ... ...

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

該方法的實(shí)現(xiàn)代碼比較多猫态,我們只關(guān)注重點(diǎn)部分佣蓉。由源碼可知,在獲取了Activity的Window相關(guān)參數(shù)之后亲雪,執(zhí)行了r.activity.makeVisible()方法勇凭,即接下來(lái)回去執(zhí)行Activity的makeVisible()方法,這個(gè)方法就是DecorView被添加到Window上去的起點(diǎn)义辕。

二虾标、Activity中的makeVisible方法

makeVisible字面上的意思是讓它變?yōu)榭梢?jiàn),我們看一下它的具體實(shí)現(xiàn):

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

由源碼可知灌砖,這里的mWindowAdded是一個(gè)布爾類(lèi)型的成員變量璧函,很明顯這里的主要邏輯if分支只會(huì)執(zhí)行一次,因?yàn)閳?zhí)行過(guò)后mWindowAdded就會(huì)被賦值為true基显,之后便再也不會(huì)執(zhí)行if分支代碼蘸吓。緊接著,通過(guò)getWindowManager()方法獲取ViewManager對(duì)象撩幽,我們看一下具體實(shí)現(xiàn)美澳。

    // Activity # getWindowManager()
    /** Retrieve the window manager for showing custom windows. */
    public WindowManager getWindowManager() {
        return mWindowManager;
    }

從方法的注釋來(lái)看是返回一個(gè)Window管理器,并且是以成員變量的形式返回。那么制跟,很自然我們就要去尋找它是在哪邊被賦值的舅桩,通過(guò)代碼檢索,我們很容易發(fā)現(xiàn)它是在Activity的attch()方法中被賦值的雨膨。

    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*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ... ...
        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);
    }

這邊我稍微精簡(jiǎn)了一下代碼擂涛,我們可以看到mWindowManager是通過(guò)PhoneWindow的getWindowManager()方法返回的。但是聊记,通過(guò)查看源碼撒妈,我們可以發(fā)現(xiàn)PhoneWindow繼承于Window,而且沒(méi)有重寫(xiě)getWindowManager()方法排监,所以最終還是會(huì)調(diào)用到Window的getWindowManager()方法狰右。

    // Window # getWindowManager()
    /**
     * Return the window manager allowing this Window to display its own
     * windows.
     *
     * @return WindowManager The ViewManager.
     */
    public WindowManager getWindowManager() {
        return mWindowManager;
    }

好吧,Window的getWindowManager()方法同樣是返回它的成員變量舆床,所以我們依舊需要去找它是在哪邊被賦值的棋蚌。這個(gè)還是比較好找的,最終我們可以發(fā)現(xiàn)它是在setWindowManager()方法中被賦值的挨队。

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

由源碼可知谷暮,mWindowManager最終是通過(guò)WindowManagerImpl的createLocalWindowManager()方法創(chuàng)建的。

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

可以看到盛垦,這個(gè)方法很簡(jiǎn)單湿弦,就是通過(guò)new的方式創(chuàng)建了一個(gè)WindowManagerImpl對(duì)象并返回。兜了這么一大圈腾夯,我們終于知道Activity的makeVisible()方法中g(shù)etWindowManager()返回的其實(shí)就是一個(gè)WindowManagerImpl對(duì)象颊埃。剛好,我們順便理一下繼承關(guān)系蝶俱,就是WindowManage繼承自ViewManager竟秫,而WindowManagerImpl又繼承自WindowManage。

接下來(lái)跷乐,我們繼續(xù)分析Activity的makeVisible()方法肥败,在通過(guò)getWindowManager()方法獲取到WindowManagerImpl對(duì)象之后,接著執(zhí)行了如下方法:

wm.addView(mDecor, getWindow().getAttributes());

通過(guò)以上分析愕提,我們很容易得知馒稍,這其實(shí)就是去調(diào)用WindowManagerImpl的addView方法,并且把DecorView作為入?yún)ⅰ?/p>

    // WindowManagerImpl # addView()
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

由源碼可知浅侨,mGlobal是一個(gè)WindowManagerGlobal的單例對(duì)象纽谒,是Window處理的工具類(lèi),用于操作View組件如输。我們具體看一下mGlobal的addView()方法的實(shí)現(xiàn):

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ... ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        ... ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ... ...
            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;
            }
        }
    }

由于代碼實(shí)現(xiàn)較長(zhǎng)鼓黔,稍微做了一下精簡(jiǎn)央勒,我們只關(guān)注重點(diǎn)部分。首先說(shuō)一下上述代碼中的3個(gè)ArrayList澳化,其中mViews主要用于保存Activity的DecorView(Activity的根View)崔步,mRoots主要用于保存ViewRootImpl,mParams主要用于保存Window的LayoutParams缎谷。最后井濒,調(diào)用了ViewRootImpl的setView()方法,這個(gè)方法就很關(guān)鍵了列林,將在下面進(jìn)行具體分析瑞你。

三、ViewRootImpl的setView()方法

在ViewRootImpl的setView()方法希痴,會(huì)完成對(duì)View的測(cè)量者甲、布局和繪制。并且砌创,通過(guò)Binder跨進(jìn)程的方式向WMS(WindowManagerService)發(fā)起一個(gè)調(diào)用虏缸,從而將DecorView最終添加到Window上。在這個(gè)過(guò)程中纺铭,ViewRootImpl、DecorView和WMS會(huì)彼此向相互關(guān)聯(lián)刀疙。

我們先來(lái)看一下View的測(cè)量舶赔、布局和繪制過(guò)程,在ViewRootImpl的setView()方法中谦秧,會(huì)調(diào)用requestLayout()方法竟纳。

    // ViewRootImpl # setView()
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ...
        requestLayout();
        ...
    }

下面我們看下requestLayout()方法的具體實(shí)現(xiàn):

    @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.");
        }
    }

我們可以看到,這里首先回去調(diào)用checkThread()方法疚鲤,主要檢查當(dāng)前線程是否為UI線程锥累,若當(dāng)前線程非UI線程,則拋出非UI線程更新UI的錯(cuò)誤集歇。其中桶略,mThread成員變量是在ViewRootImpl的構(gòu)造方法中被賦值的,而ViewRootImpl的構(gòu)造方法又是在UI線程中被調(diào)用的诲宇,所以mThread自然也就被賦值為UI線程际歼。在完成UI線程檢查之后,接著會(huì)去調(diào)用scheduleTraversals()方法姑蓝,我們看下它的具體實(shí)現(xiàn):

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

由源碼可知鹅心,mChoreographer.postCallback,內(nèi)部會(huì)調(diào)用一個(gè)異步消息纺荧,用于執(zhí)行mTraversalRunnable的run()方法旭愧,我們來(lái)具體看一下mTraversalRunnable的具體定義:

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

很明顯颅筋,最后后調(diào)用到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;
            }
        }
    }

我們可以看到议泵,doTraversal()方法內(nèi)部又會(huì)去調(diào)用performTraversals()方法,其實(shí)這個(gè)方法就是整個(gè)View的繪制流程的起始方法用押,我們一起來(lái)看下肢簿,這個(gè)方法非常非常的長(zhǎng),我們只關(guān)注重點(diǎn)部分的代碼蜻拨。

    private void performTraversals() {
        ... ...
        // View測(cè)量
            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(TAG,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        }
        ... ...
        // View布局
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);

            // By this point all views have been sized and positioned
            // We can compute the transparent area

            if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
                // start out transparent
                // TODO: AVOID THAT CALL BY CACHING THE RESULT?
                host.getLocationInWindow(mTmpLocation);
                mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                        mTmpLocation[0] + host.mRight - host.mLeft,
                        mTmpLocation[1] + host.mBottom - host.mTop);

                host.gatherTransparentRegion(mTransparentRegion);
                if (mTranslator != null) {
                    mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
                }

                if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                    mPreviousTransparentRegion.set(mTransparentRegion);
                    mFullRedrawNeeded = true;
                    // reconfigure window manager
                    try {
                        mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
                    } catch (RemoteException e) {
                    }
                }
            }
        }
        ... ...
        // View繪制
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }

                performDraw();
            }
        } else {
            if (viewVisibility == View.VISIBLE) {
                // Try again
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }

        mIsInTraversal = false;
    }

由源碼可知池充,在performTraversals()方法中,主要是依次調(diào)用了performMeasure()缎讼,performLayout()收夸,performDraw()這三個(gè)方法,用于完成View的測(cè)量血崭、布局和繪制卧惜。

View的測(cè)量

我們一起先來(lái)看下performMeasure()方法的具體實(shí)現(xiàn)。

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

我們看到夹纫,在該方法中調(diào)用了mView的measure()方法咽瓷,通過(guò)源碼追蹤我們可以很容易發(fā)現(xiàn)這個(gè)成員變量mView其實(shí)就是傳遞進(jìn)來(lái)的DecorView對(duì)象,所以最終調(diào)用的是DecorView的measure()方法舰讹,我們具體看下茅姜。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            ... ...
        if (forceLayout || needsLayout) {
            ... ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        ... ...
    }

我們可以看到,measure方法主要是做一些判斷月匣,如果View需要測(cè)量就會(huì)去調(diào)用它的onMeasure()方法钻洒。下面,我們就來(lái)看一下DecorView的onMeasure()方法锄开。

    // DecorView # onMeasure()
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = getMode(widthMeasureSpec);
        final int heightMode = getMode(heightMeasureSpec);
        ... ...
        boolean measure = false;
        ... ...
        if (!fixedWidth && widthMode == AT_MOST) {
            ... ...
                if (width < min) {
                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
                    measure = true;
                }
            }
        }

        if (measure) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

我們可以看到素标,DecorView的onMeasure()方法主要是用來(lái)判斷DecorView需不需要測(cè)量,如果需要測(cè)量萍悴,則調(diào)用其父類(lèi)FrameLayout的onMeasure()方法头遭。

    // FrameLayout # onMeasure()
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        ... ...
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                final int childWidthMeasureSpec;
                ... ...
                final int childHeightMeasureSpec;
                ... ...
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

由源碼可知,這里主要通過(guò)for循環(huán)癣诱,獲取該View的所有子View任岸,并執(zhí)行所有子View的measure方法,如果子View是ViewGroup就會(huì)調(diào)用ViewGroup的onMeasure()方法狡刘,若果子View是View就會(huì)調(diào)用View的onMeasure()方法享潜。因此,我們還需要看一下View的onMeasure()方法嗅蔬。

    // View # onMeasure()
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

這樣剑按,通過(guò)一系列的方法調(diào)用就可以把View即其子View的大小測(cè)量出來(lái)了疾就,并且保存在了成員變量mMeasuredWith和mMeasuredHeight中。

View的布局

回到performTraversals()方法中艺蝴,我們繼續(xù)看performLayout()方法猬腰。

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ... ...
        final View host = mView;
        if (host == null) {
            return;
        }
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            ... ...
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

我們可以看到,該方法中主要調(diào)用了host的layout()方法猜敢,而host又是通過(guò)mView(DecorView)賦值的姑荷,所以就是去調(diào)用DecorView的layout()方法。通過(guò)源碼可以發(fā)現(xiàn)最終調(diào)用的是ViewGroup的layout()方法缩擂。

    // ViewGroup # layout()
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

好吧鼠冕,這里又調(diào)用了其父類(lèi)的layout()方法,所以又會(huì)去調(diào)用View的layout()方法胯盯。

    public void layout(int l, int t, int r, int b) {
        ... ...
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ... ...
        }
    }

我們可以看到懈费,layout()方法主要還是做一些判斷,如果View有變化需要重新布局博脑,則會(huì)去調(diào)用它的onLayout()方法憎乙。所以,接下來(lái)我們需要看一下DecorView的onLayout()方法叉趣。

    // DecorView # onLayout()
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        getOutsets(mOutsets);
        if (mOutsets.left > 0) {
            offsetLeftAndRight(-mOutsets.left);
        }
        if (mOutsets.top > 0) {
            offsetTopAndBottom(-mOutsets.top);
        }
        if (mApplyFloatingVerticalInsets) {
            offsetTopAndBottom(mFloatingInsets.top);
        }
        if (mApplyFloatingHorizontalInsets) {
            offsetLeftAndRight(mFloatingInsets.left);
        }

        // If the application changed its SystemUI metrics, we might also have to adapt
        // our shadow elevation.
        updateElevation();
        mAllowUpdateElevation = true;

        if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
            getViewRootImpl().requestInvalidateRootRenderNode();
        }
    }

我們看到這個(gè)方法中又會(huì)去調(diào)用其父類(lèi)Framelayout的onLayout()方法泞边。

    // Framelayout # onLayout()
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                ... ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

Framelayout的onLayout()方法又會(huì)去調(diào)用layoutChildren()方法,具體代碼上面已經(jīng)貼出來(lái)了疗杉。與measure類(lèi)似阵谚,這里也是通過(guò)for循環(huán)獲取該View的所有子View,如果子View是ViewGroup則執(zhí)行ViewGroup的layout()方法乡数,如果子View是View則執(zhí)行View的layout()方法椭蹄。
這樣闻牡,通過(guò)一系列的方法調(diào)用就可以把View及其子View的位置信息計(jì)算出來(lái)并保存在成員變量中净赴。

View的繪制

好了,我們?cè)俅位氐絧erformTraversals()方法中罩润,繼續(xù)看performDraw()方法玖翅。

    private void performDraw() {
        ... ...
        draw(fullRedrawNeeded);
        ... ...
    }
    private void draw(boolean fullRedrawNeeded) {
            ... ...
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
               return;
            }
            ... ...
    }
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        ... ...
        mView.draw(canvas);
        ... ...
        return true;
    }

對(duì)源碼做了大量的精簡(jiǎn),我們看到最后會(huì)輾轉(zhuǎn)調(diào)用到mView的draw()方法割以,也就是調(diào)用DecorView的draw()方法金度。

    // DecorView # draw()
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        if (mMenuBackground != null) {
            mMenuBackground.draw(canvas);
        }
    }

DecorView的draw()方法又會(huì)去調(diào)用其父類(lèi)FrameLayout的draw()方法,最終又會(huì)調(diào)用到View的draw()方法严沥,一起來(lái)看下猜极。

    // View # draw()
    @CallSuper
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }
    }

我們可以看到,View的整個(gè)繪制流程還是比較清楚的消玄,整個(gè)執(zhí)行邏輯一共大概需要六步跟伏,并且在執(zhí)行draw()方法的過(guò)程中丢胚,如果包含子View,那么也會(huì)執(zhí)行子View的draw()方法受扳。這樣携龟,經(jīng)過(guò)一系列的方法調(diào)用之后,DectorView及其子View就被繪制出來(lái)了勘高。
至此峡蟋,ViewRootImpl的setView()方法中的requestLayout()部分的分析差不多已經(jīng)結(jié)束了。

四华望、DecorView添加到Window上

我們回到ViewRootImpl的setView()方法中蕊蝗,requestLayout()方法調(diào)用完畢之后,View的測(cè)量立美、布局和繪制已經(jīng)完成了匿又,下面就是要把DecorView真正添加到Window上去了。

    // ViewRootImpl # setView()
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ...
        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();
            }
        }
        ...
    }

我們可以看到建蹄,通過(guò)mWindowSession的addToDisplay()方法調(diào)用碌更,將DecorView添加到Window上去。我們先看一下mWindowSession是如何賦值的洞慎。

    mWindowSession = WindowManagerGlobal.getWindowSession();
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager imm = InputMethodManager.getInstance();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            },
                            imm.getClient(), imm.getInputContext());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }
    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
                try {
                    if (sWindowManagerService != null) {
                        ValueAnimator.setDurationScale(
                                sWindowManagerService.getCurrentAnimatorScale());
                    }
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowManagerService;
        }
    }

熟悉Binder跨進(jìn)程通信的同學(xué)應(yīng)該知道痛单,最后應(yīng)該是通過(guò)WindowManagerService的openSession()方法獲取到了mWindowSession對(duì)象。

    @Override
    public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
            IInputContext inputContext) {
        if (client == null) throw new IllegalArgumentException("null client");
        if (inputContext == null) throw new IllegalArgumentException("null inputContext");
        Session session = new Session(this, callback, client, inputContext);
        return session;
    }

我們看到openSession()方法通過(guò)new的方式返回了一個(gè)IWindowSession類(lèi)型的對(duì)象劲腿。
回到ViewRootImpl的setView()方法旭绒,我們看到它調(diào)用了mWindowSession的addToDisplay()方法,這里也就是調(diào)用了Session的addToDisplay()方法焦人。

    // Session # addToDisplay()
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

由源碼可知挥吵,這里的成員變量mService是WindowManagerService對(duì)象,所以最終會(huì)去調(diào)用WindowManagerService的addWindow()方法花椭。因此忽匈,最終是由WindowManagerService來(lái)完成將DecorView添加到Window上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末矿辽,一起剝皮案震驚了整個(gè)濱河市丹允,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袋倔,老刑警劉巖雕蔽,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異宾娜,居然都是意外死亡批狐,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)前塔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嚣艇,“玉大人缘眶,你說(shuō)我怎么就攤上這事∷璺希” “怎么了巷懈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)慌洪。 經(jīng)常有香客問(wèn)我顶燕,道長(zhǎng),這世上最難降的妖魔是什么冈爹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任涌攻,我火速辦了婚禮,結(jié)果婚禮上频伤,老公的妹妹穿的比我還像新娘恳谎。我一直安慰自己,他們只是感情好憋肖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布因痛。 她就那樣靜靜地躺著,像睡著了一般岸更。 火紅的嫁衣襯著肌膚如雪鸵膏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天怎炊,我揣著相機(jī)與錄音谭企,去河邊找鬼。 笑死评肆,一個(gè)胖子當(dāng)著我的面吹牛债查,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瓜挽,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盹廷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了秸抚?” 一聲冷哼從身側(cè)響起速和,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤歹垫,失蹤者是張志新(化名)和其女友劉穎剥汤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體排惨,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吭敢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暮芭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹿驼。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡欲低,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出畜晰,到底是詐尸還是另有隱情砾莱,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布凄鼻,位于F島的核電站腊瑟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏块蚌。R本人自食惡果不足惜闰非,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望峭范。 院中可真熱鬧财松,春花似錦、人聲如沸纱控。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)甜害。三九已至胚迫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唾那,已是汗流浹背访锻。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闹获,地道東北人期犬。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓党巾,卻偏偏與公主長(zhǎng)得像耻涛,于是被迫代替她去往敵國(guó)和親茉贡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奖磁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 金陵現(xiàn)江蘇南京祟滴,明朝時(shí)期極盡奢華的地方豁辉,蘇州山塘 金陵秦淮河不同凡響的藝術(shù)修養(yǎng)與卑賤的身份混合于一體庶橱,構(gòu)成了封建社...
    酒青梅閱讀 1,753評(píng)論 0 0
  • 總是猜不透夷恍,我在想什么拱雏。 其實(shí)棉安,我一直很茫然。 自己到底在追求什么铸抑。 到現(xiàn)在才明白贡耽。 原來(lái) 。 是空白。 是否是記...
    米小姐Y閱讀 244評(píng)論 0 2
  • 以前每次讀完一本書(shū)或者看完一部電影蒲赂,就會(huì)在豆瓣搜書(shū)評(píng)或影評(píng)阱冶,仿佛看了那些評(píng)論,就覺(jué)得那是我自己對(duì)書(shū)或電影的理解和感...
    薄荷的夢(mèng)想天空閱讀 2,525評(píng)論 1 0