热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

android布局优化的一些实用建议

这篇文章主要给大家介绍了关于android布局优化的一些实用建议,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。

本文主要包括以下内容

  1. 为什么要进行布局优化及android绘制,布局加载原理
  2. 获取布局文件加载耗时的方法
  3. 介绍一些布局优化的手段与方法
  4. 一些常规优化手段

为什么要进行布局优化?

为什么要进行布局优化?答案是显而易见的,如果布局嵌套过深,或者其他原因导致布局渲染性能不佳,可能会导致应用卡顿 那么布局到底是如何导致渲染性能不佳的呢?首先我们应该了解下android绘制原理与布局加载原理

android绘制原理

Android的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍)

  • CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU
  • GPU:进一步处理数据,并将数据缓存起来
  • 屏幕:由一个个像素点组成,以固定的频率(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被锁定了,它会放弃这次交换。

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

布局加载原理

由上面可知,导致掉帧的原因是CPU无法在16.6ms内完成绘制数据的计算。而之所以布局加载可能会导致掉帧,正是因为它在主线程上进行了耗时操作,可能导致CPU无法按时完成数据计算

布局加载主要通过setContentView来实现,我们就不在这里贴源码了,一起来看看它的时序图

我们可以看到,在setContentView中主要有两个耗时操作

  • 解析xml,获取XmlResourceParser,这是IO过程
  • 通过createViewFromTag,创建View对象,用到了反射

以上两点就是布局加载可能导致卡顿的原因,也是布局的性能瓶颈

获取布局文件加载耗时的方法

我们如果需要优化布局卡顿问题,首先最重要的就是:确定定量标准 所以我们首先介绍几种获取布局文件加载耗时的方法

常规获取

首先介绍一下常规方法

val start = System.currentTimeMillis()
setContentView(R.layout.activity_layout_optimize)
val inflateTime = System.currentTimeMillis() - start

这种方法很简单,因为setContentView是同步方法,如果想要计算耗时,直接将前后时间计算相减即可得到结果了

AOP(Aspectj,ASM)

上面的方式虽然简单,但是却不够优雅,同时代码有侵入性,如果要对所有Activity测量时,就需要在基类中复写相关方法了,比较麻烦了 下面介绍一种AOP的方式计算耗时

    @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();
        }
        Log.i("aop inflate",name + " cost " + (System.currentTimeMillis() - time));
    }

上面用的Aspectj,比较简单,上面的注解的意思是在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法 这样就可以获取相应的耗时 我们可以看下打印的日志

I/aop inflate: AppCompatActivity.setContentView(..) cost 69
I/aop inflate: AppCompatActivity.setContentView(..) cost 25

这样就可以实现无侵入的监控每个页面布局加载的耗时 具体源码可见文末

获取任一控件耗时

有时为了更精确的知道到底是哪个控件加载耗时,比如我们新添加了自定义View,需要监控它的性能 我们可以利用setFactory2来监听每个控件的加载耗时 首先我们来回顾下setContentView方法

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        ...
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        ...
        return view;
    }

在真正进行反射实例化xml结点前,会调用mFactory2的onCreateView方法 这样如果我们重写onCreateView方法,在其前后加上耗时统计,即可获取每个控件的加载耗时

    private fun initItemInflateListener(){
        LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2 {
            override fun onCreateView(
                parent: View?,
                name: String,
                context: Context,
                attrs: AttributeSet
            ): View? {
                val time = System.currentTimeMillis()
                val view = delegate.createView(parent, name, context, attrs)
                Log.i("inflate Item",name + " cost " + (System.currentTimeMillis() - time))
                return view
            }

            override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
                return null
            }
        })
    }

如上所示:真正的创建View的方法,仍然是调用delegate.createView,我们只是其之前与之后做了埋点 注意,initItemInflateListener需要在onCreate之前调用 这样就可以比较方便地实现监听每个控件的加载耗时

布局加载优化的一些方法介绍

布局加载慢的主要原因有两个,一个是IO,一个是反射 所以我们的优化思路一般有两个

  1. 侧面缓解(异步加载)
  2. 根本解决(不需要IO,反射过程,如X2C,Anko,Compose等)

AsyncLayoutInflater方案

AsyncLayoutInflater 是来帮助做异步加载 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法运行结束之后 OnInflateFinishedListener 会在主线程回调返回 View;这样做旨在 UI 的懒加载或者对用户操作的高响应。

简单的说我们知道默认情况下 setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:Xml的解析、View的反射创建等过程同样是在UI线程执行的,AsyncLayoutInflater 就是来帮我们把这些过程以异步的方式执行,保持UI线程的高响应。

使用如下:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(AsyncLayoutActivity.this)
                .inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
                    @Override
                    public void onInflateFinished(View view, int resid, ViewGroup parent) {
                        setContentView(view);
                    }
                });
        // 别的操作
    }

这样做的优点在于将UI加载过程迁移到了子线程,保证了UI线程的高响应 缺点在于牺牲了易用性,同时如果在初始化过程中调用了UI可能会导致崩溃

X2C方案

X2C是掌阅开源的一套布局加载框架 它的主要是思路是在编译期,将需要翻译的layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件。这就将运行时的开销转移到了编译时 如下所示,原始xml文件:

<&#63;xml version="1.0" encoding="utf-8"&#63;>


  

  


X2C 生成的 Java 文件

public class X2C_2131296281_Activity_Main implements IViewCreator {
  @Override
  public View createView(Context ctx, int layoutId) {
        Resources res = ctx.getResources();

        RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
        relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);

        View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
        RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        view1.setLayoutParams(layoutParam1);
        relativeLayout0.addView(view1);
        view1.setId(R.id.head);
        layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);

        ImageView imageView2 = new ImageView(ctx);
        RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
        imageView2.setLayoutParams(layoutParam2);
        relativeLayout0.addView(imageView2);
        imageView2.setId(R.id.ccc);
        layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);

        return relativeLayout0;
  }
}

使用时如下所示,使用X2C.setContentView替代原始的setContentView即可

// this.setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);

X2C优点

  1. 在保留xml的同时,又解决了它带来的性能问题
  2. 据X2C统计,加载耗时可以缩小到原来的1/3

X2C问题

  1. 部分属性不能通过代码设置,Java不兼容
  2. 将加载时间转移到了编译期,增加了编译期耗时
  3. 不支持kotlin-android-extensions插件,牺牲了部分易用性

Anko方案

Anko是JetBrains开发的一个强大的库,支持使用kotlin DSL的方式来写UI,如下所示

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle&#63;, persistentState: PersistableBundle&#63;) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent {
    override fun createView(ui: AnkoContext) = with(ui) {
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello, ${name.text}!") }
            }
        }
    }
}

如上所示,Anko使用kotlin DSL实现布局,它比我们使用Java动态创建布局方便很多,主要是更简洁,它和拥有xml创建布局的层级关系,能让我们更容易阅读 同时,它去除了IO与反射过程,性能更好,以下是Anko与XML的性能对比

不过由于AnKo已经停止维护了,这里不建议大家使用,了解原理即可 AnKo建议大家使用Jetpack Compose来替代使用

Compose方案

Compose 是 Jetpack 中的一个新成员,是 Android 团队在2019年I/O大会上公布的新的UI库,目前处于Beta阶段 Compose使用纯kotlin开发,使用简洁方便,但它并不是像Anko一样对ViewGroup的封装 Compose 并不是对 View 和 ViewGroup 这套系统做了个上层包装来让写法更简单,而是完全抛弃了这套系统,自己把整个的渲染机制从里到外做了个全新的。

可以确定的是,Compose是取代XML的官方方案

Compose的主要优点就在于它的简单好用,具体来说就是两点

  1. 它的声明式 UI
  2. 去掉了 xml,只使用 Kotlin 一种语言

由于本文并不是介绍Compose的,所以就不继续介绍Compose了,总得来说,Compose是未来android UI开发的方向,读者可以自行查阅相关资料

一些常规优化手段

上面介绍了一些改动比较大的方案,其实我们在实际开发中也有些常规的方法可以优化布局加载 比如优化布局层级,避免过度绘制等,这些简单的手段可能正是可以应用到项目中的

优化布局层级及复杂度

  1. 使用ConstraintLayout,可以实现完全扁平化的布局,减少层级
  2. RelativeLayout本身尽量不要嵌套使用
  3. 嵌套的LinearLayout中,尽量不要使用weight,因为weight会重新测量两次
  4. 推荐使用merge标签,可以减少一个层级
  5. 使用ViewStub延迟加载

避免过度绘制

  1. 去掉多余背景色,减少复杂shape的使用
  2. 避免层级叠加
  3. 自定义View使用clipRect屏蔽被遮盖View绘制

总结

到此这篇关于android布局优化的一些实用建议的文章就介绍到这了,更多相关android布局优化内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • Android 九宫格布局详解及实现:人人网应用示例
    本文深入探讨了人人网Android应用中独特的九宫格布局设计,解析其背后的GridView实现原理,并提供详细的代码示例。这种布局方式不仅美观大方,而且在现代Android应用中较为少见,值得开发者借鉴。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • Android LED 数字字体的应用与实现
    本文介绍了一种适用于 Android 应用的 LED 数字字体(digital font),并详细描述了其在 UI 设计中的应用场景及其实现方法。这种字体常用于视频、广告倒计时等场景,能够增强视觉效果。 ... [详细]
  • RecyclerView初步学习(一)
    RecyclerView初步学习(一)ReCyclerView提供了一种插件式的编程模式,除了提供ViewHolder缓存模式,还可以自定义动画,分割符,布局样式,相比于传统的ListVi ... [详细]
  • 本文介绍如何在 Android 中通过代码模拟用户的点击和滑动操作,包括参数说明、事件生成及处理逻辑。详细解析了视图(View)对象、坐标偏移量以及不同类型的滑动方式。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 本文详细介绍了如何使用Spring Boot进行高效开发,涵盖了配置、实例化容器以及核心注解的使用方法。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • Python自动化处理:从Word文档提取内容并生成带水印的PDF
    本文介绍如何利用Python实现从特定网站下载Word文档,去除水印并添加自定义水印,最终将文档转换为PDF格式。该方法适用于批量处理和自动化需求。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • 将Web服务部署到Tomcat
    本文介绍了如何在JDeveloper 12c中创建一个Java项目,并将其打包为Web服务,然后部署到Tomcat服务器。内容涵盖从项目创建、编写Web服务代码、配置相关XML文件到最终的本地部署和验证。 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • 本文介绍如何在 Unity 的 XML 配置文件中,将参数传递给自定义生命周期管理器的构造函数。我们将详细探讨 CustomLifetimeManager 类的实现及其配置方法。 ... [详细]
  • 本文详细介绍了 Java 中 org.apache.xmlbeans.SchemaType 类的 getBaseEnumType() 方法,提供了多个代码示例,并解释了其在不同场景下的使用方法。 ... [详细]
  • 本文详细介绍了如何解决MyBatis中常见的BindingException错误,提供了多种排查和修复方法,确保Mapper接口与XML文件的正确配置。 ... [详细]
author-avatar
祢好世界末日
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有