View的绘制流程那些事

78 阅读18分钟

lyldalek.notion.site/View-afe222… 

文章的原始链接,可以直接在文章上 comment 哦!

聊斋镇楼

莱阳有个叫宋玉叔的先生,当部曹官的时候,租赁了一套宅院,很是荒凉。有一天夜里,两个丫鬟侍奉着宋先生的母亲睡在正屋,听到院里有扑扑的声音,就像裁缝向衣服上喷水一样。宋母催促丫鬟起来,叫他们把窗纸捅破个小孔偷偷地往外看看……那么,他们看到了什么呢?欲知后事如何,请听下回分解。

我们来先分析一下一个 xml 是如何显示到 Activity 上的。

下面的源码分析基于 android-33

Activity 的创建

当我们调用了 startActivity 后,只见老爹突然发出了绿光,使出不知道哪种魔法,ActivityThread 的 handleLaunchActivity 就被调用了:

android.app.ActivityThread#handleLaunchActivity

/**
 * Extended implementation of activity launch. Used when server requests a launch or relaunch.
 */
@Override
public Activity handleLaunchActivity(ActivityClientRecord r,
        PendingTransactionActions pendingActions, Intent customIntent) {

  ...
        // ① 这里是启动了渲染线程
    HardwareRenderer.preload();
  ...

  // ② 获取 WMS 对象
    WindowManagerGlobal.initialize();

  // ③ 继续启动 activity
    final Activity a = performLaunchActivity(r, customIntent);

    return a;
}

这个方法里面我们需要关注的暂时只有上面3个地方。

第一个地方,启动了渲染线程,也就是 systrace 图中的 RenderThread。所以这里其实有一个可能优化的点,那就是渲染线程是在 activity 创建的时候做的,那么将它提前到 application 创建的时候是否能有收益呢?

第二个地方,WindowManagerGlobal 这个东西只是一个提供了与 Context 无关的和 WMS 通信的类,没啥别的作用。Activity 是一个 Context,每个 Activity 都有自己的 WindowManager 实例,但是他们都中转给 WindowManagerGlobal ,让它来操作,其实就是一些全局函数而已。

我们继续跟踪第三个地方调用的函数:

android.app.ActivityThread#performLaunchActivity

/**  Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

  ...
    try {
    // ① 创建出来了 activity 对象
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
    ...
    } catch (Exception e) {
        ...
    }

  try {

        if (activity != null) {
            ...
      // ② 执行 activity 的 attach 方法
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
                    r.assistToken, r.shareableActivityToken);

            ...
      // ③ 执行 activity 的 onCreate 方法
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
            ...
        }
        ...
    
   } catch (SuperNotCalledException e) {
     ...
   }
    return activity;
}

这个方法同样的还是只有3个地方我们需要关注。

第一个地方,可以看到 activity 是由 mInstrumentation 创建的,里面就是使用 classLoader 加载这个 activity 的类名,然后使用 newInstance 创建一个对象出来。

第二个地方,activity 的 attach 方法里面做了不少事情,其中就有设置 WindowManager,但是这个 WindowManager 不是 WindowManagerService。下面这个图来说明一下关系:

可以看到,activity 里面设置的 WindowManager 就只是一个普通类,它实现了一些接口:addView/removeView 等等。那么既然它是一个普通类,是如何将 View 添加到 Window 上的呢?上面我们说过,WindowManager 将操作都委托给了 WindowManagerGlobal,而 WindowManagerGlobal 有个成员变量可以跟 WMS 通信,这样就可以解释了。

第三个地方,activity 执行 onCreate 方法,这里就轮到我们的回合了。在继续分析之前,顺便说一个事,就是有一个很常见的面试题:为啥 Looper 里面有个死循环,但是不会卡死?

其实这个问题的核心逻辑应该在于,死循环会导致其外部的代码无法执行,但是它里面的代码会不断的运行,而我们创建一个 app 后,Application 和 Activity 等都是模板类,我们在模板类中写的代码都会在循环里面被调用。下面具体分析:

上面我们说过老爹使用了魔法,就执行到了 handleLaunchActivity ,我们稍微时光倒流一下,看看是谁调用了这个方法:

android.app.ActivityThread.H#handleMessage

class H extends Handler {
  ...
  public void handleMessage(Message msg) {
        ...
        switch (msg.what) {
      ...
      case EXECUTE_TRANSACTION:
            final ClientTransaction transaction = (ClientTransaction) msg.obj;
            mTransactionExecutor.execute(transaction);
         ...
            break;
      ...
    }
  }
}

不知道从哪个版本开始,activity 对应的生命周期的消息都封装成了 EXECUTE_TRANSACTION 这个消息,然后细节由对应的子类来处理,比如 launch 的:

public class LaunchActivityItem extends ClientTransactionItem {
  ...
    @Override
    public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
        ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,
                client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
                mTaskFragmentToken);
        client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }
  ...
}

这里不重要,重要的是 handleLaunchActivity 是在一个 message 里面。我们知道,MessageQueue 将 message 分发给 handler 去处理,handler 就会执行对应 message.what 的逻辑。所以,我们在 activity 里面写的代码,都在这个 messag 执行时的调用堆栈里面。MainLooper 对应的 handler 执行消息的时候,就会执行到我们的代码,所以死循环不会引起卡顿,相反,正是有了这个死循环,我们的代码才能有不断的得到执行的机会。

我刚接触java的时候,写一个命令行程序,不断读取字符然后显示回屏幕,里面也是有一个 while true,在死循环里面处理各种逻辑,和这个是同样的道理。

话扯远了,我们回来继续看 activity 的 onCreate 方法:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    // 使用了 ViewBinding
        binding = ActivityMainBinding.inflate(layoutInflater)
    // ① 加载 activity 的布局
        setContentView(binding.root)
    }

}

我们自定义的 Activity,都会加载一个 xml,这个 xml 最终会添加到该 activity 对应的 window 上。

加载 xml 到 Activity 上

我们追踪一下 setContentView 的逻辑:

androidx.appcompat.app.AppCompatActivity#setContentView(android.view.View)

@Override
public void setContentView(View view) {
    initViewTreeOwners();
    getDelegate().setContentView(view);
}

initViewTreeOwners 做了一些初始化的工作,暂时不关心。

getDelegate().setContentView(view); 从这行代码可以看出,AppCompatActivity 将很多事都委托给了别人,就是 AppCompatDelegateImpl。看下它做了啥:

androidx.appcompat.app.AppCompatDelegateImpl#setContentView(android.view.View)

@Override
public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    ...
}

逻辑还是蛮清楚的,创建一个 SubDecor,然后找到里面 id 为 content 的控件,将从 xml 创建出来的 View 添加到 content 里面去。

我们看看这个 SubDecor是啥,创建 SubDecor 的逻辑也不多:

首先它会根据设置的 theme 来加载不同的 layout,比如这里调试的发现它使用的是 R.layout.abc_screen_simple,搜索一下,发现它的内容如下:

include 里面就是一个 androidx.appcompat.widget.ContentFrameLayout 没有其他布局。

SubDecor 的整个布局如下:

将View添加到Window上

window 的 DecorView 的创建流程类似 subDecor 这里就不再展开了!!

上面的分析,我们知道了我们写的 xml 被装饰了一下,无缘无故的就多了2个层级,往后看还有TM的惊喜。

上面有个地方没有说得就是在创建 SubDecor 的时候,顺便做了一件事,就是调用了 Window 的 setContentView 方法:

androidx.appcompat.app.AppCompatDelegateImpl#createSubDecor

private ViewGroup createSubDecor() {
 ...
 mWindow.setContentView(subDecor);
 ...
}

window的唯一一个实现类就是 PhoneWindow,看看里面的逻辑:

com.android.internal.policy.PhoneWindow#setContentView(int)

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    ...

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        view.setLayoutParams(params);
        final Scene newScene = new Scene(mContentParent, view);
        transitionTo(newScene);
    } else {
        mContentParent.addView(view, params);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

这里我们发现,subDecor 被添加到了 mContentParent 里面,mContentParent 是啥呢?它是DecorView里面的 id 叫 Content 的一个FrameLayout 控件。但是 subDecor 里面不也有一个 id 叫 Content 的控件吗?

这里解释一下,subDecor-content 在xml中的ID是 action_bar_activity_content,经过一番操作后,它的 id 会被代码替换成 R.id.content,而 decorView-content 控件的 id 被设置为了 NO_ID。由于 mContentParent 已经保存了 decorView-content 控件的引用,所以设置成 NO_ID 也没关系。

所以,最终整个界面的布局如下:

惊了!!!啥都没干,不算 xml ,布局就已经有5层了,所以使用 AppCompatActivity 要注意层次啊。

回到正题,有了 DecorView,那是在什么时候添加到 Window 上的呢?时机是在 activity 执行 onResume 之后,我们看看调用流程:

android.app.ActivityThread#handleResumeActivity

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
      boolean isForward, String reason) {
  ...

  // ① 这里调用了 activity 的 onResume 方法
  if (!performResumeActivity(r, finalStateRequest, reason)) {
      return;
  }
  ...

  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();
      ...
      if (a.mVisibleFromClient) {
          if (!a.mWindowAdded) {
              a.mWindowAdded = true;
       // ② 这里将 decorView 添加到了 windowManager 上
              wm.addView(decor, l);
          } else {
              ..
          }
      }

      ...
  } else if (!willBeVisible) {
      ...
  }

  ...
}

这里标注了两个地方,发现 View 添加到 Window 的时机是在 onResume 执行完之后,也就是说,View 的测量等流程都是在 onResume 之后才正式开始进行的。

上面分析了 WindowManager 一系列相关的类,这里就不重复了,所以我们直接看 WindowManagerGlobal 的逻辑:

android.view.WindowManagerGlobal#addView

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
    ...

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        ...
    // ① 创建了一个 ViewRootImpl 对象。
        if (windowlessSession == null) {
            root = new ViewRootImpl(view.getContext(), display);
        } else {
            root = new ViewRootImpl(view.getContext(), display,
                    windowlessSession);
        }

        view.setLayoutParams(wparams);

    // ② 添加 View 到集合
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
      // ③ 将 DecorView 与 ViewRootImpl 关联起来
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            ...
        }
    }
}

标记了3个地方法,1和3 简单,后面我们继续分析。

第2个地方还是很有意思的,就是我在看新版 LeakCanary 源码的时候,发现支持了像 dialog,toast 等的泄露。其原理就是 hook 了这个 mViews 集合,然后给里面的View都添加一个 addOnAttachStateChangeListener 监听,在 onViewDetachedFromWindow 里面去检测这个对象是不是泄露了。

ViewRootImpl

android.view.ViewRootImpl#setView(android.view.View, android.view.WindowManager.LayoutParams, android.view.View, int)

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
  ...
  mView = view;
  ...
  requestLayout();
  ...
}

这个方法里面的逻辑很长,但是我们此时只关系两处,第一个就是保存了 DecorView,RootViewImpl 只会有一个 child。第二处就是请求View树开始布局,这里就到了我们熟悉的地方了。

android.view.ViewRootImpl#requestLayout

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    // ① 这里检查的线程
        checkThread();
        mLayoutRequested = true;
    // ② 准备开始遍历 View 树
        scheduleTraversals();
    }
}

这里做了一个过滤,是针对 View 树已经在 layout 的过程中,发现 child 又设置了 layout 标识,这个时候就过滤掉这次 layout,等到本次layout完成/下一帧的时候再重新 layout。

第1处是检查线程,也就是不让非创建该 ViewRootImpl 对象的线程来更新View树,一般情况下,创建 ViewRootImpl 的是主线程,也就是我们常说的不能在子线程更新UI。但是如果我们在子线程创建了 ViewRootImpl 对象呢?那么就能在那个子线程去更新UI。

异步更新UI其实会引发很多疑难杂症,但是需要其他储备知识,我们后面再说。

android.view.ViewRootImpl#scheduleTraversals

void scheduleTraversals() {
  // 过滤
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
    // ① 往消息队列里面发送一个同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    // ② 向 CALLBACK_TRAVERSAL 队列里面添加一个 runnable
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

过滤是因为,mTraversalRunnable 没有执行到之前,没必要重复执行一次。

第1处标记,发送了一个同步屏障,这个屏障会阻塞所有非同步消息,消息队列,只能处理异步消息。异步消息从哪里来呢?后面会说到

第2处是将 mTraversalRunnable 放到一个队列里面,然后等待执行。什么时候执行呢?由mChoreographer决定,mChoreographer会统一流程。

Choreographer

Choreographer 顾名思义,编舞者。那么舞者是谁呢?又是如何编这支舞呢?下面我们一一道来。

在继续之前,我们需要两个预备知识,Vsync + TripleBuffer。 TripleBuffer 是双缓冲的增强版。他们一起是用来解决画面撕裂问题的同时保证显示效率。Vsync的作用像一个锁一样,看下图:

看一下 Choreographer 的工作流程:

android.view.Choreographer#Choreographer

private Choreographer(Looper looper, int vsyncSource) {
    mLooper = looper;
  // handler
    mHandler = new FrameHandler(looper);
  // 初始化 FrameDisplayEventReceiver ,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    ...
  // 创建了5个队列
  // CALLBACK_ANIMATION - CALLBACK_INSETS_ANIMATION - CALLBACK_TRAVERSAL - CALLBACK_COMMIT - unknown
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
    ...
}

构造函数里面主要是做了一些初始化工作,其中我们需要先关注 FrameDisplayEventReceiver 这个类。这个类可以收到 VSYNVC 信号:

android.view.Choreographer.FrameDisplayEventReceiver

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    ...

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
            VsyncEventData vsyncEventData) {
        try {
            ...
      // ① 发送了一个异步消息,将 this 传进去
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        } finally {
            ...
        }
    }

    @Override
    public void run() {
        ...
    // 调用 doFrame 方法
        doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
    }
}

在 onVsync 回调里面,也就是收到 VSYNC 信号后,使用 handler (主线程的)发送了一个异步消息。当这个消息被执行的时候,会调用到 run 方法里面,也就是会调用 doFrame。

之前我们说过,ViewRootImpl 在 scheduleTraversals 的时候,往 Choreographer 的队列里面发送了一个 runnable,我们看下逻辑:

android.view.Choreographer#postCallbackDelayedInternal

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    ...

    synchronized (mLock) {
        ...

        if (dueTime <= now) {
      // 超时了,直接调用 doFrame
            scheduleFrameLocked(now);
        } else {
      // 发送一个异步消息
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

这个方法除了 post 一个 runnable 到对应的队列,还会检查该 runnable 的执行事件是否已经超时,如果是,则直接调用 scheduleFrameLocked 准备触发下一帧,没有就发送一个异步消息。

其实 scheduleFrameLocked 也是发送了一个异步消息:

android.view.Choreographer#scheduleFrameLocked

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            ...
            if (isRunningOnLooperThreadLocked()) {
                ...
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            ...
        }
    }
}

所以最终,这两个分支,都会走到 FrameHandler 的 handleMessage 方法里面。

android.view.Choreographer.FrameHandler

private final class FrameHandler extends Handler {
    ...

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                doFrame(System.nanoTime(), 0new DisplayEventReceiver.VsyncEventData());
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

可以看到,如果超时了,那么走到 MSG_DO_FRAME 里面,如果没有超时,走到 doScheduleCallback 里面,但是神奇的是,doScheduleCallback 最终也会走到 doFrame:

android.view.Choreographer#doScheduleCallback

void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
        if (!mFrameScheduled) {
            final long now = SystemClock.uptimeMillis();
            if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                scheduleFrameLocked(now);
            }
        }
    }
}

所以,上面我们分析了那么多,Choreographer 的 postCallbackXXX 方法,所有分支最终都会走到 scheduleFrameLocked,然后执行 doFrame,唯一的区别就是执行的时机不同而已。

ViewRootImpl 在 scheduleTraversals 的时候,不仅往 Choreographer 的队列里面发送了一个 runnable,还往 MainLooper 的队列里面发送了一个同步屏障。这个同步屏障就非常的巧妙,因为根据上面的分析我们知道,发送了同步屏障后,接着就该发送一个异步消息了,而这个异步消息就是 doFrame,这样就保证了 doFrame 优先执行,在 doFrame 执行的过程中,同步屏障也会被移除,避免导致其他消息得不到执行。

上面说到过,异步更新会有很多奇葩的问题,其原因就是,scheduleTraversals 方法不是同步的,而且 mTraversalBarrier 变量只保存了一个同步屏障的结果,如果有多线程同时调用 scheduleTraversals 方法,那么就可能会导致发送了两个同步屏障,最后只移除一个的事情发生,这个时候有些同步消息得不到执行,就会发生ANR。

那么 doFrame 里面都是啥呢?其实就是执行那5个队列里面的 runnable。

 void doFrame(long frameTimeNanos, int frame,
        DisplayEventReceiver.VsyncEventData vsyncEventData) {
    ...
    try {
        ...
        doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData,
                frameIntervalNanos);

        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);
    } finally {
        ...
    }

    ...
}

Choreographer.CALLBACK_INPUT 就是事件队列,事件的处理就在这个里面执行。

Choreographer.CALLBACK_ANIMATION 就是动画队列,比如 fling 等动画就在里面执行。

Choreographer.CALLBACK_TRAVERSAL 就是执行View树的构建与限制。ViewRootImpl 在 scheduleTraversals 的时候 post 的 runnable 就储存在了这里,在这里被执行。

runnable 里面的逻辑超长,就不贴代码了,只用知道它里面会调用 DecorView 的 measure + layout + draw。

但是有两个需要注意的地方:

android.view.ViewRootImpl#performTraversals

private void performTraversals() {
  ...
  if (mFirst) {
        ...
    // ① 这里调用了 View 和 listener 的 onWindowAttached 方法
    host.dispatchAttachedToWindow(mAttachInfo, 0); 
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        ...
    }

  ...
  // ② 
  getRunQueue().executeActions(mAttachInfo.mHandler);
  ...
}

第2处方法的作用,就是执行在 View 还没有 attach 到 Window 上的时候,使用 View post 的一些逻辑。这里并不是直接执行了那些 runnable ,而是又重新 post 了一下。

所以为啥使用 View 的 post 方法,能获取到 View 的控件大小,其实就是 post 的逻辑是在第一次 performTraversals 之后执行的。

到这里,整体的流程差不多讲完了,我们也可以回答最开始回答的问题了,舞者就是5个队列,让他们在 vsync 信号来的时候统一的执行。后面再说说 View 的测量布局绘制等方法。

Measure + Layout + Draw

控件的测量,我们主要关注的方法是 onMeasure 方法,对于自定义 View 来说,只需要根据业务逻辑来测量大小,然后再参考父控件传递过来的值修正一下即可,大概模板如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec)
      
      val w = calW()
      val h = calH()
      
      val fixW = resolveSize(w, widthMeasureSpec)
      val fixH = resolveSize(h, heightMeasureSpec)
      
      setMeasuredDimension(fixW, fixH)
  }

为啥需要修正一下呢?是因为我们计算出来的值可能与父控件传递给我们的值不一样。举个例子,父控件传递过来的 widthMeasureSpec 说明了留给 child 的最多只有 100px 了,你计算出来的 w 是 110px,这个时候,应该以 widthMeasureSpec 的优先级更高。

因为 widthMeasureSpec 是根据 xml 的宽高信息算出来的,肯定要以 xml 里面的优先级为高,不然谁用你这个叼毛控件。

对于自定义 ViewGroup 来说,有需要的话,测量 child 直接使用 measureChildWithMargins ,很多ViewGroup 都是使用的它。但是似乎很少有需求要直接继承 ViewGroup 的,都是继承至现有的,比如 LinearLayout 等,然后做一些事情。一般我们关注的是 onLayout 方法,在这个方法里面甚至可以忽略 child 的测量值,强制给 child layout 你想要的范围。举个例子:

class TestView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(100100)
    }

}

这个控件永远强制自己的宽高是 100 * 100,不关心父布局传递过来的参考值。该控件在 LinearLayout 上是展示正常的,但是在 ConstraintLayout 下就会出问题。原因是 ConstraintLayout 在 layout 的时候会强制改变该控件显示的大小,ConstraintLayout 给 child 设置的 right - left 是铺满的,而不是 100px。

View 的绘制,这里就需要参考 aige 的自定义控件其实很简单系列了,里面有非常多的 api 介绍与炫酷的技巧。不过,说到绘制,之前遇到过两个问题。

第一个是关于 api 的性能的,google 也有相关的文档介绍:

呈现性能:RenderThread 有些画布操作虽然记录开销很低,但会在 RenderThread 上触发开销非常大的计算。Systrace 通常会通过提醒来指出这类操作。

Canvas.saveLayer()

避免 Canvas.saveLayer() – 它可能会触发以开销非常大且未缓存的屏幕外方式呈现每帧。虽然 Android 6.0 中的性能得到了提升(进行了优化以避免 GPU 上的呈现目标切换),但仍然最好尽可能避免使用这个开销非常大的 API,或者至少确保传递 Canvas.CLIP_TO_LAYER_SAVE_FLAG(或调用不带标志的变体)。

为大型路径添加动画效果

对传递至视图的硬件加速画布调用 Canvas.drawPath() 时,Android 会首先在 CPU 上绘制这些路径,然后将它们上传到 GPU。如果路径较大,请避免逐帧修改,以便高效地对其进行缓存和绘制。drawPoints()、drawLines() 和 drawRect/Circle/Oval/RoundRect() 的效率更高 – 即使您最终使用了更多绘制调用,也最好使用它们。

Canvas.clipPath

clipPath(Path) 会触发开销非常大的裁剪行为,因此通常应避免使用它。如果可能,请选择使用绘制形状,而不是裁剪为非矩形。它的效果更好,并支持抗锯齿功能。

这里顺便说一下 canvas 的 saveLayer 与 setLayerType 这两个方法的意义。

saveLayer 是开启一个离屏缓冲,是针对View的这一次绘制操作,所以就是每次绘制都会开启一个离屏缓冲,开销非常大。Google 建议使用 LAYER_TYPE_HARDWARE 来代替 saveLayer 的使用,但是有些时候使用 LAYER_TYPE_HARDWARE 搞不定,就需要想想别的办法了。

setLayerType 是针对的这个 View 开启离屏缓冲,整个View的生命周期内只有一次。LAYER_TYPE_HARDWARE 是开启一个使用硬件加速的离屏缓冲,同样的 LAYER_TYPE_SOFTWARE 也开启一个使用软件绘制(关闭硬件加速)的离屏缓冲,等于针对该View关闭了硬件加速。其实它比直接关闭硬件加速还要差劲,毕竟是额外开了一个缓冲区。直接关闭硬件加速,还能省一个缓冲区。一般都是设置 Hardware Layer 对 alpha\translation \ scale \ rotation \ 这几个属性动画性能有帮助,没有遇到其他需要设置的地方。

第二个是关于硬件加速的,我们知道调用 View 的 invalidate 方法,最终会触发到 ViewRootImpl 的 invalidate 方法。但是有一次翻看源码的时候,发现逻辑变了:

android.view.ViewGroup#invalidateChild

/**
 * Don't call or override this method. It is used for the implementation of
 * the view hierarchy.
 *
 * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
 * draw state in descendants.
 */
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null && attachInfo.mHardwareAccelerated) {
        // HW accelerated fast path
    // ① 硬件加速走这个分支
        onDescendantInvalidated(child, child);
        return;
    }

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

        do {
            ...
      // ② 非硬件加速走这个分支
            parent = parent.invalidateChildInParent(location, dirty);
            if (view != null) {
                ...
            }
        } while (parent != null);
    }
}

不知道在什么时候,硬件加速与非硬件加速出现了区别。

第2处是我们熟悉的逻辑,一直会走到 ViewRootImpl 的 invalidateChildInParent 里面去。

第1处的逻辑是新加的,发现它一直会走到 ViewRootImpl 的 onDescendantInvalidated 里面去。

android.view.ViewRootImpl#onDescendantInvalidated

@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
    // TODO: Re-enable after camera is fixed or consider targetSdk checking this
    // checkThread();
    if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
        mIsAnimating = true;
    }
    invalidate();
}

这个方法里面有一个重要的注释就是它将 checkThread 检查给干掉了,这会导致什么呢?就是你在子线程里面去 invalidate 一个控件,它不会触发线程检查异常(但是可能会有别的问题,比如上面说到的ANR问题)。

看看对应的 commit 说了啥,为啥要将这个检查给干掉:

临时禁掉,牛逼!!!都已经4年了,还没回退。

最后