每天一个面试知识点,文章持续更新,Android模拟面试,解锁大厂一对一面试体验。
前段时间公司招人,作为面试官,我经常让面试者简述View的绘制流程。他们基本都能讲明白View的测量(measure)、布局(layout)、绘制(draw)等过程。还有少数人会提到DecorView和ViewRootImp的作用。但是,当我继续追问关于Window的内容时,几乎没有人回答上来。而本章将会带你深入理解Window、DecorView、ViewRootImp。除此之外,你还能在本章找到以下问题的答案:
1. 为什么要有设计Window? 2. 子线程真的不能更新UI吗? 3. 为什么在Activity的onCreate方法中无法获取View的宽和高?
上图出现了五个对象:Activity、Window、WindowManager、DecorView、ViewRootImpl,我分别解释一下它们的作用。
public interface WindowManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
在Activity的启动流程,其中handleLaunchActivity()方法是启动Activity的核心方法。本节就以它为切入点开始分析。
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//省略代码...
//performLaunchActivity
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
//handleResumeActivity
handleResumeActivity(r.token, false, r.isForward,!r.activity.mFinished && !r.startsNotResumed);
//省略代码...
}
}
handleLaunchActivity()主要调用了两个方法:performLaunchActivity()和handleResumeActivity()
我们进入performLaunchActivity()方法,核心代码如下:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
ComponentName component = r.intent.getComponent();
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//创建Activity
Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
//创建Context
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
//调用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);
//省略代码...
//调用Activity.onCreate()方法。
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
r.activity = activity;
if (!r.activity.mFinished) {
//调用Activity.onStart()方法。
activity.performStart();
}
}
r.paused = true;
mActivities.put(r.token, r);
return activity;
}
performLaunchActivity()主要做了以下几件事:
上面说了,在Activity.attach()方法执行时会创建Window并为Window关联WindowManager。我们看一下伪代码。
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
//创建Window
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
//设置UI线程
mUiThread = Thread.currentThread();
mMainThread = aThread;
//关联WindowManager
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
}
顺着流程图,Activity.setContentView()方法会被调用。Activity.setContentView()内又直接调用了PhoneWindow.setContentView()。我们直接看PhoneWindow.setContentView()的源码。
private DecorView mDecor;
//setContentView传过来的View会被add到mContentParent中。mContentParent的Id是R.id.content。
private ViewGroup mContentParent;
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
}else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//省略代码。。。
}
private void installDecor() {
if (mDecor == null) {
//初始化DecorView
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
//省略代码。。。
}
}
如果mContentParent为null,会执行installDecor()方法。generateDecor()源代码很长,但是它的逻辑很简单主要是根据Activity Theme的不同,初始化不同的布局。DecorView的布局虽然不同,但它们都一个Id为R.id.content的FrameLayout。Activity.setContentView()就是在这个FrameLayout中添加子View。
performLaunchActivity()执行完后,紧接着执行handleResumeActivity()。
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//处理Activity的onRestart onResume生命周期。
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
if (r.window == null && !a.mFinished) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
//设置DecorView不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//利用WindowManager添加DecorView。
wm.addView(decor, l);
}
}
if (!r.activity.mFinished && r.activity.mDecor != null) {
if (r.activity.mVisibleFromClient) {
//设置DecorView可见。
r.activity.makeVisible();
}
}
//IPC调用,通知AMS Activity启动完成。
ActivityManagerNative.getDefault().activityResumed(token);
} else {
try {
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null, false);
} catch (RemoteException ex) {
}
}
}
handleResumeActivity()处理的事情比较多。我总结为以下几个过程:
前面说过,WindowManger是一个抽象类,它的实现类是WindowManagerImpl。WindowManager.addView()封装了View绘制的细节。我们着重看一下。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Display mDisplay;
private final Window mParentWindow;
private IBinder mDefaultToken;
public WindowManagerImpl(Display display) {
this(display, null);
}
private WindowManagerImpl(Display display, Window parentWindow) {
mDisplay = display;
mParentWindow = parentWindow;
}
public void setDefaultToken(IBinder token) {
mDefaultToken = token;
}
@Override
//这里的View是PhoneWindow内创建的DecorView。
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
if (mDefaultToken != null && mParentWindow == null) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (wparams.token == null) {
wparams.token = mDefaultToken;
}
}
}
}
WindowManagerImpl.addView会调用WindowManagerGlobal.addView()。在WindowManagerGlobal.addView()方法执行之前,会先执行applyDefaultToken()方法。这个方法其实是给传进来的DecorView加一个身份标识,表示这个DecorView属于哪个Activity。这样系统(WindowManagerService)才会知道要把DecorView绘制到哪个Activity。
我们继续追踪WindowManagerGlobal.addView(),伪代码如下:
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
//这里的View是PhoneWindow内创建的DecorView。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//省略代码....
root = new ViewRootImpl(view.getContext(), display);
//省略代码...
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(view, wparams, panelParentView);
}
首先会创建ViewRootImpl,随后把View、ViewRootImpl、LayoutParams都保存在List中,以供将来更新UI使用。它们的index值相同,这样就三者就对应起来了。最后,调用ViewRootImpl.setView()方法。
public class ViewRootImpl{
View mView;
//这里的View是PhoneWindow内创建的DecorView。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
//省略代码。。。
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
//省略代码。。。
//IPC通信,通知WMS渲染。
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
//省略代码。。。
}
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查当前执行的线程是不是UI线程
checkThread();
mLayoutRequested = true;
//处理DecorView的measure、layout、draw。
scheduleTraversals();
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
}
ViewRootImpl.setView()的伪代码有两句,但我们只关心requestLayout。因为mWindowSession.addToDisplay()就是通过IPC通知WMS去渲染,我们再去分析WMS意义已经不大了。requestLayout()方法首先会检查当前执行的线程是不是UI线程,随后调用scheduleTraversals()。scheduleTraversals会把本次请求封装成一个TraversalRunnable对象,这个对象最后会交给Handler去处理。最后ViewRootImpl.performTraversals()被调用。调用链如下:
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//省略代码。。。
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//省略代码。。。
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//处理DecorView的measure、layout、draw。
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()主要是处理View树的measure、layout、draw等流程。不清楚的同学可以去看《Android开发艺术探索》第四章,我在这里就不继续深入了。
下面我回答文章前言部分提出的几个问题。
要理解Window需要从面向对象的角度出发。
更新视图时,线程检查是在ViewRootImpl的checkThread()中。ViewRootImpl的初始化是在Activity的onResume()方法之后。因此,如果有子线程在onResume之前更新UI是可以成功的。当然还有一种Hook ViewRootImpl的mThread的方法也可以更新UI。这里不做介绍了。
这个问题和子线程不能更新UI的问题很像,也是方法执行时机的一个问题。View的measure、layout、draw。发生在Activity.onResume()之后,因此在onResume()之前都是无法获取View的宽、高等信息的。
面试造火箭,工作拧螺丝。虽然我只想拧螺丝,但是我们却需要通过造火箭来找到拧螺丝的工作。
有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
一些基础知识和理论肯定是要背的,要理解的背,用自己的语言总结一下背下来。
最后我在这里分享一下这段时间从朋友,大佬那里收集到的一些2019-2020BAT 面试真题解析,里面内容很多也很系统,包含了很多内容:Android 基础、Java 基础、Android 源码相关分析、常见的一些原理性问题
等等,可以很好地帮助我们深刻理解Android相关知识点的原理以及面试相关知识。
这份资料把大厂面试中常被问到的技术点整理成了 PDF ,包知识脉络 + 诸多细节;还有 高级架构技术进阶脑图 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
以上内容均放在了开源项目:【github】 中已收录,里面包含不同方向的自学Android路线、面试题集合/面经、及系列技术文章等,资源持续更新中...
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。