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操作
完整流程如下
对于上图的解释如下:
- 每一个Activiy都有一个Window对象,它的具体实例是一个PhoneWindow,phoneWindow的最顶层容器是一个DecorView,DecorView是布局顶层容器
- 每一个Activity都有一个windowManger对象,它的具体实例是WindowMangerImpl,WindowMangerImpl的addView调用了WindowMangerGlobal对象的addView方法
- WindowMangerGlobal是一个单例对象,是一个全局掌控者,addView的方法将用户的xml文件填充到了DecorView的布局中
- 所以实际上我们在setContentView(xml layout)上实际上还包含了多层父布局层级,自顶向下包含了
- 最顶层Window,包含DecorView对象
- DecorView包含了statusBar、actionBar,以及一个id为content的FrameLayout(根据布局属性不同内部有差别)
- xml layout最终被添加填充到了FrameLayout中
- 简单来讲,就是mWindow负责创建了窗体和布局容器DecorView,而mWindowManager负责将布局依附到布局容器DecorView中
- 除去中间的处理流程,其余的测量布局绘制流程基本上就是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
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的方式
-
主线程申请成功后子线程申请
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() 方法了!)
-
在子线程中创建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() } -
利用硬件加速机制绕开 requestLayout()
在硬件加速的支持下,如果控件只是进行了 invalidate() ,而没有触发 requestLayout() 是 不会触发 ViewRootImpl#checkThread() 的。 -
SurfaceView
Android 中有一个控件 SurfaceView ,它可以通过 holder 获得 Canvas 对象,可以直接在子线 程中更新 UI。
view.post() 传入的 Runnable 一定会被执行吗?为什么?
- View.post()只有在View attachedToWindow的时候才会立即执行。
- 在attach之前调用的话,低于7.0时可以自己new一个Handler或者自定义View重写dispatchAttachedToWindow自己储存pendingTask并在attached之后执行。 其他答案
- Android View.post(Runable)某些情况不执行的原因
- View的post(Runnable)方法非100%执行的原因和处理方法解析
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);
...
}