安卓UI的重点之一就是View的绘制流程,经常出现在面试题中。熟悉View的绘制流程,不仅能轻松通过View相关的面试,也可以让我们更加方便的使用自定义View以及官方View。此篇先以常见面试题为切入点,说明自定义View的重要性,然后又以getMeasuredHeight值的获取作为问题点,带着问题从源码角度分析View的绘制流程。
1. 面试题介绍
1.1 Android 基础与底层机制
1. 数据库的操作类型有哪些,如何导入外部数据库?
2. 是否使用过本地广播,和全局广播有什么差别?
3. 是否使用过IntentService,作用是什么,AIDL解决了什么问题?(小米)
4. Activity、Window、View三者的差别,Fragment的特点?(360)
5. 描述一次网络请求的流程(新浪)
6. Handler、Thread和HandlerThread的差别(小米)
7. 低版本SDK实现高版本API(小米)
8. launch mode 应用场景(百度、小米、乐视)
9. touch 事件流程传递(小米)
> 10. view 绘制流程(百度)
11. 什么情况导致内存泄露(美团)
12. ANR定位和修正
13. 什么情况导致OOM (乐视、美团)
14. Android Service 与Activity 之间通信的几种方式
15. Android 各个版本API的区别
16. 如何保证一个后台服务不被杀死,比较省电的方式是什么?(百度)
17. RequestLayout、onLayout、onDraw 、DrawChild 区别与联系(猎豹)
18. Invalidate() 和 postInvalidate() 的区别及使用(百度)
19. Android 动画框架实现原理
2. 不同位置获取 getMeasuredHeight 的值
public class MainActivity extends AppCompatActivity {private TextView mTextView;private String TAG = "view8";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mTextView = findViewById(R.id.text_view);Log.e(TAG, "onCreate: " + "height1 = " + mTextView.getMeasuredHeight());mTextView.post(new Runnable() {@Overridepublic void run() {Log.e(TAG, "onCreate: " + "height2 = " + mTextView.getMeasuredHeight());}});}@Overrideprotected void onResume() {super.onResume();Log.e(TAG, "onCreate: " + "height3 = " + mTextView.getMeasuredHeight());}
}
从上面代码和运行结果可知,在Activity onCreate 和 onResume 的时候都无法获取到 getMeasuredHeight
值,而使用 mTextView.post(new Runnable())
方式可以获取到值,为何如此呢?
3. View 的绘制流程
3.1 View的添加流程 (是如何被添加到屏幕窗口上)
3.1.1 创建顶层布局容器DecorView
//View8/app/src/main/java/com/example/view8/MainActivity.java
// 这里主要是以默认继承的 AppCompatActivity 源码分析,如果是继承 Activity,
// 则直接进到PhoneWindow 的 setContentView,但基本流程都差不多
public class MainActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) {setContentView(R.layout.activity_main); // onCreate中主要就是操作了这一行
-------->
//.gradle/caches/modules-2/files-2.1/androidx.appcompat/appcompat/1.6.1/ace9a78b961165396147e8691faa18c1b0e48e20/appcompat-1.6.1-sources.jar!/androidx/appcompat/app/AppCompatActivity.javapublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);
-------->
//.gradle/caches/modules-2/files-2.1/androidx.appcompat/appcompat/1.6.1/ace9a78b961165396147e8691faa18c1b0e48e20/appcompat-1.6.1-sources.jar!/androidx/appcompat/app/AppCompatDelegate.javapublic abstract void setContentView(View v);
-------->
//.gradle/caches/modules-2/files-2.1/androidx.appcompat/appcompat/1.6.1/ace9a78b961165396147e8691faa18c1b0e48e20/appcompat-1.6.1-sources.jar!/androidx/appcompat/app/AppCompatDelegateImpl.javapublic void setContentView(View v) {ensureSubDecor();private void ensureSubDecor() {if (!mSubDecorInstalled) {mSubDecor = createSubDecor();private ViewGroup createSubDecor() {mWindow.getDecorView(); // 这里的mWindow就是PhoneWindow
-------->
//Android/Sdk/sources/android-33/com/android/internal/policy/PhoneWindow.javapublic final @NonNull View getDecorView() {if (mDecor == null || mForceDecorInstall) {installDecor();private void installDecor() {if (mDecor == null) {mDecor = generateDecor(-1);protected DecorView generateDecor(int featureId) {return new DecorView(context, featureId, this, getAttributes()); // 在这里new DecorView
3.1.2 在顶层布局中加载基础布局ViewGroup
//Android/Sdk/sources/android-33/com/android/internal/policy/PhoneWindow.javaprivate void installDecor() {if (mContentParent == null) {mContentParent = generateLayout(mDecor);protected ViewGroup generateLayout(DecorView decor) {int layoutResource;// 通过不同的条件(主题),对 layoutResource 进行初始化,然后传入 onResourcesLoaded// 假设 layoutResource 走了这个,如果走了其他的,布局中也会有FrameLayout,只是上面的东西不一样layoutResource = R.layout.screen_simple; mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);// ID_ANDROID_CONTENT = com.android.internal.R.id.content,也就是 layoutResource 中的 FrameLayout 布局ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); return contentParent; // 将 FrameLayout 布局 返回
-------->
//Android/Sdk/sources/android-33/com/android/internal/policy/DecorView.java void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {final View root = inflater.inflate(layoutResource, null);if (mDecorCaptionView != null) {mDecorCaptionView.addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));} else {// 解析layoutResource,执行了addView,添加到 mDecor 里addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
-------->
<!-- Android/Sdk/platforms/android-33/data/res/layout/screen_simple.xml -->
<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" /><FrameLayoutandroid: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>
3.1.3 将ContentView添加到基础布局中的FrameLayout中
// 如果 MainActivity extends Activity
//Android/Sdk/sources/android-33/com/android/internal/policy/PhoneWindow.javapublic void setContentView(int layoutResID) {// 将我们创建的 R.layout.activity_main 布局,放到 mContentParent 即系统创建的 FrameLayout 布局中mLayoutInflater.inflate(layoutResID, mContentParent); // 如果 MainActivity extends AppCompatActivity
//.gradle/caches/modules-2/files-2.1/androidx.appcompat/appcompat/1.6.1/ace9a78b961165396147e8691faa18c1b0e48e20/appcompat-1.6.1-sources.jar!/androidx/appcompat/app/AppCompatDelegateImpl.javapublic void setContentView(int resId) {// 将我们创建的 R.layout.activity_main 布局,放到 mContentParent 即系统创建的 FrameLayout 布局中LayoutInflater.from(mContext).inflate(resId, contentParent);
PhoneWindow 是 Window 的唯一实现类,以上操作,只是把我们的布局 R.layout.activity_main
加载到 DecorView
中。此时还是什么都不会显示的。
3.2 View的绘制流程
上面已经走了一遍View的添加流程,即创建DecorView
,然后将我们的布局R.layout.activity_main
添加进去,但是还没有走View的测量,所以还是拿不到getMeasuredHeight
值的。上节我们已经看了onCreate方法,即ActivityThread#performLaunchActivity
,这次就不看了,直接从ActivityThread#handleResumeActivity
开始。
//Android/Sdk/sources/android-33/android/app/ActivityThread.javapublic void handleRelaunchActivity(ActivityClientRecord tmp,PendingTransactionActions pendingActions) {handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,PendingTransactionActions pendingActions, boolean startsNotResumed,Configuration overrideConfig, String reason) {handleLaunchActivity(r, pendingActions, customIntent);public Activity handleLaunchActivity(ActivityClientRecord r,PendingTransactionActions pendingActions, Intent customIntent) {final Activity a = performLaunchActivity(r, customIntent);public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, String reason) {// 这里的performResumeActivity就会回到 Activity 的 Resume 方法if (!performResumeActivity(r, finalStateRequest, reason)) {return;
此时,onCreate方法和onResume方法都已经调用了,但是因为没有调用onMeasure方法,所以还是拿不到getMeasuredHeight
的值。
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, String reason) {// 这里的performResumeActivity就会回到 Activity 的 Resume 方法if (!performResumeActivity(r, finalStateRequest, reason)) {return;if (r.window == null && !a.mFinished && willBeVisible) {ViewManager wm = a.getWindowManager(); // 这个 ViewManager 是一个接口,我们需要找这个的实现if (a.mVisibleFromClient) {if (!a.mWindowAdded) {wm.addView(decor, l); // 这里的 wm 就是 ViewManager 的 getWindowManager
-------->
//Android/Sdk/sources/android-33/android/app/Activity.javapublic WindowManager getWindowManager() {return mWindowManager; // 接着找这个 mWindowManager 在哪里实现}final void attach(Context context, ActivityThread aThread, ...) {mWindowManager = mWindow.getWindowManager(); // 这里是mWindow,我们知道Window的唯一实现类就是PhoneWindow
-------->
//Android/Sdk/sources/android-33/com/android/internal/policy/PhoneWindow.javafinal ViewManager wm = getWindowManager(); // 接着进入 getWindowManager
-------->
//Android/Sdk/sources/android-33/android/view/Window.javapublic WindowManager getWindowManager() {return mWindowManager; // 发现这里又返回了 mWindowManager,我们继续找这个的实现public void setWindowManager(WindowManager wm, IBinder appToken, String appName,boolean hardwareAccelerated) {// 接着需要进入 WindowManagerImpl ,所以上面的wm 返回的就是 WindowManagerImpl ,接着在这个类里面找 addViewmWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
-------->
//Android/Sdk/sources/android-33/android/view/WindowManagerImpl.javapublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyTokens(params);mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,mContext.getUserId()); // 发现这里又调用了 mGlobal.addView}
-------->
//Android/Sdk/sources/android-33/android/view/WindowManagerGlobal.javapublic void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow, int userId) {ViewRootImpl root; // 申明root// 通过 ViewRootImpl 的构造方法,将root实例化if (windowlessSession == null) {root = new ViewRootImpl(view.getContext(), display);} else {root = new ViewRootImpl(view.getContext(), display,windowlessSession);}view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);try {root.setView(view, wparams, panelParentView, userId); // 将这几个参数关联起来
-------->
//Android/Sdk/sources/android-33/android/view/ViewRootImpl.javapublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {synchronized (this) {if (mView == null) {requestLayout();