目录:
一、 为什么要学习android UI绘制原理呢?对我们有什么帮助?
1.解决复杂布局问题:了解UI绘制原理可以帮助我们更好地理解和解决布局问题,比如使用自定义View、优化布局层级等。
2.知道何时触发布局(Layout)、绘制(Draw)和测量(Measure)过程,以及如何减少这些过程的调用次数,避免在UI线程上进行耗时的操作,可以显著提升应用的流畅度和响应速度。
二、为什么子线程不能刷新UI呢?原因是什么?
比如,我们写如下这样的代码,那么就有可能报错。
kt
Thread(object :Runnable{
override fun run() {
tvTestUi.text = "123412341234"
}
}).start()
报错内容:只有在主线程种对UI进行操作才行。
xml
Only the original thread that created a view hierarchy can touch its views.
我们可以追踪到源码里面看看。后面我们会讲一下原因。
下面我们可以看到,绘制UI的线程,如果不是主线程,那么就报错。
但,为什么不能子线程呢?多线程更新UI不是会更加高效? 只是因为代码里面限制?!!当然不是。
子线程不能直接刷新UI的原因主要与Android系统的UI线程(主线程)的设计和机制有关。在Android中,UI组件(如视图、控件等)不是线程安全的,这意味着它们的设计初衷是为了在单个线程(即UI线程或主线程)上被访问和修改。如果多个线程尝试同时修改UI组件,就可能会导致不可预见的行为,比如视图的不一致状态、崩溃等。
假如你可以多线程更新 ,那么你得花时间确定更新状态是否一致,界面重复刷新问题,像素结果是否统一的问题,要同步,所以代价是相当大的,所以绝大多数的系统,对UI刷新,都是采用单线程的方式。
但,为什么在oncreate中开子线程刷新ui不会报错呢?看源码我们就会知道,viewRootImpl 的初始化在 onCreate 之后,onResume 之后。所以也就没调用checkThread方法。
具体来说,当Activity调用setContentView()时,它会通过WindowManager(实际上是WindowManagerGlobal和WindowManagerImpl)来请求添加一个窗口(Window)。这个过程中,会创建并初始化ViewRootImpl实例,然后将其与Activity的根视图(DecorView)关联起来。
由于ViewRootImpl的初始化是异步的,并且涉及到与窗口系统的交互,因此很难直接通过Activity的生命周期方法来准确判断ViewRootImpl的初始化完成时刻。但是,我们可以知道,在onResume()之后,并且视图开始绘制之前,ViewRootImpl应该已经被初始化了。
三、UI绘制原理
我们再回到上面的代码。
Thread current = Thread.currentThread(); 这行代码的意思是获取当前正在执行的线程对象,那么当前运行的线程是什么线程 ??为什么会调用ViewRootImpl的checkThread方法呢??为什么text的时候,会重新绘制呢?
这,就需要我们了解UI的绘制流程。
Android UI的绘制流程是一个从数据加载到Activity启动,再到View的测量、布局和绘制的过程。我们直接从创建Activity实例这里开始。
下面我们讲一下流程。从创建Activity开始。
3.1 创建Activity 实例和view的树型结构
ActivityThread,通过handleResumeActivity方法创建Activity实例后,并为其创建一个PhoneWindow,合成DecorView。
我们可以看到当我们调用setContentView的时候,就是调用了window的。
DecorView是顶级容它内部包含了一个或多个子View或ViewGroup,用于承载应用的UI内容。
那么接下来,view创建好后,如果我想要进行view的渲染和刷新?由谁来做呢?是如何触发刷新的?下面我们看看ViewRootImpl
3.2 是如何触发刷新view的?
我们先了解一下,VSYNC是什么?VSYNC信号由屏幕(显示设备)产生,并以固定频率发送给Android系统。Android系统中的SurfaceFlinger接收VSYNC信号后,会遍历其层列表以查找新的缓冲区进行渲染。这种机制提升了渲染任务的优先级,优化了渲染性能。
刷新也分为手动刷新和自动刷新(VSYNC就是自动刷新),比如我们调用textView的text方法,就是手动刷新,会调用requestLayout方法,不断的递归requestLayout方法去进行刷新。因为它是一个树形结构。
自动刷新,其实就是一个回调。下面是源码,可以粗略看看。
performTraversals();方法就是会绘制的方法,比如测量等
3.3 管理绘制的类:ViewRootImpl
我们都知道,写的这些xml布局代码,都是一个树形的层次结构,比如下面的代码,就对应一个这样的结构(如图),举例哈:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.main.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/home_fragmentcontainerview"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/home_nav" />
<TextView
android:id="@+id/tv_test_ui"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
而这些结构呢,是由ViewRootImpl来进行管理,比如进行测量,布局以及绘制等等。
View的绘制流程是通过ViewRootImpl类来进行管理,在ActivityThread(主线程)中,当Activity对象被创建完毕后,会将DecorView添加到Window中,并创建对应的ViewRootImpl对象,将两者建立关联。
我们可以看到是ActivityThread线程创建了ViewRootImpl,所以Thread current = Thread.currentThread();获得的当前线程,就是主线程。如果是子线程刷新,那么Thread current = Thread.currentThread()就是子线程。
2.4 View的绘制流程:测量(Measure)
为什么需要测量呢?确定View的宽高,用于后续绘制。
有没有想过,wrap_content和match_parcent的宽高如何确定呀,就需要测量,并且每个view的宽高,还要取自于上一层的,所以ViewGroup遍历所有子View进行测量,根据子View的LayoutParams和自身的MeasureSpec计算出子View的测量规格。
2.5 View的绘制流程:布局(Layout)
根据测量的宽高确定View在其父View中的位置(即四个顶点的坐标)。也是会递归遍历对子View进行布局。
2.6 View的绘制流程:绘制(Draw)
这个阶段的作用,就是将View的内容绘制到屏幕上。也是会递归遍历子View,调用子View的draw方法。
到这里了,view的绘制流程就大致完成了。
三、学习总结
刚开始看UI绘制原理的时候,完全看不懂,硬着头皮去看,渐渐的有些可以看懂了,但绝大部分还是不懂。这个时候,我就从"为什么子线程不能刷新UI呢?"入手,比如不能刷新原因是什么,了解原因后,你懂了,但你会发现你不懂的地方也会更多,但是,你已经知道你有哪些不懂了,这个时候,你重新回头去看第二篇的时候,你思路就清晰很多了,你又能看懂很多了。
所以,第一次看肯定有很多不懂,那么就第二次,第三次。慢慢的你就有思路,开始知道一些东西,熟能生巧,很多人都是看一次,觉得难就不学了,但很多东西,都是需要经历无数次,你才会熟悉,才会熟练。