DEVLOG 10.21 Android UI体系知识&面试题
房顶上的铝皮水塔
2021年10月21日 15:47
收录于文集
共19篇

参考内容:AndroidUI体系​


问题:

  1. 在Activity中Window和View如何分工协作?

  2. 何时在Activity中获取View宽高?

这篇文章主要是回答以上的问题,但是在回答以上的问题中也会出现一些子问题。弄清楚这些问题,有利于我们了解Window WindowManager Activity View的关系。


在Activity中Window和View如何分工协作?

我们知道ActivityThread相当于是App的主函数类,Activity#onCreate的开始可以追溯到ActivityThread#​handleLaunchActivity中:

代码块
JavaScript
自动换行
复制代码
 public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        // .....

        // 初始化全局的WindowManagerGlobal
        WindowManagerGlobal.initialize();

        // 在performLaunchActivity中会调用Activity#attach
        final Activity a = performLaunchActivity(r, customIntent);

        // ....

        return a;
    }
复制成功

handleLaunchActivity中首先初始化了WindowManagerGlobal,从名字可以看出,这个类一定和Window以及WindowManager是有关的,至于这个类如何和Window和WM产生关联,我们待会会总结。然后在handleLaunchActivity中,又调用了performLaunchActivity,在这个方法中会调用Activity#attach。

ActivityThread#performLaunchActivity

代码块
JavaScript
自动换行
复制代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // ... 
        Activity activity = null;

        // 通过反射的方式获取Activity,Instrumentation是一个和Activity的启动密切相关的类
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            // .....
        } catch (Exception e) {
           // ......
        }

        // 获取当前的Application的实例
        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

                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.configCallback,
                        r.assistToken);

            }
            r.setState(ON_CREATE);

            // updatePendingActivityConfiguration() reads from mActivities to update
            // ActivityClientRecord which runs in a different thread. Protect modifications to
            // mActivities to avoid race.
            synchronized (mResourcesManager) {
                mActivities.put(r.token, r);
            }

        } catch (SuperNotCalledException e) {
            throw e;

        } catch (Exception e) {
             // .....
            }
        }

        return activity;
    }
复制成功

performActivity通过反射机制创建了Activity的实例,并且构建了Application的实例,当attach方法执行完成之后,使用记录当前的Activity状态是ON_CREATE。那么,不言而喻,attach方法中应该会调用我们实现的Activity#onCreate回调。接着我们来看看Activity#​attach方法:

Activity#attach

代码块
JavaScript
自动换行
复制代码
 // ... 省略参数
 final void attach(  ) {
        // ....

        // 创建PhoneWindow实例
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(mWindowControllerCallback);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
       
        // .....
       
        // 将Window和WindowManager绑定
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        // .....
    }
复制成功

因为Activity的回调方法中通常我们使用setContentView加载当前resId指定的布局,但是这个布局是在DecorView的ContentView中,而DecorView又在PhoneWindow中。所以当前Activity#attach时需要创建PhoneWindow的实例,并且绑定PhoneWindow到WindowManager中。

继续看ActivityThread#performLaunchActivity

当Activity#attach执行完成之后,performLaunchActivity继续执行,会执行到这一行代码:

代码块
JavaScript
自动换行
复制代码
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
复制成功

在Instrumentation#callActivityOnCreate中会调用Activity#​performCreate,这里面就回调用我们写的onCreate回调。因此,一个不太标准的从ActivityThread#handlePerformLaunchActivity到Activity#onCreate的时序图,如下图所示:

虽然我们知道在Activity的生命周期方法调度完成时我们可以看到我们写的布局文件,但是我们目前并不能看到Window和View的关系,于是我们可以猜想,既然onCreate方法中没有,那么,Window加载View的代码可能会在onResume上。这是因为在官方文档中也说明,onResume是程序到前台的标志。

ActivityThread#handleResumeActivity

ActivityThread#handleResumeActivity方法首先调用performResumeActivity,在performResumeActivity完成之后在会调用WindowManager#addView向Window中添加布局。

代码块
JavaScript
自动换行
复制代码
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
        
        // ...
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

        // 当前的Activity已经在onCreate中创建好了
        final Activity a = r.activity;

        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();
            // ....
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    // ....
                }
            }
            //... 
        } 
        // ....
    }
复制成功

所以,实际上布局是在onResume完成之后才被加载在PhoneWindow中的,不过具体的内容我们还是需要看看performResumeActivity:

ActivityThread#performResumeActivity

代码块
JavaScript
自动换行
复制代码
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
            String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        // 对于Lifecycle的一些判断
        if (r.getLifecycleState() == ON_RESUME) {
            if (!finalStateRequest) {
               // ... 如果已经是onResume 就回抛出异常
            }
            return null;
        }
       
        // ....

        try {
            // ...

            // 调用Activity#performResume
            r.activity.performResume(r.startsNotResumed, reason);

            r.state = null;
            r.persistentState = null;
            r.setState(ON_RESUME);

            reportTopResumedActivityChanged(r, r.isTopResumedActivity, "topWhenResuming");
        } catch (Exception e) {
           // ....
        }
        return r;
    }
复制成功

套路和前面的都差不多,在ActivityThread中执行Activity#performResume然后转到启动相关类Instrumentation#callActivityOnResume,在执行onResume回调。

因此我们可以做一个小小的总结,关于Activity中Window和View,他们之间的合作关系和Activity的生命周期是密切相关的:

onCreate阶段:

但是在onCreate中,并不会把View加载到PhoneWindow中,这个说来也非常好理解,毕竟我们在回调中才解析布局文件xml,怎么会在PhoneWindow创建之前addView呢?

onResume阶段:

ViewRootImpl如何成为Window和View的桥梁?

刚才我们看到WindowManager可以将DecorView加载到PhoneWindow中,这个过程还可以仔细地分析一番。在分析之前我们先总结一下Window相关的类之间的关系:

ViewManager只是一个接口,定义了基本的对于View的添加和删除工作

代码块
JavaScript
自动换行
复制代码
public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}
复制成功

WindowManager也是一个接口,但是我们通常操作的都是这个类,他的实现类是WindowManageImpl。WindowManagerImpl又通过将职责委托给WindowManagerGlobal实现,之所以使用这么复杂的【套娃】逻辑,好处有两点:

  1. 这是一种外观模式,我们通过WindowManager就可以操作WindowManagerGlobal和framework层通信。

  2. WindowManagerGlobal也是一个全局单例。根据上面的代码分析,创建Activity就回创建对应的WindowManager和PhoneWindow,但是这些所有的WindowManager都基于WindowManagerGlobal,节省内存。

回到问题【ViewRootImpl如何成为Window和View的桥梁?】本身,我们需要查看一下WindowManager#addView的代码:

代码块
JavaScript
自动换行
复制代码
// WindowManagerImpl#addView
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }


    // WindowManagerGlobal#addView
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
         
        // ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
           
            // ...

            // 创建了ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                // 调用ViewRootImpl#setView
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
               // ..
            }
        }
    }
复制成功

在WindowManagerGlobal中初始化了ViewRootImpl,然后调用了setView。跟踪ViewRootImpl#setView可以发现这个方法最后会调用WindowSession#addToDispaly,再调用WindowManagerService#addWindow,整体的调用链如图:

所以可以看到ViewRootImpl确实充当了一个桥梁,上面抓住了WindowManager,下面连接的是WindowSession(是IWindowSession.Stub的实现类,是Binder机制的一部分)。

如何onResume中获取View的宽高?

这个问题可以转换成另外的一个子问题,View#measure是在什么时候执行的?

View#measure的执行时机

上面已经说过个, onResume会先于WindowManager#addView,所以回调函数本身会在测量之前执行,这样在onResume中尝试获取宽高一定会返回0更不用说在onCreate中。

解决问题的思路:

  1. handler.post( Runnable {}, delay):这里不可以不加delay。如果不加delay,这个消息在消息队列中还是会先执行,而我们的目的是想等到onResume执行完成,WindowManager#addView之后再拿到View宽高,这个大小设置为100ms。

  2. View.post(Runnable {}): View.post的原理就是将当前的Runnable放入了HandlerActionQueue的数组中,然后在ViewRootImpl#performTraversals中执行。ViewRootImpl#performTraversals是执行测量 布局 绘制的开始,肯定也会在WindowManager#addView之后

代码块
JavaScript
自动换行
复制代码
public boolean post(Runnable action) {
        
        // ....
        // getRunQueue返回一个HandlerActionQueue
        getRunQueue().post(action);
        return true;
    }

   public class HandlerActionQueue {
        private HandlerAction[] mActions;
        private int mCount;

        public void post(Runnable action) {
            postDelayed(action, 0);
        }

        // 放入数组,具体的执行动作是在ViewRootImpl#performTraversals
        public void postDelayed(Runnable action, long delayMillis) {
            final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

            synchronized (this) {
                if (mActions == null) {
                    mActions = new HandlerAction[4];
                }
                mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
                mCount++;
            }
        }
    }
复制成功

3.  onWindowFocusChanged回调:当失去焦点时会被调用

4. addOnGlobalLayoutListener:当ViewTree变化的时候会被调用。

在子线程中能不能更新UI?

这个例子中代码会报错吗?

代码块
JavaScript
自动换行
复制代码
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE)
        setContentView(R.layout.activity_main)

        // 这行代码会报错吗?
        thread {
            val textView = findViewById<TextView>(R.id.test).apply {
                this.text = "new message"
            }
        }
    }
复制成功

其实并不会,因为在这里,textView被setContentView加载之后设置setText时只是将String变量存储到TextView中,此时TextView并没有开始performTraversals,不会检查UI线程,所以没有问题。