android表格布局详解(android表格布局合并行)

为什么要进行布局优化

如果布局嵌套过深,或者其他原因导致布局渲染性能不佳,可能会导致应用卡顿

Android绘制原理

Android的屏幕刷新中涉及到最重要的三个概念。

1、CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU。

2、GPU:进一步处理数据,并将数据缓存起来。

3、屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点。

总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示。

android表格布局详解(android表格布局合并行)

image.png

双缓冲机制

看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制)。

如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?

有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?

所以,在屏幕刷新中,Android系统引入了双缓冲机制。

GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步。

android表格布局详解(android表格布局合并行)

image.png

虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理。

当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。

如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换。

这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的掉帧。

布局加载分析

首先要从setContentView方法开始说起了,其中调用了getDeleate().setContentView(resid)方法,接着调用了 LayoutInflater.from(this.mContext).inflate(resId, contentParent)来填充布局。紧接着调用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通过反射来创建。具体流程图如下:

android表格布局详解(android表格布局合并行)

具体源码逻辑分析清参考:juejin.cn/post/687044…

从这里可以分析到,布局加载有2个可优化点

IO操作优化反射优化获取界面布局耗时

做优化,首先要知道在什么地方进行优化,所以要获取到界面布局耗时

手动埋点

在setContentView执行前后手动打点,但是这种方式有如下缺点

不够优雅代码有侵入性AOP

简单说一下AOP的使用

首先,为了在Android使用AOP埋点需要引入AspectJ,在项目根目录的build.gradle下加入:

classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'复制代码

然后,在app目录下的build.gradle下加入:

apply plugin: 'android-aspectjx'implement 'org.aspectj:aspectjrt:1.8.+'复制代码

我们要使用AOP去获取界面布局的耗时,那么我们的切入点就是setContentView方法,声明一个@Aspect注解的PerformanceAop类,然后,我们就可以在里面实现对setContentView进行切面的方法,如下所示:

@Around("execution(* 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(); } LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));}复制代码

为了获取方法的耗时,我们必须使用@Around注解,这样第一个参数ProceedingJoinPoint就可以提供proceed方法去执行我们的setContentView方法,在此方法的前后就可以获取setContentView方法的耗时。后面的execution表明了在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法,后面括号内的*是通配符,表示匹配任何Activity的setContentView方法,并且方法参数的个数和类型不做限定。

LayoutInflaterCompat.setFactory2

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

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() { @Override public 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; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } }); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}复制代码

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

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

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

具体源码逻辑清参考:juejin.cn/post/687044…

布局加载优化AsyncLayoutInflater

基于布局加载的两个性能问题,谷歌给我们提供了一个类AsyncLayoutInflater,它可以从侧面解决布局加载耗时的问题,他的特点如下

1、工作线程加载布局。2、回调主线程。3、节省主线程时间。

需要我们在gradle中配置,如:

implementation 'com.android.support:asynclayoutinflater:28.0.0-alpha1'复制代码

使用:

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

具体源码解析逻辑请参考:juejin.cn/post/684490…

X2C

X2C项目地址

X2C框架保留了XML的优点,并解决了其IO操作和反射的性能问题。开发人员只需要正常写XML代码即可,在编译期,X2C会利用APT工具将XML代码翻译为Java代码。等于是将运行期耗时装换为了编译期耗时

配置:

annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'implementation 'com.zhangyue.we:x2c-lib:1.0.6'复制代码

使用:

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

但是,X2C框架还存在一些问题:

部分Java属性不支持。失去了系统的兼容(AppCompat)

对于第2个问题,我们需要修改X2C框架的源码,当发现是TextView等控件时,需要直接使用new的方式去创建一个AppCompatTextView等兼容类型的控件。于此同时,它还有如下两个小的点不支持,但是这个问题不大:

merge标签 ,在编译期间无法确定xml的parent,所以无法支持。系统style,在编译期间只能查到应用的style列表,无法查询系统style,所以只支持应用内style。其他方式anko:已停止维护jetpact compose:谷歌新推出响应式布局,暂时资料较少常规布局优化减少层级

合理使用RelativeLayout和LinearLayout。 合理使用Merge。

合理使用RelativeLayout和LinearLayout RelativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,但是因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高。 注意 由于Android的碎片化程度很高,所以使用RelativeLayout能使构建的布局适应性更强。

合理使用Merge merge的原理:在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中。 注意

Merge只能用在布局XML文件的根元素。使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true。不能在ViewStub中使用Merge标签。原因就是ViewStub的inflate方法中根本没有attachToRoot的设置。提高显示速度

ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inflate()时,ViewStub所指向的布局才会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。

注意:

ViewStub只能加载一次,之后ViewStub对象会被置为空。所以它不适用于需要按需显示隐藏的情况。ViewStub只能用来加载一个布局文件,而不是某个具体的View。ViewStub中不能嵌套Merge标签。布局复用

Android的布局复用可以通过 include 标签来实现。

小结

最后,下面列出了我平常做布局优化时的一些小技巧:

使用标签加载一些不常用的布局。尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。使用TextView替换RL、LL。使用低端机进行优化,以发现性能瓶颈。使用TextView的行间距替换多行文本:lineSpacingExtra/lineSpacingMultiplier。使用Spannable/Html.fromHtml替换多种不同规格文字。尽可能使用LinearLayout自带的分割线。使用Space添加间距。多利用lint + alibaba规约修复问题点。嵌套层级过多可以考虑使用约束布局。布局优化分析工具Systrace关注Frames

首先,先在左边栏选中我们当前的应用进程,在应用进程一栏下面有一栏Frames,我们可以看到有绿、黄、红三种不同的小圆圈,如下图所示:

图中每一个小圆圈代表着当前帧的状态,大致的对应关系如下:

正常:绿色。丢帧:黄色。严重丢帧:红色。

并且,选中其中某一帧,我们还可以在视图最下方的详情框看到该帧对应的相关的Alerts报警信息,以帮助我们去排查问题;此外,如果是大于等于Android 5.0的设备(即API Level21),创建帧的工作工作分为UI线程和render线程。而在Android 5.0之前的版本中,创建帧的所有工作都是在UI线程上完成的。接下来,我们看看该帧对应的详情图,如下所示:

android表格布局详解(android表格布局合并行)

对应到此帧,我们发现这里可能有两个绘制问题:Bitmap过大、布局嵌套层级过多导致的measure和layout次数过多,这就需要我们去在项目中找到该帧对应的Bitmap进行相应的优化,针对布局嵌套层级过多的问题去选择更高效的布局方式,这块后面我们会详细介绍。

关注Alerts栏

此外,Systrace的显示界面还在在右边侧栏提供了一栏Alert框去显示出它所检测出所有可能有绘制性能问题的地方及对应的数量,如下图所示:

android表格布局详解(android表格布局合并行)

在这里,我们可以将Alert框看做是一个是待修复的Bug列表,通常一个区域的改进可以消除应用程序中的所有类中该类型的警报,所以,不要为这里的警报数量所担忧。

Layout Inspector

Layout Inspector是AndroidStudio自带的工具,它的主要作用就是用来查看视图层级结构的。

具体的操作路径为:

点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程复制代码

Choreographer

Choreographer是用来获取FPS的,并且可以用于线上使用,具备实时性,但是仅能在Api 16之后使用,具体的调用代码如下:

Choreographer.getInstance().postFrameCallback();复制代码

使用Choreographer获取FPS的完整代码如下所示:

private long mStartFrameTime = 0;private int mFrameCount = 0;/** * 单次计算FPS使用160毫秒 */private static final long MONITOR_INTERVAL = 160L; private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;/** * 设置计算fps的单位时间间隔1000ms,即fps/s */private static final long MAX_INTERVAL = 1000L; @TargetApi(Build.VERSION_CODES.JELLY_BEAN)private void getFPS() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return; } Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { if (mStartFrameTime == 0) { mStartFrameTime = frameTimeNanos; } long interval = frameTimeNanos - mStartFrameTime; if (interval > MONITOR_INTERVAL_NANOS) { double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL; // log输出fps LogUtils.i("当前实时fps值为: " + fps); mFrameCount = 0; mStartFrameTime = 0; } else { ++mFrameCount; } Choreographer.getInstance().postFrameCallback(this); } });}复制代码

通过以上方式我们就可以实现实时获取应用的界面的FPS了。但是我们需要排除掉页面没有操作的情况,即只在界面存在绘制的时候才做统计。我们可以通过 addOnDrawListener 去监听界面是否存在绘制行为,代码如下所示:

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener复制代码

当出现丢帧的时候,我们可以获取应用当前的页面信息、View 信息和操作路径上报至 APM后台,以降低二次排查的难度。此外,我们将连续丢帧超过 700 毫秒定义为冻帧,也就是连续丢帧 42 帧以上。这时用户会感受到比较明显的卡顿现象,因此,我们可以统计更有价值的冻帧率。冻帧率就是计算发生冻帧时间在所有时间的占比。通过解决应用中发生冻帧的地方我们就可以大大提升应用的流畅度。

秒鲨号所有文章资讯、展示的图片素材等内容均为注册用户上传(部分报媒/平媒内容转载自网络合作媒体),仅供学习参考。用户通过本站上传、发布的任何内容的知识产权归属用户或原始著作权人所有。如有侵犯您的版权,请联系我们反馈!本站将在三个工作日内改正。
(0)

大家都在看

品牌推广 在线咨询
返回顶部