setContentView流程简析

1,252 阅读5分钟

setContentView流程简述

  • setContentView调用了window.setContentView方法,window是一个抽象类,在Android中,window只有一个实现类,phoneWindow
  • ActivityThread通过performLaunchActivity创建了Activity,Activity中的生命周期函数如onResume等实际上就是ActivityThread调用了Activity的各个方法而已
    • 如onCreate,是ActivityThread调用了performCreate方法
    • attach方法是在onCreate方法之前调用的,在这个方法中,window完成了赋值
    • mWindow = new PhoneWindow(...)
  • DecorView是window中的顶层容器,用户的xml视图通过DecorView的addView方法被添加到了Decordview中,不同的设置对应不同的父布局xml,在用户不进行任何requestWindowFeature(如隐藏标题等)设置的情况下,最终用户的xml中会添加到screen_simple.xml中,具体是通过inflater.inflate的方式添加到了id为content的FrameLayout中去
    • 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" />
        <FrameLayout
             android: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>
    
    • 其他的比如requestWindowFeature(FEATURE_NO_TITLE)其实就是替换了顶层xml布局,甚至其中也包含常见的VISIBLE、GONE操作 <img alt="48" src="" />

完整流程如下

对于上图的解释如下:

  1. 每一个Activiy都有一个Window对象,它的具体实例是一个PhoneWindow,phoneWindow的最顶层容器是一个DecorView,DecorView是布局顶层容器
  2. 每一个Activity都有一个windowManger对象,它的具体实例是WindowMangerImpl,WindowMangerImpl的addView调用了WindowMangerGlobal对象的addView方法
  3. WindowMangerGlobal是一个单例对象,是一个全局掌控者,addView的方法将用户的xml文件填充到了DecorView的布局中
  4. 所以实际上我们在setContentView(xml layout)上实际上还包含了多层父布局层级,自顶向下包含了
    • 最顶层Window,包含DecorView对象
    • DecorView包含了statusBar、actionBar,以及一个id为content的FrameLayout(根据布局属性不同内部有差别)
    • xml layout最终被添加填充到了FrameLayout中
  5. 简单来讲,就是mWindow负责创建了窗体和布局容器DecorView,而mWindowManager负责将布局依附到布局容器DecorView中
  6. 除去中间的处理流程,其余的测量布局绘制流程基本上就是View的绘制流程measure、layout、draw

关键函数调用

  • performLaunchActivity创建了Activity,执行Activity初始化,这个过程创建了一些对象,包括PhoneWindow,PhoneWindowManager等,然后会创建出DecorView,执行onCreate函数等
  • handleResumeActivity调用了onResume,由于这个时候还没有走到绘制路程,所以这时候不能获取view的宽高,之后会调用windowManager的addView方法,它调用了WindowManagerGloabal的addView方法,new创建了viewImpl对象,调用了setView方法,调用之后,会执行scheduleTraversals,会向主线程post一个callback
  • 下一次进行轮询的时候,会执行performTraversals,这次的执行会调用decorView的measure和layout方法,第一次调用并不会马上绘制
  • 然后在下次轮询的时候,再次执行performTraversals的时候,进行真正的绘制draw

子线程到底能不能更新UI

CSDN _ Android中子线程真的不能更新UI吗?

onCreate{
    //可以更新
    thread{
      tv_setview.text = "${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
    }
    //不可以更新
    thread{
      sleep(2000)
      tv_setview.text = "${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
    }
}
    
  • 可以更新的原因是ViewRootImpl没有创建出来,不会触发checkThread方法
  • 不可以更新的原因是睡了两秒之后ViewRootImpl被创建出来,在访问UI的时候,ViewRootImpl会去检查当前是哪个线程访问的UI,ViewRootImpl的checkThread方法
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    
    • 如果不是主线程,那就会抛出如下异常:Only the original thread that created a view hierarchy can touch its views
  • 如果想要在子线程中弹吐司,需要添加Looper代码,否则会报异常Can't toast on a thread that has not called Looper.prepare()
    //子线程吐司不报错写法
    thread{
      Looper.prepare()
      Toast.makeText(this,"sss",0).show();
      Looper.loop()
    }
    

子线程更新UI的方式

  1. 主线程申请成功后子线程申请

    tv_setview.setOnClickListener(View.OnClickListener {
          tv_setview.text = "Main"
          thread{
            tv_setview.text = "${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
          }
        })
    

    (补充解释: textview.text = "Main" 这一行代码会在主线程运行,这会在主线程正常的执行 requsetLayout 的整个流程,这样就完成了「申请」修改布局。 此时,在子线程立即调用 textview.text = "xx.." 这个代码就会因为它已经「申请」过 requestLayout 了,就不会层层往上调用 parent 的 requsetLayout() 方法,也就不会在子线程 触发 checkThread() 方法了!)

  2. 在子线程中创建ViewImpl

    thread {
            Looper.prepare()
            val button = Button(this)
            windowManager.addView(button, WindowManager.LayoutParams())
            button.setOnClickListener(OnClickListener {
              button.text = "${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
            })
            Looper.loop()
          }
    
  3. 利用硬件加速机制绕开 requestLayout()
    在硬件加速的支持下,如果控件只是进行了 invalidate() ,而没有触发 requestLayout() 是 不会触发 ViewRootImpl#checkThread() 的。

  4. SurfaceView
    Android 中有一个控件 SurfaceView ,它可以通过 holder 获得 Canvas 对象,可以直接在子线 程中更新 UI。

view.post() 传入的 Runnable 一定会被执行吗?为什么?

感觉简书上的这个答案靠谱

Dialog的布局流程

与Activity类似,最终顶层布局也是DecorView,默认布局是alert_dialog.xml
其他情况如下,核心类是AlertController

AlertController(...){
    ...
    
    //获取不同布局在安卓系统中对应的id
    mAlertDialogLayout = a.getResourceId(com.android.internal.R.styleable.AlertDialog_layout,
            com.android.internal.R.layout.alert_dialog);
    mButtonPanelSideLayout = a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_buttonPanelSideLayout, 0);

    mListLayout = a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_listLayout,
            com.android.internal.R.layout.select_dialog);
    mMultiChoiceItemLayout = a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_multiChoiceItemLayout,
            com.android.internal.R.layout.select_dialog_multichoice);
    mSingleChoiceItemLayout = a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout,
            com.android.internal.R.layout.select_dialog_singlechoice);
    mListItemLayout = a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_listItemLayout,
            com.android.internal.R.layout.select_dialog_item);
    
    ...
}