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(), 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 介绍与炫酷的技巧。不过,说到绘制,之前遇到过两个问题。
第一个是关于 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年了,还没回退。
最后
完