百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Android布局优化详解

bigegpt 2024-08-07 17:51 9 浏览

为什么要进行布局优化

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

Android绘制原理

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

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

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

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

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

双缓冲机制

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

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

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

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

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

虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,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通过反射来创建。具体流程图如下:

具体源码逻辑分析清参考: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线程上完成的。接下来,我们看看该帧对应的详情图,如下所示:

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

关注Alerts栏

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

在这里,我们可以将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 帧以上。这时用户会感受到比较明显的卡顿现象,因此,我们可以统计更有价值的冻帧率。冻帧率就是计算发生冻帧时间在所有时间的占比。通过解决应用中发生冻帧的地方我们就可以大大提升应用的流畅度。

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...