探究 Android View 绘制流程,Activity 的 View 如何展示到屏幕

探究 Android View 绘制流程,Activity 的 View 如何展示到屏幕在上一篇探究Android View 绘制流程,Xml 文件到 View 对象的转换过程我们了解了setContentView(resId) 如何把 xml 文件转换成 Java 中的 View 对象。本篇文章再次基础上继续探究,View 是如何展示到 Activity 上的。…

基于 Android API 26 Platform 源码

写作背景

在上一篇探究Android View 绘制流程,Xml 文件到 View 对象的转换过程我们了解了setContentView(resId) 如何把 xml 文件转换成 Java 中的 View 对象。本篇文章再次基础上继续探究,View 是如何展示到 Activity 上的。

很多 Android 开发者都知道一个事情

当 Activity 执行 onResume() 方法后,代表 Activity 显示到前台

这句话很短,但是背后隐藏了多少方法的调用呢?下面我们将一层一层的剥开源码寻找真相。

onion.jpg

先从 setContentView(resId) 入手

先说明一下,从 Android 的 Launcher 上点击应用的 Icon 的启动过程比较复杂,本人仍在学习。如果想了解如何启动一个 Activity 的过程可以参考Android Launcher 启动 Activity 的工作过程,这里我们只从关注 Activity 中的 View 显示出来。所以直接从 Activity 的一些方法入手。

在 Activity 的 onCreate(savedInstanceState) 中调用 setContentView(resId),而setContentView(resId)则会调用 PhoneWindow.setContentView(layoutResID)

源码并不是太长

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

这里忽略转场动画和一些回调相关的逻辑代码后如下

 if (mContentParent == null) {
     installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
     mContentParent.removeAllViews();
 }
 mLayoutInflater.inflate(layoutResID, mContentParent);
 mContentParent.requestApplyInsets();

其中 mContentParent 是一个 ViewGroup 引用

private ViewGroup mContentParent;

这样开代码比较简单明了

1. 判断 mContentParent 是否为空,如果为空执行 installDecor()
2. 如果 mContentParent 不为空,清除 mContentParent 的所有子 View
3. 把传入的布局文件转换为 View 对象添加到 mContentParent

分析 installDecor()

然后我们再看下 installDecor() ,因为源码比较长,我们分成几个部分解读

第一部分
  if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }

这几行代码最重要的是调用了方法 generateDecor() 其实就是创建一个 DecorView。这里是不是能想到探究Android View 绘制流程,Canvas 的由来中最后的那张图,我们做个类似的截图截个图

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new TextView(getApplicationContext()));
    }

    @Override
    protected void onResume() {
        super.onResume();
    }
}

activity_view_01.png

我们看到一个 Activity 页面最底层的 View 就是我们刚看到的 DecorView

第二部分
if (mContentParent == null) {
    mContentParent = generateLayout(mDecor);
    ……
}

这里看到了对 mContentParent 的赋值操作,调用了 generateLayout(mDecor)

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    TypedArray a = getWindowStyle();
    //设置 Windows Style ,title 、action_bar 、设置键盘弹出方式之类的属性
    //……
    //……
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        ……
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        ……
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        ……
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        ……
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        ……
    } else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;
    //……
    //……
    mDecor.finishChanging();

    return contentParent;
}

这里把 generateLayout(mDecor) 做了很大的简化,大部分都是设置一些窗体属性,软键盘弹出方式之类的东西。我们关心的 View 相关的就以下几行

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;
    mDecor.finishChanging();

layoutResource 是什么呢?我们随便选择一个 R.layout.screen_simple 在 AndroidSdk 中搜到这个文件,内容如下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

这个时候再回到我们刚的那个截图,我们找到了第二层内容 LinearLayout 的来源,这一层LinearLayout包含两个部分

1. id 为 action_mode_bar_stub 的 ViewStub ,用来设置 actionBar 之类的
2. id 为 android.R.id.content 的 FrameLayout。里面会存放我们在 Activity.setContentView(resId) 传入的文件布局

然后再看下最后 mDecor.finishChanging()

public void finishChanging() {
        mChanging = false;
        drawableChanged();
    }
    
private void drawableChanged() {
        if (mChanging) {
            return;
        }

        //……
        //……
        requestLayout();
        invalidate();

        //……
        //……
       
    }

nani.jpg

根据我们对 View 的了解,requestLayout()invalidate() 会引发 View 的重新布局和重新绘制,难道这个时候就绘制 View 了。 这不科学

而事实上,这个真的不科学。此时并不会执行绘制和计算。 原因是此时的 View 还没有和 ViewRootImpl 关联上 。留个悬念,这个我们在后面的章节会讲解。

第三部分

第三部分就是第二部分省略的代码,代码特别长,这里也缩减一下。

 final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
         R.id.decor_content_parent);
 if (decorContentParent != null) {
     //……
 } else {
     mTitleView = (TextView)findViewById(R.id.title);
     //……
 }
 if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
     mDecor.setBackgroundFallback(mBackgroundFallbackResource);
 }
 // Only inflate or create a new TransitionManager if the caller hasn't
 // already set a custom one.
 if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
    //……
 }

这里简单的归纳一下代码做的事情

1. 设置 title
2. 设置背景色
3. 处理 FEATURE_ACTIVITY_TRANSITIONS 属性

requestLayout()invalidate() 源码追踪

requestLayout()invalidate() 的源码都在 View 类里面

先看 requestLayout()

public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

我们看到此时的 View 会调用 mParent.requestLayout()mParent 会是 ViewGroup 吗?我们看下声明变量的地方

protected ViewParent mParent;

然后再搜下mParent赋值的地方,发现只有一处

void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}

那接下来就看 assignParent(parent) 被谁调用了,发现 View 中只有声明,没有调用。所以我们就去 ViewGroup 看看。发现也只有一处调用

private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {

    ……
    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }

    ……
}

顺着这个方法追溯一下,如下图

activity_view_02.png

这时候我们又疑问了:

DecorView 的 mParent 是谁呢???

question.png

答案只有一个,是 NULL

我们刚说了 mDecor.finishChanging()不会执行绘制和计算相。 原因是此时的 View 还没有和 ViewRootImpl 关联上

先看 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) {
    ……
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }

        ……
    }
}

我们又在跟踪 invalidate() 方法时发现了 p.invalidateChild(this, damage) 这里似乎又是一层一层的向上迭代。为了确保,我们去看下 ViewGroup 的 invalidateChild()

public final void invalidateChild(View child, final Rect dirty) {
    ……

    ViewParent parent = this;
    if (attachInfo != null) {
        ……

        do {
            ……
            parent = parent.invalidateChildInParent(location, dirty);
            ……
            }
        } while (parent != null);
    }
}

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
         ……
         
        return mParent;
    }

    return null;
}

所以和 requestLayout() 一样层层追溯,又到了 DecorView 中。我们可以准确的说 DecorViewmParent 其实是 ViewRootImpl。但是怎么证明呢???

DecorViewViewRootImpl 的关系

本文开盘就已经说了 当 Activity 执行 onResume() 方法后,代表 Activity 显示到前台,这是为什么呢?

我们都是 Activity 的由 ActivityManager 管理,Activity 页面的操作必须在主线程中,而主线程就是 ActivityThread 。在 ActivityThread 的源码中,找到了一个 H 类,该类继承 Handler 。在 HhandleMessage(Message msg) 发现以下代码

   public void handleMessage(Message msg) {
        if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
        switch (msg.what) {
            ……
            case RESUME_ACTIVITY:
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
                handleResumeActivity((IBinder) msg.obj, true, msg.arg1 != 0, true);
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                break;
            ……
    }

然后看下 handleResumeActivity

 final void handleResumeActivity(IBinder token,
           ……
           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 (a.mVisibleFromClient) {
                   a.mWindowAdded = true;
                   wm.addView(decor, l);
               }
           ……
   }

这里我们看到了 DecorView 被添加到了 ViewManager 之中。

ViewManager 只是一个接口,它的实现类为 WindowManagerImpl。在 WindowManagerImpl 我查找 addView() 方法

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

这里的 mGlobal 又是 WindowManagerGlobal 的实例。所有我们又要跳转到 WindowManagerGlobal.addView()

keep.jpg

O__O “… 这时千万别放弃,胜利就在眼前,同志们要坚持往下看啊。

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

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

然后再看下 ViewRootImpl.setView()

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             ……
             view.assignParent(this);
         }
     }
 }

亲人啊!终于看到 ***root.setView(view, wparams, panelParentView)***,我们上面一直说的 View 和 ViewRootImpl 的关系终于在这关联上了。为了更清晰一点我们画一个时序图

activity_view_03.png

ViewRootImpl 绘制 View

现在进入了本文的压轴部分,View 绘制的核心源码。

通过以上的讲解,我们也知道要去找 ViewRootImplrequestLayout()invalidateChildInParent() 方法

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

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

mTraversalRunnable 是一个 Runnable 的子类

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

这个时候我们又要去看下 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() 方法, ***注意 performTraversals() 里面有重大内容***该方法很长(真的是特别长),我们这里看一下简化后的

 private void performTraversals() {
            ……
            if (!mStopped || mReportNextDraw) {
               ……
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
               ……
            }
        ……
        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ……
        }
        ……

                performDraw();
        ……
 }

success.jpg

看到了 performMeasureperformLayoutperformDraw 这里就不用多说了吧。也就解释了为啥 View 的绘制顺序是 measure -> layout -> draw 了吧

ViewRootImpl.invalidateChildInParent()()

这里我们不啰嗦太多,直接上源码

 public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
     checkThread();
     ……

     invalidateRectOnScreen(dirty);

     return null;
 }
 
private void invalidateRectOnScreen(Rect dirty) {
    ……
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}

看到这里就不用多说了,下面的执行顺序 ViewRootImpl.requestLayout() 已经分析过了。

这个时候大家再看下网上很多分析 requestLayout() 和 invalidate() 方法区别的,大家可以去先去查一下,等后面有时间我也会写一篇分析这两个方法区别的文章。

View 到底什么时候绘制到屏幕上?

通过以上分析我们知道

  1. setContentView() 只是把 View 添加到 DecorView 上
  2. onResume() 中 ViewRootImpl 和 DecorView 做了关联
  3. requestLayout() 和 invalidate() 会触发 ViewRootImpl 绘制 View

但是!setContentView() 中调用了 requestLayout() 和 invalidate() 不会触发绘制,我们上面只讲了 onResume() 中 ViewRootImpl 和 DecorView 做了关联 。到底什么时候又调用了 requestLayout() 或者 invalidate() ???

往上翻我们发现在 ViewRootImpl.setView() 中有一个 requestLayout

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                ……
                requestLayout();
                 ……
                view.assignParent(this);
                ……
            }
        }
    }

但是!居然在 view.assignParent(this) 这尼玛逗我吧!

nani.jpg

我们在回头看下 requestLayout()

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

这里重点看一下这句

mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

了解 Android Handler Looper 都知道 postSyncBarrier 是创建一个障碍,阻止后面的 Message 对象被执行。那这里也就解决了我刚刚的疑问, 虽然request()在 view.assignParent(this) 之前被调用,但是会被阻塞。 doTraversal() 执行的时候 DecorView 和 ViewRootImpl 已经关联了

这里留个坑

我没有找到 ViewRootImpl 怎么执行到 removeSyncBarrier(mTraversalBarrier) 的代码。

总结

对以上内容做个总结

1. View 在 Activity 的 onCreate() 方法中通过 setContentView() 方法添加到 Activity 的 DecorView 上
2. 此时 ViewRootImpl 和 DecorView 没有关联上,不会绘制 View
3. 在 Activity 的 onResume() 方法执行后,DecorView 会被添加带 ViewRootImpl 中。然后执行 requestlayout()

参考资料

Android Launcher 启动 Activity 的工作过程

Activity到底是什么时候显示到屏幕上的呢?

今天的文章探究 Android View 绘制流程,Activity 的 View 如何展示到屏幕分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15309.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注