AI智能
改变未来

Android性能优化之布局优化实战


​本文首发于微信公众号「Android开发之旅」,欢迎关注

Jetpack版Wan-Android项目地址:Android Jetpack架构开发组件化应用实战 欢迎star

Flutter版Wan-Android项目地址:Flutter版Wan-Android 欢迎star

Android绘制原理

手机渲染主要依赖于两个硬件:CPU和GPU,其中CPU主要负责计算显示内容,其中包括视图创建、布局计算、图片解码和文本绘制等。GPU主要负责栅格化(UI元素绘制到屏幕上),比如将Button、Bitmap拆分成不同的像素进行显示,最后完成绘制。

手机上显示的文字就是先通过CPU换算成纹理后在交给GPU进行渲染。而图片的显示首先通过CPU进行计算,然后再加载到内存中,传给GPU进行渲染。

我们都知道Android系统每隔16ms就会发出Vsync信号(具体是由RootViewImpl类发起)触发UI渲染,即要求每一帧都要在16ms内渲染完成,所以不管你的布局逻辑多么的复杂,你都要在16ms内绘制完成,否则就会出现界面卡顿的现象。

我们市面上绝大部分Android手机的屏幕刷新频率基本都是60Hz,因为60Hz每秒是人眼和大脑之间合作的极限,就像动画每秒24帧一样。

优化工具选择

Systrace

这个我们在启动优化中讲过具体的使用,这里呢,我们主要关注他的Frames一行,显示绿色圆点表示正常,显示黄色或者红色表示出现了丢帧,出现丢帧的情况的时候我们需要去查看Alerts栏。

Layout Inspector

这个是Android Studio自带的检测工具,在Tools栏目下。它可以帮助我们查看视图的层次结构。

从图中我们可以看到左侧一览显示布局的层级。

ChoreoGrapher

choreoGrapher可以帮助我们获取应用的FPS,即上文中的60Hz,并且可以线上使用,具备实时性。但是有一点需要注意的是必须API 16后使用。如下代码:

private var mStartFrameTime: Long = 0private var mFrameCount = 0private val MONITOR_INTERVAL = 160L //单次计算FPS使用160毫秒private val MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000Lprivate val MAX_INTERVAL = 1000L //设置计算fps的单位时间间隔1000ms,即fps/s;​override fun onCreate(savedInstanceState: Bundle?) {setTheme(R.style.AppTheme)super.onCreate(savedInstanceState)​setContentView(R.layout.activity_main)getFPS()}​​@TargetApi(Build.VERSION_CODES.JELLY_BEAN)private fun getFPS() {if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {return}Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {override fun doFrame(frameTimeNanos: Long) {if (mStartFrameTime == 0L) {mStartFrameTime = frameTimeNanos}val interval = frameTimeNanos - mStartFrameTimeif (interval > MONITOR_INTERVAL_NANOS) {val fps = (mFrameCount.toLong() * 1000L * 1000L).toDouble() / interval * MAX_INTERVALLog.i(\"fps\", fps.toString())mFrameCount = 0mStartFrameTime = 0} else {++mFrameCount}​Choreographer.getInstance().postFrameCallback(this)}})​​}

执行代码后输出:

fps: 60.0158955700371fps: 60.00346688030940fps: 60.01226146521353fps: 59.98537016806971fps: 60.00205735054243

每次打印的数据都在60左右,说明页面刷新没有出现卡顿。

布局加载原理

我们经常写的XML布局文件是如何被加载的呢?又是如何显示出来的?下面就带着大家顺着源码往下看,这里就不截图了,读者朋友们看完本章后自己可以去熟悉下这块代码。

首先要从setContentView方法开始说起了,其中调用了getDeleate().setContentView(resid)方法,接着调用了 LayoutInflater.from(this.mContext).inflate(resId, contentParent)来填充布局,这个API我们大家应该都很熟悉了吧。紧接着调用getLayout方法,在getlayout方法中通过loadXmlResourceParser加载并解析XML布局文件,后面调用createViewFromTag方法,根据标签创建相对应为view,具体view的创建则是由Factory或者Factory2来完成的,首先先判断了Factory2为否为null,不为null,则用其创建view,否则就判断Factory是否为null,不为null,则由其创建。如果两个都为null,则不创建view,紧接着判断了mPrivateFactory是否为null,这里需要说明的是mPrivateFactory是一个隐藏的API只有framework才能调用,如果都没创建,那么view则由后续逻辑通过onCreateView或者createView通过反射来创建。具体流程图如下:

从上面的分析中我们可以看出加载布局是有瓶颈的。其中有两个瓶颈分别是在布局文件解析的时候是一个IO过程,这肯定是比较耗时的。再一个就是最后创建View的时候是通过反射的方式进行的。既然是反射性能肯定也是有影响的,后面我们也是围绕这两点进行布局加载的优化。

获取界面布局耗时

我们做优化的前提就是得知道哪里是比较耗时的,所以检测耗时的UI还是蛮重要的。只有知道问题在哪了才能针对性的解决它。这里讲到检测耗时,读过我启动优化一文的读者肯定能想到至少两种方式,一种是手动埋点,另外一种就是AOP的方式。手动埋点呢就是在setContentView方法的前后执行的地方手动打点。

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)LaunchRecord.startRecord()setContentView(R.layout.activity_main)LaunchRecord.endRecord(\"setContentView\")​}

打印:

===setContentView===170

这种方式呢不够优雅而且对代码有侵入性。

下面我们看下AOP的方式,操作和启动优化一文中的一样。

@Around(\"call(* android.app.Activity.setContentView(..))\")public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {Signature signature = joinPoint.getSignature();String name = signature.toShortString();long time = System.currentTimeMillis();try {joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}Log.d(\"ContentViewTime\", name + \" cost \" + (System.currentTimeMillis() - time));}

控制台打印:

ContentViewTime: MainActivity.setContentView(..) cost 74

以上两种方法都是获取全部布局被加载完成后的时间,那么如果想获取单个控件的加载耗时如何做呢?这里给大家介绍LayoutInflaterCompat.setFactory2方式(大家以后看到带有Compat字段的都是兼容的API),其使用必须在super.onCreate之前调用。

public class MainActivity extends AppCompatActivity {​@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {​LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {​long start = System.currentTimeMillis();View view = getDelegate().createView(parent, name, context, attrs);long cost = System.currentTimeMillis() - start;Log.d(\"onCreateView\", \"==\" + name + \"==cost==\" + cost);return view;}​@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}});​super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}}

控制台打印:

onCreateView: ==LinearLayout=cost==16onCreateView: ==ViewStub=cost==0onCreateView: ==FrameLayout=cost==0onCreateView: ==android.support.v7.widget.ActionBarOverlayLayout=cost==0onCreateView: ==android.support.v7.widget.ContentFrameLayout=cost==0onCreateView: ==android.support.v7.widget.ActionBarContainer=cost==0onCreateView: ==android.support.v7.widget.Toolbar=cost==0onCreateView: ==android.support.v7.widget.ActionBarContextView=cost==0onCreateView: ==android.support.constraint.ConstraintLayout=cost==0onCreateView: ==TextView=cost==3onCreateView: ==ImageView=cost==24

LayoutInflaterCompat.setFactory2的API不仅仅是可以统计View创建的时间,其实我们还可以用来替换系统控件的操作,比如某一天产品经理提了一个需求要我们将应用的TextView统一改成某种样式,我们就可以使用这种方式来做。如:

LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {​if(TextUtils.equals(\"TextView\",name)){//替换为我们自己的TextView​}​return null;//返回自定义View}​@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}});

只要我们在基类Activity的onCreate中定义这个方法,就可以实现相关效果。

布局加载优化

基于布局加载的两个性能问题,谷歌给我们提供了一个类AsyncLayoutInflater,它可以从侧面解决布局加载耗时的问题,AsyncLayoutInflater是在工作线程中加载布局,加载好后会回调到主线程,这样可以节省主线程的时间。这个类没有包含在SDK中,需要我们在gradle中配置,如:

implementation \'com.android.support:asynclayoutinflater:28.0.0-alpha1\'

使用:

public class MainActivity extends AppCompatActivity {​@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {​new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null,new AsyncLayoutInflater.OnInflateFinishedListener() {@Overridepublic void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {setContentView(view); //view以及加载完成//可以在这里findViewById相关操作}});​super.onCreate(savedInstanceState);//  setContentView(R.layout.activity_main); //这里就不用设置布局文件了}}

我们在inflate的时候就将布局文件设置给AsyncLayoutInflater,所以下面我们就不需要在setContentView了。

上面说的AsyncLayoutInflater是从侧面解决布局加载耗时问题,那么我们如何从根本上解决这个问题呢?主要问题就是我们书写的XML文件需要加载解析和绘制,那如果我们不使用XML文件写布局文件,问题是不是就解决?在Android中,还有另外一种方式来写布局文件,那就是Java代码,通过Java代码来写布局,本质上是解决了性能问题,但是不便于开发,没有实时预览,而且可维护性太差。那么如果能有一种解决方式就是,我们开发人员还是正常写 XML文件,但是在加载的时候加载的是Java代码,那这样是不是很完美了。

所以下面给大家介绍一个新的框架:X2C,这是掌阅开源的一个框架,它保留了XML的优点,同时解决了性能问题,开发人员写XML文件,加载的时候只加载Java代码。

X2C的原理就是通过APT编译期时将XML翻译为Java代码。使用也很简单,首先gradle配置:

annotationProcessor \'com.zhangyue.we:x2c-apt:1.1.2\'implementation \'com.zhangyue.we:x2c-lib:1.0.6\'

Java代码使用:

@Xml(layouts = \"activity_main\")public class MainActivity extends AppCompatActivity {​@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);//  setContentView(R.layout.activity_main); //这里就不用设置布局文件了}}

编译之后会在build/generated/source/apt/debug/ 下面生成相关的文件。如我们的activity_main的布局文件会被翻译为:

Resources res = ctx.getResources();​ConstraintLayout constraintLayout0 = new ConstraintLayout(ctx);​TextView textView1 = new TextView(ctx);ConstraintLayout.LayoutParams layoutParam1 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);textView1.setId(R.id.mTextView);layoutParam1.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,res.getDisplayMetrics())) ;textView1.setText(\"Hello World!\");layoutParam1.leftToLeft = 0 ;layoutParam1.rightToRight = 0 ;layoutParam1.topToTop = 0 ;layoutParam1.validate();textView1.setLayoutParams(layoutParam1);constraintLayout0.addView(textView1);​ImageView imageView2 = new ImageView(ctx);ConstraintLayout.LayoutParams layoutParam2 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);layoutParam2.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20,res.getDisplayMetrics())) ;imageView2.setImageResource(R.mipmap.ic_launcher);layoutParam2.leftToLeft = 0 ;layoutParam2.rightToRight = 0 ;layoutParam2.topToBottom = R.id.mTextView ;layoutParam2.validate();imageView2.setLayoutParams(layoutParam2);constraintLayout0.addView(imageView2);

运行APP,效果也是一样的。

X2C虽好,但也有一些问题,就是部分属性Java不支持,而且失去了系统的兼容性(AppCompat)。所以如果要带到线上使用,那么就要兼容不同的版本,所以需要定制化修改源码。

视图绘制优化

我们知道视图的绘制通常经历三个阶段,测量,确定view的大小。布局,确定view的具体位置包括view和viewGroup等。绘制,将view绘制完成。不管是测量、布局还是绘制,每一个阶段都是比较耗时的,都是自上而下的遍历每一个view,在某些场景下还会触发多次,比如嵌套使用RelativeLayout布局。

所以为了减少三个阶段的耗时,我们需要减少view树的层级,不要嵌套使用RelativeLayout布局,不在嵌套使用的LinearLayout中使用weight属性。适当的使用merge标签,它可以减少一个view层级,但是必须使用在根view上。

这里推荐大家使用ConstraintLayout布局,ConstraintLayout几乎实现了完全扁平化的布局,而且在构建复杂布局上面性能更高,同时他还具备了RelativeLayout和LinearLayout的特性,使用很方便。

同时我们在书写布局的时候还要注意避免过度绘制。Android手机在开发者选项中有个功能叫:调试GPU过度绘制。打开后手机界面会有一层蒙版,其中蓝色表示可以接受,红色表色出现过度绘制了。那我们如何避免过度绘制呢?首先是去掉多余的背景色,减少复杂shape的使用,避免层级叠加,在用自定义view的时候使用ClipRect屏蔽被遮盖view的绘制。

还有其他的一些优化视图绘制,比如使用Viewstub,它是一个高效的占位符,可以用来延迟加载view布局。还有就是我们在onDraw中避免创建较大的对象和做耗时的操作等等。

总结

以上就是相关布局优化相关的操作,也是从耗时到优化各个阶段的说明和操作。读者朋友们在看完本章节后,自己动手实践下,只有实际实践了才能发现问题,加深自己印象。

推荐阅读:

App性能概览与平台化实践理论

Android性能优化之启动优化实战

如何监测Android应用卡顿?这篇就够了

扫描下方二维码关注公众号,及时获取文章推送。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Android性能优化之布局优化实战