服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Android View从绘制到上屏全过程解析

日期: 来源:郭霖收集编辑:a5right


/   今日科技快讯   /

近日,微软和谷歌支持的非营利性组织人工智能教育项目(aiEDU)宣布将扩大人工智能教育的覆盖范围,推动更多学区的学生能了解人工智能。

aiEDU是一个由微软、谷歌、OpenAI和AT&T公司等公司支持的非营利组织,提供免费材料和教师培训服务,目的是加深学生对人工智能的理解。aiEDU的想法是让孩子们了解人工智能技术的本质、局限性以及前景,并为他们需要使用人工智能的工作做好准备。

/   作者简介   /

本篇文章来自aprz512的投稿,文章主要分享了View从绘制到上屏的整个流程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

aprz512的博客地址:
https://lyldalek.top/

/   前言   /

聊斋镇楼

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

我们来先分析一下一个 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(), 0, new 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(100, 100)
    }

}

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

View 的绘制,这里就需要参考 aige 的自定义控件其实很简单系列了,里面有非常多的 api 介绍与炫酷的技巧。不过,说到绘制,之前遇到过两个问题。文章地址:
https://blog.csdn.net/aigestudio/category_9263410.html

第一个是关于 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年了,还没回退。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android 13 Developer Preview一览
利用反编译,仿写一个小红书图片指示器吧

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

相关阅读

  • 掌握这个方法,飞速提升你的写作能力

  • 这是古典古少侠的第535篇原创文章很多人读书的时候会划线、抄金句,甚至小心翼翼地腾到本子上,但最后发现书还是书,我还是我。知识没提高,写作能力也没什么进化。为什么呢?这种在
  • 记一次某推上的session利用trick

  • 本文为看雪论坛优秀文章看雪论坛作者ID:RoboTerh在一次浏览某推中发现了发现了了一个web challenge的赏金ctf,这里从来学习一下由于使session_start()报错引发的危害。正文题
  • 事件相机的原理与应用简介

  • 点击下方卡片,关注“新机器视觉”公众号重磅干货,第一时间送达本文转自空中机器人前沿,作者西湖大学-郑业。一.概述事件相机(Event-based camera)是一种受生物启发的新型视觉传感
  • 这里有一份38女生节礼物,等你来领取!

  • |点击上方卡片关注|大概是中文互联网最实用 / 最硬核的时尚|穿搭|审美|科普号注:文末附留言红包活动不说废话,直入主题,先预祝各位女神节日快乐。38女生节如约而至,年度优惠价格,超值
  • 利用反编译,仿写一个小红书图片指示器吧

  • / 今日科技快讯 /近日,微软宣布将爆火聊天机器人ChatGPT背后的AI技术集成到Power Platform等更多开发工具中,该平台允许用户在很少甚至不需要编码的情况下构建应用程序,这
  • 复试|谈谈你的毕业论文

  • 本文由考研斯基原创,转载须注明来源出处本文约560字,预计需要2分钟【复试面试导师提问】请谈一下你的毕业论文设计?是否完成?如何完成?【问题分析】这是复试时很多导师爱问的一
  • 什么样的文章才能发Angew、JACS、Nature、Science?

  • 来源丨科学网、小木虫“德国应用化学”(Angew.Chem.Int.Ed)是最著名的化学类杂志,能在上面发表学术论文,表明你的研究工作处于国际领先水平,是所有从事化学、材料和相关专业人员
  • 你缺客户?教你引流拓客好方法

  • 知乎的引流怎么样?知乎里面的粉丝精准吗?小红书呢?还有抖音、快手引流怎么样?等等…… 当别人跟你说某个引流渠道特别好,粉丝很多,量很大,都不要轻信,你要去测试才会知道答案……这
  • 9种流程优化方法,提升业务效率

  • 更容易成功的人,是有高级工具的人。编者按:简化工作流程、不断发现工作流程中的错误并有效整合、提高内部团队成员和客户的满意度,是当今每个企业、每个管理者乃至每个员工的共

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 中芯国际设计服务岗位招聘!

  • 中国半导体论坛 振兴国产半导体产业! 中芯国际2023设计服务岗位招聘万物生发,焕新职涯趁现在!加入中芯国际成就闪耀的自己!公司介绍中芯国际(证券代码:00981.HK/688981.SH)是世界
  • Android View从绘制到上屏全过程解析

  • / 今日科技快讯 /近日,微软和谷歌支持的非营利性组织人工智能教育项目(aiEDU)宣布将扩大人工智能教育的覆盖范围,推动更多学区的学生能了解人工智能。aiEDU是一个由微软、谷
  • 京东方将为苹果供应LTPO OLED屏幕!

  • 中国半导体论坛 振兴国产半导体产业! 不拘中国、放眼世界!关注世界半导体论坛↓↓↓3月9日消息,据供应链人士透露,京东方将为苹果iPhone Pro系列供应搭载LTPO技术的OLED屏幕,并
  • 依规合法 电子烟专委会推动非持证会员转型发展

  • 点击蓝字,关注我们2022年10月《电子烟管理办法》正式落地实施,标志着电子烟行业进入法治化、规范化时代。管理办法中明确规定,电子烟生产企业需持有《烟草专卖生产企业许可证》
  • 库存超标260%!内存要继续降价?

  • 中国半导体论坛 振兴国产半导体产业! 不拘中国、放眼世界!关注世界半导体论坛↓↓↓3月9日,据数据显示,今年1月份韩国芯片库存积压严重,库存创下26年以来新高,库存率达265.7%。