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

为什么要布局优化?如果布局嵌套太深,或者因为其他原因布局的渲染性能不好,都有可能造成应用卡顿。安卓画图原理Android屏幕刷新涉及到三个最重要的概念。1.CPU:执行测量、布局

本文最后更新时间:  2023-03-26 09:54:14

为什么要布局优化?

如果布局嵌套太深,或者因为其他原因布局的渲染性能不好,都有可能造成应用卡顿。

安卓画图原理

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

1.CPU:执行测量、布局、绘图等操作。在应用层,画图后将数据提交给GPU。

2.GPU:进一步处理数据并缓存。

3.屏幕:由像素组成,以固定的频率(16.6ms,即每秒60帧)从缓冲区取出数据填充像素。

综上所述,CPU在画图后提交数据,GPU进一步处理并缓存数据,最后屏幕从缓冲区读取数据并显示。

image.png

双重缓冲机制

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

如果屏幕正在从缓冲区读取数据,而GPU正在向缓冲区写入数据,会发生什么情况?

有可能前一帧的一些图像和另一帧的一些图像会出现在屏幕上,这显然是不能接受的。那么如何才能解决这个问题呢?

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

GPU只将绘图数据写入后台缓冲区,GPU定期交换后台缓冲区和帧缓冲区,交换频率为60次/秒,与屏幕的刷新频率保持同步。

image.png

虽然我们引入了双缓冲机制,但是我们知道当布局复杂或者设备性能较差时,CPU无法保证在16.6ms内完成绘图数据的计算,所以系统在这里做了另外的处理。

当您的应用程序将数据填充到后台缓冲区时,系统将锁定后台缓冲区。

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

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

负载布局分析

首先,让我们从调用getDelete()的setContentView方法开始。SetContentView (resist)方法,然后调用LayoutInflater。来自(this.mcontext)。膨胀(resist,contentparent)以填充布局。然后调用getLayout方法,其中XML布局文件由loadXmlResourceParser加载并解析,然后调用createviewFromTag方法。根据标签,创建应该是视图,具体视图是由Factory或Factor2创建的。首先确定Factory2是否为null,然后用它创建视图;否则,确定Factory2是否为空,然后创建它。如果两者都为空,则不创建视图,然后判断mPrivateFactory是否为空。这里需要说明的是,mPrivateFactory是一个隐藏的API,只有框架可以调用。如果都没有创建,则视图由后续逻辑通过onCreateView或通过反射创建。具体流程图如下:

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

从这里可以分析出,布局加载有两个可优化点。

IO操作优化、反射优化、耗时的接口布局获取

要优化,首先要知道从哪里优化,所以得到界面布局是需要时间的。

人工埋设点

在setContentView执行前后手动打点,但这种方法有以下缺点

不够优雅的代码,侵入式AOP

浅谈AOP的使用

首先,为了在Android中使用AOP嵌入,需要引入AspectJ。在项目根目录的build.gradle下添加以下内容:

类路径'com . hujiang . aspectjx:gradle-Android-plugin-aspectjx:2 . 0 . 0 '复制代码。然后,添加:

应用插件:'Android-aspectjx '实现'org.aspectj:aspectjrt:1.8。+'复制代码我们要用AOP来获取界面布局的耗时,所以我们的出发点是setContentView方法,它声明了一个由@Aspect注释的PerformanceAop类。然后,我们可以在其中实现切割setContentView的方法,如下所示:

@左右("执行(* Android . app . activity . set content view(..))")public void getSetContentViewTime(ProceedingJoinPoint join point){ Signature Signature = join point . get Signature();string name = signature . toshortstring();long time = system . current time millis();请尝试{ join point . proceed();} catch(Throwable Throwable){ Throwable . printstacktrace();} log helper . I(name+"成本"+(system . current time millis()-time));}复制代码为了得到方法的耗时,我们必须使用@Around注释,这样第一个参数ProceedingJoinPoint就可以提供执行我们的setContentView方法的过程方法,在这个方法前后可以得到setContentView方法的耗时。下面的执行表明我们编写的getSetContentViewTime方法是在setContentView方法的执行内部调用的。后面括号中的*是通配符,表示setContentView方法匹配任何活动,方法参数的数量和类型不受限制。

layoutinflatercompt . set factory 2

以上两种方法都是获取所有布局加载后的时间,那么如果想获取单个控件的加载时间应该怎么做呢?下面介绍layoutinflate Compat . setfactory 2(以后会看到所有带Compat字段的API都兼容),它的使用必须在super.onCreate之前调用

公共类MainActivity扩展了app compat activity { @ Override protected void onCreate(@ Nullable Bundle savedInstanceState){ layoutinflatercompt . setfactory 2(getLayoutInflater(),new LayoutInflater)。factory 2(){ @ Override public View oncreate View(View parent,String name,Context context,AttributeSet attrs){ long start = system . current time millis();View view = getDelegate()。createView(父项、名称、上下文、属性);long cost = system . current time millis()-start;log . d("onCreateView & # 034, "=="+name+"==成本= = "+成本);返回视图;} @ Override public View oncreate View(String name,Context context,AttributeSet attrs){ return null;} });super . oncreate(savedInstanceState);setContentView(r . layout . activity _ main);}}复制代码layoutinflatercompt . setfactory 2的API不仅可以统计视图的创建时间,还可以用来代替系统控件的操作。比如有一天,产品经理让我们把应用的TextView改成某种样式,我们可以这样做。比如:

layoutinflatercompt . set factory 2(getLayoutInflater(),new LayoutInflater。factory 2(){ @ Override public View oncreate View(View parent,String name,Context context,AttributeSet attrs){ if(textutils . equals("文本视图",name)){ //替换为我们自己的TextView}返回null//返回自定义视图} @ override public view create view(string name,context context,attrs et attrs){ return null;} });复制代码复制代码只要我们在基类Activity的onCreate中定义这个方法,就可以达到相关的效果。

具体出处逻辑清晰参考:juejin.cn/post/687044…

负载布局优化AsyncLayoutInflater

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

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

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

实现'com . Android . support:asynclayoutinflater:28 . 0 . 0-alpha 1 '使用以下方式复制代码:

公共类MainActivity扩展了app compat activity { @ Override protected void onCreate(@ Nullable Bundle savedInstanceState){ new AsyncLayoutInflater(main activity . this)。inflate(R.layout.activity_main,null,new AsyncLayoutInflater。oninflateffinished listener(){ @ Override public void oninflatefined(@ NonNull View View,int i,@ Nullable View group View group){ setContentView(View);//查看和加载完成//findViewById相关操作可以在这里进行} });super . oncreate(savedInstanceState);//setContentView(r . layout . activity _ main);//这里不需要设置布局文件}}请参考:juejin.cn/post/684490…

X2C

X2C项目地址

X2C框架保留了XML的优点,解决了IO操作和反射的性能问题。开发者只需要正常编写XML代码即可。在编译期间,X2C将使用APT工具将XML代码翻译成Java代码。这相当于用编译时耗时替换运行时耗时

配置:

注释处理器'com . zhangyue . we:x2c-apt:1 . 1 . 2 '实现'com . zhangyue . we:x2c-lib:1 . 0 . 6 '使用以下方式复制代码:

@ Xml(layouts = "activity _ main & # 034)公共类MainActivity扩展app compat activity { @ Override protected void onCreate(@ Nullable Bundle savedInstanceState){ super . onCreate(savedInstanceState);//setContentView(r . layout . activity _ main);//这里不需要设置布局文件}}复制代码。然而,X2C框架仍然存在一些问题:

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

对于第二个问题,我们需要修改X2C框架的源代码。当我们发现它是一个TextView之类的控件时,我们需要使用new方法直接创建一个AppCompatTextView之类的兼容控件。同时它还有以下两个小点不支持,不过这个问题不大:

xml的父级Merge标记在编译期间无法确定,因此不支持它。系统样式,编译时只能找到应用的样式列表,系统样式无法查询,所以只支持应用内样式。其他方式anko:jet pact compose的维护已经停止:Google推出了新的响应式布局,临时数据较少。传统的布局优化降低了级别。

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

合理使用RelativeLayout和LinearLayout RelativeLayout也有性能低的问题,因为RelativeLayout会在子视图上进行两次测量。但是,如果LinearLayout中有一个weight属性,则需要测量两次,但仍然会比RelativeLayout更高效,因为没有更多的依赖关系。注意,Android是高度碎片化的,所以使用RelativeLayout可以使构建的布局适应性更强。

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

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

ViewStub是一个轻量级视图,是一个不占用布局位置,占用资源非常少的不可见视图对象。您可以为ViewStub指定布局。加载布局时,将只初始化ViewStub。然后,当ViewStub设置为可见或者调用ViewStub.inflate()时,ViewStub指向的布局将被加载并实例化,然后ViewStub的布局属性将被传递给它指向的布局。

注意:

ViewStub只能加载一次,之后ViewStub对象将被设置为空。所以不适合隐藏需要点播显示的情况。ViewStub只能用于加载布局文件,而不能加载特定视图。合并标记不能嵌套在ViewStub中。布局复用

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

总结

最后,下面是我在布局优化中经常做的一些小技巧:

使用标签加载一些不常见的布局。尽量少用wrap_content。布局测量时,wrap_content会增加计算成本。当宽度和高度已知为固定值时,不使用wrap_content。使用TextView替换RL和LL。使用低端机器进行优化,找到性能瓶颈。使用TextView的行距替换多行文本:行距额外/行距乘数。使用span able/html . from html替换各种不同的规范。尽可能使用LinearLayout自带的分割线。使用Space添加间距。利用lint+alibaba协议解决问题。如果嵌套层数太多,请考虑使用约束布局。优化分析工具Systrace关注帧。

首先在左栏选择我们当前的申请流程。申请流程一栏下有一栏框。我们可以看到绿色、黄色和红色三个不同的小圆圈,如下图所示:

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

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

此外,如果选择了其中一个框架,我们还可以在视图底部的详细信息框中看到与该框架对应的相关警报信息,以便帮助我们解决问题;另外,如果是等于或大于Android 5.0(即API Level21)的设备,创建框架的工作可以分为UI线程和渲染线程。在Android 5.0之前,所有创建框架的工作都是在UI线程上完成的。接下来我们来看看这个框架对应的详细示意图,如下图:

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

请注意“警报”列

此外,SYSTRACE的显示界面还在右侧栏提供了一个警告框,显示Systrace检测到的所有可能的绘图性能问题及其对应的数字,如下图所示:

在这里,我们可以把警告框想象成一个需要修复的bug列表。通常,一个方面的改进可以消除应用程序中所有类的这种类型的警报,所以不要担心这里的警报数量。

布局检查器

布局检查器是AndroidStudio自带的工具,主要功能是查看视图层次结构。

具体操作路径是:

点击工具工具栏->第三列布局检查器->所选流程复制代码编排器。

Choreographer是用来获取FPS的,可以在线使用,有实时性能,但是只能在Api 16以后使用。具体调用代码如下:

Choreographer.getInstance()。postframcallback();复制代码使用Choreographer获得FPS的完整代码如下:

private long mStartFrameTime = 0;private int mFrameCount = 0;/* * * FPS单次计算耗时160 ms */Private Static Final Long Monitor _ interval = 160 l;私有静态最终长MONITOR _ INTERVAL _ NANOS = MONITOR _ INTERVAL * 1000 l * 1000 l;/* * *设置计算fps的单位时间间隔为1000ms,即FPS/S */Private static final longmax _ interval = 1000 l;@TargetApi(Build。版本代码。JELLY _ BEAN)private void get fps(){ if(Build。VERSION . SDK _ INT & lt建造。版本代码。JELLY _ BEAN){ return;} Choreographer.getInstance()。postFrameCallback(新编舞。frame callback(){ @ Override public void do frame(long frametime nanos){ if(mStartFrameTime = = 0){ mStartFrameTime = frametime nanos;}长间隔= frametime nanos-mStartFrameTime;if(时间间隔& gtMONITOR _ INTERVAL _ NANOS){ double fps =(((double)(mFrameCount * 1000 l * 1000 l))/INTERVAL)* MAX _ INTERVAL;//日志输出fps logutils . I("当前实时fps值为:"+fps);mFrameCount = 0;mStartFrameTime = 0;} else { ++ mFrameCount;} Choreographer.getInstance()。postframcallback(this);} });}通过以上方法复制代码,我们可以实现应用程序界面FPS的实时采集。但是我们需要排除页面上没有操作的情况,也就是只有在绘制界面的时候才进行统计。我们可以使用addOnDrawListener来监控界面中是否有绘制行为。代码如下:

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

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

温馨提示:内容均由网友自行发布提供,仅用于学习交流,如有版权问题,请联系我们。